docs: clarify session accessor contracts

This commit is contained in:
Josh Lehman
2026-06-04 14:56:23 -07:00
parent 81d9693132
commit 944e4f88da

View File

@@ -32,32 +32,55 @@ import { streamSessionTranscriptLines } from "./transcript-stream.js";
import { resolveSessionTranscriptFile } from "./transcript.js";
import type { SessionEntry } from "./types.js";
/**
* Session access API for callers that need entries or transcripts without
* depending on the persisted store layout. Callers provide stable session
* identity, and this module resolves the current entry/transcript target while
* preserving canonical-key, transcript-linking, and update-notification rules.
*/
export type SessionAccessScope = {
/** Agent owner used when the session key does not already encode one. */
agentId?: string;
/**
* Set false only for internal read-only hot paths that will not retain or
* mutate the returned entry.
*/
clone?: boolean;
/** Environment override used when resolving agent-scoped store paths in tests/tools. */
env?: NodeJS.ProcessEnv;
/** Set false for metadata-only reads that do not need hydrated prompt refs. */
hydrateSkillPromptRefs?: boolean;
/** Canonical or alias session key for the entry being read or written. */
sessionKey: string;
/** Explicit store path for callers that already resolved the owning store. */
storePath?: string;
};
export type SessionTranscriptReadScope = Omit<SessionAccessScope, "sessionKey"> & {
/** Explicit transcript file path; bypasses store lookup when already known. */
sessionFile?: string;
/** Runtime session id used to derive a transcript file when no explicit file is provided. */
sessionId: string;
/** Optional key for read callers that can resolve via the session entry. */
sessionKey?: string;
/** Channel thread suffix used when deriving topic transcript paths. */
threadId?: string | number;
};
export type SessionTranscriptAccessScope = SessionTranscriptReadScope & {
/** Required for writes because write paths may update entry metadata. */
sessionKey: string;
};
export type SessionTranscriptWriteScope = Omit<SessionTranscriptAccessScope, "sessionId"> & {
/** Optional for appenders that can operate on an existing explicit transcript target. */
sessionId?: string;
};
export type SessionEntrySummary = {
/** Persisted key for the entry. */
sessionKey: string;
/** Entry value cloned from the backing store unless the caller requested borrowed reads. */
entry: SessionEntry;
};
@@ -67,44 +90,62 @@ export type ExactSessionEntry = {
entry: SessionEntry;
};
/** Raw transcript record for non-message events; message records use appendTranscriptMessage. */
export type TranscriptEvent = unknown;
export type TranscriptMessageAppendOptions<TMessage> = {
/** Runtime config used for message redaction and transcript header metadata. */
config?: OpenClawConfig;
/** Working directory recorded in a newly created transcript header. */
cwd?: string;
/** How duplicate message idempotency keys are detected before append. */
idempotencyLookup?: "scan" | "caller-checked";
/** Provider/channel message payload to persist. */
message: TMessage;
/** Testable timestamp override for the generated transcript entry. */
now?: number;
/** Optional finalizer that runs after duplicate detection but before persistence. */
prepareMessageAfterIdempotencyCheck?: (message: TMessage) => TMessage | undefined;
/** Allow append without parent-link migration for large legacy linear transcripts. */
useRawWhenLinear?: boolean;
};
export type TranscriptMessageAppendResult<TMessage> = {
/** False when idempotency lookup found an existing transcript message. */
appended: boolean;
/** Redacted message payload as persisted or replayed from the transcript. */
message: TMessage;
/** Existing or newly generated transcript message id. */
messageId: string;
};
/** Transcript update fields supplied by callers; sessionFile is resolved here. */
export type TranscriptUpdatePayload = Omit<SessionTranscriptUpdate, "sessionFile">;
export type SessionEntryUpdateOptions = {
/** Skip prune/cap/rotation maintenance for specialized internal updates. */
skipMaintenance?: boolean;
/** Let the writer cache retain the updated object without cloning. */
takeCacheOwnership?: boolean;
};
export type SessionEntryPatchOptions = {
/** Entry to synthesize when a patch operation is allowed to create. */
fallbackEntry?: SessionEntry;
/** Keep the previous updatedAt value when the patch should not count as activity. */
preserveActivity?: boolean;
/** Replace the whole entry instead of merging the returned patch. */
replaceEntry?: boolean;
};
export type SessionEntryPatchContext = {
/** Present when the patched entry already existed before fallback synthesis. */
existingEntry?: SessionEntry;
};
export type { SessionLifecycleArtifactCleanupParams, SessionLifecycleArtifactCleanupResult };
/** Loads one session entry through the storage-neutral accessor seam. */
/** Returns the entry for a canonical or alias session key, if one exists. */
export function loadSessionEntry(scope: SessionAccessScope): SessionEntry | undefined {
if (scope.clone === false) {
const store = loadSessionStore(resolveAccessStorePath(scope), {
@@ -117,8 +158,9 @@ export function loadSessionEntry(scope: SessionAccessScope): SessionEntry | unde
}
/**
* Loads one entry only when the persisted key exactly matches the requested key.
* Approval routing uses this to avoid canonical alias lookup crossing accounts.
* Returns only the row persisted under the exact key provided.
* Use this for authorization-sensitive routing where alias canonicalization
* could cross an account or agent boundary.
*/
export function loadExactSessionEntry(scope: SessionAccessScope): ExactSessionEntry | undefined {
const sessionKey = scope.sessionKey.trim();
@@ -133,7 +175,7 @@ export function loadExactSessionEntry(scope: SessionAccessScope): ExactSessionEn
return entry ? { sessionKey, entry } : undefined;
}
/** Lists session entries through the storage-neutral accessor seam. */
/** Lists entries from the resolved store, preserving the persisted key for each row. */
export function listSessionEntries(
scope: Partial<Omit<SessionAccessScope, "sessionKey">> = {},
): SessionEntrySummary[] {
@@ -148,7 +190,7 @@ export function listSessionEntries(
return listFileSessionEntries(scope);
}
/** Reads a session activity timestamp through the storage-neutral accessor seam. */
/** Reads the last activity timestamp for one session entry, or undefined when absent. */
export function readSessionUpdatedAt(scope: SessionAccessScope): number | undefined {
if (scope.storePath) {
return readFileSessionUpdatedAt({
@@ -159,7 +201,7 @@ export function readSessionUpdatedAt(scope: SessionAccessScope): number | undefi
return loadSessionEntry(scope)?.updatedAt;
}
/** Applies a partial entry update through the storage-neutral accessor seam. */
/** Creates or updates one entry from a partial patch and returns the persisted entry. */
export async function upsertSessionEntry(
scope: SessionAccessScope,
patch: Partial<SessionEntry>,
@@ -171,7 +213,7 @@ export async function upsertSessionEntry(
});
}
/** Replaces one entry through the storage-neutral accessor seam. */
/** Replaces one entry with the supplied value and returns the persisted entry. */
export async function replaceSessionEntry(
scope: SessionAccessScope,
entry: SessionEntry,
@@ -184,7 +226,11 @@ export async function replaceSessionEntry(
});
}
/** Patches one entry atomically through the storage-neutral accessor seam. */
/**
* Applies an atomic patch to one entry.
* The updater sees the current entry plus whether it was synthesized from a
* fallback; returning null skips persistence.
*/
export async function patchSessionEntry(
scope: SessionAccessScope,
update: (
@@ -202,7 +248,7 @@ export async function patchSessionEntry(
});
}
/** Updates an existing session entry through the storage-neutral accessor seam. */
/** Updates an existing entry only; returns null when the session is absent. */
export async function updateSessionEntry(
scope: SessionAccessScope,
update: (
@@ -219,14 +265,14 @@ export async function updateSessionEntry(
});
}
/** Cleans scoped session lifecycle entries and transcript artifacts through the accessor seam. */
/** Removes entries and orphan transcript artifacts owned by a named session lifecycle. */
export async function cleanupSessionLifecycleArtifacts(
params: SessionLifecycleArtifactCleanupParams,
): Promise<SessionLifecycleArtifactCleanupResult> {
return await cleanupFileSessionLifecycleArtifacts(params);
}
/** Loads raw transcript events through the storage-neutral accessor seam. */
/** Reads parsed transcript records from an explicit or derived transcript target. */
export async function loadTranscriptEvents(
scope: SessionTranscriptReadScope,
): Promise<TranscriptEvent[]> {
@@ -238,7 +284,11 @@ export async function loadTranscriptEvents(
return events;
}
/** Appends one raw transcript event through the storage-neutral accessor seam. */
/**
* Appends a non-message transcript record such as session or metadata events.
* Message records must use appendTranscriptMessage so parent links, idempotency,
* and redaction are preserved.
*/
export async function appendTranscriptEvent(
scope: SessionTranscriptAccessScope,
event: TranscriptEvent,
@@ -264,7 +314,10 @@ function assertNonMessageTranscriptEvent(event: TranscriptEvent): void {
}
}
/** Appends one transcript message through the storage-neutral writer seam. */
/**
* Appends one transcript message with message-id generation and optional
* idempotency lookup. The returned message is the redacted persisted value.
*/
export async function appendTranscriptMessage<TMessage>(
scope: SessionTranscriptWriteScope,
options: TranscriptMessageAppendOptions<TMessage> & {
@@ -297,7 +350,7 @@ export async function appendTranscriptMessage<TMessage>(
});
}
/** Publishes a transcript update after resolving the current storage target. */
/** Emits a transcript update after resolving the current transcript target. */
export async function publishTranscriptUpdate(
scope: SessionTranscriptWriteScope,
update: TranscriptUpdatePayload = {},