@robota-sdk/agent-session 3.2.1 → 3.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.js","names":["digestOf","Buffer","#states","#heldOrder","#handoffOrder","#keepsHeld","#edit","#settle","BASE64URL_PATTERN","Buffer","Buffer","NEWLINE","digestOf","#sealer","#files","#memory","#claims","#underWriteMarker","#onUnlock","#tagPendingAtUnlock","#handOut","#isOwners","#heldOutcome","#isRunHere","Buffer","#kindOfSave","#tag","#handedOut","#tagInPlace","#textOfFile","#writeTagged","#tagPendingOne","#sealer","#files","#markers","#claims","#authorship","#underWriteMarker","#read","#takeMarker","#readPlain","#rebuild","#guardWrite","#write","#withTransientMarker","#assertPlainReplaceable","#currentQuietly","createProcessOperationLock","NEWLINE","#sealer","#kind","#decode","#written","#lines","join","PAYLOAD_DIRECTORY_SUFFIX","resolve","existsSync","readFileSync","openSync","fstatSync","readSync","#parts","#length","join","readdirSync","join","openSync","fstatSync","readdirSync","join","readdirSync","join","createProcessOperationLock","digestOf","join","openSync","readdirSync","NEWLINE","openSync","fstatSync","readSync","#lines","resolve"],"sources":["../src/stored-session-codec.ts","../src/sealed-session-memory.ts","../src/session-record-authorship.ts","../src/stored-session-authorship.ts","../src/stored-session-record.ts","../src/session-store.ts","../src/stored-append-lines.ts","../src/node-session-log-sink.ts","../src/session-log-sources.ts","../src/append-file-rewrite.ts","../src/stale-temporaries.ts","../src/staged-replacements.ts","../src/session-log-migration.ts","../src/session-record-migration.ts","../src/tool-result-spill-store.ts","../src/prompt-history-file.ts","../src/prompt-history-compaction.ts","../src/workspace-path-resolver.ts","../src/process-exit-flush.ts"],"sourcesContent":["/**\n * The stored form of one session record, apart from who wrote it and what a store holds of it: the\n * versioned envelope, plain or sealed to the record's own id, what a store finds at a record's path,\n * and the conversions a migration makes. Stateless; {@link StoredSessionRecords} keeps the rest.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createHash } from 'node:crypto';\n\nimport {\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isSealedArtifactBytes,\n} from '@robota-sdk/agent-core';\n\nimport {\n SESSION_RECORD_ENVELOPE_VERSION,\n decodeVersionedInteractiveSessionRecord,\n} from './session-record-codec/index.js';\n\nimport type { IArtifactContext, IArtifactSealer } from '@robota-sdk/agent-core';\nimport type {\n IInteractiveSessionRecord,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/** What a listing says about a sealed file that does not open; fixed, so it can carry no content. */\nexport const DOES_NOT_OPEN_MESSAGE =\n 'the sealed session file does not open for this session: it is damaged or sealed with other keys';\n\n/** What a load says about a record whose own id is another session's; fixed, so it carries no content. */\nconst OTHER_ID_MESSAGE = 'the record belongs to another session id';\n\n/** How every store names stored bytes: the SHA-256, lowercase hex. */\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\n/** Whether `value` names stored bytes the way the stores do. */\nexport function isSha256Hex(value: unknown): value is string {\n return typeof value === 'string' && SHA256_HEX.test(value);\n}\n\nexport function digestOf(stored: Uint8Array): string {\n return createHash('sha256').update(stored).digest('hex');\n}\n\nexport function sameBytes(left: Uint8Array, right: Uint8Array): boolean {\n return Buffer.from(left.buffer, left.byteOffset, left.byteLength).equals(right);\n}\n\n/** The id binds a sealed record to its own file, so a record copied to another session's path does not open. */\nexport function recordContext(id: string): IArtifactContext {\n return { kind: 'session-record', id };\n}\n\nexport function utf8Text(bytes: Uint8Array): string {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf8');\n}\n\n/**\n * The versioned envelope of `record` (TRANS-007 — without a version on disk \"written by a build this\n * one does not read\" cannot be told from \"damaged\"), the same pretty-printed JSON the stores always wrote.\n */\nexport function envelopeText(record: IInteractiveSessionRecord): string {\n return JSON.stringify({ schemaVersion: SESSION_RECORD_ENVELOPE_VERSION, record }, null, 2);\n}\n\n/** A stored record: the bytes that go on disk and the plaintext envelope they were made from. */\nexport interface IEncodedSession {\n readonly stored: Uint8Array;\n readonly text: string;\n}\n\n/** The bytes to store for `record`: its envelope, sealed when a sealer is given. */\nexport function encodeStoredSession(\n record: IInteractiveSessionRecord,\n sealer: IArtifactSealer | undefined,\n): IEncodedSession {\n const text = envelopeText(record);\n const bytes = Buffer.from(text, 'utf8');\n return {\n stored: sealer === undefined ? bytes : sealer.sealBytes(bytes, recordContext(record.id)),\n text,\n };\n}\n\n/** What a record's stored bytes are to a migration that seals what is still plain (#3679). */\nexport type TStoredSessionMigrationState =\n /** Not sealed framing: whatever it holds — a valid, corrupt, unsupported or bare record. */\n | 'plain'\n /** Sealed in this build's framing, and it opens for this id. */\n | 'sealed'\n /** Sealed in this build's framing and it does not open for this id: damaged, or other keys. */\n | 'does-not-open'\n /** Sealed in a framing a newer build writes. */\n | 'newer-format';\n\n/**\n * Where the bytes of session `id` stand for a migration: decided by whether they open for this id,\n * never by framing alone. Opening needs the keys unlocked; with them locked it throws\n * `ArtifactLockedError`, since \"intact but locked\" decides nothing.\n */\nexport function storedSessionMigrationState(\n id: string,\n stored: Uint8Array,\n sealer: IArtifactSealer,\n): TStoredSessionMigrationState {\n if (!isSealedArtifactBytes(stored)) return 'plain';\n if (!isCurrentSealedArtifactBytes(stored)) return 'newer-format';\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, recordContext(id));\n } catch (error) {\n if (isArtifactLockedError(error)) throw error;\n return 'does-not-open';\n }\n plain.fill(0);\n return 'sealed';\n}\n\n/**\n * `raw` — a plain record's bytes exactly as stored, whatever they hold — sealed to session `id`, so a\n * store that opens it reports the same status the plain bytes gave: a corrupt or unsupported record\n * stays one, only no longer readable without the keys.\n */\nexport function sealStoredSessionBytes(\n id: string,\n raw: Uint8Array,\n sealer: IArtifactSealer,\n): Uint8Array {\n return sealer.sealBytes(raw, recordContext(id));\n}\n\n/** Decode envelope text into an outcome, keeping \"not JSON\" and \"not a record\" the same answer. */\nexport function decodeStoredSessionText(raw: string): TSessionLoadOutcome {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw) as unknown;\n } catch {\n return {\n status: 'corrupt',\n issues: [{ path: '', message: 'the session file is not JSON' }],\n };\n }\n const outcome = decodeVersionedInteractiveSessionRecord(parsed);\n if (outcome.status === 'valid') return { status: 'valid', record: outcome.record };\n if (outcome.status === 'unsupported') {\n return {\n status: 'unsupported',\n schemaVersion: outcome.schemaVersion,\n ...(outcome.cwd !== undefined ? { cwd: outcome.cwd } : {}),\n };\n }\n return {\n status: 'corrupt',\n issues: outcome.issues,\n ...(outcome.cwd !== undefined ? { cwd: outcome.cwd } : {}),\n };\n}\n\n/**\n * {@link decodeStoredSessionText} for the record stored under `id`. A record that names another\n * session is not this session's, wherever it was copied from, so it is `corrupt` — the same for a\n * plain file, which anyone can copy, and a sealed one, which opens here only when sealed to this id.\n */\nexport function decodeRecordText(id: string, raw: string): TSessionLoadOutcome {\n const outcome = decodeStoredSessionText(raw);\n if (outcome.status !== 'valid' || outcome.record.id === id) return outcome;\n return { status: 'corrupt', issues: [{ path: 'id', message: OTHER_ID_MESSAGE }] };\n}\n\n/** Whether plain `bytes` hold a valid record of session `id`: what a migration may offer to mark as the owner's. */\nexport function isPlainSessionRecordOf(id: string, bytes: Uint8Array): boolean {\n return decodeRecordText(id, utf8Text(bytes)).status === 'valid';\n}\n\n/** What a store found at a record's path before it writes over it. */\nexport type TStoredSessionFile =\n /** Nothing is there: \"no such file\" and nothing else. */\n | { readonly kind: 'absent' }\n /**\n * Something is there and it is not sealed framing; its bytes when the store read them whole (a store\n * that seals does, so it can tell whether they are exactly what it holds).\n */\n | { readonly kind: 'plain'; readonly bytes?: Uint8Array }\n /** A sealed file, read whole. */\n | { readonly kind: 'sealed'; readonly bytes: Uint8Array }\n /** Something is there and it could not be read: it may be sealed, so it is protected like one. */\n | { readonly kind: 'unreadable' }\n /** Something is there, too large for this store to read back at all. */\n | { readonly kind: 'oversized' };\n\n/** What `bytes` (or their absence) are, for a store that read the whole file. */\nexport function classifyStoredSessionBytes(bytes: Uint8Array | undefined): TStoredSessionFile {\n if (bytes === undefined) return { kind: 'absent' };\n return isSealedArtifactBytes(bytes) ? { kind: 'sealed', bytes } : { kind: 'plain', bytes };\n}\n\n/** The bytes a file was read with, when it was read whole. */\nexport function bytesOf(file: TStoredSessionFile): Uint8Array | undefined {\n return file.kind === 'sealed' || file.kind === 'plain' ? file.bytes : undefined;\n}\n","/**\n * What one store instance that seals knows of each session id, and the token a held record crosses\n * to another process as.\n *\n * Locking drops the ability to READ, not to keep saving what a process is already running: sealing\n * needs only the public key, and the process holds its session in memory. So a store remembers, per\n * id, the digest of the sealed bytes it last wrote or was asked to hold, the plaintext envelope they\n * hold, and whose they are — the digest proves a file is exactly what this instance put there or\n * read, and the plaintext lets a locked process go on updating a session it could not read back. A\n * fork record written for another process is remembered apart, so a fork never pushes a running\n * session out. Beside them it keeps which ids it handed unverified content out for (never tagged\n * again until the owner adopts them) and which owner bytes wait for a tag. Memory only, never\n * persisted, and a fresh instance (a restart)\n * knows nothing; what it remembers is bounded except for the records a running session needs.\n */\n\nimport { Buffer } from 'node:buffer';\n\nimport { isSha256Hex } from './stored-session-codec.js';\n\n/**\n * Whose a store holds remembered bytes to be. `owner`: bytes it wrote from the owner's content, bytes\n * it opened whose authorship tag verifies, or bytes the owner adopted. `pinned`: a record a write-only\n * child was handed (`importHeld`) and its own later writes of it, judged by exact bytes only, since it\n * holds no keys. `unverified`: anything else.\n */\nexport type THeldRecordKind = 'owner' | 'pinned' | 'unverified';\n\n/** Stored bytes a store remembers for one id: their digest, the envelope they hold, and whose they are. */\nexport interface IRememberedRecord {\n readonly digest: string;\n readonly text: string;\n readonly kind: THeldRecordKind;\n /**\n * A `pinned` record the process that handed it over held as the owner's. That process holds the\n * keys and started this one, so its word is what a write-only child has for whose the record is.\n */\n readonly vouched?: true;\n}\n\n/** Everything one store instance knows of one id. */\nexport interface ISessionRecordState {\n /** The bytes it last wrote or read to hold, for a session it runs or may go on saving. */\n readonly held?: IRememberedRecord;\n /** A record a handoff save left for another process to run, until it is handed over or taken. */\n readonly handoff?: IRememberedRecord;\n /** A read handed a writer content of the id that nobody vouched for while nothing here ran it. */\n readonly tainted: boolean;\n /** Owner bytes the locked keys could not tag: tagged at the next unlock if still in place. */\n readonly pendingDigest?: string;\n}\n\n/** More sessions than one process runs at once; the oldest is forgotten past this, which only means it is refused while locked. */\nconst MAX_HELD_RECORDS = 32;\n/**\n * Fork records written for a handoff and not yet handed over. A fork job is spawned right after its\n * record is written, so a few suffice; the oldest goes first, and its job then needs a fresh fork.\n */\nconst MAX_HANDOFF_RECORDS = 8;\n\ntype TMutableState = { -readonly [K in keyof ISessionRecordState]: ISessionRecordState[K] };\n\n/** The per-id state of one store that seals; see the module documentation. */\nexport class SealedSessionMemory {\n readonly #states = new Map<string, TMutableState>();\n /** Held ids, oldest first, for the bound. */\n readonly #heldOrder = new Set<string>();\n readonly #handoffOrder = new Set<string>();\n readonly #keepsHeld: (id: string) => boolean;\n\n /** @param keepsHeld whether an id's held record must outlast the bound: a session here runs it. */\n constructor(keepsHeld: (id: string) => boolean = () => false) {\n this.#keepsHeld = keepsHeld;\n }\n\n /** How many held records it remembers now. */\n get size(): number {\n return this.#heldOrder.size;\n }\n\n state(id: string): ISessionRecordState | undefined {\n return this.#states.get(id);\n }\n\n /** Remembers `record` as what it holds for `id`, the newest; the oldest it may forget goes past the bound. */\n hold(id: string, record: IRememberedRecord): void {\n this.#edit(id).held = record;\n this.#heldOrder.delete(id);\n this.#heldOrder.add(id);\n for (const oldest of this.#heldOrder) {\n if (this.#heldOrder.size <= MAX_HELD_RECORDS) break;\n if (this.#states.get(oldest)?.held?.kind === 'pinned' || this.#keepsHeld(oldest)) continue;\n this.dropHeld(oldest);\n }\n }\n\n /** Remembers `record` as the record a handoff save left for `id`. */\n holdHandoff(id: string, record: IRememberedRecord): void {\n this.#edit(id).handoff = record;\n this.#handoffOrder.delete(id);\n this.#handoffOrder.add(id);\n for (const oldest of this.#handoffOrder) {\n if (this.#handoffOrder.size <= MAX_HANDOFF_RECORDS) break;\n this.dropHandoff(oldest);\n }\n }\n\n dropHeld(id: string): void {\n this.#heldOrder.delete(id);\n this.#settle(id, (state) => delete state.held);\n }\n\n dropHandoff(id: string): void {\n this.#handoffOrder.delete(id);\n this.#settle(id, (state) => delete state.handoff);\n }\n\n taint(id: string): void {\n this.#edit(id).tainted = true;\n }\n\n untaint(id: string): void {\n this.#settle(id, (state) => (state.tainted = false));\n }\n\n setPending(id: string, digest: string): void {\n this.#edit(id).pendingDigest = digest;\n }\n\n clearPending(id: string): void {\n this.#settle(id, (state) => delete state.pendingDigest);\n }\n\n /** Every id with owner bytes that wait for a tag, and those bytes' digest. */\n pending(): readonly (readonly [string, string])[] {\n const pending: [string, string][] = [];\n for (const [id, state] of this.#states) {\n if (state.pendingDigest !== undefined) pending.push([id, state.pendingDigest]);\n }\n return pending;\n }\n\n /** Forgets the id's records and pending tag: the session is deleted. A taint outlives it. */\n forget(id: string): void {\n this.#heldOrder.delete(id);\n this.#handoffOrder.delete(id);\n this.#settle(id, (state) => {\n delete state.held;\n delete state.handoff;\n delete state.pendingDigest;\n });\n }\n\n #edit(id: string): TMutableState {\n let state = this.#states.get(id);\n if (state === undefined) {\n state = { tainted: false };\n this.#states.set(id, state);\n }\n return state;\n }\n\n /** Applies `change` to the id's state, and drops a state that is left knowing nothing. */\n #settle(id: string, change: (state: TMutableState) => unknown): void {\n const state = this.#states.get(id);\n if (state === undefined) return;\n change(state);\n if (\n state.held === undefined &&\n state.handoff === undefined &&\n state.pendingDigest === undefined &&\n !state.tainted\n ) {\n this.#states.delete(id);\n }\n }\n}\n\n/**\n * A held record as it crosses to another process: `rsh2.`, whose the record was to the store that\n * handed it over (`owner` or `unverified`), the SHA-256 of the sealed bytes it came from, and the\n * canonical unpadded base64url of the plaintext envelope. The digest is what the receiving store\n * compares the file with before it trusts the record over it; whose it was is the only word a\n * receiver without keys has for it.\n */\nconst HELD_TOKEN_PREFIX = 'rsh2.';\n/** Whose the handed record was to the store that handed it over, as the token says it. */\nconst HELD_TOKEN_KINDS = { owner: true, unverified: false } as const;\nconst BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true });\n\nexport function formatHeldToken(digest: string, text: string, owner: boolean): string {\n const kind = owner ? 'owner' : 'unverified';\n return `${HELD_TOKEN_PREFIX}${kind}.${digest}.${Buffer.from(text, 'utf8').toString('base64url')}`;\n}\n\n/**\n * The digest and envelope a token names, and whether the store that issued it held the record as the\n * owner's; `undefined` for anything this build does not issue.\n */\nexport function parseHeldToken(\n token: unknown,\n): { readonly digest: string; readonly text: string; readonly owner: boolean } | undefined {\n if (typeof token !== 'string' || !token.startsWith(HELD_TOKEN_PREFIX)) return undefined;\n const [kind, digest, encoded, ...extra] = token.slice(HELD_TOKEN_PREFIX.length).split('.');\n if (kind !== 'owner' && kind !== 'unverified') return undefined;\n if (extra.length > 0 || digest === undefined || encoded === undefined) return undefined;\n if (!isSha256Hex(digest) || !BASE64URL_PATTERN.test(encoded)) return undefined;\n const bytes = Buffer.from(encoded, 'base64url');\n if (bytes.toString('base64url') !== encoded) return undefined;\n try {\n return { digest, text: STRICT_UTF8.decode(bytes), owner: HELD_TOKEN_KINDS[kind] };\n } catch {\n return undefined;\n }\n}\n","/**\n * Where a sealed session record carries its authorship tag (#3679 step 3e): a byte header in front of\n * the exact envelope bytes, inside the sealed plaintext —\n *\n * robota-authorship/v1 <canonical unpadded base64url of the 32-byte tag>\\n<envelope bytes>\n *\n * The header is authenticated with the record, since it is sealed with it, and the envelope is never\n * re-serialised, so a change to how the codec formats an envelope never invalidates an earlier tag.\n * Plain records never carry it: a plain file that begins with it is just a plain file. A token handed\n * to another process carries the envelope only.\n */\n\nimport { Buffer } from 'node:buffer';\n\n/** Versioned: a later framing is a new prefix, which this build reads as no tag at all. */\nexport const AUTHORSHIP_HEADER_PREFIX = 'robota-authorship/v1 ';\n\nconst PREFIX_BYTES = Buffer.from(AUTHORSHIP_HEADER_PREFIX, 'ascii');\nconst NEWLINE = 0x0a;\nconst TAG_BYTES = 32;\nconst BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/** `envelope` with `tag` in front of it, as a tagged record's sealed plaintext. */\nexport function withAuthorshipHeader(tag: Uint8Array, envelope: Uint8Array): Buffer {\n const encoded = Buffer.from(tag.buffer, tag.byteOffset, tag.byteLength).toString('base64url');\n return Buffer.concat([Buffer.from(`${AUTHORSHIP_HEADER_PREFIX}${encoded}\\n`, 'ascii'), envelope]);\n}\n\n/** A record's sealed plaintext split into its envelope and the tag in front of it, if any. */\nexport interface ISplitRecordPlaintext {\n readonly envelope: Uint8Array;\n /** The tag the header carries; `malformed` for a header that is not one this build writes. */\n readonly tag: Uint8Array | 'malformed' | undefined;\n}\n\n/**\n * Splits a sealed record's plaintext. Without the header the whole plaintext is the envelope, an\n * untagged record. A header whose tag is not canonical base64url of 32 bytes is `malformed`, which a\n * reader treats as a tag that does not verify; one with no line end leaves the plaintext whole, which\n * then does not decode as a record.\n */\nexport function splitAuthorshipHeader(plain: Uint8Array): ISplitRecordPlaintext {\n const bytes = Buffer.from(plain.buffer, plain.byteOffset, plain.byteLength);\n if (\n bytes.length < PREFIX_BYTES.length ||\n !bytes.subarray(0, PREFIX_BYTES.length).equals(PREFIX_BYTES)\n ) {\n return { envelope: plain, tag: undefined };\n }\n const end = bytes.indexOf(NEWLINE, PREFIX_BYTES.length);\n if (end < 0) return { envelope: plain, tag: 'malformed' };\n const encoded = bytes.subarray(PREFIX_BYTES.length, end).toString('latin1');\n const envelope = plain.subarray(end + 1);\n if (!BASE64URL_PATTERN.test(encoded)) return { envelope, tag: 'malformed' };\n const tag = Buffer.from(encoded, 'base64url');\n if (tag.length !== TAG_BYTES || tag.toString('base64url') !== encoded) {\n return { envelope, tag: 'malformed' };\n }\n return { envelope, tag };\n}\n","/**\n * Where a store that seals decides whose each record is, and keeps it so (#3679).\n *\n * A file the same OS user can write is never a trust anchor. A sealed file that opens says it is\n * bound to this session's id and unaltered since it was sealed; it says nothing about who sealed it,\n * since anyone with the public key can. So whose a record is is decided when its bytes are opened —\n * an authorship tag only unlocked keys make (`session-record-authorship.ts`), bytes this instance\n * wrote from the owner's content, or bytes the owner adopted by their digest — and every `valid`\n * outcome says which. The ids a session in this process runs are claimed, and a claimed id is\n * answered only from content the owner wrote: unverified content from disk is never handed to it, so\n * nothing unverified can be merged into it, and a file something unverifiable changed is never\n * overwritten by it either. A save tags only content the store can vouch for; an owner save the\n * locked keys could not tag is tagged at the next unlock, only while this store holds the id's writer\n * marker or can take it, so a process the id is lent to or another writer keeps the bytes it holds.\n * Adopting or tagging in place never replaces bytes another writer put there since.\n */\n\nimport { Buffer } from 'node:buffer';\n\nimport {\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isSealedArtifactBytes,\n} from '@robota-sdk/agent-core';\n\nimport { splitAuthorshipHeader, withAuthorshipHeader } from './session-record-authorship.js';\nimport { SealedSessionUnreadableError } from './sealed-session-refusal.js';\nimport { formatHeldToken, parseHeldToken } from './sealed-session-memory.js';\nimport {\n DOES_NOT_OPEN_MESSAGE,\n bytesOf,\n decodeRecordText,\n digestOf,\n isSha256Hex,\n recordContext,\n sameBytes,\n utf8Text,\n} from './stored-session-codec.js';\n\nimport type { SessionClaims } from './session-claims.js';\nimport type {\n IRememberedRecord,\n ISessionRecordState,\n SealedSessionMemory,\n THeldRecordKind,\n} from './sealed-session-memory.js';\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\nimport type {\n ISessionExportOptions,\n ISessionSaveOptions,\n TSessionAdoptResult,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/** Every read a store makes of a record, and the check before a save. */\nexport type TReadPurpose =\n /** A load that only looks: a report, an existence check, an attach. */\n | 'probe'\n /** A load that holds: a resume or a writer about to save. */\n | 'hold'\n /** One entry of a listing; no writer merges it. */\n | 'list'\n /** The held copy a writer asks for once `load` says `locked`. */\n | 'held'\n /** A record handed to another process as a token. */\n | 'export'\n /** The read a claim makes, which the session restores from. */\n | 'claim'\n /** The check before a save replaces the file. */\n | 'save';\n\n/** What a read of each purpose may do. */\ninterface IReadPurpose {\n /** It feeds a writer, so a claimed id is never answered with content it cannot vouch for. */\n readonly writer: boolean;\n /** It takes a handoff record into the memory of the sessions the store runs. */\n readonly takes: boolean;\n /** It remembers what it opened. */\n readonly holds: boolean;\n /** Unverified content it hands out taints the id: a writer could merge from it. */\n readonly taints: boolean;\n}\n\nconst READ_PURPOSES: Readonly<Record<TReadPurpose, IReadPurpose>> = {\n probe: { writer: false, takes: false, holds: false, taints: true },\n hold: { writer: true, takes: true, holds: true, taints: true },\n list: { writer: false, takes: false, holds: false, taints: false },\n held: { writer: true, takes: true, holds: false, taints: true },\n export: { writer: true, takes: false, holds: false, taints: true },\n claim: { writer: true, takes: true, holds: true, taints: true },\n save: { writer: true, takes: false, holds: false, taints: false },\n};\n\n/** How the store reads and replaces a record's file; see `IStoredSessionFiles`. */\nexport interface IAuthorshipFiles {\n current(id: string): TStoredSessionFile;\n replace(id: string, stored: Uint8Array): void;\n}\n\n/** How a write of an id that carries no claim holds the id's writer marker (`StoredSessionRecords`). */\nexport type TUnderWriteMarker = <T>(id: string, write: () => T) => T | 'has_writer';\n\n/** The bytes a save writes, and what the store holds them as once written. */\nexport interface IPreparedSave {\n readonly stored: Uint8Array;\n readonly text: string;\n readonly heldAs: THeldRecordKind;\n /** An owner write the locked keys could not tag, to be tagged at the next unlock if still in place. */\n readonly tagPending: boolean;\n}\n\n/** The envelope a sealed file holds and whether its authorship tag verifies, or why this process cannot get it. */\ntype TOpened =\n | { readonly kind: 'text'; readonly text: string; readonly verified: boolean }\n | { readonly kind: 'locked' }\n | { readonly kind: 'no-keys' }\n | { readonly kind: 'does-not-open' };\n\n/** A sealed file a process without keys meets: intact, and no key in this process opens it. */\nexport const NO_KEYS_OUTCOME: TSessionLoadOutcome = Object.freeze({\n status: 'locked',\n reason: 'no-keys',\n});\n/** A session this instance holds whose file is no longer the bytes it wrote or read. */\nconst CHANGED_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'locked', reason: 'changed' });\n/** A sealed file the locked keys cannot open: whether it is the owner's is decided at unlock. */\nconst LOCKED_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'locked' });\nexport const MISSING_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'missing' });\n\ntype TValid = Extract<TSessionLoadOutcome, { status: 'valid' }>;\n\n/** `outcome` with whose it is, which bytes it came from, and whether it is a held copy. */\nfunction annotated(\n outcome: TValid,\n verified: boolean,\n digest: string | undefined,\n diverged: boolean,\n): TSessionLoadOutcome {\n return {\n status: 'valid',\n record: outcome.record,\n authorship: verified ? 'verified' : 'unverified',\n ...(digest === undefined ? {} : { digest }),\n ...(diverged ? { diverged: true as const } : {}),\n };\n}\n\n/** Opens a current-framing sealed file, wiping the plaintext buffer once it is turned into text. */\nfunction openSealed(sealer: IArtifactSealer, id: string, stored: Uint8Array): TOpened {\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, recordContext(id));\n } catch (error) {\n // A write-only sealer (a process handed only a write grant) holds no keys.\n if (isArtifactLockedError(error)) {\n return (error as { readonly reason?: unknown }).reason === 'no-keys'\n ? { kind: 'no-keys' }\n : { kind: 'locked' };\n }\n // Any other failure of the sealer is no evidence the file is fine, and it must neither take a\n // listing down nor read as an absent file: it is reported as a file that does not open.\n return { kind: 'does-not-open' };\n }\n try {\n // The one place a record's authorship header is taken off: what is held, decoded or handed\n // over from here on is the envelope alone.\n const { envelope, tag } = splitAuthorshipHeader(plain);\n let verified = false;\n if (tag instanceof Uint8Array && sealer.verifyAuthorship !== undefined) {\n try {\n verified = sealer.verifyAuthorship(envelope, tag, recordContext(id)) === true;\n } catch {\n // allow-fallback: a check that cannot run shows nothing, and nothing shown is unverified.\n verified = false;\n }\n }\n return { kind: 'text', text: utf8Text(envelope), verified };\n } finally {\n plain.fill(0);\n }\n}\n\n/**\n * What the bytes of session `id` say to a store that seals with `sealer` and holds nothing yet: the\n * outcome, owner's only when the tag verifies. Stateless; a migration reads with it.\n */\nexport function openStoredSessionRecord(\n id: string,\n stored: Uint8Array,\n sealer: IArtifactSealer,\n): TSessionLoadOutcome {\n let text: string;\n let verified = false;\n if (!isSealedArtifactBytes(stored)) {\n text = utf8Text(stored);\n } else {\n if (!isCurrentSealedArtifactBytes(stored))\n return { status: 'unsupported', schemaVersion: undefined };\n const opened = openSealed(sealer, id, stored);\n if (opened.kind === 'locked') return LOCKED_OUTCOME;\n if (opened.kind === 'no-keys') return NO_KEYS_OUTCOME;\n if (opened.kind === 'does-not-open') {\n return { status: 'corrupt', issues: [{ path: '', message: DOES_NOT_OPEN_MESSAGE }] };\n }\n text = opened.text;\n verified = opened.verified;\n }\n const outcome = decodeRecordText(id, text);\n return outcome.status === 'valid'\n ? annotated(outcome, verified, digestOf(stored), false)\n : outcome;\n}\n\n/** The authorship of one store that seals; see the module documentation. */\nexport class StoredSessionAuthorship {\n readonly #sealer: IArtifactSealer;\n readonly #files: IAuthorshipFiles;\n readonly #memory: SealedSessionMemory;\n readonly #claims: SessionClaims;\n /** Runs a write of an id holding its writer marker; `has_writer` when another writer holds it. */\n readonly #underWriteMarker: TUnderWriteMarker;\n /** Held here so the sealer may hold it weakly: it lives exactly as long as this object. */\n readonly #onUnlock: (() => void) | undefined;\n\n constructor(\n sealer: IArtifactSealer,\n files: IAuthorshipFiles,\n memory: SealedSessionMemory,\n claims: SessionClaims,\n underWriteMarker: TUnderWriteMarker,\n ) {\n this.#sealer = sealer;\n this.#files = files;\n this.#memory = memory;\n this.#claims = claims;\n this.#underWriteMarker = underWriteMarker;\n if (sealer.tagAuthorship !== undefined && sealer.onUnlock !== undefined) {\n this.#onUnlock = () => this.#tagPendingAtUnlock();\n sealer.onUnlock(this.#onUnlock);\n }\n }\n\n /**\n * One read of session `id` for `purpose`. A claimed, handed-off or pinned id is answered only from\n * content the owner wrote (see {@link guarded}), before any rebuild an absent file would otherwise\n * get; any other id is what its bytes say, and a rebuild from a log is never the owner's.\n */\n read(\n id: string,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n rebuild: () => TSessionLoadOutcome,\n purpose: TReadPurpose,\n ): TSessionLoadOutcome {\n const guarded = this.guarded(id, file, purpose);\n if (guarded !== undefined) return guarded;\n if (file.kind === 'absent') {\n const rebuilt = rebuild();\n // A rebuild from a log carries no tag and comes from no single stored file.\n return rebuilt.status === 'valid'\n ? this.#handOut(id, rebuilt, 'unverified', undefined, purpose, false)\n : rebuilt;\n }\n const bytes = bytesOf(file);\n return bytes === undefined ? unreadable : this.decode(id, bytes, purpose);\n }\n\n /** What the stored bytes of session `id` say, for `purpose`; whose they are by tag or by what is held. */\n decode(id: string, stored: Uint8Array, purpose: TReadPurpose): TSessionLoadOutcome {\n let text: string;\n let verified = false;\n if (!isSealedArtifactBytes(stored)) {\n text = utf8Text(stored);\n } else {\n if (!isCurrentSealedArtifactBytes(stored)) {\n return { status: 'unsupported', schemaVersion: undefined };\n }\n const opened = openSealed(this.#sealer, id, stored);\n if (opened.kind === 'locked') return LOCKED_OUTCOME;\n if (opened.kind === 'no-keys') return NO_KEYS_OUTCOME;\n if (opened.kind === 'does-not-open') {\n return { status: 'corrupt', issues: [{ path: '', message: DOES_NOT_OPEN_MESSAGE }] };\n }\n text = opened.text;\n verified = opened.verified;\n }\n const outcome = decodeRecordText(id, text);\n if (outcome.status !== 'valid') return outcome;\n const digest = digestOf(stored);\n const state = this.#memory.state(id);\n const kind = verified || this.#isOwners(state, state?.held, digest) ? 'owner' : 'unverified';\n if (READ_PURPOSES[purpose].holds) this.#memory.hold(id, { digest, text, kind });\n return this.#handOut(id, outcome, kind, digest, purpose, false);\n }\n\n /**\n * The claim's own read: a resumed id this instance already holds exactly the bytes of is answered\n * from them; anything else is read as {@link read} reads a claim.\n */\n claimRead(\n id: string,\n fresh: boolean,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n rebuild: () => TSessionLoadOutcome,\n ): TSessionLoadOutcome {\n const held = this.#memory.state(id)?.held;\n const bytes = bytesOf(file);\n if (!fresh && held !== undefined && bytes !== undefined && digestOf(bytes) === held.digest) {\n return this.#heldOutcome(id, held, 'claim', false);\n }\n return this.read(id, file, unreadable, rebuild, 'claim');\n }\n\n /**\n * Readies `id` for a claim: a record a write-only child holds as handed to it is claimed only by the\n * child's own claim under its lease, so no other session here becomes its second writer; for a new\n * session whatever this instance remembered under its id is not that session's.\n */\n beforeClaim(id: string, fresh: boolean, leased: boolean): void {\n const state = this.#memory.state(id);\n if (state?.held?.kind === 'pinned' && !leased) throw this.#claims.refusal(id, 'has_writer');\n if (fresh) {\n this.#memory.dropHeld(id);\n this.#memory.dropHandoff(id);\n }\n }\n\n /**\n * The answer for an id this store holds for a session it runs — claimed, pinned, or a handoff record\n * not yet handed over — or `undefined` for any other id. Every read of such an id answers the same\n * way, before any rebuild an absent file would otherwise get:\n *\n * - the file is exactly the held bytes, or (claimed only) opens with an authorship tag that\n * verifies, as another of the owner's unlocked processes wrote it: that content;\n * - the file is gone: `missing` when nothing is held yet (a fresh claim), otherwise the held copy —\n * a re-create writes only the owner's content;\n * - anything else — plain, untagged, a tag that does not verify, a sealed file the locked keys cannot\n * open: a writer's read is `locked` (with the reason `changed`, or none when only the locked keys\n * stand in the way and an unlock may show the owner wrote it), so the save is refused and the file\n * left as it is; a probe or listing gets the held copy marked `diverged`, for display only.\n *\n * A handoff record is answered from its copy or as `changed`, and a writer's read takes it over.\n */\n guarded(\n id: string,\n file: TStoredSessionFile,\n purpose: TReadPurpose,\n ): TSessionLoadOutcome | undefined {\n const state = this.#memory.state(id);\n const runHere = this.#isRunHere(id, state);\n const handoff = runHere ? undefined : state?.handoff;\n if (!runHere && handoff === undefined) return undefined;\n const use = READ_PURPOSES[purpose];\n const bytes = bytesOf(file);\n const digest = bytes === undefined ? undefined : digestOf(bytes);\n if (handoff !== undefined) {\n if (use.takes) this.#memory.dropHandoff(id);\n if (bytes === undefined || digest !== handoff.digest) return CHANGED_OUTCOME;\n if (use.takes) this.#memory.hold(id, handoff);\n return this.#heldOutcome(id, handoff, purpose, false);\n }\n const entry = state?.held;\n if (entry !== undefined && digest === entry.digest) {\n return this.#heldOutcome(id, entry, purpose, false);\n }\n let lockedOnly = false;\n if (\n entry?.kind !== 'pinned' &&\n file.kind === 'sealed' &&\n isCurrentSealedArtifactBytes(file.bytes)\n ) {\n const opened = openSealed(this.#sealer, id, file.bytes);\n if (opened.kind === 'text' && opened.verified && digest !== undefined) {\n const outcome = decodeRecordText(id, opened.text);\n if (outcome.status === 'valid') {\n if (purpose !== 'save')\n this.#memory.hold(id, { digest, text: opened.text, kind: 'owner' });\n return annotated(outcome, true, digest, false);\n }\n }\n lockedOnly = opened.kind === 'locked';\n }\n if (file.kind === 'absent') {\n return entry === undefined ? MISSING_OUTCOME : this.#heldOutcome(id, entry, purpose, true);\n }\n if (use.writer || entry === undefined) return lockedOnly ? LOCKED_OUTCOME : CHANGED_OUTCOME;\n return this.#heldOutcome(id, entry, purpose, true);\n }\n\n /**\n * Throws {@link SealedSessionUnreadableError} unless `existing` may be replaced by a save: a run-here\n * id's file when it is exactly what is held, verifies or is gone (see {@link guarded}); any other\n * id's when it is absent, plain, exactly the bytes this instance last wrote or holds, or a sealed\n * file this store opens.\n */\n assertReplaceable(id: string, existing: TStoredSessionFile): void {\n const guarded = this.guarded(id, existing, 'save');\n if (guarded !== undefined) {\n if (guarded.status !== 'locked') return;\n throw new SealedSessionUnreadableError(\n id,\n guarded.reason === 'changed' ? 'changed' : 'locked',\n );\n }\n switch (existing.kind) {\n case 'absent':\n case 'plain':\n return;\n case 'unreadable':\n case 'oversized':\n // Nothing past the read limit can be shown to be anything, and it may be a sealed session.\n throw new SealedSessionUnreadableError(id, 'unreadable');\n case 'sealed':\n break;\n }\n const bytes = existing.bytes;\n if (!isCurrentSealedArtifactBytes(bytes)) {\n throw new SealedSessionUnreadableError(id, 'newer-format');\n }\n // Checked first, since it needs no keys and no decryption.\n const held = this.#memory.state(id)?.held;\n if (held !== undefined && held.digest === digestOf(bytes)) return;\n const opened = openSealed(this.#sealer, id, bytes);\n if (opened.kind === 'text') return;\n throw new SealedSessionUnreadableError(\n id,\n held !== undefined\n ? 'changed'\n : opened.kind === 'locked' || opened.kind === 'no-keys'\n ? opened.kind\n : 'does-not-open',\n );\n }\n\n /**\n * The sealed bytes a save of envelope `text` for `id` writes. Tagged only when the store can vouch\n * for the content and the keys are unlocked: the id is held as the owner's, or it is held as\n * nothing, was never handed out unverified, and the save says its content is the owner's\n * (`provenance`). An owner save while locked is written untagged and tagged at the next unlock if it\n * is still in place and this store can hold the id's marker then — unless it is a handoff save, which\n * another process runs. Every other save is untagged, and a pinned record is never tagged.\n */\n prepare(id: string, text: string, options: ISessionSaveOptions): IPreparedSave {\n const envelope = Buffer.from(text, 'utf8');\n const heldAs = this.#kindOfSave(id, options);\n let plaintext: Buffer = envelope;\n let tagPending = false;\n if (heldAs === 'owner') {\n const tag = this.#tag(id, envelope);\n if (tag === 'locked') {\n tagPending = options.handoff !== true;\n } else if (tag !== undefined) {\n plaintext = withAuthorshipHeader(tag, envelope);\n }\n }\n try {\n return {\n stored: this.#sealer.sealBytes(plaintext, recordContext(id)),\n text,\n heldAs,\n tagPending,\n };\n } finally {\n if (plaintext !== envelope) plaintext.fill(0);\n }\n }\n\n /**\n * Records that `prepared` is now at session `id`'s path, so the next save recognises it as its own.\n * A handoff write is kept apart, for one export: the record is another process's to run.\n */\n wrote(id: string, prepared: IPreparedSave, handoff: boolean): void {\n const prior = this.#memory.state(id)?.held;\n // A write-only child's own saves continue the record it was handed, on the same word.\n const vouched = prepared.heldAs === 'pinned' && prior?.kind === 'pinned' && prior.vouched;\n const record: IRememberedRecord = {\n digest: digestOf(prepared.stored),\n text: prepared.text,\n kind: prepared.heldAs,\n ...(vouched === true ? { vouched } : {}),\n };\n if (handoff && !this.#claims.has(id) && prepared.heldAs !== 'pinned') {\n this.#memory.dropHeld(id);\n this.#memory.holdHandoff(id, record);\n } else {\n this.#memory.dropHandoff(id);\n this.#memory.hold(id, record);\n }\n if (prepared.tagPending) this.#memory.setPending(id, record.digest);\n else this.#memory.clearPending(id);\n }\n\n /** {@link wrote} after a write that threw, only when `existing` is exactly its bytes. */\n wroteIfPresent(\n id: string,\n prepared: IPreparedSave,\n existing: TStoredSessionFile,\n handoff: boolean,\n ): void {\n if (existing.kind === 'sealed' && sameBytes(existing.bytes, prepared.stored)) {\n this.wrote(id, prepared, handoff);\n }\n }\n\n /**\n * The record this instance last wrote or holds, for a writer whose `load` said `locked`: offered only\n * while `existing` is exactly those bytes; `changed` when it holds the id and the file moved on. A\n * run-here id is answered as every read of it is, and a handoff record a writer asks for is taken over.\n */\n held(id: string, existing: TStoredSessionFile): TSessionLoadOutcome | undefined {\n const guarded = this.guarded(id, existing, 'held');\n if (guarded !== undefined) return guarded;\n const entry = this.#memory.state(id)?.held;\n if (entry === undefined) return undefined;\n const bytes = bytesOf(existing);\n if (bytes === undefined || digestOf(bytes) !== entry.digest) return CHANGED_OUTCOME;\n return this.#heldOutcome(id, entry, 'held', false);\n }\n\n /**\n * The record of session `id` as a token for another store over the same files, carrying the\n * envelope only: a handoff record while `existing` is still its bytes (handed over once, or left in\n * place with `keep`), or else — unless `handoffOnly` — the record opened from `existing`, a probe that\n * holds nothing. A run-here id's token is only what the owner wrote. The process a token goes to\n * runs the id under a lease ({@link IInteractiveSessionStore.lend}), whose marker keeps this store\n * from tagging or adopting it meanwhile.\n */\n exportHeld(\n id: string,\n existing: TStoredSessionFile,\n options: ISessionExportOptions,\n ): string | undefined {\n if (existing.kind !== 'sealed' || !isCurrentSealedArtifactBytes(existing.bytes))\n return undefined;\n const digest = digestOf(existing.bytes);\n const handOver = (text: string, kind: THeldRecordKind): string =>\n formatHeldToken(digest, text, kind === 'owner');\n const state = this.#memory.state(id);\n if (this.#isRunHere(id, state)) {\n if (options.handoffOnly === true) return undefined;\n const outcome = this.guarded(id, existing, 'export');\n const entry = this.#memory.state(id)?.held;\n if (outcome?.status !== 'valid' || outcome.diverged === true || entry?.digest !== digest) {\n return undefined;\n }\n return handOver(entry.text, outcome.authorship === 'verified' ? 'owner' : 'unverified');\n }\n const handoff = state?.handoff;\n if (handoff !== undefined) {\n if (options.keep !== true) this.#memory.dropHandoff(id);\n if (handoff.digest !== digest) return undefined;\n this.#handedOut(id, handoff.kind, 'export');\n return handOver(handoff.text, handoff.kind);\n }\n if (options.handoffOnly === true) return undefined;\n const opened = openSealed(this.#sealer, id, existing.bytes);\n if (opened.kind !== 'text' || decodeRecordText(id, opened.text).status !== 'valid') {\n return undefined;\n }\n const kind =\n opened.verified || this.#isOwners(state, state?.held, digest) ? 'owner' : 'unverified';\n this.#handedOut(id, kind, 'export');\n return handOver(opened.text, kind);\n }\n\n /**\n * Holds the record a token carries as pinned: every read of the id answers only from it and this\n * instance's own later writes, and a save may replace exactly the bytes the token names. Throws\n * `TypeError` (fixed text) for a token this build does not issue or one that does not hold a valid\n * record of session `id`, and {@link SessionClaimedError} for an id a session here has claimed.\n */\n importHeld(id: string, token: string): void {\n const parsed = parseHeldToken(token);\n if (parsed === undefined) {\n throw new TypeError('The held record is not a token this build issues.');\n }\n if (decodeRecordText(id, parsed.text).status !== 'valid') {\n throw new TypeError('The held record is not a valid record of this session.');\n }\n this.#claims.assertUnclaimed(id);\n this.#memory.dropHandoff(id);\n this.#memory.hold(id, {\n digest: parsed.digest,\n text: parsed.text,\n kind: 'pinned',\n ...(parsed.owner ? { vouched: true as const } : {}),\n });\n }\n\n /**\n * Raises the record of session `id` whose stored bytes have SHA-256 `digest` to the owner's, and\n * clears the id's taint:\n *\n * 1. This instance holds those bytes (a resume the owner accepted): they are held as the owner's\n * from now on, and tagged in place while the file is still them — at the next unlock when the keys\n * are locked (`pending`). A file that changed since is not touched; a claimed id is answered with\n * the adopted copy and its next save writes it.\n * 2. It holds nothing for them and the file is exactly them (a fork record a child wrote): they are\n * tagged in place, or at the next unlock (`pending`), and nothing is held for them here.\n * 3. Otherwise nothing is raised (`changed`).\n *\n * The caller holds the id's writer marker for it, so nothing is raised under another writer.\n */\n adopt(id: string, digest: string): TSessionAdoptResult {\n if (!isSha256Hex(digest)) return 'changed';\n const state = this.#memory.state(id);\n const file = this.#files.current(id);\n const bytes = bytesOf(file);\n const onDisk = bytes === undefined ? undefined : digestOf(bytes);\n const entry = state?.held;\n if (entry !== undefined && entry.digest === digest && entry.kind !== 'pinned') {\n this.#memory.hold(id, { ...entry, kind: 'owner' });\n this.#memory.untaint(id);\n if (onDisk !== digest) return 'adopted';\n // The bytes are raised in memory whether or not the file is still them when the tag is written.\n return this.#tagInPlace(id, digest, entry.text, true) === 'pending' ? 'pending' : 'adopted';\n }\n if (bytes === undefined || onDisk !== digest || this.#sealer.tagAuthorship === undefined) {\n return 'changed';\n }\n const text = this.#textOfFile(id, file, bytes);\n if (text === 'locked') {\n this.#memory.untaint(id);\n this.#memory.setPending(id, digest);\n return 'pending';\n }\n if (text === undefined || decodeRecordText(id, text).status !== 'valid') return 'changed';\n const result = this.#tagInPlace(id, digest, text, false);\n if (result !== 'changed') this.#memory.untaint(id);\n return result;\n }\n\n /** The digest of the bytes this instance holds for `id`, or `undefined`. */\n heldDigest(id: string): string | undefined {\n return this.#memory.state(id)?.held?.digest;\n }\n\n /** Forgets the id's records and pending tag: the session was deleted. Its taint stays. */\n forget(id: string): void {\n this.#memory.forget(id);\n }\n\n /** Drops only a claim's held plaintext; pending tags, taint and handoffs remain intact. */\n discardHeld(id: string): void {\n this.#memory.dropHeld(id);\n }\n\n /** Subscribes `listener` to the unlocks of this store's keys (held as weakly as the sealer holds it). */\n onUnlock(listener: () => void): () => void {\n return this.#sealer.onUnlock?.(listener) ?? (() => undefined);\n }\n\n /** Whether a session in this process runs `id`: claimed, or held pinned by a write-only child. */\n #isRunHere(id: string, state: ISessionRecordState | undefined): boolean {\n return this.#claims.has(id) || state?.held?.kind === 'pinned';\n }\n\n /**\n * Whether bytes `digest` of an id are the owner's by what this store holds for it: `record` is held\n * as the owner's with those bytes, or those are owner bytes waiting for a tag. The one place that\n * says so; a tag that verifies says so on its own.\n */\n #isOwners(\n state: ISessionRecordState | undefined,\n record: IRememberedRecord | undefined,\n digest: string,\n ): boolean {\n return (\n state?.pendingDigest === digest || (record?.kind === 'owner' && record.digest === digest)\n );\n }\n\n /** The record `entry` holds, as an outcome of a read for `purpose`. */\n #heldOutcome(\n id: string,\n entry: IRememberedRecord,\n purpose: TReadPurpose,\n diverged: boolean,\n ): TSessionLoadOutcome {\n const outcome = decodeRecordText(id, entry.text);\n if (outcome.status !== 'valid') return outcome;\n if (entry.kind === 'pinned') {\n // A write-only child's record is the owner's to it only on the word of the process that\n // handed it over; it is run here, so handing it out taints nothing.\n return annotated(outcome, entry.vouched === true, entry.digest, diverged);\n }\n const kind = this.#isOwners(this.#memory.state(id), entry, entry.digest)\n ? 'owner'\n : 'unverified';\n return this.#handOut(id, outcome, kind, entry.digest, purpose, diverged);\n }\n\n /** `outcome` annotated, and the id tainted when unverified content leaves for a writer of an id not run here. */\n #handOut(\n id: string,\n outcome: TValid,\n kind: THeldRecordKind,\n digest: string | undefined,\n purpose: TReadPurpose,\n diverged: boolean,\n ): TSessionLoadOutcome {\n this.#handedOut(id, kind, purpose);\n return annotated(outcome, kind === 'owner', digest, diverged);\n }\n\n #handedOut(id: string, kind: THeldRecordKind, purpose: TReadPurpose): void {\n if (\n kind !== 'owner' &&\n READ_PURPOSES[purpose].taints &&\n !this.#isRunHere(id, this.#memory.state(id))\n ) {\n this.#memory.taint(id);\n }\n }\n\n /** Whose a save of `id` writes (see {@link prepare}). */\n #kindOfSave(id: string, options: ISessionSaveOptions): THeldRecordKind {\n const state = this.#memory.state(id);\n const entry = state?.held ?? state?.handoff;\n if (entry?.kind === 'pinned') return 'pinned';\n if (options.provenance === 'unverified') return 'unverified';\n // A claimed id's saves come only from the session that claimed it, whose content no read of the\n // id ever mixed with unverified content; any other writer may have merged what a read handed out.\n if (!this.#claims.has(id) && state?.tainted === true) return 'unverified';\n if (entry !== undefined) return entry.kind;\n return options.provenance === 'owner' ? 'owner' : 'unverified';\n }\n\n /** The authorship tag of `envelope` for `id`; `locked` while the keys are; `undefined` when this sealer cannot tag. */\n #tag(id: string, envelope: Uint8Array): Uint8Array | 'locked' | undefined {\n if (this.#sealer.tagAuthorship === undefined) return undefined;\n try {\n return this.#sealer.tagAuthorship(envelope, recordContext(id));\n } catch (error) {\n if (isArtifactLockedError(error)) return 'locked';\n throw error;\n }\n }\n\n /** The envelope a file of `id` holds; `locked` while the keys are; `undefined` when it does not open. */\n #textOfFile(\n id: string,\n file: TStoredSessionFile,\n bytes: Uint8Array,\n ): string | 'locked' | undefined {\n if (file.kind !== 'sealed') return utf8Text(bytes);\n if (!isCurrentSealedArtifactBytes(bytes)) return undefined;\n const opened = openSealed(this.#sealer, id, bytes);\n if (opened.kind === 'locked') return 'locked';\n return opened.kind === 'text' ? opened.text : undefined;\n }\n\n /**\n * Re-seals `text` with a tag over the file of `id`, which held the owner's bytes `digest`; while\n * locked, it is tagged at the next unlock instead (`pending`); `changed` when the file is no longer\n * those bytes by the time the tag is written. With `hold`, this instance holds the new bytes.\n */\n #tagInPlace(id: string, digest: string, text: string, hold: boolean): TSessionAdoptResult {\n const envelope = Buffer.from(text, 'utf8');\n const tag = this.#tag(id, envelope);\n if (tag === 'locked') {\n this.#memory.setPending(id, digest);\n return 'pending';\n }\n if (tag === undefined) return 'adopted';\n return this.#writeTagged(id, envelope, text, tag, digest, hold) ? 'adopted' : 'changed';\n }\n\n /**\n * Writes `text` sealed with `tag` over the file of `id` — only while the file is still the bytes\n * `expected`, read again immediately before the write, so a newer write by another of the owner's\n * processes since the check is never replaced with these older bytes. Returns whether it wrote.\n */\n #writeTagged(\n id: string,\n envelope: Uint8Array,\n text: string,\n tag: Uint8Array,\n expected: string,\n hold: boolean,\n ): boolean {\n const plaintext = withAuthorshipHeader(tag, envelope);\n let stored: Uint8Array;\n try {\n stored = this.#sealer.sealBytes(plaintext, recordContext(id));\n } finally {\n plaintext.fill(0);\n }\n const now = bytesOf(this.#files.current(id));\n if (now === undefined || digestOf(now) !== expected) return false;\n const prepared: IPreparedSave = { stored, text, heldAs: 'owner', tagPending: false };\n try {\n this.#files.replace(id, stored);\n } catch (error) {\n if (hold) this.wroteIfPresent(id, prepared, this.#files.current(id), false);\n throw error;\n }\n this.#memory.clearPending(id);\n if (hold) this.wrote(id, prepared, false);\n return true;\n }\n\n /**\n * At unlock: tags, synchronously, every owner write and deferred adopt that waited for the keys,\n * while its file is still exactly those bytes and this store holds the id's writer marker or can take\n * it, and drops each either way. An id another writer holds — a process it is lent to, another\n * session — is left untagged, since tagging would change the bytes that writer holds and refuse its\n * saves; the record then stays unverified.\n */\n #tagPendingAtUnlock(): void {\n for (const [id, digest] of this.#memory.pending()) {\n this.#memory.clearPending(id);\n try {\n this.#underWriteMarker(id, () => this.#tagPendingOne(id, digest));\n } catch {\n // allow-fallback: one record's failure must not stop the others; its next owner save tags it.\n }\n }\n }\n\n /** {@link #tagPendingAtUnlock} for one id, holding its marker. */\n #tagPendingOne(id: string, digest: string): void {\n const file = this.#files.current(id);\n const bytes = bytesOf(file);\n if (bytes === undefined || digestOf(bytes) !== digest) return;\n const entry = this.#memory.state(id)?.held;\n const hold = entry?.digest === digest;\n const text = hold ? entry.text : this.#textOfFile(id, file, bytes);\n if (text === undefined || text === 'locked') return;\n if (decodeRecordText(id, text).status !== 'valid') return;\n const envelope = Buffer.from(text, 'utf8');\n const tag = this.#tag(id, envelope);\n if (!(tag instanceof Uint8Array)) return;\n this.#writeTagged(id, envelope, text, tag, digest, hold);\n }\n}\n","/**\n * The one place both session stores (the user's directory and the trusted project's) keep their\n * records through, so they cannot disagree about what a file means. A record is stored in one of two\n * forms:\n *\n * - plain: the versioned JSON envelope, or\n * - sealed: the same JSON bytes sealed by an {@link IArtifactSealer}, bound to the record's own id.\n *\n * A store that seals WRITES sealed and reads both, so a directory part-way through a migration lists\n * correctly and a plain record turns sealed the next time its session is saved. A store that seals\n * nothing writes plain and meets a sealed file as `locked`: this process cannot read it, which is a\n * different fact from \"damaged\" and from \"not there\".\n *\n * A save over a sealed file this store cannot open goes ahead only when the file is provably exactly\n * the bytes it last wrote or holds (`sealed-session-memory.ts`); anything else on disk is a session\n * this instance never held, or one that changed since, and is not replaced. Every store claims the\n * ids its sessions run (`session-claims.ts`), exclusively across processes and fencing their saves,\n * and every write of an id holds the id's writer marker, the claim's own or one taken for the write;\n * a store that seals also decides and keeps whose each record is (`stored-session-authorship.ts`).\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\n\nimport { isCurrentSealedArtifactBytes, isSealedArtifactBytes } from '@robota-sdk/agent-core';\n\nimport { SealedSessionUnreadableError } from './sealed-session-refusal.js';\nimport { SealedSessionMemory } from './sealed-session-memory.js';\nimport { SessionClaims, isSessionClaimedError, writerHolderOf } from './session-claims.js';\nimport { assertSafeSessionId } from './session-id.js';\nimport {\n MISSING_OUTCOME,\n NO_KEYS_OUTCOME,\n StoredSessionAuthorship,\n} from './stored-session-authorship.js';\nimport {\n bytesOf,\n decodeRecordText,\n encodeStoredSession,\n envelopeText,\n utf8Text,\n} from './stored-session-codec.js';\n\nimport type { ISessionClaimRefusalWords } from './session-claims.js';\nimport type { TReadPurpose } from './stored-session-authorship.js';\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type {\n IArtifactSealer,\n IProcessMarkerHandle,\n IProcessMarkers,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\nimport type {\n IInteractiveSessionRecord,\n ISessionAuthorship,\n ISessionClaim,\n ISessionClaimOptions,\n ISessionExportOptions,\n ISessionLease,\n ISessionLoadOptions,\n ISessionSaveOptions,\n TSessionAdoptResult,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/**\n * How a session store is built. `sealing` is required: every store states whether it keeps its\n * records sealed or plain, because a store that silently kept them plain beside sealed ones would\n * write plaintext next to them. A host that seals states it for every store it builds.\n * `releaseCommand` is the host's command that releases a writer marker whose holder is not running,\n * for a `has_writer` refusal to name; without it the refusal names none.\n */\nexport interface ISessionStoreOptions extends ISessionClaimRefusalWords {\n readonly sealing: TArtifactSealing;\n}\n\n/** The writer markers of a store's `claims` directory, which every store is built with. */\nfunction assertProcessMarkers(value: unknown): asserts value is IProcessMarkers {\n const markers = value as Partial<IProcessMarkers> | null | undefined;\n if (\n typeof markers?.acquire !== 'function' ||\n typeof markers.takeOver !== 'function' ||\n typeof markers.endLease !== 'function' ||\n typeof markers.inspect !== 'function'\n ) {\n throw new TypeError(\n 'A session store holds a writer marker for every id it writes: build it with the markers of its claims directory.',\n );\n }\n}\n\n/** The sealing `options` state; one that is not a {@link TArtifactSealing} (a call that ignores the types) is refused. */\nexport function sessionSealingOf(options: ISessionStoreOptions): TArtifactSealing {\n if (typeof options !== 'object' || options === null) {\n throw new TypeError('Session store options must be an object, such as { sealing }.');\n }\n assertSessionSealing(options.sealing);\n return options.sealing;\n}\n\n/** Refuses anything that is not a {@link TArtifactSealing}. */\nexport function assertSessionSealing(value: unknown): asserts value is TArtifactSealing {\n if (value === 'plain') return;\n const sealer =\n typeof value === 'object' && value !== null\n ? (value as { readonly sealer?: unknown }).sealer\n : undefined;\n if (\n typeof sealer === 'object' &&\n sealer !== null &&\n typeof (sealer as Partial<IArtifactSealer>).sealBytes === 'function' &&\n typeof (sealer as Partial<IArtifactSealer>).openBytes === 'function'\n ) {\n return;\n }\n throw new TypeError(\"A session store states how it seals: { sealer }, or 'plain'.\");\n}\n\n/**\n * How a store reaches the file of each record — the one thing it gives {@link StoredSessionRecords}.\n * Each method validates the id the way the store addresses its files.\n */\nexport interface IStoredSessionFiles {\n /**\n * What is at session `id`'s path for a load or a listing, read whole, and what such a read reports\n * when it could not read the file whole.\n */\n load(id: string): { readonly file: TStoredSessionFile; readonly unreadable: TSessionLoadOutcome };\n /** What is at session `id`'s path now, read whole (plain files too in a store that seals), before it is replaced. */\n current(id: string): TStoredSessionFile;\n /** Replaces session `id`'s file with `stored`, whole. */\n replace(id: string, stored: Uint8Array): void;\n /** What an absent record is rebuilt as (a project session's replay log); without it, `missing`. */\n rebuild?(id: string): TSessionLoadOutcome;\n}\n\n/**\n * One store's stored form of its records: how it encodes, decodes, guards a replace, claims and\n * remembers what it holds, over the files {@link IStoredSessionFiles} reaches. A store routes every\n * read and write of a record through one of these.\n */\nexport class StoredSessionRecords {\n readonly #sealer: IArtifactSealer | undefined;\n readonly #files: IStoredSessionFiles;\n readonly #markers: IProcessMarkers;\n readonly #claims: SessionClaims;\n readonly #authorship: StoredSessionAuthorship | undefined;\n /** What a store that seals offers as `IInteractiveSessionStore.authorship`; absent from one that never does. */\n readonly authorship: ISessionAuthorship | undefined;\n\n /**\n * @param markers the writer markers of the store's `claims` directory, beside its records: every\n * claim, lend and write of an id holds the id's marker, so a store without them does not exist.\n * @param words how refusals are worded (`ISessionStoreOptions.releaseCommand`).\n */\n constructor(\n sealing: TArtifactSealing,\n files: IStoredSessionFiles,\n markers: IProcessMarkers,\n words: ISessionClaimRefusalWords = {},\n ) {\n assertSessionSealing(sealing);\n assertProcessMarkers(markers);\n this.#sealer = sealing === 'plain' ? undefined : sealing.sealer;\n this.#files = files;\n this.#markers = markers;\n this.#claims = new SessionClaims(\n words.releaseCommand === undefined ? {} : { releaseCommand: words.releaseCommand },\n { pid: process.pid, windows: process.platform === 'win32' },\n );\n if (this.#sealer === undefined) {\n this.#authorship = undefined;\n this.authorship = undefined;\n return;\n }\n const engine = new StoredSessionAuthorship(\n this.#sealer,\n files,\n new SealedSessionMemory((id) => this.#claims.has(id)),\n this.#claims,\n (id, write) => this.#underWriteMarker(id, write),\n );\n this.#authorship = engine;\n this.authorship = Object.freeze({\n adopt: (id: string, digest: string): TSessionAdoptResult => {\n assertSafeSessionId(id);\n // The raise may tag the file in place: a write of the id, so it holds the id's marker.\n const result = this.#underWriteMarker(id, () => engine.adopt(id, digest));\n return result === 'has_writer' ? 'has_writer' : result;\n },\n heldDigest: (id: string) => {\n assertSafeSessionId(id);\n return engine.heldDigest(id);\n },\n onUnlock: (listener: () => void) => engine.onUnlock(listener),\n });\n }\n\n /** Whether this store writes its records sealed. */\n get seals(): boolean {\n return this.#sealer !== undefined;\n }\n\n /**\n * What session `id` holds, saying which of the five things happened. Only a load that asks to `hold`\n * remembers a record it read, so that this instance can go on saving the session after the keys\n * lock; a probe remembers nothing, and so can never push a held session out of the bounded memory\n * or keep plaintext it was only looking at.\n */\n load(id: string, options: ISessionLoadOptions = {}): TSessionLoadOutcome {\n return this.#read(id, options.hold === true ? 'hold' : 'probe');\n }\n\n /** {@link load} for one entry of a listing, which no writer merges. */\n listed(id: string): TSessionLoadOutcome {\n return this.#read(id, 'list');\n }\n\n /**\n * Claims session `id` for a session this process runs and reads it in the same call (see\n * `IInteractiveSessionStore.claim`): the id's writer marker is taken first — published, or taken\n * over under `lease` — and the read follows, so nothing another writer saves can come between them.\n * An in-process runner's claim of a record a handoff save left takes it over; a copy's read takes\n * no marker. Throws `SessionClaimedError`: `has_writer` for an id claimed or lent here or held by\n * another writer, naming it, and `released` for a lease that has ended.\n */\n claim(id: string, options: ISessionClaimOptions = {}): ISessionClaim {\n this.#claims.assertUnclaimed(id);\n const fresh = options.fresh === true;\n this.#authorship?.beforeClaim(id, fresh, options.lease !== undefined);\n const marker = options.copy === true ? undefined : this.#takeMarker(id, options.lease);\n const open = (): ISessionClaim =>\n this.#claims.open(id, fresh, marker, () => {\n const { file, unreadable } = this.#files.load(id);\n return this.#authorship === undefined\n ? this.#readPlain(id, file, unreadable)\n : this.#authorship.claimRead(id, fresh, file, unreadable, () => this.#rebuild(id));\n }, () => this.#authorship?.discardHeld(id));\n if (marker === undefined) return open();\n try {\n return this.#guardWrite(id, marker, open);\n } catch (error) {\n marker.release();\n throw error;\n }\n }\n\n /**\n * Lends session `id` to another process (see `IInteractiveSessionStore.lend`): its marker is\n * published for this process with the digest of a fresh nonce, which the borrowing process claims\n * with, and the id counts as written here until the end, which removes the marker whoever holds it.\n */\n lend(id: string): ISessionLease {\n this.#claims.assertUnclaimed(id);\n const nonce = randomBytes(32).toString('hex');\n const digest = createHash('sha256').update(nonce, 'utf8').digest('hex');\n const published = this.#markers.acquire(id, { leaseDigest: digest });\n if (published.status !== 'acquired') {\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n const unlend = this.#claims.lend(id, () => {\n const marker = this.#markers.inspect(id);\n return marker.status === 'held' ? marker.holder : undefined;\n });\n let ended = false;\n return Object.freeze({\n nonce,\n end: (): void => {\n if (ended) return;\n ended = true;\n try {\n this.#markers.endLease(id, digest);\n } finally {\n unlend();\n }\n },\n });\n }\n\n /**\n * Whether session `id` may have a writer now: a claim or a lend of this store, or the writer marker\n * of any other holder whose owner is not provably gone. A surface that would open the id as another\n * session asks this first.\n */\n hasWriter(id: string): boolean {\n if (this.#claims.has(id) || this.#claims.isLent(id)) return true;\n const marker = this.#markers.inspect(id);\n return marker.status === 'held' && marker.holder.liveness !== 'gone';\n }\n\n /**\n * Saves `record` after the checks a save must pass: a claimed id is saved only under its live claim,\n * and a claim that ended or lost its marker is refused (the write fence); a save without a claim\n * takes the id's marker for the write and is refused while another writer holds it. Then the file\n * there must be replaceable — absent, plain, exactly what this instance last wrote or holds, or\n * sealed and openable here (a claimed id's only when it is what the owner wrote). What the check\n * stops is a store replacing a sealed session nobody in this process can read. A write that fails\n * with its bytes already in place is remembered as this store's own.\n */\n save(record: IInteractiveSessionRecord, options: ISessionSaveOptions = {}): void {\n const id = record.id;\n const marker = this.#claims.markerForSave(id, options.claim);\n if (marker !== undefined) {\n this.#guardWrite(id, marker, () => this.#write(record, options));\n return;\n }\n this.#withTransientMarker(id, () => this.#write(record, options));\n }\n\n /** Removes session `id`'s record with `remove`, holding the id's marker; refused while another writer holds it. */\n delete(id: string, remove: () => void): void {\n this.#claims.markerForSave(id, undefined);\n this.#withTransientMarker(id, () => {\n this.forget(id);\n remove();\n });\n }\n\n #write(record: IInteractiveSessionRecord, options: ISessionSaveOptions): void {\n const id = record.id;\n const existing = this.#files.current(id);\n const engine = this.#authorship;\n if (engine === undefined) {\n this.#assertPlainReplaceable(id, existing);\n this.#files.replace(id, encodeStoredSession(record, undefined).stored);\n return;\n }\n engine.assertReplaceable(id, existing);\n const prepared = engine.prepare(id, envelopeText(record), options);\n const handoff = options.handoff === true;\n try {\n this.#files.replace(id, prepared.stored);\n } catch (error) {\n // A step after the rename (the directory flush) can fail with the new bytes already in place;\n // they are this store's own, and the next save must know them, or one unlucky write would block\n // this session while the keys are locked. Only bytes provably at the path are remembered.\n engine.wroteIfPresent(id, prepared, this.#currentQuietly(id), handoff);\n throw error;\n }\n engine.wrote(id, prepared, handoff);\n }\n\n /**\n * The record this instance itself last wrote or holds, for a writer whose `load` said `locked`:\n * offered only while the file is exactly the bytes it remembers. `undefined` when it holds nothing\n * for `id` (and always in a store that seals nothing); `locked` with the reason `changed` when it\n * does and the file is no longer those bytes, so the writer can say which of the two stopped it.\n */\n loadHeld(id: string): TSessionLoadOutcome | undefined {\n return this.#authorship?.held(id, this.#files.current(id));\n }\n\n /**\n * The sealed record of session `id` as a token another store over the same files can take with\n * {@link importHeld} (see `StoredSessionAuthorship.exportHeld`); `undefined` from a store that seals\n * nothing, whose records anyone with the store reads from disk.\n */\n exportHeld(id: string, options: ISessionExportOptions = {}): string | undefined {\n return this.#authorship?.exportHeld(id, this.#files.current(id), options);\n }\n\n /**\n * Holds the record a token from {@link exportHeld} carries, pinned, so a store that cannot open the\n * file (a write-only subagent process) can go on with the session. Throws `TypeError` in a store\n * that seals nothing: it would write the record in plaintext over the sealed one.\n */\n importHeld(id: string, token: string): void {\n if (this.#authorship === undefined) {\n throw new TypeError(\n 'A store that seals nothing does not take a held record: it would write it in plaintext.',\n );\n }\n this.#authorship.importHeld(id, token);\n }\n\n /** Forgets what this instance holds of session `id`, which was deleted. */\n forget(id: string): void {\n this.#authorship?.forget(id);\n }\n\n /**\n * The marker a claim of `id` holds: published for this process, or taken over from the process that\n * lent it under `lease`. Throws `has_writer` naming the holder while another writer holds it, and\n * `released` when no marker carries the lease any more.\n */\n #takeMarker(id: string, lease: string | undefined): IProcessMarkerHandle {\n if (lease === undefined) {\n const published = this.#markers.acquire(id);\n if (published.status === 'acquired') return published.handle;\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n const taken = this.#markers.takeOver(id, lease);\n if (taken.status === 'acquired') return taken.handle;\n if (taken.status === 'absent') throw this.#claims.refusal(id, 'released', { lease: true });\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(taken.holder) });\n }\n\n /** Runs `write` holding the id's marker for the write alone, refused while another writer holds it. */\n #withTransientMarker<T>(id: string, write: () => T): T {\n const published = this.#markers.acquire(id);\n if (published.status !== 'acquired') {\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n try {\n return this.#guardWrite(id, published.handle, write);\n } finally {\n published.handle.release();\n }\n }\n\n #guardWrite<T>(id: string, marker: IProcessMarkerHandle, write: () => T): T {\n const result = marker.withGuard(write);\n if (result.status === 'ran') return result.value;\n throw this.#claims.refusal(id, result.status === 'lost' ? 'released' : 'has_writer');\n }\n\n /**\n * Runs a write of `id` that carries no claim (an adopt, a tag at the unlock) holding the id's marker:\n * the live claim's when this store claims the id, else one taken for the write. `has_writer` when\n * another writer holds it, and nothing is written.\n */\n #underWriteMarker<T>(id: string, write: () => T): T | 'has_writer' {\n try {\n const marker = this.#claims.markerForWrite(id);\n if (marker !== undefined) return this.#guardWrite(id, marker, write);\n } catch (error) {\n if (isSessionClaimedError(error)) return 'has_writer';\n throw error;\n }\n const published = this.#markers.acquire(id);\n if (published.status !== 'acquired') return 'has_writer';\n try {\n return this.#guardWrite(id, published.handle, write);\n } catch (error) {\n if (isSessionClaimedError(error)) return 'has_writer';\n throw error;\n } finally {\n published.handle.release();\n }\n }\n\n #read(id: string, purpose: TReadPurpose): TSessionLoadOutcome {\n const { file, unreadable } = this.#files.load(id);\n return this.#authorship === undefined\n ? this.#readPlain(id, file, unreadable)\n : this.#authorship.read(id, file, unreadable, () => this.#rebuild(id), purpose);\n }\n\n /** A read by a store that seals nothing: what the bytes say, and a sealed file is `locked` with no keys. */\n #readPlain(\n id: string,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n ): TSessionLoadOutcome {\n if (file.kind === 'absent') return this.#rebuild(id);\n const bytes = bytesOf(file);\n if (bytes === undefined) return unreadable;\n if (!isSealedArtifactBytes(bytes)) return decodeRecordText(id, utf8Text(bytes));\n return isCurrentSealedArtifactBytes(bytes)\n ? NO_KEYS_OUTCOME\n : { status: 'unsupported', schemaVersion: undefined };\n }\n\n /**\n * A store that seals nothing replaces absence, a plain file and one past its read limit; a sealed\n * file is a session no key in this process opens, and an unreadable one may be one.\n */\n #assertPlainReplaceable(id: string, existing: TStoredSessionFile): void {\n switch (existing.kind) {\n case 'absent':\n case 'plain':\n case 'oversized':\n return;\n case 'unreadable':\n throw new SealedSessionUnreadableError(id, 'unreadable');\n case 'sealed':\n throw new SealedSessionUnreadableError(\n id,\n isCurrentSealedArtifactBytes(existing.bytes) ? 'no-keys' : 'newer-format',\n );\n }\n }\n\n #rebuild(id: string): TSessionLoadOutcome {\n return this.#files.rebuild?.(id) ?? MISSING_OUTCOME;\n }\n\n /** What is at the path after a failed write, which may have left it in any state: a read that fails proves nothing was written. */\n #currentQuietly(id: string): TStoredSessionFile {\n try {\n return this.#files.current(id);\n } catch {\n // allow-fallback: an unreadable path proves nothing was written, so nothing is remembered.\n return { kind: 'unreadable' };\n }\n }\n}\n","/**\n * NodeSessionStore — persists conversation sessions as JSON files.\n *\n * The caller explicitly supplies a host-owned base directory.\n * This adapter does not interpret that directory as a trusted project root.\n * The store directory is created on first write if it does not exist.\n *\n * Sealed records or plain ones, as the caller states when it builds the store (see\n * `stored-session-record.ts`); plain files are the JSON they always were. Only a store that seals\n * offers `authorship`, and says whose each record it reads is. The writer markers of its claims and\n * lends are kept in `claims/` beside the records, so every process over these files sees them.\n */\n\nimport { createProcessOperationLock } from '@robota-sdk/agent-node-lock';\n\nimport {\n closeSync,\n existsSync,\n fstatSync,\n openSync,\n readFileSync,\n readSync,\n readdirSync,\n unlinkSync,\n} from 'fs';\nimport { join, resolve, sep } from 'path';\n\nimport { SEALED_ARTIFACT_MAGIC, isSealedArtifactBytes } from '@robota-sdk/agent-core';\nimport {\n createProcessMarkers,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n writeOwnerOnlyFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { assertSafeSessionId, isSafeSessionId } from './session-id.js';\nimport { classifyStoredSessionBytes } from './stored-session-codec.js';\nimport { StoredSessionRecords, sessionSealingOf } from './stored-session-record.js';\n\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\nimport type {\n IInteractiveSessionRecord,\n IInteractiveSessionStore,\n ISessionAuthorship,\n ISessionClaim,\n ISessionClaimOptions,\n ISessionExportOptions,\n ISessionLease,\n ISessionListEntry,\n ISessionLoadOptions,\n ISessionSaveOptions,\n ISessionStoreScan,\n TSessionLoadOutcome,\n TSessionRestoreCommitOutcome,\n} from '@robota-sdk/agent-interface-session';\n\nfunction errnoCode(error: unknown): unknown {\n return typeof error === 'object' && error !== null\n ? (error as { readonly code?: unknown }).code\n : undefined;\n}\n\n/** What a load reports for a record it could not read: fixed text, and the errno code when there is one. */\nfunction unreadableMessage(error: unknown): string {\n const code = errnoCode(error);\n return typeof code === 'string' && /^E[A-Z0-9]+$/.test(code)\n ? `could not read the session file (${code})`\n : 'could not read the session file';\n}\n\n/**\n * The rest of a file whose first bytes `head` holds, read through the same descriptor, so the bytes\n * judged are the bytes of one file. Reads to the size the descriptor reported: a file that grew since\n * is then not the bytes any store remembers, which fails closed.\n */\nfunction readRest(descriptor: number, head: Buffer): Buffer {\n const bytes = Buffer.alloc(Math.max(fstatSync(descriptor).size, head.length));\n head.copy(bytes, 0);\n let offset = head.length;\n while (offset < bytes.length) {\n const read = readSync(descriptor, bytes, offset, bytes.length - offset, offset);\n if (read === 0) break;\n offset += read;\n }\n return bytes.subarray(0, offset);\n}\n\nconst MISSING: TSessionLoadOutcome = Object.freeze({ status: 'missing' });\n\n/**\n * Where a store keeps the writer markers of its claims and lends: a directory beside the records that\n * is not a `.json` file, so a listing never reads it as a session.\n */\nexport const SESSION_CLAIMS_DIRECTORY = 'claims';\n\n/**\n * Most recent first, and an unreadable entry sorts last rather than being dropped.\n *\n * An entry with no record has no `updatedAt` to sort by; giving it the epoch keeps it present and\n * out of the way, which is the whole point of listing it at all.\n */\n/** How many of `entries` are `locked`, by why: the keys are locked, or this store holds none. */\nfunction countLeftOut(entries: readonly ISessionListEntry[]): Omit<ISessionStoreScan, 'entries'> {\n let locked = 0;\n let noKeys = 0;\n for (const { outcome } of entries) {\n if (outcome.status !== 'locked') continue;\n if (outcome.reason === 'no-keys') noKeys += 1;\n else locked += 1;\n }\n return noKeys === 0 ? { locked } : { locked, noKeys };\n}\n\nfunction compareListEntriesByRecency(left: ISessionListEntry, right: ISessionListEntry): number {\n const at = (entry: ISessionListEntry): number =>\n entry.outcome.status === 'valid' ? new Date(entry.outcome.record.updatedAt).getTime() : 0;\n return at(right) - at(left);\n}\n\n/** How a {@link NodeSessionStore} is built. */\nexport interface INodeSessionStoreOptions extends ISessionStoreOptions {\n /**\n * An ancestor of the store's directory the HOST also owns, tightened along with it (SEC-020).\n * Optional because this adapter does not interpret its base directory as a trusted root and must\n * not guess that a parent belongs to the product — which of them do is composition's knowledge.\n * Left out, a store root that already exists keeps whatever mode it was given: the leaf is made\n * 0700 and the directory above it is not touched.\n */\n readonly ownedRoot?: string;\n}\n\n/**\n * Persistent session store backed by individual JSON files.\n *\n * Construct with a host-owned `baseDir`; framework project composition uses a separate\n * authority-backed adapter over the same neutral port.\n */\nexport class NodeSessionStore implements IInteractiveSessionStore {\n readonly supportsClaimDiscard = true as const;\n private readonly baseDir: string;\n private readonly ownedRoot: string | undefined;\n /** How records are stored, and what this instance claims, wrote or holds. */\n private readonly records: StoredSessionRecords;\n /** What only a store that seals offers (`IInteractiveSessionStore.authorship`); absent from one that never does. */\n declare readonly authorship?: ISessionAuthorship;\n\n /**\n * @param baseDir the directory holding the records.\n * @param options `sealing` (required): `{ sealer }` seals what `save` writes and opens what `load`\n * reads; `'plain'` writes plain JSON and reports a sealed file it finds as `locked`.\n * `ownedRoot`: see {@link INodeSessionStoreOptions}. `releaseCommand`: see `ISessionStoreOptions`.\n */\n constructor(baseDir: string, options: INodeSessionStoreOptions) {\n const sealing = sessionSealingOf(options);\n this.records = new StoredSessionRecords(\n sealing,\n {\n load: (id) => this.loadFile(id),\n current: (id) => this.readExisting(this.filePath(id)),\n replace: (id, stored) => {\n this.ensureDir();\n writeOwnerOnlyFile(this.filePath(id), stored);\n },\n },\n createProcessMarkers(join(resolve(baseDir), SESSION_CLAIMS_DIRECTORY), {\n operationLock: createProcessOperationLock(join(resolve(baseDir), SESSION_CLAIMS_DIRECTORY)),\n }),\n options.releaseCommand === undefined ? {} : { releaseCommand: options.releaseCommand },\n );\n this.baseDir = baseDir;\n this.ownedRoot = options.ownedRoot;\n // Only on a store that seals: a store whose records carry no authorship has nothing to adopt.\n const authorship = this.records.authorship;\n if (authorship !== undefined) this.authorship = authorship;\n }\n\n /**\n * Claims `id` for a session this process runs, reading it in the same call\n * (`IInteractiveSessionStore.claim`): exclusive across processes and store instances, and every\n * later save of the id must carry the claim until it is released.\n */\n claim(id: string, options?: ISessionClaimOptions): ISessionClaim {\n this.filePath(id);\n return this.records.claim(id, options);\n }\n\n /** Lends `id` to another process that will write it (`IInteractiveSessionStore.lend`). */\n lend(id: string): ISessionLease {\n this.filePath(id);\n return this.records.lend(id);\n }\n\n /** Whether session `id` may have a writer now (`IInteractiveSessionStore.hasWriter`). */\n hasWriter(id: string): boolean {\n this.filePath(id);\n return this.records.hasWriter(id);\n }\n\n /**\n * Ensure the storage directory exists AND that only its owner can enter it (SEC-020).\n *\n * The `existsSync` guard this replaces is the whole defect. It skipped the case that matters: a\n * directory some earlier version, a shared CI checkout, or another local user left at a wider\n * mode was adopted as ours with no signal. Measured under umask 022 before this change, a fresh\n * sessions directory came out 0755 and its records 0644 — and a directory pre-created at 0777\n * stayed 0777.\n */\n private ensureDir(): void {\n ensureOwnerOnlyDirectory(\n this.baseDir,\n this.ownedRoot === undefined ? {} : { withinRoot: this.ownedRoot },\n );\n }\n\n /**\n * Absolute path to a session's JSON file.\n *\n * SEC-006: every public method routes through here, so validating the id at this one point covers\n * `save` (write), `load` (read), and `delete` (unlink) at once.\n *\n * Issue #2240: `assertSafeSessionId` is the guard, and it is sound — no separator, no `.`/`..`\n * can pass it. But it is a regex reject behind a helper, which static analysis does not model as\n * a path sanitizer, so `js/path-injection` re-opened on `load` every time this function changed\n * length. The containment check below is the shape such tools DO recognise: the resolved path\n * must stay inside the resolved base directory. It is unreachable after the assertion and costs\n * one `resolve`; it exists so the guard is visible where the sink is, not to replace the guard.\n */\n private filePath(id: string): string {\n assertSafeSessionId(id);\n const base = resolve(this.baseDir);\n const candidate = resolve(base, `${id}.json`);\n if (!candidate.startsWith(base + sep)) {\n throw new Error(\n `Invalid session id: ${JSON.stringify(id)} resolves outside the session store.`,\n );\n }\n return candidate;\n }\n\n /**\n * Persist a session record to disk atomically (CORE-019).\n * Creates the storage directory if needed.\n *\n * Bytes go to a same-directory temp file first, then move into place with rename —\n * a crash mid-write can therefore never leave a truncated JSON where the previous\n * record used to be. Same-directory is load-bearing: cross-device rename is a copy.\n *\n * SEC-020: the atomic write now comes from `writeOwnerOnlyFile`, which carries the mode from the\n * moment the temp file is created. The hand-rolled version here wrote it at the umask's default\n * and let `rename` carry that mode to the final path, so every record was 0644 — and even setting\n * the mode after the write would leave a window in which the full transcript was world-readable\n * on disk.\n */\n save(session: IInteractiveSessionRecord, options: ISessionSaveOptions = {}): void {\n this.ensureDir();\n this.filePath(session.id);\n // A sealed file this store cannot open is a session nobody here has read; replacing it would\n // destroy the only copy — unless it is provably exactly what this instance last wrote or holds,\n // which is how a locked process goes on saving the sessions it is running. A file it could not\n // read at all may be one of those, so it is protected the same way. A plain or absent file is\n // replaced.\n // TRANS-007: the versioned envelope, not the bare record. Without a version on disk there is\n // nothing to compare, so \"written by a build this one does not read\" cannot be told from\n // \"damaged\" even in principle — and those two need different things from the user.\n // SEC-020's owner-only atomic write carries it: the envelope is WHAT is written, the mode is HOW.\n // A store that seals writes the same envelope sealed to this session's id, tagged when it can\n // vouch that the content is the owner's and the keys are unlocked.\n this.records.save(session, options);\n }\n\n saveRestoreTransaction(\n session: IInteractiveSessionRecord,\n options?: ISessionSaveOptions,\n ): TSessionRestoreCommitOutcome {\n if (!options?.claim)\n throw new TypeError('A live session claim is required for restore commit.');\n try {\n this.save(session, options);\n return { status: 'committed' };\n } catch (error) {\n return { status: 'unknown', error };\n }\n }\n\n /**\n * What is at `path` now, for the guard before a save. Only \"no such file\" means there is nothing\n * to protect; a file that cannot be opened or read is `unreadable`. The first bytes tell plain from\n * sealed, so the common plain case costs one small read in a store that seals nothing, and the rest\n * of the file is read through the same descriptor, so the bytes judged are those of one file: a\n * sealed file's always, a plain one's in a store that seals, which must tell whether it is exactly\n * what the store holds.\n */\n private readExisting(path: string): TStoredSessionFile {\n let descriptor: number;\n try {\n descriptor = openSync(path, 'r');\n } catch (error) {\n return errnoCode(error) === 'ENOENT' ? { kind: 'absent' } : { kind: 'unreadable' };\n }\n try {\n const head = Buffer.alloc(SEALED_ARTIFACT_MAGIC.length);\n const length = readSync(descriptor, head, 0, head.length, 0);\n if (!isSealedArtifactBytes(head.subarray(0, length))) {\n return this.records.seals\n ? { kind: 'plain', bytes: readRest(descriptor, head.subarray(0, length)) }\n : { kind: 'plain' };\n }\n return { kind: 'sealed', bytes: readRest(descriptor, head) };\n } catch {\n return { kind: 'unreadable' };\n } finally {\n closeSync(descriptor);\n }\n }\n\n /**\n * Load a session by its ID, saying WHICH of the five things happened.\n *\n * `undefined` used to answer the first four — never saved, damaged, written by a build this one\n * cannot read, and the read failed — and a caller that meant to preserve fields it does not own\n * then treated \"damaged\" as \"no prior record\" and overwrote the file. The outcome type is what\n * removes that, by making the caller answer the question it was not asking. The fifth, `locked`,\n * is a sealed file this process cannot open right now: present and intact, so neither of the two.\n *\n * Only a load that asks to `hold` the session remembers a sealed record it opens (see\n * {@link StoredSessionRecords.load}); a probe and the listing remember nothing.\n */\n load(id: string, options: ISessionLoadOptions = {}): TSessionLoadOutcome {\n this.filePath(id);\n return this.records.load(id, options);\n }\n\n /** What is at session `id`'s path, read whole, for one load or listing of it. */\n private loadFile(id: string): {\n readonly file: TStoredSessionFile;\n readonly unreadable: TSessionLoadOutcome;\n } {\n const path = this.filePath(id);\n let raw: Buffer;\n try {\n raw = readFileSync(path);\n } catch (error) {\n if (errnoCode(error) === 'ENOENT') return { file: { kind: 'absent' }, unreadable: MISSING };\n // Only \"no such file\" is missing. A file that cannot be read — or a path that cannot even be\n // looked at — may be a session that is still there; reporting it as missing is what let a\n // recovery path run over one. Fixed text and the errno code: the error's own message names the\n // path and says nothing the code does not.\n return {\n file: { kind: 'unreadable' },\n unreadable: {\n status: 'corrupt',\n issues: [{ path: '', message: unreadableMessage(error) }],\n },\n };\n }\n // SEC-020 / #3679: `save` tightens a record it REWRITES, but a session that is only resumed or\n // listed keeps whatever mode its writer left — so a record from a pre-SEC-020 build stays 0644.\n // Repair it on read: `list` routes through here, so browsing or resuming a legacy session makes\n // it owner-only. A no-op once the mode is already tight, and on non-POSIX hosts.\n //\n // Best-effort on this READ path, unlike the write callers of `tightenExistingFile`: a record\n // owned by another account (EPERM) or on a chmod-ignoring mount makes it throw, and this is the\n // one place `tightenExistingFile` runs from `list` — where a single unrepairable file must not\n // take the whole listing down (the invariant `outcomeForListedId` upholds). A failed repair\n // leaves the mode as it was and the record is still returned.\n try {\n tightenExistingFile(path);\n } catch {\n // allow-fallback: repairing the mode is opportunistic; reading the session is the job.\n }\n return { file: classifyStoredSessionBytes(raw), unreadable: MISSING };\n }\n\n /**\n * The record this instance itself last wrote or holds, offered only while the file on disk is\n * still exactly those bytes. It is what a writer needs when `load` says `locked` for a session it\n * is running: the keys that read it back are locked, the copy it holds is not. `undefined` for\n * a session this instance never held, or whose file has changed since.\n */\n loadHeld(id: string): TSessionLoadOutcome | undefined {\n this.filePath(id);\n return this.records.loadHeld(id);\n }\n\n /** The sealed record of `id` as a token for another store over these files (see {@link StoredSessionRecords.exportHeld}). */\n exportHeld(id: string, options?: ISessionExportOptions): string | undefined {\n this.filePath(id);\n return this.records.exportHeld(id, options);\n }\n\n /** Holds the record a token from another store's `exportHeld` carries (see {@link StoredSessionRecords.importHeld}). */\n importHeld(id: string, token: string): void {\n this.filePath(id);\n this.records.importHeld(id, token);\n }\n\n /**\n * Every session this directory holds, each with what the store concluded about it.\n *\n * Unreadable entries are REPORTED rather than skipped. A store that distinguishes four outcomes on\n * `load` and then hides two of them from the surface a person browses has moved the defect rather\n * than removed it: the difference a user experiences is between \"my session vanished\" and \"my\n * session needs a different build\".\n *\n * The one exception is a sealed session this process cannot open: there is no record to describe\n * (no title, no time, no workspace), so it is not an entry, and {@link scan} is how a surface\n * still says that some sessions are there.\n */\n list(): readonly ISessionListEntry[] {\n return this.scan().entries;\n }\n\n /**\n * The listing and how many sealed sessions it left out because they cannot be opened right now,\n * from one read of each file, counted by why: locked keys, or no keys here at all. A record that\n * cannot be read has nothing to show, so the counts are what keep those sessions from looking gone.\n */\n scan(): ISessionStoreScan {\n const all = this.readAll();\n return {\n entries: all\n .filter((entry) => entry.outcome.status !== 'locked')\n .sort(compareListEntriesByRecency),\n ...countLeftOut(all),\n };\n }\n\n private readAll(): ISessionListEntry[] {\n if (!existsSync(this.baseDir)) {\n return [];\n }\n return readdirSync(this.baseDir)\n .filter((file) => file.endsWith('.json'))\n .map((file) => file.slice(0, -'.json'.length))\n .map((id) => ({ id, outcome: this.outcomeForListedId(id) }));\n }\n\n /**\n * The outcome for one directory entry, without letting a bad NAME throw out of `list`.\n *\n * `load` validates the id, because an id reaching it is a caller's value and a malformed one is a\n * bug or an attack (SEC-006). A name read out of the directory is neither: the store did not\n * choose it, and one file it cannot use as an id must not take the whole listing down with it.\n * Routing `list` through `load` made exactly that happen — a single `my session.json` in the\n * sessions directory threw, and the resume picker went with it.\n *\n * Reporting it is the same answer `list` gives for every other file it cannot read, which is the\n * property this work exists to establish.\n */\n private outcomeForListedId(id: string): TSessionLoadOutcome {\n if (!isSafeSessionId(id)) {\n return {\n status: 'corrupt',\n issues: [{ path: '', message: 'the file name is not a usable session id' }],\n };\n }\n return this.records.listed(id);\n }\n\n /**\n * Delete a session by its ID, holding its writer marker: refused while another writer holds it.\n * No-ops silently if the session does not exist.\n */\n delete(id: string): void {\n const path = this.filePath(id);\n this.records.delete(id, () => {\n if (existsSync(path)) unlinkSync(path);\n });\n }\n}\n","/**\n * What a line in an append-only store means, and what a sidecar beside a session log holds.\n *\n * The prompt-history file, both session-log sinks and both session-log sources route every append\n * and every read through this one module — the pattern `StoredSessionRecords` set for records — so\n * none of them can disagree about a sealed line, a torn one, or one written by a newer build.\n *\n * A store that seals appends each call as ONE sealed line that opens on its own: no line waits for\n * another, so a reader walking a file backwards, or paging it, opens exactly the lines it shows.\n * Every sealed append starts with a newline as well as ending with one. Appends are not atomic\n * (Node loops on a short write), and a writer can die part-way; the leading newline puts whatever\n * fragment it left on a line of its own, so the next record is never glued to it. A reader skips\n * empty lines, and the final fragment of a file, if it carries sealed framing (or the start of it)\n * and no newline yet, is an append still in progress rather than a line.\n *\n * A store that seals nothing writes plain lines, and meets a sealed line as one no key in its\n * process opens. Plain and sealed lines may sit in one file, each read by its\n * own framing.\n *\n * An externalized payload of a sealing log is sealed under a RANDOM name. A content-addressed name\n * would let any program of the same user confirm a guessed payload, or see which payloads are equal;\n * the digest now lives only inside the sealed line, where the resolver still checks it against the\n * bytes it opens, so two sealed sidecars swapped with each other are caught. Dedupe is in-process.\n */\nimport { randomBytes } from 'node:crypto';\n\nimport {\n ArtifactCorruptError,\n ArtifactLockedError,\n SEALED_ARTIFACT_MAX_OVERHEAD_BYTES,\n isArtifactCorruptError,\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isCurrentSealedArtifactLine,\n isSealedArtifactBytes,\n isSealedArtifactLine,\n} from '@robota-sdk/agent-core';\n\nimport { SessionLogPayloadResolutionError } from './external-payload-resolution-contracts.js';\nimport { SessionLogDecodeError } from './session-log-codec/index.js';\nimport { createSessionLogExternalPayloadReference } from './session-log-sinks.js';\nimport { assertSessionSealing } from './stored-session-record.js';\n\nimport type { IExternalPayloadReference } from './session-log-reference-types.js';\nimport type {\n IArtifactContext,\n IArtifactSealer,\n TArtifactKind,\n TArtifactLockedReason,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\n\n/** The kinds of artifact an append store's lines are sealed as. */\nexport type TAppendLineKind = Extract<TArtifactKind, 'prompt-history-line' | 'session-log-line'>;\n\n/** One physical line of an append store, as this process can read it. Never carries key material. */\nexport type TStoredLine =\n /** A plain line (one element, verbatim), or a sealed record opened to the lines it holds. */\n | { readonly kind: 'lines'; readonly lines: readonly string[] }\n /** Sealed and intact as far as anyone can tell; `reason` says whether an unlock here would read it. */\n | { readonly kind: 'locked'; readonly reason: TArtifactLockedReason }\n /** Sealed in this build's framing and it does not open for this owner: damaged, torn, or sealed with other keys. */\n | { readonly kind: 'does-not-open' }\n /** Sealed in a framing version a newer build writes: never opened, never counted as damage. */\n | { readonly kind: 'newer-format' };\n\nconst NEWLINE = '\\n';\nconst DOES_NOT_OPEN: TStoredLine = Object.freeze({ kind: 'does-not-open' });\nconst NEWER_FORMAT: TStoredLine = Object.freeze({ kind: 'newer-format' });\nconst NO_KEYS: TStoredLine = Object.freeze({ kind: 'locked', reason: 'no-keys' });\n/** The start of a sealed-line prefix, too short to carry its colon yet: `R`, `RS`, `RSL`, `RSL` and a version digit. */\nconst PARTIAL_SEALED_LINE_PREFIX = /^R(?:S(?:L[0-9]?)?)?$/;\n\nfunction sealerOf(sealing: TArtifactSealing): IArtifactSealer | undefined {\n assertSessionSealing(sealing);\n return sealing === 'plain' ? undefined : sealing.sealer;\n}\n\nfunction lockedReasonOf(error: unknown): TArtifactLockedReason {\n return (error as { readonly reason?: unknown }).reason === 'no-keys' ? 'no-keys' : 'locked';\n}\n\n/** How one append store keeps its lines: sealed as `kind`, bound to the owner id each call names. */\nexport class StoredLines {\n readonly #sealer: IArtifactSealer | undefined;\n readonly #kind: TAppendLineKind;\n\n constructor(sealing: TArtifactSealing, kind: TAppendLineKind) {\n this.#sealer = sealerOf(sealing);\n this.#kind = kind;\n }\n\n /**\n * What to append for `text` — one or more `\\n`-terminated lines, exactly what a plain store\n * appends in that call. Plain: `text` itself. Sealing: one sealed line with a newline before and\n * after it. Throws whatever sealing throws; it never falls back to plaintext.\n */\n encode(text: string, ownerId: string): string {\n if (this.#sealer === undefined) return text;\n return `${NEWLINE}${this.#sealer.sealLine(text, { kind: this.#kind, id: ownerId })}${NEWLINE}`;\n }\n\n /** One physical line (no `\\n` in it), as this process can read it. Never throws. */\n decode(line: string, ownerId: string): TStoredLine {\n try {\n return this.#decode(line, ownerId);\n } catch {\n // A failure of the sealer that says neither \"locked\" nor \"does not open\" is no evidence the\n // line is fine, and must not take a tolerant reader down: it is a line that does not open.\n return DOES_NOT_OPEN;\n }\n }\n\n /**\n * Strict: the plain-equivalent text of a whole store's `text`, for decoders that fail on any bad\n * line (see {@link openedText}). A failure of the sealer that is neither `ArtifactLockedError` nor\n * `ArtifactCorruptError` is a defect (a refused context, a misused key), not a damaged line, so it\n * is thrown as it was rather than reported as corruption.\n */\n openText(text: string, ownerId: string): string {\n const lines = splitStoredLines(text);\n const decode = (line: string): TStoredLine => this.#decode(line, ownerId);\n return openedText(\n (function* decodeInOrder() {\n for (const line of lines) yield decode(line);\n })(),\n );\n }\n\n /** One physical line; throws only what the sealer threw that is neither locked nor corrupt. */\n #decode(line: string, ownerId: string): TStoredLine {\n if (!isSealedArtifactLine(line)) return { kind: 'lines', lines: [line] };\n if (!isCurrentSealedArtifactLine(line)) return NEWER_FORMAT;\n // A sealed line and no sealer: no unlock in this process would open it.\n if (this.#sealer === undefined) return NO_KEYS;\n let text: string;\n try {\n text = this.#sealer.openLine(line, { kind: this.#kind, id: ownerId });\n } catch (error) {\n if (isArtifactLockedError(error)) return { kind: 'locked', reason: lockedReasonOf(error) };\n if (isArtifactCorruptError(error)) return DOES_NOT_OPEN;\n throw error;\n }\n return { kind: 'lines', lines: text.split(NEWLINE).filter((opened) => opened.length > 0) };\n }\n}\n\n/**\n * The physical lines of a store's text. An UNTERMINATED final fragment with sealed framing, or so\n * short that it is still only the start of it (`RSL` and a version digit, cut anywhere), is left\n * out: every sealed append ends with a newline, so such a fragment is an append in progress (or one\n * whose writer died), not a record. A plain fragment is kept as it is.\n */\nexport function splitStoredLines(text: string): string[] {\n const lines = text.split(NEWLINE);\n const tail = lines[lines.length - 1] as string;\n if (tail.length > 0 && (isSealedArtifactLine(tail) || PARTIAL_SEALED_LINE_PREFIX.test(tail))) {\n lines.pop();\n }\n return lines;\n}\n\n/**\n * The plain-equivalent text of a whole store, for decoders that fail on any bad line: what a plain\n * store would hold, so a file of plain lines comes back byte for byte. The first line that cannot be\n * read decides the failure — `ArtifactLockedError` (with its reason) for a locked one,\n * `ArtifactCorruptError` for one that does not open, `SessionLogDecodeError` (`UNSUPPORTED_VERSION`)\n * for one a newer build sealed.\n */\nexport function openedText(stored: Iterable<TStoredLine>): string {\n const parts: string[] = [];\n let lineNumber = 0;\n for (const line of stored) {\n lineNumber += 1;\n switch (line.kind) {\n case 'lines':\n parts.push(line.lines.join(NEWLINE));\n break;\n case 'locked':\n throw new ArtifactLockedError(line.reason);\n case 'does-not-open':\n throw new ArtifactCorruptError();\n case 'newer-format':\n throw new SessionLogDecodeError('UNSUPPORTED_VERSION', [\n {\n path: `line ${lineNumber}`,\n message: 'the line is sealed in a framing version this build does not open',\n },\n ]);\n }\n }\n return parts.join(NEWLINE);\n}\n\n/** 16 random bytes: a name no one can derive from the payload or guess. */\nconst SEALED_PAYLOAD_NAME_BYTES = 16;\nconst SEALED_PAYLOAD_EXTENSION = '.sealed';\n\n/** One sidecar a sink is asked to write. */\nexport interface IStoredPayloadWrite {\n readonly relativePath: string;\n readonly data: string | Uint8Array;\n /**\n * The name is the content's own digest, so a file already at it holds these exact bytes and need\n * not be written again. A sealed sidecar's name is random: a file already there is not this one.\n */\n readonly contentAddressed: boolean;\n}\n\nfunction payloadError(\n code: 'PAYLOAD_UNREADABLE' | 'MAX_TOTAL_BYTES_EXCEEDED',\n relativePath: string,\n maxBytes: number,\n): SessionLogPayloadResolutionError {\n return code === 'PAYLOAD_UNREADABLE'\n ? new SessionLogPayloadResolutionError(\n code,\n `External payload is sealed and does not open here: ${relativePath}.`,\n { relativePath },\n )\n : new SessionLogPayloadResolutionError(\n code,\n `External payload exceeds the remaining byte budget of ${maxBytes}.`,\n { relativePath, expected: maxBytes },\n );\n}\n\n/**\n * How a session-log sink places externalized JSON values beside its log, and how a source opens what\n * it reads back. Plain: the content-addressed `<id>.payloads/<sha256>.json`. Sealing:\n * `<id>.payloads/<22 random base64url characters>.sealed`, holding the payload sealed as\n * `session-log-payload` for the session, written once per payload per instance.\n */\nexport class StoredPayloads {\n readonly #sealer: IArtifactSealer | undefined;\n /** Per session: plaintext digest → the sealed sidecar this instance already wrote for it. */\n readonly #written = new Map<string, Map<string, string>>();\n\n constructor(sealing: TArtifactSealing) {\n this.#sealer = sealerOf(sealing);\n }\n\n /**\n * The reference a log line carries for `serialized`, after `write` has put its sidecar in place.\n * `write` is called at most once; a sealed name is remembered only once `write` returned, so a\n * failed write is attempted again for the next reference to the same payload.\n */\n place(\n sessionId: string,\n sha256: string,\n serialized: string,\n write: (target: IStoredPayloadWrite) => void,\n ): IExternalPayloadReference {\n const reference = createSessionLogExternalPayloadReference(sessionId, sha256, serialized);\n if (this.#sealer === undefined) {\n write({ relativePath: reference.relativePath, data: serialized, contentAddressed: true });\n return reference;\n }\n const written = this.#written.get(sessionId) ?? new Map<string, string>();\n const known = written.get(sha256);\n if (known !== undefined) return { ...reference, relativePath: known };\n const name = randomBytes(SEALED_PAYLOAD_NAME_BYTES).toString('base64url');\n // Always `/`, like a plain reference: every reader accepts either slash.\n const relativePath = `${sessionId}.payloads/${name}${SEALED_PAYLOAD_EXTENSION}`;\n const plain = Buffer.from(serialized, 'utf8');\n let data: Uint8Array;\n try {\n data = this.#sealer.sealBytes(plain, { kind: 'session-log-payload', id: sessionId });\n } finally {\n plain.fill(0);\n }\n write({ relativePath, data, contentAddressed: false });\n written.set(sha256, relativePath);\n this.#written.set(sessionId, written);\n return { ...reference, relativePath };\n }\n\n /** How many bytes to read for a payload whose plaintext budget is `maxBytes`. */\n rawBudget(maxBytes: number): number {\n if (this.#sealer === undefined) return maxBytes;\n return Math.min(maxBytes + SEALED_ARTIFACT_MAX_OVERHEAD_BYTES, Number.MAX_SAFE_INTEGER);\n }\n\n /**\n * The payload bytes in `stored` (what was read at `relativePath`), held to the plaintext budget\n * exactly. A plain store reads every payload as bytes: only a sealed line can\n * reference a sealed payload, and a plain reader never gets past the line. A sealing store passes\n * plain bytes through too, so a directory written before sealing still resolves — a JSON payload\n * never starts with the sealed magic — and opens sealed bytes bound to `ownerId`:\n * `ArtifactLockedError` while they cannot be opened here, and `PAYLOAD_UNREADABLE` when they do\n * not open, were sealed by a newer build, or have no owner to open them for. Any other failure of\n * the sealer is a defect, thrown as it was. The caller then checks the result against the\n * reference's digest and length.\n */\n open(\n stored: Uint8Array,\n maxBytes: number,\n ownerId: string | undefined,\n relativePath: string,\n ): Uint8Array {\n if (this.#sealer === undefined) return stored;\n if (!isSealedArtifactBytes(stored)) {\n if (stored.byteLength > maxBytes) {\n throw payloadError('MAX_TOTAL_BYTES_EXCEEDED', relativePath, maxBytes);\n }\n return stored;\n }\n if (!isCurrentSealedArtifactBytes(stored)) {\n throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n }\n if (ownerId === undefined) throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n let plain: Uint8Array;\n try {\n plain = this.#sealer.openBytes(stored, { kind: 'session-log-payload', id: ownerId });\n } catch (error) {\n if (isArtifactLockedError(error)) throw new ArtifactLockedError(lockedReasonOf(error));\n if (isArtifactCorruptError(error)) {\n throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n }\n throw error;\n }\n if (plain.byteLength > maxBytes) {\n plain.fill(0);\n throw payloadError('MAX_TOTAL_BYTES_EXCEEDED', relativePath, maxBytes);\n }\n return plain;\n }\n}\n\n/** What a stored sidecar is to a migration that seals what is still plain in place (#3679). */\nexport type TStoredPayloadMigrationState = 'plain' | 'sealed' | 'does-not-open' | 'newer-format';\n\nfunction payloadContext(ownerId: string): IArtifactContext {\n return { kind: 'session-log-payload', id: ownerId };\n}\n\n/**\n * Where a sidecar of session `ownerId` stands: decided by whether it opens for that session, never by\n * framing alone. With the keys locked it throws `ArtifactLockedError`.\n */\nexport function storedPayloadMigrationState(\n stored: Uint8Array,\n ownerId: string,\n sealer: IArtifactSealer,\n): TStoredPayloadMigrationState {\n if (!isSealedArtifactBytes(stored)) return 'plain';\n if (!isCurrentSealedArtifactBytes(stored)) return 'newer-format';\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, payloadContext(ownerId));\n } catch (error) {\n if (isArtifactLockedError(error)) throw error;\n return 'does-not-open';\n }\n plain.fill(0);\n return 'sealed';\n}\n\n/**\n * A plain sidecar's bytes sealed for session `ownerId`, to be put back under the SAME name: a source\n * opens sealed bytes whatever the name ({@link StoredPayloads.open}), and the digest and length every\n * reference — nested ones included — checks are the plaintext's, so no reference changes.\n */\nexport function sealStoredPayloadBytes(\n plain: Uint8Array,\n ownerId: string,\n sealer: IArtifactSealer,\n): Uint8Array {\n return sealer.sealBytes(plain, payloadContext(ownerId));\n}\n\n/** Where a rewrite of an append store puts the lines it produces. */\nexport interface IStoredLineSink {\n write(text: string): void;\n}\n\n/** One physical line of a session log as both transcript pages read it: a trailing `\\r` is not content. */\nfunction logLineContent(physical: string): string {\n return physical.endsWith('\\r') ? physical.slice(0, -1) : physical;\n}\n\n/**\n * Seals the plain lines of a session log as a migration does: one sealed record per non-empty plain\n * line, a sealed line kept as it is, an empty line dropped — exactly the records both transcript\n * pages count, so a stored-record offset names the same line before and after.\n */\nexport class SessionLogLineSealer {\n sealedLines = 0;\n carriedLines = 0;\n readonly #lines: StoredLines;\n\n constructor(\n sealer: IArtifactSealer,\n private readonly ownerId: string,\n ) {\n this.#lines = new StoredLines({ sealer }, 'session-log-line');\n }\n\n /** `leadingNewline` frames each line `\\n<line>\\n`, for a file other writers append to meanwhile. */\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void {\n for (const physical of lines) {\n const line = logLineContent(physical);\n if (line.length === 0) continue;\n if (isSealedArtifactLine(line)) {\n out.write(`${leadingNewline ? NEWLINE : ''}${line}${NEWLINE}`);\n this.carriedLines += 1;\n continue;\n }\n const stored = this.#lines.encode(`${line}${NEWLINE}`, this.ownerId);\n out.write(leadingNewline ? stored : stored.slice(NEWLINE.length));\n this.sealedLines += 1;\n }\n }\n}\n\n/** The plain records a session log's text holds, as a migration would seal them. */\nexport function countPlainSessionLogLines(text: string): number {\n return splitStoredLines(text).filter((physical) => {\n const line = logLineContent(physical);\n return line.length > 0 && !isSealedArtifactLine(line);\n }).length;\n}\n\n/** A whole session log's text with every plain line sealed, for a store that replaces the file whole. */\nexport interface ISealedSessionLogText {\n readonly text: string;\n readonly sealedLines: number;\n readonly carriedLines: number;\n}\n\n/**\n * `text` (a whole log, nothing appending to it) with every plain record sealed for session\n * `ownerId` as {@link SessionLogLineSealer} seals it. An unfinished sealed append at the end, which\n * no reader shows, is dropped; an unfinished plain line is a record, and is sealed.\n */\nexport function sealSessionLogText(\n text: string,\n ownerId: string,\n sealer: IArtifactSealer,\n): ISealedSessionLogText {\n const sealing = new SessionLogLineSealer(sealer, ownerId);\n const parts: string[] = [];\n sealing.rewrite(splitStoredLines(text), { write: (part) => parts.push(part) }, false);\n return {\n text: parts.join(''),\n sealedLines: sealing.sealedLines,\n carriedLines: sealing.carriedLines,\n };\n}\n","import { appendFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createLogger } from '@robota-sdk/agent-core';\nimport {\n OWNER_ONLY_FILE_MODE,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { assertSafeSessionId } from './session-id.js';\nimport { StoredLines, StoredPayloads } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type { IExternalPayloadReference } from './session-log-reference-types.js';\nimport type { IExternalPayloadSink, ISessionLogSink } from './session-log-sinks.js';\nimport type { IStoredPayloadWrite } from './stored-append-lines.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\n\nconst logger = createLogger('NodeSessionLogSink');\n\n/** Explicit host-filesystem sink for JSONL logs and their content-addressed sidecars. */\nexport class NodeSessionLogSink implements ISessionLogSink, IExternalPayloadSink {\n readonly externalPayloadSink: IExternalPayloadSink = this;\n private readonly enabled: boolean;\n private readonly lines: StoredLines;\n private readonly payloads: StoredPayloads;\n\n /**\n * @param options `sealing` (required): `{ sealer }` seals every append (one sealed line per call)\n * and every sidecar; `'plain'` writes plain lines and content-addressed sidecars.\n */\n constructor(\n private readonly logDirectory: string,\n options: ISessionStoreOptions,\n ) {\n const sealing = sessionSealingOf(options);\n this.lines = new StoredLines(sealing, 'session-log-line');\n this.payloads = new StoredPayloads(sealing);\n try {\n // SEC-020: `mkdirSync(dir, { recursive: true, mode })` does NOT set the mode of a directory\n // that already exists — it returns successfully and adopts whatever is there. Measured: a log\n // directory pre-created at 0777 stayed 0777 while its records were written 0600, which means\n // another local account could not read a record but could unlink and replace one, and could\n // enumerate every session id.\n ensureOwnerOnlyDirectory(logDirectory);\n this.enabled = true;\n } catch (error) {\n // allow-fallback: session logging is diagnostic and must not disable the session. It now also\n // disables on a directory that cannot be made owner-only, which is the correct direction: no\n // log is better than a log any account can read or replace.\n this.enabled = false;\n logger.warn('session log directory could not be created — session logging is disabled', {\n logDirectory,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n append(sessionId: string, text: string): void {\n assertSafeSessionId(sessionId);\n if (!this.enabled) return;\n const path = join(this.logDirectory, `${sessionId}.jsonl`);\n // Sealed before the file is touched, so a sealing failure writes nothing at all.\n const stored = this.lines.encode(text, sessionId);\n // SEC-020: `mode` on a write applies only when the file is CREATED, so a log an older version\n // left at 0644 keeps 0644 for its whole life however many times it is appended to. Tightening\n // first is the repair path for a store written before this change.\n tightenExistingFile(path);\n appendFileSync(path, stored, { mode: OWNER_ONLY_FILE_MODE });\n }\n\n writeJson(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference {\n return this.payloads.place(sessionId, sha256, serialized, (target) =>\n this.writePayload(sessionId, target),\n );\n }\n\n private writePayload(sessionId: string, target: IStoredPayloadWrite): void {\n if (!this.enabled) return;\n ensureOwnerOnlyDirectory(join(this.logDirectory, `${sessionId}.payloads`));\n const payloadPath = join(this.logDirectory, target.relativePath);\n try {\n writeFileSync(payloadPath, target.data, {\n ...(typeof target.data === 'string' ? { encoding: 'utf8' as const } : {}),\n mode: OWNER_ONLY_FILE_MODE,\n flag: 'wx',\n });\n } catch (error) {\n // A random sealed name that already exists is not this payload, so only a content-addressed\n // one may be taken as already written.\n if (!target.contentAddressed || (error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw error;\n }\n // SEC-020: the payload is content-addressed, so an existing file already holds these exact\n // bytes and `wx` correctly declines to rewrite it. Its MODE is a different question — one\n // written by an older version is 0644 and nothing else would ever repair it.\n tightenExistingFile(payloadPath);\n }\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, dirname, extname, isAbsolute, resolve, win32 } from 'node:path';\n\nimport {\n createStableRootedFileReader,\n StableFileAuthorityError,\n} from '@robota-sdk/agent-file-authority';\n\nimport { SessionLogPayloadResolutionError } from './external-payload-resolution-contracts.js';\nimport { StoredLines, StoredPayloads, splitStoredLines } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type {\n IExternalPayloadSource,\n ISessionLogSource,\n} from './external-payload-source-types.js';\nimport type { TStoredLine } from './stored-append-lines.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\n\nfunction payloadPathSegments(relativePath: string): readonly string[] {\n if (\n relativePath.trim().length === 0 ||\n relativePath.includes('\\0') ||\n isAbsolute(relativePath) ||\n win32.isAbsolute(relativePath)\n ) {\n throw outsideRootError(relativePath);\n }\n const segments = relativePath.split(/[\\\\/]+/u);\n if (segments.some((segment) => segment === '.' || segment === '..')) {\n throw outsideRootError(relativePath);\n }\n return segments;\n}\n\nfunction validateMaxBytes(maxBytes: number): void {\n if (!Number.isFinite(maxBytes) || !Number.isSafeInteger(maxBytes) || maxBytes < 0) {\n throw new SessionLogPayloadResolutionError(\n 'INVALID_LIMIT',\n 'External-payload maxBytes must be a finite, non-negative safe integer.',\n { actual: String(maxBytes) },\n );\n }\n}\n\nfunction outsideRootError(relativePath: string, cause?: Error): SessionLogPayloadResolutionError {\n return new SessionLogPayloadResolutionError(\n 'OUTSIDE_ROOT',\n `External payload path escapes its base directory or contains a link: ${relativePath}.`,\n { relativePath },\n cause,\n );\n}\n\nfunction mapStableFileAuthorityError(\n error: StableFileAuthorityError,\n relativePath: string,\n maxBytes: number,\n): SessionLogPayloadResolutionError {\n if (error.code === 'INVALID_PATH' || error.code === 'UNSAFE_ENTRY') {\n return outsideRootError(relativePath, error);\n }\n if (error.code === 'UNSUPPORTED_BACKEND') {\n return new SessionLogPayloadResolutionError(\n 'STABLE_PAYLOAD_READ_UNAVAILABLE',\n 'Stable root-relative external-payload reads are unavailable on this host.',\n { relativePath },\n error,\n );\n }\n if (error.code === 'OVER_BUDGET') {\n return new SessionLogPayloadResolutionError(\n 'MAX_TOTAL_BYTES_EXCEEDED',\n `External payload exceeds the remaining byte budget of ${maxBytes}.`,\n { relativePath, expected: maxBytes },\n error,\n );\n }\n return new SessionLogPayloadResolutionError(\n 'PAYLOAD_UNREADABLE',\n `External payload could not be read: ${relativePath}.`,\n { relativePath },\n error,\n );\n}\n\nexport interface INodeExternalPayloadSourceOptions extends ISessionStoreOptions {\n /**\n * The session the payloads belong to, which a sealed one is bound to. Left out, it is the session\n * whose `<id>.payloads` directory the reference names, the layout every sink writes.\n */\n readonly ownerId?: string;\n}\n\nconst PAYLOAD_DIRECTORY_SUFFIX = '.payloads';\n\n/** The session whose `<id>.payloads` directory holds a payload, or `undefined` for any other layout. */\nfunction payloadDirectoryOwner(segments: readonly string[]): string | undefined {\n const directory = segments.length === 2 ? segments[0] : undefined;\n if (directory === undefined || !directory.endsWith(PAYLOAD_DIRECTORY_SUFFIX)) return undefined;\n return directory.slice(0, -PAYLOAD_DIRECTORY_SUFFIX.length) || undefined;\n}\n\n/** Explicit host-filesystem adapter. A file path is never accepted by the neutral parser itself. */\nexport class NodeExternalPayloadSource implements IExternalPayloadSource {\n private readonly baseDirectory: string;\n private readonly payloads: StoredPayloads;\n private readonly ownerId: string | undefined;\n\n /**\n * @param options `sealing` (required): `{ sealer }` opens sealed payloads (plain ones still\n * resolve); `'plain'` meets a sealed payload as one no key in this process opens.\n */\n constructor(baseDirectory: string, options: INodeExternalPayloadSourceOptions) {\n if (baseDirectory.trim().length === 0) {\n throw new Error('External-payload base directory must not be empty.');\n }\n this.baseDirectory = resolve(baseDirectory);\n this.payloads = new StoredPayloads(sessionSealingOf(options));\n this.ownerId = options.ownerId;\n }\n\n readBytes(relativePath: string, maxBytes: number): Uint8Array | undefined {\n validateMaxBytes(maxBytes);\n const segments = payloadPathSegments(relativePath);\n const stored = this.readStored(relativePath, segments, maxBytes);\n if (stored === undefined) return undefined;\n return this.payloads.open(\n stored,\n maxBytes,\n this.ownerId ?? payloadDirectoryOwner(segments),\n relativePath,\n );\n }\n\n /** The bytes at the path, within the raw budget a sealed payload of `maxBytes` needs. */\n private readStored(\n relativePath: string,\n segments: readonly string[],\n maxBytes: number,\n ): Uint8Array | undefined {\n try {\n const reader = createStableRootedFileReader(this.baseDirectory);\n try {\n return reader.readBytes(segments, this.payloads.rawBudget(maxBytes));\n } finally {\n reader.close();\n }\n } catch (error) {\n if (error instanceof SessionLogPayloadResolutionError) throw error;\n if (error instanceof StableFileAuthorityError) {\n throw mapStableFileAuthorityError(error, relativePath, maxBytes);\n }\n throw new SessionLogPayloadResolutionError(\n 'PAYLOAD_UNREADABLE',\n `External payload could not be read: ${relativePath}.`,\n { relativePath },\n error,\n );\n }\n }\n}\n\n/**\n * Explicit host-filesystem adapter for a JSONL session log. The log's owner — the session id its\n * sealed lines and payloads are bound to — is the file's stem, `<id>.jsonl`, the name every sink\n * writes; a sealed log copied under another session's name does not open as that session's.\n */\nexport class NodeSessionLogSource implements ISessionLogSource {\n readonly externalPayloadSource: IExternalPayloadSource;\n private readonly lines: StoredLines;\n private readonly ownerId: string;\n\n /**\n * @param options `sealing` (required): `{ sealer }` opens sealed lines and payloads (plain ones\n * still read); `'plain'` meets a sealed line as one no key in this process opens.\n */\n constructor(\n private readonly logFile: string,\n options: ISessionStoreOptions,\n ) {\n if (logFile.trim().length === 0) {\n throw new Error('Session log-file path must not be empty.');\n }\n const sealing = sessionSealingOf(options);\n this.lines = new StoredLines(sealing, 'session-log-line');\n this.ownerId = basename(logFile, extname(logFile));\n this.externalPayloadSource = new NodeExternalPayloadSource(dirname(logFile), {\n sealing,\n ownerId: this.ownerId,\n });\n }\n\n /** Strict: the plain-equivalent text, failing on the first line it cannot read (see {@link StoredLines.openText}). */\n readText(): string | undefined {\n if (!existsSync(this.logFile)) return undefined;\n return this.lines.openText(readFileSync(this.logFile, 'utf8'), this.ownerId);\n }\n\n /** Tolerant: every physical line in file order, each as this process can read it; `undefined` when there is no log. */\n readStoredLines(): readonly TStoredLine[] | undefined {\n if (!existsSync(this.logFile)) return undefined;\n return splitStoredLines(readFileSync(this.logFile, 'utf8')).map((line) =>\n this.lines.decode(line, this.ownerId),\n );\n }\n}\n","/**\n * Rewriting a file other processes append to by path, without losing what they append meanwhile\n * (#3679): the prompt history's compaction and the sealing of a plain session log in place both go\n * through here, so the two agree on what a snapshot is and how a late append is caught up.\n *\n * Such a file has no lock, so it cannot be rewritten in place. The new file is written beside it and\n * renamed over it, and what reached the old one meanwhile is caught up:\n *\n * 1. The snapshot is the old file up to just after its last newline; anything after that is an\n * append still in progress and is left to the catch-up.\n * 2. Every line of the snapshot goes through the caller's rewriter into a temporary file, which is\n * flushed and renamed over the original, and the directory is flushed.\n * 3. An append that opened the old file before the rename still lands in it, so the old descriptor\n * is read from the snapshot's end until it stays quiet, and what it finds is rewritten onto the new\n * file. A catch-up that fails part-way is reported with how much of the old file it left unread,\n * since what was appended there is lost with the old file.\n * 4. The new file is read once more. A plain line there is plaintext an older build appended by path\n * after the rename; it is counted for the caller to report, never hidden.\n *\n * Just before the rename the path must still name the file that was opened: another rewrite that\n * renamed its own file in meanwhile holds what was appended since, and renaming over it would lose\n * that, so the rewrite is abandoned instead (a rewrite that renames in the instant between that look\n * and the rename is not caught; cooperating callers keep two rewrites from running at all). A file\n * deleted meanwhile is not put back either.\n *\n * A file that is not a regular file of its own — a symbolic link or a second hard link, as a dotfile\n * sync makes — is refused: the rename would replace the link and leave the plaintext where it points.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n closeSync,\n constants,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n readSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\n\nimport { isSealedArtifactLine } from '@robota-sdk/agent-core';\nimport { OWNER_ONLY_FILE_MODE, ensureOwnerOnlyDirectory } from '@robota-sdk/agent-core/node';\n\nimport { splitStoredLines } from './stored-append-lines.js';\n\nimport type { IStoredLineSink } from './stored-append-lines.js';\n\nconst NEWLINE_BYTE = 0x0a;\n/** Writes are batched up to this many bytes. */\nconst WRITE_BATCH_BYTES = 1024 * 1024;\n/** Quiet passes, each after a pause, before the old file counts as stable. */\nconst QUIET_PASSES = 2;\nconst DEFAULT_PAUSE_MS = 20;\nconst TEMPORARY_SUFFIX_BYTES = 6;\n\n/** Why a file was left as it is. */\nexport type TAppendFileRefusal =\n /** A symbolic link, a directory, or anything else that is not a regular file. */\n | 'not-a-regular-file'\n /** A regular file with another name too, which a rename would leave holding the old bytes. */\n | 'hard-linked';\n\nexport type TOpenedAppendFile =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TAppendFileRefusal }\n | { readonly status: 'open'; readonly descriptor: number };\n\nfunction errorCode(error: unknown): string | undefined {\n return (error as NodeJS.ErrnoException | undefined)?.code;\n}\n\n/** The file opened for reading, after the checks that make it safe to replace by rename. */\nexport function openAppendFile(path: string): TOpenedAppendFile {\n let linked;\n try {\n linked = lstatSync(path);\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return { status: 'missing' };\n throw error;\n }\n if (!linked.isFile()) return { status: 'refused', reason: 'not-a-regular-file' };\n let descriptor: number;\n try {\n descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return { status: 'missing' };\n if (errorCode(error) === 'ELOOP') return { status: 'refused', reason: 'not-a-regular-file' };\n throw error;\n }\n const opened = fstatSync(descriptor);\n // Replaced between the two looks: what was opened is not the file that was checked.\n const sameFile = opened.isFile() && opened.ino === linked.ino && opened.dev === linked.dev;\n if (!sameFile || opened.nlink > 1) {\n closeSync(descriptor);\n return { status: 'refused', reason: sameFile ? 'hard-linked' : 'not-a-regular-file' };\n }\n return { status: 'open', descriptor };\n}\n\nfunction readRange(descriptor: number, from: number, to: number): Buffer {\n const buffer = Buffer.alloc(Math.max(0, to - from));\n let filled = 0;\n while (filled < buffer.length) {\n const bytesRead = readSync(descriptor, buffer, filled, buffer.length - filled, from + filled);\n if (bytesRead === 0) break;\n filled += bytesRead;\n }\n return buffer.subarray(0, filled);\n}\n\n/** The whole of an opened file, as its descriptor reports it now. */\nexport function readWhole(descriptor: number): Buffer {\n return readRange(descriptor, 0, fstatSync(descriptor).size);\n}\n\n/** The physical lines of `buffer`, which ends with a newline or is empty. */\nfunction* linesOf(buffer: Buffer): Generator<string> {\n let start = 0;\n while (start < buffer.length) {\n const end = buffer.indexOf(NEWLINE_BYTE, start);\n const stop = end === -1 ? buffer.length : end;\n yield buffer.toString('utf8', start, stop);\n start = stop + 1;\n }\n}\n\nfunction writeAll(descriptor: number, text: string): void {\n const bytes = Buffer.from(text, 'utf8');\n let written = 0;\n while (written < bytes.length) {\n written += writeSync(descriptor, bytes, written, bytes.length - written);\n }\n}\n\n/** Batches writes to one descriptor. */\nclass BatchedWriter {\n #parts: string[] = [];\n #length = 0;\n\n constructor(private readonly descriptor: number) {}\n\n write(text: string): void {\n this.#parts.push(text);\n this.#length += text.length;\n if (this.#length >= WRITE_BATCH_BYTES) this.flush();\n }\n\n flush(): void {\n if (this.#parts.length === 0) return;\n writeAll(this.descriptor, this.#parts.join(''));\n this.#parts = [];\n this.#length = 0;\n }\n}\n\n/** Flushes a directory's entries where the platform can (not on Windows). */\nfunction flushDirectory(directory: string): void {\n let descriptor: number | undefined;\n try {\n descriptor = openSync(directory, constants.O_RDONLY);\n fsyncSync(descriptor);\n } catch {\n // allow-fallback: a directory that cannot be opened or flushed (Windows) still holds the rename.\n } finally {\n if (descriptor !== undefined) closeSync(descriptor);\n }\n}\n\nfunction removeQuietly(path: string): void {\n try {\n unlinkSync(path);\n } catch {\n // allow-fallback: a temporary file that cannot be removed holds rewritten lines only.\n }\n}\n\n/** Plain lines in the file now, read by path as every reader reads it. */\nfunction countPlainLines(path: string): number {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return 0;\n try {\n return splitStoredLines(readWhole(opened.descriptor).toString('utf8')).filter(\n (line) => line.length > 0 && !isSealedArtifactLine(line),\n ).length;\n } finally {\n closeSync(opened.descriptor);\n }\n}\n\n/** What turns a file's physical lines into the lines its replacement holds. */\nexport interface IAppendFileRewriter {\n /**\n * Writes what `lines` become to `out`. `leadingNewline` is set for the catch-up, which appends to\n * a file other writers append to at the same time and so frames each line `\\n<line>\\n`.\n */\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void;\n}\n\nexport interface IRewriteAppendFileOptions {\n /** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */\n readonly ownedRoot?: string;\n /**\n * Runs once the snapshot is rewritten and flushed, just before the new file replaces the old.\n * Throwing abandons the rewrite with nothing replaced and no temporary file left.\n */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over the replaced file; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n}\n\n/** A catch-up that failed after the rename: what of the replaced file it never carried over. */\nexport interface IAppendFileCatchUpFailure {\n /** Bytes of the replaced file past the last line carried over (at most what was lost). */\n readonly unreadBytes: number;\n /** The failure's name; never its message, which may quote what was being rewritten. */\n readonly reason: string;\n}\n\nexport type TAppendFileRewrite =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TAppendFileRefusal }\n /** Another rewrite replaced the file before this one's rename; nothing was replaced. */\n | { readonly status: 'replaced' }\n /** The file was deleted before this one's rename; nothing was put in its place. */\n | { readonly status: 'removed' }\n | {\n readonly status: 'rewritten';\n /** The replaced file ended in an unfinished sealed append, which no reader shows; it was dropped. */\n readonly droppedTail: boolean;\n /** Plain lines in the new file afterwards: plaintext an older build appended meanwhile. */\n readonly plainLinesAfter: number;\n /** Set when the catch-up failed part-way. */\n readonly catchUpFailed?: IAppendFileCatchUpFailure;\n };\n\n/**\n * Reads the replaced file from `progress.read` until it stays quiet, rewriting what it finds onto\n * `out` and moving `progress.read` past each chunk carried over. Returns whether it ended in an\n * unfinished sealed append, which was dropped.\n */\nasync function catchUp(\n oldDescriptor: number,\n progress: { read: number },\n rewriter: IAppendFileRewriter,\n out: BatchedWriter,\n pause: () => Promise<void>,\n): Promise<boolean> {\n let quiet = 0;\n for (;;) {\n const size = fstatSync(oldDescriptor).size;\n if (size > progress.read) {\n const chunk = readRange(oldDescriptor, progress.read, size);\n const end = chunk.lastIndexOf(NEWLINE_BYTE);\n if (end !== -1) {\n rewriter.rewrite(linesOf(chunk.subarray(0, end + 1)), out, true);\n out.flush();\n progress.read += end + 1;\n quiet = 0;\n continue;\n }\n }\n if (quiet >= QUIET_PASSES) break;\n quiet += 1;\n await pause();\n }\n const end = fstatSync(oldDescriptor).size;\n const tail = readRange(oldDescriptor, progress.read, end).toString('utf8');\n if (tail.length === 0) return false;\n // A writer that stayed quiet this long is gone: a plain fragment is still a line to rewrite, while\n // a sealed one is what no reader shows (`splitStoredLines`).\n const kept = splitStoredLines(tail);\n rewriter.rewrite(kept, out, true);\n out.flush();\n progress.read = end;\n return kept.length === 0;\n}\n\n/** What `path` names now, by `lstat`: the file open at `descriptor`, another one, or nothing. */\nfunction namedNow(path: string, descriptor: number): 'same' | 'replaced' | 'removed' {\n let linked;\n try {\n linked = lstatSync(path);\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return 'removed';\n throw error;\n }\n const opened = fstatSync(descriptor);\n return linked.isFile() && linked.ino === opened.ino && linked.dev === opened.dev\n ? 'same'\n : 'replaced';\n}\n\nfunction waitBriefly(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, DEFAULT_PAUSE_MS));\n}\n\n/** Rewrites the file at `path` through `rewriter`, as the module documentation describes. */\nexport async function rewriteAppendFile(\n path: string,\n rewriter: IAppendFileRewriter,\n options: IRewriteAppendFileOptions = {},\n): Promise<TAppendFileRewrite> {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return opened;\n const oldDescriptor = opened.descriptor;\n const directory = dirname(path);\n const temporary = join(\n directory,\n `.${basename(path)}.${process.pid}.${randomBytes(TEMPORARY_SUFFIX_BYTES).toString('hex')}.tmp`,\n );\n let newDescriptor: number | undefined;\n let committed = false;\n try {\n const buffer = readWhole(oldDescriptor);\n const snapshotEnd = buffer.lastIndexOf(NEWLINE_BYTE) + 1;\n ensureOwnerOnlyDirectory(\n directory,\n options.ownedRoot === undefined ? {} : { withinRoot: options.ownedRoot },\n );\n newDescriptor = openSync(\n temporary,\n constants.O_WRONLY |\n constants.O_CREAT |\n constants.O_EXCL |\n constants.O_APPEND |\n (constants.O_NOFOLLOW ?? 0),\n OWNER_ONLY_FILE_MODE,\n );\n const out = new BatchedWriter(newDescriptor);\n rewriter.rewrite(linesOf(buffer.subarray(0, snapshotEnd)), out, false);\n out.flush();\n fsyncSync(newDescriptor);\n options.beforeCommit?.();\n const named = namedNow(path, oldDescriptor);\n if (named !== 'same') return { status: named };\n renameSync(temporary, path);\n committed = true;\n flushDirectory(directory);\n const progress = { read: snapshotEnd };\n let droppedTail = false;\n let catchUpFailed: IAppendFileCatchUpFailure | undefined;\n try {\n droppedTail = await catchUp(\n oldDescriptor,\n progress,\n rewriter,\n out,\n options.pause ?? waitBriefly,\n );\n } catch (error) {\n catchUpFailed = {\n unreadBytes: Math.max(0, fstatSync(oldDescriptor).size - progress.read),\n reason: error instanceof Error ? error.name : 'unknown',\n };\n }\n fsyncSync(newDescriptor);\n return {\n status: 'rewritten',\n droppedTail,\n plainLinesAfter: countPlainLines(path),\n ...(catchUpFailed === undefined ? {} : { catchUpFailed }),\n };\n } finally {\n if (newDescriptor !== undefined) closeSync(newDescriptor);\n closeSync(oldDescriptor);\n if (!committed) removeQuietly(temporary);\n }\n}\n","/**\n * Temporary files a crashed write left in a store directory of the user state (#3679): an atomic\n * owner-only write's `<name>.<pid>.tmp`, and an append-file rewrite's `.<name>.<pid>.<hex>.tmp`. Such\n * a file may hold a whole record in plaintext, and nothing ever reads or removes it.\n *\n * A write takes milliseconds, so a temp file five minutes older than its directory's last change is\n * a crash's; the age is judged by the filesystem's clock, never this process's. A directory nothing\n * changed in since the crash would keep its temp file forever by that rule, so a migration also\n * judges it against when the migration started, as its marker's time on the filesystem says. A temp\n * file of this process is never one. Only regular files are considered, and links are never followed.\n */\nimport { lstatSync, readdirSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst STALE_TEMPORARY_AGE_MS = 5 * 60 * 1000;\nconst OWNER_ONLY_TEMPORARY = /^.+\\.(\\d{1,10})\\.tmp$/u;\nconst REWRITE_TEMPORARY = /^\\..+\\.(\\d{1,10})\\.[0-9a-f]{12}\\.tmp$/u;\n\nfunction ownerPidOf(name: string): number | undefined {\n const digits = (REWRITE_TEMPORARY.exec(name) ?? OWNER_ONLY_TEMPORARY.exec(name))?.[1];\n return digits === undefined ? undefined : Number(digits);\n}\n\n/**\n * The stale temp files in `directory`, removed when `apply` is set; returns how many there were.\n * `migrationStartedAt` is when a migration started, in milliseconds by the filesystem's clock; a temp\n * file older than it or than the directory's last change by the stale age is stale. A directory that\n * is missing or is not a real directory has none.\n */\nexport function sweepStaleStoreTemporaries(\n directory: string,\n apply: boolean,\n migrationStartedAt?: number,\n): number {\n let now: number;\n let names: string[];\n try {\n const metadata = lstatSync(directory);\n if (!metadata.isDirectory()) return 0;\n now = Math.max(metadata.mtimeMs, migrationStartedAt ?? Number.NEGATIVE_INFINITY);\n names = readdirSync(directory);\n } catch {\n return 0; // allow-fallback: a directory that cannot be listed has nothing to sweep.\n }\n let swept = 0;\n for (const name of names) {\n const pid = ownerPidOf(name);\n if (pid === undefined || pid === process.pid) continue;\n const path = join(directory, name);\n try {\n const metadata = lstatSync(path);\n if (!metadata.isFile() || now - metadata.mtimeMs <= STALE_TEMPORARY_AGE_MS) continue;\n if (apply) unlinkSync(path);\n swept += 1;\n } catch {\n // allow-fallback: one removed meanwhile, or one that cannot be removed, is retried next time.\n }\n }\n return swept;\n}\n","/**\n * Replacing many files of a store directory at once, for the converters that seal session records and\n * session-log sidecars in place (#3679). A file is read without following a link; every replacement\n * is written and flushed beside its file before any is put in place, so no rename is published ahead\n * of content not yet flushed; and each replace is atomic, made only while the file still holds the\n * bytes that were read under no other name, so a file a running process saved meanwhile is left.\n */\nimport { createHash } from 'node:crypto';\nimport { closeSync, constants, fstatSync, openSync } from 'node:fs';\n\nimport { stageOwnerOnlyFile } from '@robota-sdk/agent-core/node';\n\nimport { readWhole } from './append-file-rewrite.js';\n\nimport type { IStagedOwnerOnlyFile } from '@robota-sdk/agent-core/node';\n\n/** A regular file's bytes and how many names it has. */\nexport interface IRegularFile {\n readonly bytes: Buffer;\n readonly links: number;\n}\n\n/** The regular file at `path`, never through a link; `undefined` when it is gone or not regular. */\nexport function readRegularFile(path: string): IRegularFile | undefined {\n let descriptor: number;\n try {\n descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n try {\n const metadata = fstatSync(descriptor);\n return metadata.isFile() ? { bytes: readWhole(descriptor), links: metadata.nlink } : undefined;\n } finally {\n closeSync(descriptor);\n }\n}\n\nconst digestOf = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');\n\n/** Stages one replacement: the file at `path`, read as `read`, is to hold `content`. */\nexport type TStageReplacement = (path: string, read: Uint8Array, content: Uint8Array) => void;\n\n/** What {@link replaceStagedFiles} put in place, and what it left because it changed meanwhile. */\nexport interface IStagedReplacements {\n readonly replaced: number;\n readonly changed: readonly string[];\n}\n\n/** A file changed between the read and its replace; that replace is abandoned. */\nclass FileChangedError extends Error {\n constructor() {\n super('The file changed while its replacement was staged.');\n this.name = 'FileChangedError';\n }\n}\n\n/**\n * Runs `collect`, which stages the replacements, then puts each in place, as the module\n * documentation describes; `beforeCommit` runs first each time, and a throw stops there with that\n * file as it was. Whatever is not put in place is removed, however this ends.\n */\nexport function replaceStagedFiles(\n collect: (stage: TStageReplacement) => void,\n beforeCommit?: () => void,\n aroundCommit: (path: string, commit: () => void) => boolean = (_path, commit) => {\n commit();\n return true;\n },\n): IStagedReplacements {\n const staged: { path: string; file: IStagedOwnerOnlyFile; digest: string }[] = [];\n const changed: string[] = [];\n let replaced = 0;\n try {\n collect((path, read, content) => {\n staged.push({ path, file: stageOwnerOnlyFile(path, content), digest: digestOf(read) });\n });\n for (const { path, file, digest } of staged) {\n try {\n const committed = aroundCommit(path, () =>\n file.commit(() => {\n beforeCommit?.();\n const now = readRegularFile(path);\n if (now === undefined || now.links > 1 || digestOf(now.bytes) !== digest) {\n throw new FileChangedError();\n }\n }),\n );\n if (committed) replaced += 1;\n else changed.push(path);\n } catch (error) {\n if (!(error instanceof FileChangedError)) throw error;\n changed.push(path);\n }\n }\n } finally {\n for (const { file } of staged) file.discard();\n }\n return { replaced, changed };\n}\n","/**\n * Sealing in place the session logs a host-filesystem sink (`NodeSessionLogSink`) wrote in plaintext,\n * for a host migrating its state once it seals (#3679): every `<id>.jsonl` in one directory, with its\n * `<id>.payloads/` sidecars.\n *\n * - A log becomes one sealed record per non-empty plain line, its sealed lines kept as they are and\n * its empty lines dropped, so a stored-record offset (`/agent read`) names the same line after.\n * - A sidecar is sealed in place under its existing name: a source opens sealed bytes whatever the\n * name, and the digest and length a reference checks are the plaintext's, so no reference — nested\n * ones inside other payloads included — changes. A migrated sidecar keeps its content-derived name.\n * - Order: every sidecar of a session sealed beside it and flushed, then each put in place (atomic,\n * made only while the file still holds what was read), then its log, which is the commit. A log is\n * rewritten as the prompt history is compacted (`append-file-rewrite.ts`), so a line a writer\n * appends meanwhile is caught up; a plain line that still reaches it afterwards, a log another\n * rewrite replaced first, or a plain sidecar that appears, is reported for a rerun, and a catch-up\n * that failed with how much it left unread. A log deleted meanwhile stays deleted, and uncounted.\n *\n * Whether something is done is decided by whether it opens for its session, never by framing alone; a\n * sealed sidecar that does not open here is left as it is and reported. Links, anything that is not a\n * regular file or directory, and a file with another hard link (replacing it would leave the\n * plaintext under the other name) are never followed or touched.\n */\nimport { closeSync, lstatSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { openAppendFile, readWhole, rewriteAppendFile } from './append-file-rewrite.js';\nimport { isSafeSessionId } from './session-id.js';\nimport { sweepStaleStoreTemporaries } from './stale-temporaries.js';\nimport { readRegularFile, replaceStagedFiles } from './staged-replacements.js';\nimport {\n SessionLogLineSealer,\n countPlainSessionLogLines,\n sealStoredPayloadBytes,\n storedPayloadMigrationState,\n} from './stored-append-lines.js';\n\nimport type { IAppendFileCatchUpFailure } from './append-file-rewrite.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst LOG_EXTENSION = '.jsonl';\nconst PAYLOAD_DIRECTORY_SUFFIX = '.payloads';\nconst TEMPORARY_SUFFIX = '.tmp';\n\nexport interface IMigrateSessionLogsOptions {\n /** Seals what is plain and opens what is sealed; the keys must be unlocked. */\n readonly sealer: IArtifactSealer;\n /** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */\n readonly apply: boolean;\n /** An ancestor of the directory the host also owns, tightened along with it (SEC-020). */\n readonly ownedRoot?: string;\n /** Runs before each sidecar replace and each log replace; throwing stops the migration there. */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over a replaced log; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n /**\n * Also removes the temp files crashed writes left; leave it off while another writer may be running.\n * Off, they are still counted in `staleTemporaries` and left where they are.\n */\n readonly sweepTemporaries?: boolean;\n /**\n * When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp\n * file is also judged stale against it, so one in a directory nothing changed in since is found.\n */\n readonly migrationStartedAt?: number;\n}\n\n/** A log whose catch-up failed after its rename, and what of the replaced file it left unread. */\nexport interface ISessionLogCatchUpFailure extends IAppendFileCatchUpFailure {\n readonly path: string;\n}\n\n/** What a migration of one directory's logs did, or (planning) would do. */\nexport interface ISessionLogMigration {\n /** Logs that held plain lines: sealed (or to seal). */\n readonly logs: number;\n /** The plain lines among them. */\n readonly lines: number;\n /** Plain sidecars sealed in place (or to seal). */\n readonly payloads: number;\n /** Logs and sidecars that hold no plaintext. */\n readonly alreadySealed: number;\n /** Sealed sidecars that do not open for their session, or that a newer build sealed: left as they are. */\n readonly unopenable: readonly string[];\n /**\n * Links, entries that are not regular, logs and sidecars with another hard link, names that are not\n * session ids.\n */\n readonly refused: readonly string[];\n /**\n * Logs and sidecars that changed while being sealed, that plaintext reached meanwhile, or that\n * another rewrite replaced first.\n */\n readonly changed: readonly string[];\n /** Logs sealed whose catch-up failed: lines appended to the replaced file meanwhile are lost. */\n readonly catchUpFailed: readonly ISessionLogCatchUpFailure[];\n /** Temp files crashed writes left: removed (or to remove) with `sweepTemporaries`, else left. */\n readonly staleTemporaries: number;\n}\n\ninterface IMutableLogMigration {\n logs: number;\n lines: number;\n payloads: number;\n alreadySealed: number;\n unopenable: string[];\n refused: string[];\n changed: string[];\n catchUpFailed: ISessionLogCatchUpFailure[];\n staleTemporaries: number;\n}\n\n/** The entries of `directory` by kind as `lstat` sees them; none when it is missing. */\nfunction entriesOf(directory: string): ReadonlyMap<string, 'file' | 'directory' | 'other'> {\n const kinds = new Map<string, 'file' | 'directory' | 'other'>();\n let names: string[];\n try {\n names = readdirSync(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return kinds;\n throw error;\n }\n for (const name of names.sort()) {\n try {\n const metadata = lstatSync(join(directory, name));\n kinds.set(name, metadata.isFile() ? 'file' : metadata.isDirectory() ? 'directory' : 'other');\n } catch (error) {\n // allow-fallback: an entry removed since the listing is simply not there.\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n return kinds;\n}\n\n/** The sessions a directory holds logs or sidecars for, each with which of the two it has. */\nfunction sessionsOf(\n directory: string,\n result: IMutableLogMigration,\n): Map<string, { log: boolean; payloads: boolean }> {\n const sessions = new Map<string, { log: boolean; payloads: boolean }>();\n for (const [name, kind] of entriesOf(directory)) {\n const isLog = name.endsWith(LOG_EXTENSION);\n const isPayloads = name.endsWith(PAYLOAD_DIRECTORY_SUFFIX);\n if (!isLog && !isPayloads) continue;\n const id = name.slice(0, -(isLog ? LOG_EXTENSION : PAYLOAD_DIRECTORY_SUFFIX).length);\n if ((isLog ? kind !== 'file' : kind !== 'directory') || !isSafeSessionId(id)) {\n result.refused.push(join(directory, name));\n continue;\n }\n const session = sessions.get(id) ?? { log: false, payloads: false };\n if (isLog) session.log = true;\n else session.payloads = true;\n sessions.set(id, session);\n }\n return sessions;\n}\n\n/** Seals one session's plain sidecars in place; returns the names it saw, for the check after the log. */\nfunction migratePayloads(\n directory: string,\n ownerId: string,\n options: IMigrateSessionLogsOptions,\n result: IMutableLogMigration,\n): ReadonlySet<string> {\n const seen = new Set<string>();\n const replacements = replaceStagedFiles((stage) => {\n for (const [name, kind] of entriesOf(directory)) {\n if (name.endsWith(TEMPORARY_SUFFIX)) continue;\n seen.add(name);\n const path = join(directory, name);\n if (kind !== 'file') {\n result.refused.push(path);\n continue;\n }\n const read = readRegularFile(path);\n if (read === undefined) continue;\n const state = storedPayloadMigrationState(read.bytes, ownerId, options.sealer);\n if (state === 'sealed') result.alreadySealed += 1;\n else if (state !== 'plain') result.unopenable.push(path);\n if (state !== 'plain') continue;\n if (read.links > 1) {\n result.refused.push(path);\n continue;\n }\n if (!options.apply) {\n result.payloads += 1;\n continue;\n }\n stage(path, read.bytes, sealStoredPayloadBytes(read.bytes, ownerId, options.sealer));\n }\n }, options.beforeCommit);\n result.payloads += replacements.replaced;\n result.changed.push(...replacements.changed);\n result.staleTemporaries += sweepStaleStoreTemporaries(\n directory,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return seen;\n}\n\n/** Plain sidecars that appeared in `directory` since `seen` was taken: what a writer added meanwhile. */\nfunction plainPayloadsSince(\n directory: string,\n ownerId: string,\n seen: ReadonlySet<string>,\n sealer: IArtifactSealer,\n): string[] {\n const appeared: string[] = [];\n for (const [name, kind] of entriesOf(directory)) {\n if (seen.has(name) || name.endsWith(TEMPORARY_SUFFIX) || kind !== 'file') continue;\n const read = readRegularFile(join(directory, name));\n if (\n read !== undefined &&\n storedPayloadMigrationState(read.bytes, ownerId, sealer) === 'plain'\n ) {\n appeared.push(join(directory, name));\n }\n }\n return appeared;\n}\n\n/** The plain lines in the log at `path`, or why it is not a log this migration rewrites. */\nfunction plainLinesOf(path: string): number | 'missing' | 'refused' {\n const opened = openAppendFile(path);\n if (opened.status === 'missing') return 'missing';\n if (opened.status === 'refused') return 'refused';\n try {\n return countPlainSessionLogLines(readWhole(opened.descriptor).toString('utf8'));\n } finally {\n closeSync(opened.descriptor);\n }\n}\n\nasync function migrateLog(\n path: string,\n ownerId: string,\n options: IMigrateSessionLogsOptions,\n result: IMutableLogMigration,\n): Promise<void> {\n const plain = plainLinesOf(path);\n if (plain === 'missing') return;\n if (plain === 'refused') {\n result.refused.push(path);\n return;\n }\n if (plain === 0) {\n result.alreadySealed += 1;\n return;\n }\n const rewritten = !options.apply\n ? undefined\n : await rewriteAppendFile(path, new SessionLogLineSealer(options.sealer, ownerId), {\n ...(options.ownedRoot === undefined ? {} : { ownedRoot: options.ownedRoot }),\n ...(options.beforeCommit === undefined ? {} : { beforeCommit: options.beforeCommit }),\n ...(options.pause === undefined ? {} : { pause: options.pause }),\n });\n // Deleted since it was counted: nothing of it is left to seal, and nothing is put back.\n if (rewritten?.status === 'missing' || rewritten?.status === 'removed') return;\n result.logs += 1;\n result.lines += plain;\n if (rewritten === undefined) return;\n if (rewritten.status === 'refused') result.refused.push(path);\n else if (rewritten.status === 'replaced') result.changed.push(path);\n else if (rewritten.status === 'rewritten') {\n if (rewritten.plainLinesAfter > 0) result.changed.push(path);\n if (rewritten.catchUpFailed !== undefined) {\n result.catchUpFailed.push({ path, ...rewritten.catchUpFailed });\n }\n }\n}\n\n/** Seals the plain logs and sidecars of `directory` as the module documentation describes. */\nexport async function migrateSessionLogDirectory(\n directory: string,\n options: IMigrateSessionLogsOptions,\n): Promise<ISessionLogMigration> {\n const result: IMutableLogMigration = {\n logs: 0,\n lines: 0,\n payloads: 0,\n alreadySealed: 0,\n unopenable: [],\n refused: [],\n changed: [],\n catchUpFailed: [],\n staleTemporaries: 0,\n };\n try {\n if (!lstatSync(directory).isDirectory()) {\n result.refused.push(directory);\n return result;\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return result;\n throw error;\n }\n for (const [id, session] of sessionsOf(directory, result)) {\n const payloadDirectory = join(directory, `${id}${PAYLOAD_DIRECTORY_SUFFIX}`);\n const seen = session.payloads\n ? migratePayloads(payloadDirectory, id, options, result)\n : new Set<string>();\n if (session.log)\n await migrateLog(join(directory, `${id}${LOG_EXTENSION}`), id, options, result);\n if (options.apply && session.payloads) {\n result.changed.push(...plainPayloadsSince(payloadDirectory, id, seen, options.sealer));\n }\n }\n result.staleTemporaries += sweepStaleStoreTemporaries(\n directory,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return result;\n}\n","/**\n * Sealing in place the records a host-filesystem session store (`NodeSessionStore`) kept in\n * plaintext, for a host migrating its state once it seals (#3679).\n *\n * Every plain record is sealed as the raw bytes it holds — valid, corrupt, unsupported or bare alike —\n * under its own file stem, so the store reports the same status after opening it, and the plaintext is\n * gone. Whether a record is already done is decided by whether it opens for its id, never by framing\n * alone: a sealed record that does not open here, or that a newer build sealed, is left as it is and\n * reported. A file whose name is not a session id, anything that is not a regular file, and a file\n * with another hard link (replacing it would leave the plaintext under the other name) is never\n * touched, and neither is a record whose writer marker stands for a process not provably gone: it may\n * be saving it now. Every sealed record is written beside its file and flushed before any is put in\n * place; each replace is then atomic, and is made only if the file still holds the bytes that were\n * read, so a record a running process saved meanwhile is left for the next run.\n */\nimport { createProcessOperationLock } from '@robota-sdk/agent-node-lock';\n\nimport { lstatSync, readdirSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\nimport { createProcessMarkers } from '@robota-sdk/agent-core/node';\n\nimport { isSafeSessionId } from './session-id.js';\nimport { SESSION_CLAIMS_DIRECTORY } from './session-store.js';\nimport { sweepStaleStoreTemporaries } from './stale-temporaries.js';\nimport { readRegularFile, replaceStagedFiles } from './staged-replacements.js';\nimport { openStoredSessionRecord } from './stored-session-authorship.js';\nimport {\n digestOf,\n isPlainSessionRecordOf,\n sealStoredSessionBytes,\n storedSessionMigrationState,\n} from './stored-session-codec.js';\n\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst RECORD_EXTENSION = '.json';\n\nexport interface IMigrateSessionRecordsOptions {\n /** Seals what is plain and opens what is sealed; the keys must be unlocked. */\n readonly sealer: IArtifactSealer;\n /** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */\n readonly apply: boolean;\n /**\n * Runs before each replace, after the sealed record is written and flushed beside it; throwing\n * stops the migration there with that record as it was.\n */\n readonly beforeCommit?: () => void;\n /**\n * Also removes the temp files crashed writes left; leave it off while another writer may be running.\n * Off, they are still counted in `staleTemporaries` and left where they are.\n */\n readonly sweepTemporaries?: boolean;\n /**\n * When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp\n * file is also judged stale against it, so one in a directory nothing changed in since is found.\n */\n readonly migrationStartedAt?: number;\n}\n\n/** What a migration of a store's records did, or (planning) would do. */\nexport interface ISessionRecordMigration {\n /** Plain records sealed (or to seal). */\n readonly sealed: number;\n /** Records that already open sealed. */\n readonly alreadySealed: number;\n /** Sealed records that do not open here, or that a newer build sealed: left as they are. */\n readonly unopenable: readonly string[];\n /** `.json` files whose name is not a session id: left as they are. */\n readonly unsafeNames: readonly string[];\n /**\n * `.json` entries that are links, not regular files, or regular files with another hard link:\n * never followed or replaced, left as they are.\n */\n readonly refused: readonly string[];\n /** Records that changed while being sealed: left as they are, for the next run. */\n readonly changed: readonly string[];\n /**\n * The ids of records open in another process — their writer marker stands for an owner not\n * provably gone — left as they are, for a run after it has closed.\n */\n readonly held: readonly string[];\n /** Temp files crashed writes left with a record in them: removed (or to remove) with `sweepTemporaries`, else left. */\n readonly staleTemporaries: number;\n /**\n * Each record this run sealed, by its id and the SHA-256 of the sealed bytes it put in place — the\n * bytes the owner may then vouch for (`ISessionAuthorship.adopt`), and no others.\n * Empty when planning.\n */\n readonly sealedRecords: readonly ISealedSessionRecord[];\n}\n\n/** One record a migration sealed: its id and the SHA-256 (lowercase hex) of the bytes it wrote. */\nexport interface ISealedSessionRecord {\n readonly id: string;\n readonly digest: string;\n}\n\n/** The `.json` entries of `baseDir`, each a regular file or refused; none when the directory is missing. */\nfunction recordEntries(baseDir: string): { readonly files: string[]; readonly refused: string[] } {\n const files: string[] = [];\n const refused: string[] = [];\n let names: string[];\n try {\n if (!lstatSync(baseDir).isDirectory()) return { files, refused: [baseDir] };\n names = readdirSync(baseDir);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { files, refused };\n throw error;\n }\n for (const name of names.sort()) {\n if (!name.endsWith(RECORD_EXTENSION)) continue;\n try {\n (lstatSync(join(baseDir, name)).isFile() ? files : refused).push(name);\n } catch (error) {\n // allow-fallback: an entry removed since the listing is simply not there.\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n return { files, refused: refused.map((name) => join(baseDir, name)) };\n}\n\n/** Seals the plain records of the store at `baseDir` as the module documentation describes. */\nexport function migrateNodeSessionStore(\n baseDir: string,\n options: IMigrateSessionRecordsOptions,\n): ISessionRecordMigration {\n const { files, refused } = recordEntries(baseDir);\n const result = {\n sealed: 0,\n alreadySealed: 0,\n unopenable: [] as string[],\n unsafeNames: [] as string[],\n refused,\n changed: [] as string[],\n held: [] as string[],\n staleTemporaries: 0,\n sealedRecords: [] as ISealedSessionRecord[],\n };\n const markers = createProcessMarkers(join(baseDir, SESSION_CLAIMS_DIRECTORY), {\n operationLock: createProcessOperationLock(join(baseDir, SESSION_CLAIMS_DIRECTORY)),\n });\n const staged: (ISealedSessionRecord & { readonly path: string })[] = [];\n const replacements = replaceStagedFiles(\n (stage) => {\n for (const name of files) {\n const path = join(baseDir, name);\n const id = name.slice(0, -RECORD_EXTENSION.length);\n if (!isSafeSessionId(id)) {\n result.unsafeNames.push(path);\n continue;\n }\n const writer = markers.inspect(id);\n if (writer.status === 'held' && writer.holder.liveness !== 'gone') {\n result.held.push(id);\n continue;\n }\n const read = readRegularFile(path);\n if (read === undefined) continue;\n const state = storedSessionMigrationState(id, read.bytes, options.sealer);\n if (state === 'sealed') result.alreadySealed += 1;\n else if (state !== 'plain') result.unopenable.push(path);\n if (state !== 'plain') continue;\n if (read.links > 1) {\n result.refused.push(path);\n continue;\n }\n if (!options.apply) {\n result.sealed += 1;\n continue;\n }\n const sealed = sealStoredSessionBytes(id, read.bytes, options.sealer);\n // Only a record that reads as this session's is a session the owner could mark as theirs; a\n // corrupt or unsupported one is sealed as it is and stays what it was.\n if (isPlainSessionRecordOf(id, read.bytes)) {\n staged.push({ path, id, digest: digestOf(sealed) });\n }\n stage(path, read.bytes, sealed);\n }\n },\n options.beforeCommit,\n (path, commit) => {\n const id = basename(path).slice(0, -RECORD_EXTENSION.length);\n const acquired = markers.acquire(id);\n if (acquired.status !== 'acquired') {\n result.held.push(id);\n return false;\n }\n try {\n const guarded = acquired.handle.withGuard(commit);\n if (guarded.status !== 'ran') {\n result.held.push(id);\n return false;\n }\n return true;\n } finally {\n acquired.handle.release();\n }\n },\n );\n result.sealed += replacements.replaced;\n result.changed.push(...replacements.changed);\n // Every staged record was put in place but those that changed meanwhile, since anything else\n // that stops a replacement throws out of it.\n for (const { path, id, digest } of staged) {\n if (!replacements.changed.includes(path)) result.sealedRecords.push({ id, digest });\n }\n result.staleTemporaries = sweepStaleStoreTemporaries(\n baseDir,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return result;\n}\n\n/**\n * The working directories the store's records name — a valid record's `cwd`, and the `cwd` a corrupt\n * or unsupported one still carries — opened with `sealer`, distinct, in file order. Writes nothing;\n * a record that does not open names none.\n */\nexport function readSessionStoreWorkingDirectories(\n baseDir: string,\n options: { readonly sealer: IArtifactSealer },\n): readonly string[] {\n const directories = new Set<string>();\n for (const name of recordEntries(baseDir).files) {\n const id = name.slice(0, -RECORD_EXTENSION.length);\n if (!isSafeSessionId(id)) continue;\n const read = readRegularFile(join(baseDir, name));\n if (read === undefined) continue;\n const outcome = openStoredSessionRecord(id, read.bytes, options.sealer);\n const cwd =\n outcome.status === 'valid'\n ? outcome.record.cwd\n : outcome.status === 'corrupt' || outcome.status === 'unsupported'\n ? outcome.cwd\n : undefined;\n if (typeof cwd === 'string' && cwd.length > 0) directories.add(cwd);\n }\n return [...directories];\n}\n","import { randomBytes } from 'node:crypto';\nimport {\n closeSync,\n constants,\n fsyncSync,\n linkSync,\n lstatSync,\n openSync,\n readdirSync,\n rmSync,\n rmdirSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\nimport { SEALED_ARTIFACT_MAX_OVERHEAD_BYTES } from '@robota-sdk/agent-core';\nimport {\n PROCESS_OWNER_RECORD_NAME,\n createProcessScopedDirectory,\n removeDirectoryOnProcessExit,\n sweepStaleProcessScopedDirectories,\n} from '@robota-sdk/agent-core/node';\n\nimport { NodeExternalPayloadSource } from './session-log-sources.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type {\n IArtifactContext,\n IArtifactSealer,\n IToolResultSpillStore,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\n\nconst REFERENCE = /^tool-result:([A-Za-z0-9_-]{22,64})$/u;\n/**\n * The product-neutral name every spill directory starts with; one in a host's parent adds its owner's\n * pid before the random suffix.\n */\nconst TOOL_RESULT_SPILL_DIRECTORY_PREFIX = 'agent-tool-results-';\nconst DEFAULT_RETENTION_MS = 60 * 60 * 1000;\nconst MAX_SPILL_BYTES = 8 * 1024 * 1024;\nconst FILE_FLAGS =\n constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);\n\nexport type TToolResultSpillErrorCode =\n | 'invalid-options'\n | 'unsafe-root'\n | 'write-failed'\n | 'missing'\n | 'expired'\n | 'invalid-reference'\n | 'read-failed'\n | 'cleanup-failed'\n | 'closed';\n\nexport class ToolResultSpillError extends Error {\n constructor(readonly code: TToolResultSpillErrorCode) {\n super(`Tool result spill failed (${code})`);\n this.name = 'ToolResultSpillError';\n }\n}\n\n/**\n * Remove the spill directories under `parent` whose recorded owner can no longer be alive (a process\n * killed before `shutdown()` or its exit removal could run). Returns the names removed.\n */\nexport function sweepStaleToolResultSpillDirectories(parent: string): readonly string[] {\n return sweepStaleProcessScopedDirectories(parent, TOOL_RESULT_SPILL_DIRECTORY_PREFIX);\n}\n\nexport interface INodeToolResultSpillStoreOptions {\n /**\n * Host-owned, owner-only parent that already exists; it is used as it is. Spilled results are\n * conversation content, so they never go to a directory the store picks: the store's directory in\n * the parent records its owner, is removed when the process ends without reaching `shutdown()`,\n * and is removed after a kill by {@link sweepStaleToolResultSpillDirectories}.\n */\n readonly parentDirectory: string;\n readonly retentionMs?: number;\n readonly now?: () => number;\n /** Receives a fixed, payload-free reason if an idle expiry timer cannot delete a file. */\n readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;\n /**\n * Required. `{ sealer }` keeps every result sealed on disk, bound to its reference, as\n * `<token>.sealed`, and opens it with the same sealer to read it back; a result that does not open\n * reads as `read-failed`. `'plain'` writes each result as it is to `<token>.txt`.\n */\n readonly sealing: TArtifactSealing;\n}\n\ninterface IStoredResult {\n readonly fileName: string;\n /** The random part of the reference: what a sealed result is bound to. */\n readonly token: string;\n readonly expiresAt: number;\n}\n\n/**\n * The raw bytes a sealed result may take: the plaintext budget and what any sealer adds to it, so a\n * result of exactly the largest size a spill takes still reads back once sealed.\n */\nconst MAX_SEALED_SPILL_BYTES = MAX_SPILL_BYTES + SEALED_ARTIFACT_MAX_OVERHEAD_BYTES;\n\nfunction spillContext(token: string): IArtifactContext {\n return { kind: 'tool-result-spill', id: token };\n}\n\n/**\n * Node host implementation of core's opaque, session-lifetime spill port. A sealing store keeps the\n * last result it opened in memory, so paging through a large result opens it once.\n */\nexport class NodeToolResultSpillStore implements IToolResultSpillStore {\n private readonly directory: string;\n private readonly retentionMs: number;\n private readonly now: () => number;\n private readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;\n private readonly sealer: IArtifactSealer | undefined;\n private readonly entries = new Map<string, IStoredResult>();\n /** The result a sealing store opened last; forgotten with its entry. */\n private opened?: { readonly reference: string; readonly content: string };\n private expiryTimer?: ReturnType<typeof setTimeout>;\n private cancelExitRemoval?: () => void;\n private closed = false;\n\n constructor(options: INodeToolResultSpillStoreOptions) {\n // A caller that ignores the types gets the same refusal as one that passes a bad value.\n const parent = options?.parentDirectory;\n if (typeof parent !== 'string' || parent.length === 0) {\n throw new ToolResultSpillError('invalid-options');\n }\n this.retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;\n this.now = options.now ?? Date.now;\n this.onCleanupFailure = options.onCleanupFailure;\n if (!Number.isSafeInteger(this.retentionMs) || this.retentionMs <= 0) {\n throw new ToolResultSpillError('invalid-options');\n }\n let sealing: TArtifactSealing;\n try {\n sealing = sessionSealingOf(options);\n } catch {\n throw new ToolResultSpillError('invalid-options');\n }\n this.sealer = sealing === 'plain' ? undefined : sealing.sealer;\n try {\n const stat = lstatSync(parent);\n if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('unsafe parent');\n // The host's parent is used as it is; the directory made in it is owner-only and records its\n // owner before any result goes in.\n this.directory = createProcessScopedDirectory(parent, TOOL_RESULT_SPILL_DIRECTORY_PREFIX, {\n prepareParent: false,\n });\n this.assertRoot();\n } catch {\n throw new ToolResultSpillError('unsafe-root');\n }\n // `shutdown()` is the normal removal; a process that ends without reaching it (an exit, or a\n // signal nothing else handles) still leaves nothing, and one killed outright is swept at the next\n // start by its owner record.\n this.cancelExitRemoval = removeDirectoryOnProcessExit(this.directory);\n }\n\n private assertRoot(): void {\n try {\n const stat = lstatSync(this.directory);\n if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('unsafe directory');\n if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {\n throw new Error('directory is not owner-only');\n }\n } catch {\n throw new ToolResultSpillError('unsafe-root');\n }\n }\n\n private scheduleExpiry(minimumDelayMs = 1): void {\n if (this.expiryTimer !== undefined) clearTimeout(this.expiryTimer);\n this.expiryTimer = undefined;\n if (this.closed || this.entries.size === 0) return;\n let firstExpiry = Number.POSITIVE_INFINITY;\n for (const entry of this.entries.values()) {\n if (entry.expiresAt < firstExpiry) firstExpiry = entry.expiresAt;\n }\n const delay = Math.min(2_147_483_647, Math.max(minimumDelayMs, firstExpiry - this.now()));\n this.expiryTimer = setTimeout(() => {\n this.expiryTimer = undefined;\n void this.cleanupExpired().catch(() => {\n try {\n if (this.onCleanupFailure) this.onCleanupFailure('cleanup-failed');\n else process.emitWarning('Tool result spill expiry cleanup failed (cleanup-failed)');\n } catch {\n process.emitWarning('Tool result spill expiry cleanup failed (cleanup-failed)');\n }\n this.scheduleExpiry(60_000);\n });\n }, delay);\n this.expiryTimer.unref?.();\n }\n\n async write(content: string): Promise<{ readonly reference: string }> {\n if (this.closed) throw new ToolResultSpillError('closed');\n await this.cleanupExpired();\n this.assertRoot();\n if (Buffer.byteLength(content, 'utf8') > MAX_SPILL_BYTES) {\n throw new ToolResultSpillError('write-failed');\n }\n const token = randomBytes(18).toString('base64url');\n const tempName = `${randomBytes(18).toString('base64url')}.partial`;\n const fileName = `${token}${this.sealer === undefined ? '.txt' : '.sealed'}`;\n const tempPath = join(this.directory, tempName);\n const finalPath = join(this.directory, fileName);\n let data: string | Uint8Array;\n try {\n data = this.storedForm(content, token);\n } catch {\n throw new ToolResultSpillError('write-failed');\n }\n let fd: number | undefined;\n let linked = false;\n try {\n fd = openSync(tempPath, FILE_FLAGS, 0o600);\n writeFileSync(fd, data, 'utf8');\n fsyncSync(fd);\n closeSync(fd);\n fd = undefined;\n linkSync(tempPath, finalPath);\n linked = true;\n unlinkSync(tempPath);\n const reference = `tool-result:${token}`;\n this.entries.set(reference, { fileName, token, expiresAt: this.now() + this.retentionMs });\n this.scheduleExpiry();\n return { reference };\n } catch {\n let cleanupFailed = false;\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n cleanupFailed = true;\n }\n }\n try {\n unlinkSync(tempPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') cleanupFailed = true;\n }\n if (linked) {\n try {\n unlinkSync(finalPath);\n } catch {\n cleanupFailed = true;\n }\n }\n throw new ToolResultSpillError(cleanupFailed ? 'cleanup-failed' : 'write-failed');\n }\n }\n\n async read(reference: string): Promise<string> {\n if (this.closed) throw new ToolResultSpillError('closed');\n if (!REFERENCE.test(reference)) throw new ToolResultSpillError('invalid-reference');\n const entry = this.entries.get(reference);\n if (!entry) throw new ToolResultSpillError('missing');\n if (this.now() >= entry.expiresAt) {\n this.removeEntry(reference, entry);\n this.scheduleExpiry();\n throw new ToolResultSpillError('expired');\n }\n if (this.opened?.reference === reference) return this.opened.content;\n this.assertRoot();\n try {\n // The raw stored bytes: this store opens a sealed result itself, bound to its token, so the\n // payload source only reads the file.\n const bytes = new NodeExternalPayloadSource(this.directory, { sealing: 'plain' }).readBytes(\n entry.fileName,\n this.sealer === undefined ? MAX_SPILL_BYTES : MAX_SEALED_SPILL_BYTES,\n );\n if (bytes === undefined) throw new ToolResultSpillError('missing');\n if (this.sealer === undefined) return Buffer.from(bytes).toString('utf8');\n const content = this.openStored(this.sealer, bytes, entry.token);\n this.opened = { reference, content };\n return content;\n } catch (error) {\n if (error instanceof ToolResultSpillError) throw error;\n throw new ToolResultSpillError('read-failed');\n }\n }\n\n /** What goes on disk for `content`: the text as it is, or sealed to the result's token. */\n private storedForm(content: string, token: string): string | Uint8Array {\n if (this.sealer === undefined) return content;\n const plain = Buffer.from(content, 'utf8');\n try {\n return this.sealer.sealBytes(plain, spillContext(token));\n } finally {\n plain.fill(0);\n }\n }\n\n /**\n * The text a sealed result holds, held to the plaintext budget exactly. Any failure to open it —\n * damaged, moved from another reference, sealed with other keys — is the caller's `read-failed`.\n */\n private openStored(sealer: IArtifactSealer, stored: Uint8Array, token: string): string {\n const plain = sealer.openBytes(stored, spillContext(token));\n try {\n if (plain.byteLength > MAX_SPILL_BYTES) throw new ToolResultSpillError('read-failed');\n return Buffer.from(plain.buffer, plain.byteOffset, plain.byteLength).toString('utf8');\n } finally {\n plain.fill(0);\n }\n }\n\n /**\n * Removes a result. What a sealing store had open of it is forgotten first, so a removal that fails\n * does not keep it in memory; a file already gone counts as removed, or one result deleted from\n * outside would fail every later cleanup, and with it every write.\n */\n private removeEntry(reference: string, entry: IStoredResult): void {\n this.assertRoot();\n if (this.opened?.reference === reference) this.opened = undefined;\n try {\n unlinkSync(join(this.directory, entry.fileName));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new ToolResultSpillError('cleanup-failed');\n }\n }\n this.entries.delete(reference);\n }\n\n async cleanupExpired(): Promise<void> {\n if (this.closed) throw new ToolResultSpillError('closed');\n for (const [reference, entry] of this.entries) {\n if (this.now() >= entry.expiresAt) this.removeEntry(reference, entry);\n }\n this.scheduleExpiry();\n }\n\n async shutdown(): Promise<void> {\n if (this.closed) return;\n if (this.expiryTimer !== undefined) clearTimeout(this.expiryTimer);\n this.expiryTimer = undefined;\n this.opened = undefined;\n for (const [reference, entry] of this.entries) this.removeEntry(reference, entry);\n this.assertRoot();\n try {\n // The owner record goes last, so a directory a failed shutdown leaves can still be swept.\n const left = readdirSync(this.directory).filter((name) => name !== PROCESS_OWNER_RECORD_NAME);\n if (left.length !== 0) throw new Error('unexpected files');\n rmSync(join(this.directory, PROCESS_OWNER_RECORD_NAME), { force: true });\n rmdirSync(this.directory);\n this.closed = true;\n this.cancelExitRemoval?.();\n } catch {\n throw new ToolResultSpillError('cleanup-failed');\n }\n }\n}\n","/**\n * SCREEN-1993 — the prompt-history file at the path the host names (its user-level `history.jsonl`):\n * one JSON object per line, append-only, owner-only. A derived projection of what the session record\n * already holds, kept so the terminal UI can search prompts across sessions and projects without\n * decoding a record.\n *\n * Writes follow the `NodeSessionLogSink` regime (SEC-020): the directory is made owner-only when the\n * file is constructed, and every append tightens the file before writing with the owner-only mode.\n * Reads walk the file BACKWARDS in fixed blocks so the newest prompts are yielded first and the UI\n * can render them before the rest is read; the file is opened no-follow like every other session\n * file this package reads. A file built with a sealer appends each entry as one sealed line that\n * opens on its own (`stored-append-lines.ts`), so no line waits for another and the first block\n * still renders before the rest is read.\n */\nimport { appendFileSync, closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport {\n OWNER_ONLY_FILE_MODE,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { StoredLines, splitStoredLines } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type { ISessionStoreOptions } from './stored-session-record.js';\nimport type { TUniversalValue } from '@robota-sdk/agent-core';\nimport type {\n IPromptHistoryBlock,\n IPromptHistoryEntry,\n IPromptHistoryReadOptions,\n IPromptHistorySource,\n IPromptHistoryWriter,\n} from '@robota-sdk/agent-interface-session';\n\nconst KIB = 1024;\nconst DEFAULT_BLOCK_KIB = 64;\n/** 64 KiB: a few hundred prompts per block — enough for the first frame, small enough to yield often. */\nexport const DEFAULT_PROMPT_HISTORY_BLOCK_BYTES = DEFAULT_BLOCK_KIB * KIB;\nconst NEWLINE = 0x0a;\n\n/** The owner id the one prompt history of a user state binds its sealed lines to. */\nexport const PROMPT_HISTORY_OWNER_ID = 'user';\n\nexport interface INodePromptHistoryFileOptions extends ISessionStoreOptions {\n /** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */\n readonly ownedRoot?: string;\n /** Test seam: the read block size in bytes. */\n readonly blockBytes?: number;\n}\n\nfunction isEntry(value: TUniversalValue): value is IPromptHistoryEntry & TUniversalValue {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const record = value as Record<string, TUniversalValue>;\n return (\n typeof record.at === 'string' &&\n typeof record.sessionId === 'string' &&\n typeof record.project === 'string' &&\n typeof record.text === 'string'\n );\n}\n\n/** One line → an entry, or `undefined` when the line is not a well-formed entry. */\nexport function parsePromptHistoryLine(line: string): IPromptHistoryEntry | undefined {\n if (line.trim().length === 0) return undefined;\n let parsed: TUniversalValue;\n try {\n parsed = JSON.parse(line) as TUniversalValue;\n } catch {\n // allow-fallback: a line JSON cannot parse is exactly what `skippedLines` counts for the caller.\n return undefined;\n }\n if (!isEntry(parsed)) return undefined;\n return { at: parsed.at, sessionId: parsed.sessionId, project: parsed.project, text: parsed.text };\n}\n\n/**\n * One block's physical lines, newest first, as entries. Each line is read on its own, so nothing in\n * a block waits for another block. Damage and lines a newer build sealed are `skippedLines`; sealed\n * lines this process cannot open now are `unreadable`, reported only when there are some.\n */\nfunction blockOf(lines: readonly string[], stored: StoredLines): IPromptHistoryBlock {\n const entries: IPromptHistoryEntry[] = [];\n let skippedLines = 0;\n let locked = 0;\n let noKeys = 0;\n for (const line of lines) {\n if (line.length === 0) continue;\n const read = stored.decode(line, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') {\n if (read.reason === 'no-keys') noKeys += 1;\n else locked += 1;\n continue;\n }\n if (read.kind !== 'lines') {\n skippedLines += 1;\n continue;\n }\n // A record holds its lines oldest first; the block is newest first.\n for (let index = read.lines.length - 1; index >= 0; index -= 1) {\n const entry = parsePromptHistoryLine(read.lines[index] as string);\n if (entry === undefined) skippedLines += 1;\n else entries.push(entry);\n }\n }\n return locked + noKeys === 0\n ? { entries, skippedLines }\n : { entries, skippedLines, unreadable: { locked, noKeys } };\n}\n\nfunction isMissingFile(error: Error): boolean {\n return (error as NodeJS.ErrnoException).code === 'ENOENT';\n}\n\nexport class NodePromptHistoryFile implements IPromptHistoryWriter, IPromptHistorySource {\n private readonly blockBytes: number;\n private readonly ownedRoot: string | undefined;\n private readonly lines: StoredLines;\n\n /**\n * @param options `sealing` (required): `{ sealer }` appends each entry as one sealed line and\n * opens sealed lines it reads (plain ones still read); `'plain'` appends plain lines and counts\n * a sealed line as one no key in this process opens.\n */\n constructor(\n private readonly path: string,\n options: INodePromptHistoryFileOptions,\n ) {\n this.lines = new StoredLines(sessionSealingOf(options), 'prompt-history-line');\n this.blockBytes = options.blockBytes ?? DEFAULT_PROMPT_HISTORY_BLOCK_BYTES;\n this.ownedRoot = options.ownedRoot;\n }\n\n append(entry: IPromptHistoryEntry): void {\n // Sealed before the file is touched, so a sealing failure writes nothing at all.\n const stored = this.lines.encode(`${JSON.stringify(entry)}\\n`, PROMPT_HISTORY_OWNER_ID);\n const directory = dirname(this.path);\n ensureOwnerOnlyDirectory(\n directory,\n this.ownedRoot === undefined ? {} : { withinRoot: this.ownedRoot },\n );\n // SEC-020: `mode` applies only when the file is created; tightening first repairs an older file.\n tightenExistingFile(this.path);\n appendFileSync(this.path, stored, { mode: OWNER_ONLY_FILE_MODE });\n }\n\n /**\n * Newest-first blocks. Only a missing file is the empty state (a fresh install, or history off);\n * any other open or read failure is thrown so the surface renders it instead of an empty list.\n */\n async *read(options: IPromptHistoryReadOptions): AsyncIterable<IPromptHistoryBlock> {\n let descriptor: number;\n try {\n descriptor = openSync(this.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if (error instanceof Error && isMissingFile(error)) return;\n throw error;\n }\n try {\n let position = fstatSync(descriptor).size;\n // The bytes after the last newline seen so far: a line the previous block cut in half.\n let carry = Buffer.alloc(0);\n // The first block yielded holds the end of the file, and so any append still in progress.\n let holdsEnd = true;\n while (position > 0 && !options.signal.aborted) {\n const length = Math.min(this.blockBytes, position);\n position -= length;\n const chunk = Buffer.alloc(length);\n const bytesRead = readSync(descriptor, chunk, 0, length, position);\n const buffer = Buffer.concat([chunk.subarray(0, bytesRead), carry]);\n const firstNewline = position === 0 ? -1 : buffer.indexOf(NEWLINE);\n // Everything before the first newline belongs to a line that continues in the block before.\n carry = firstNewline === -1 && position > 0 ? buffer : buffer.subarray(0, firstNewline + 1);\n const complete =\n position === 0\n ? buffer\n : firstNewline === -1\n ? Buffer.alloc(0)\n : buffer.subarray(firstNewline + 1);\n if (position > 0 && firstNewline === -1) continue;\n const text = complete.toString('utf8');\n const lines = (holdsEnd ? splitStoredLines(text) : text.split('\\n')).reverse();\n holdsEnd = false;\n yield blockOf(lines, this.lines);\n }\n } finally {\n closeSync(descriptor);\n }\n }\n}\n","/**\n * Compaction of the prompt-history file (#3679, secure conversation): every entry, plain or sealed,\n * rewritten one per line under the one sealer the caller hands over, so the file holds no plaintext\n * and — given a sealer of its own — its sealed lines share one writer epoch, read back with one key\n * opening instead of one per epoch the file collected.\n *\n * The file has no lock: every terminal appends to it by path whenever a prompt is sent. So it is\n * rewritten beside itself, renamed over, and what terminals appended meanwhile is caught up under the\n * same sealer (`append-file-rewrite.ts`, which also refuses a history that is a link, and abandons a\n * compaction another one overtook or whose history was deleted meanwhile). A line that does not open\n * here, or that a newer build sealed, is carried over byte for byte. A crash inside the catch-up\n * window, or a catch-up that fails, can lose what was appended in it: milliseconds, and the session\n * records hold those prompts too, since this file is only a derived copy of them.\n */\nimport { closeSync } from 'node:fs';\n\nimport { ArtifactLockedError, isSealedArtifactLine } from '@robota-sdk/agent-core';\n\nimport { openAppendFile, readWhole, rewriteAppendFile } from './append-file-rewrite.js';\nimport { PROMPT_HISTORY_OWNER_ID, parsePromptHistoryLine } from './prompt-history-file.js';\nimport { StoredLines, splitStoredLines } from './stored-append-lines.js';\n\nimport type {\n IAppendFileCatchUpFailure,\n IAppendFileRewriter,\n TAppendFileRefusal,\n} from './append-file-rewrite.js';\nimport type { IStoredLineSink } from './stored-append-lines.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst NEWLINE = '\\n';\n\n/** Why a history was left as it is. */\nexport type TPromptHistoryRefusal = TAppendFileRefusal;\n\nexport interface IInspectPromptHistoryOptions {\n /** Opens sealed lines; with the keys locked, inspecting a sealed line throws `ArtifactLockedError`. */\n readonly sealer: IArtifactSealer;\n}\n\nexport interface ICompactPromptHistoryOptions {\n /**\n * Seals every entry and opens the sealed ones. Give the compaction a sealer of its own so all of\n * its lines share one writer epoch. With the keys locked it stops with `ArtifactLockedError` and\n * replaces nothing.\n */\n readonly sealer: IArtifactSealer;\n /** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */\n readonly ownedRoot?: string;\n /**\n * Runs once the new file is written and flushed, just before it replaces the history. Throwing\n * abandons the compaction with nothing replaced and no temporary file left.\n */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over the replaced file; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n}\n\nexport type TPromptHistoryInspection =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TPromptHistoryRefusal }\n | {\n readonly status: 'present';\n /** Physical lines that are plaintext. */\n readonly plainLines: number;\n /** Physical lines sealed in this build's framing that open. */\n readonly sealedLines: number;\n /** Physical lines that do not open here or that a newer build sealed. */\n readonly unreadableLines: number;\n };\n\nexport type TPromptHistoryCompaction =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TPromptHistoryRefusal }\n /** Another compaction replaced the history before this one's rename; nothing was replaced. */\n | { readonly status: 'replaced' }\n /** The history was deleted before this compaction's rename; nothing was put in its place. */\n | { readonly status: 'removed' }\n | {\n readonly status: 'compacted';\n /** Entries written sealed, one per line. */\n readonly entries: number;\n /** Of those, entries that were plain lines. */\n readonly fromPlain: number;\n /** Of those, entries appended after the snapshot and caught up from the replaced file. */\n readonly caughtUp: number;\n /** Lines that do not open here, or that a newer build sealed, carried over unchanged. */\n readonly carriedOver: number;\n /** The replaced file ended in an unfinished sealed append, which no reader shows; it was dropped. */\n readonly droppedTail: boolean;\n /** Plain lines in the new file afterwards: plaintext an older build appended meanwhile. */\n readonly plainLinesAfter: number;\n /**\n * Set when the catch-up failed after the rename: entries appended to the replaced file from\n * then on are not in the history (their session records hold them).\n */\n readonly catchUpFailed?: IAppendFileCatchUpFailure;\n };\n\ninterface IRewriteCounts {\n entries: number;\n fromPlain: number;\n carriedOver: number;\n}\n\n/**\n * Rewrites physical lines: every entry a line holds becomes one sealed line, a line that does not\n * open or that a newer build sealed is carried over as it is, and empty lines go (readers skip them).\n * `leadingNewline` keeps the append framing of a file other writers append to at the same time.\n */\nclass EntryRewriter implements IAppendFileRewriter {\n readonly counts: IRewriteCounts = { entries: 0, fromPlain: 0, carriedOver: 0 };\n readonly #lines: StoredLines;\n\n constructor(sealer: IArtifactSealer) {\n this.#lines = new StoredLines({ sealer }, 'prompt-history-line');\n }\n\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void {\n for (const physical of lines) {\n if (physical.length === 0) continue;\n const read = this.#lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') {\n out.write(`${physical}${NEWLINE}`);\n this.counts.carriedOver += 1;\n continue;\n }\n if (!isSealedArtifactLine(physical)) this.counts.fromPlain += read.lines.length;\n for (const text of read.lines) {\n // A sealed append is framed `\\n<line>\\n`; a file this rewrite writes alone needs no leading one.\n const stored = this.#lines.encode(`${text}${NEWLINE}`, PROMPT_HISTORY_OWNER_ID);\n out.write(leadingNewline ? stored : stored.slice(NEWLINE.length));\n this.counts.entries += 1;\n }\n }\n }\n}\n\n/** What the history holds, line by line, as {@link compactPromptHistoryFile} would find it. */\nexport function inspectPromptHistoryFile(\n path: string,\n options: IInspectPromptHistoryOptions,\n): TPromptHistoryInspection {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return opened;\n const lines = new StoredLines({ sealer: options.sealer }, 'prompt-history-line');\n let plainLines = 0;\n let sealedLines = 0;\n let unreadableLines = 0;\n try {\n for (const physical of splitStoredLines(readWhole(opened.descriptor).toString('utf8'))) {\n if (physical.length === 0) continue;\n const read = lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') unreadableLines += 1;\n else if (isSealedArtifactLine(physical)) sealedLines += 1;\n else plainLines += 1;\n }\n } finally {\n closeSync(opened.descriptor);\n }\n return { status: 'present', plainLines, sealedLines, unreadableLines };\n}\n\n/**\n * The distinct `project` values the history's entries name, plain or sealed, in the order first met;\n * lines that do not open or do not parse are passed over. A history that is missing or refused names\n * none. Writes nothing.\n */\nexport function readPromptHistoryProjects(\n path: string,\n options: IInspectPromptHistoryOptions,\n): readonly string[] {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return [];\n const lines = new StoredLines({ sealer: options.sealer }, 'prompt-history-line');\n const projects = new Set<string>();\n try {\n for (const physical of splitStoredLines(readWhole(opened.descriptor).toString('utf8'))) {\n if (physical.length === 0) continue;\n const read = lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') continue;\n for (const text of read.lines) {\n const entry = parsePromptHistoryLine(text);\n if (entry !== undefined && entry.project.length > 0) projects.add(entry.project);\n }\n }\n } finally {\n closeSync(opened.descriptor);\n }\n return [...projects];\n}\n\n/**\n * Compacts the history at `path` as the module documentation describes. Rerunning it gives the same\n * entries under a new writer epoch.\n */\nexport async function compactPromptHistoryFile(\n path: string,\n options: ICompactPromptHistoryOptions,\n): Promise<TPromptHistoryCompaction> {\n const rewriter = new EntryRewriter(options.sealer);\n let entriesInSnapshot = 0;\n const result = await rewriteAppendFile(path, rewriter, {\n ...(options.ownedRoot === undefined ? {} : { ownedRoot: options.ownedRoot }),\n ...(options.pause === undefined ? {} : { pause: options.pause }),\n beforeCommit: () => {\n entriesInSnapshot = rewriter.counts.entries;\n options.beforeCommit?.();\n },\n });\n if (result.status !== 'rewritten') return result;\n return {\n status: 'compacted',\n ...rewriter.counts,\n caughtUp: rewriter.counts.entries - entriesInSnapshot,\n droppedTail: result.droppedTail,\n plainLinesAfter: result.plainLinesAfter,\n ...(result.catchUpFailed === undefined ? {} : { catchUpFailed: result.catchUpFailed }),\n };\n}\n","/**\n * Where a path a shell command names really is, for the gate's read-only command check (issue\n * #3082). Symlinks are followed, as `Read`'s own containment check does: a link committed inside\n * the workspace can point anywhere, so a string check alone cannot say a command stays inside.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, isAbsolute, relative, resolve } from 'node:path';\n\nimport type { TResolveInWorkspace } from '@robota-sdk/agent-core';\n\n/** The real path of `path`, or of its nearest existing ancestor with the rest appended. */\nfunction realOrNearest(path: string): string {\n let existing = path;\n const rest: string[] = [];\n for (;;) {\n try {\n const real = realpathSync(existing);\n return rest.length === 0 ? real : resolve(real, ...rest.reverse());\n } catch {\n const parent = dirname(existing);\n if (parent === existing) return path;\n rest.push(existing.slice(parent.length).replace(/^[\\\\/]/, ''));\n existing = parent;\n }\n }\n}\n\nfunction isInside(root: string, path: string): boolean {\n const fromRoot = relative(root, path);\n return fromRoot === '' || (!fromRoot.startsWith('..') && !isAbsolute(fromRoot));\n}\n\nexport function createNodeWorkspacePathResolver(cwd: string): TResolveInWorkspace {\n return (base, path) => {\n const root = realOrNearest(cwd);\n const real = realOrNearest(resolve(base ?? root, path));\n return isInside(root, real) ? real : undefined;\n };\n}\n","/**\n * The Node host's answer to `IFileSessionLoggerOptions.registerExitFlush`: run `flush` when the\n * process is about to end. `exit` listeners may only do synchronous work, which is what a logger's\n * flush does. Pass this very function to every logger — the logger registers once per distinct\n * function, so one process gets one listener however many loggers it creates.\n */\nexport function registerProcessExitFlush(flush: () => void): void {\n if (typeof process === 'undefined' || typeof process.on !== 'function') return;\n process.on('exit', flush);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.js","names":["digestOf","Buffer","#states","#heldOrder","#handoffOrder","#keepsHeld","#edit","#settle","BASE64URL_PATTERN","Buffer","Buffer","NEWLINE","digestOf","#sealer","#files","#memory","#claims","#underWriteMarker","#onUnlock","#tagPendingAtUnlock","#handOut","#isOwners","#heldOutcome","#isRunHere","Buffer","#kindOfSave","#tag","#handedOut","#tagInPlace","#textOfFile","#writeTagged","#tagPendingOne","#sealer","#files","#markers","#claims","#authorship","#underWriteMarker","#read","#takeMarker","#readPlain","#rebuild","#guardWrite","#write","#withTransientMarker","#assertPlainReplaceable","#currentQuietly","createProcessOperationLock","NEWLINE","#sealer","#kind","#decode","#written","#lines","join","PAYLOAD_DIRECTORY_SUFFIX","resolve","existsSync","readFileSync","openSync","fstatSync","readSync","#parts","#length","join","readdirSync","join","openSync","fstatSync","readdirSync","join","readdirSync","join","createProcessOperationLock","digestOf","join","openSync","readdirSync","NEWLINE","openSync","fstatSync","readSync","#lines","resolve"],"sources":["../src/stored-session-codec.ts","../src/sealed-session-memory.ts","../src/session-record-authorship.ts","../src/stored-session-authorship.ts","../src/stored-session-record.ts","../src/session-store.ts","../src/stored-append-lines.ts","../src/node-session-log-sink.ts","../src/session-log-sources.ts","../src/append-file-rewrite.ts","../src/stale-temporaries.ts","../src/staged-replacements.ts","../src/session-log-migration.ts","../src/session-record-migration.ts","../src/tool-result-spill-store.ts","../src/prompt-history-file.ts","../src/prompt-history-compaction.ts","../src/workspace-path-resolver.ts","../src/process-exit-flush.ts"],"sourcesContent":["/**\n * The stored form of one session record, apart from who wrote it and what a store holds of it: the\n * versioned envelope, plain or sealed to the record's own id, what a store finds at a record's path,\n * and the conversions a migration makes. Stateless; {@link StoredSessionRecords} keeps the rest.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createHash } from 'node:crypto';\n\nimport {\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isSealedArtifactBytes,\n} from '@robota-sdk/agent-core';\n\nimport {\n SESSION_RECORD_ENVELOPE_VERSION,\n decodeVersionedInteractiveSessionRecord,\n} from './session-record-codec/index.js';\n\nimport type { IArtifactContext, IArtifactSealer } from '@robota-sdk/agent-core';\nimport type {\n IInteractiveSessionRecord,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/** What a listing says about a sealed file that does not open; fixed, so it can carry no content. */\nexport const DOES_NOT_OPEN_MESSAGE =\n 'the sealed session file does not open for this session: it is damaged or sealed with other keys';\n\n/** What a load says about a record whose own id is another session's; fixed, so it carries no content. */\nconst OTHER_ID_MESSAGE = 'the record belongs to another session id';\n\n/** How every store names stored bytes: the SHA-256, lowercase hex. */\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\n/** Whether `value` names stored bytes the way the stores do. */\nexport function isSha256Hex(value: unknown): value is string {\n return typeof value === 'string' && SHA256_HEX.test(value);\n}\n\nexport function digestOf(stored: Uint8Array): string {\n return createHash('sha256').update(stored).digest('hex');\n}\n\nexport function sameBytes(left: Uint8Array, right: Uint8Array): boolean {\n return Buffer.from(left.buffer, left.byteOffset, left.byteLength).equals(right);\n}\n\n/** The id binds a sealed record to its own file, so a record copied to another session's path does not open. */\nexport function recordContext(id: string): IArtifactContext {\n return { kind: 'session-record', id };\n}\n\nexport function utf8Text(bytes: Uint8Array): string {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf8');\n}\n\n/**\n * The versioned envelope of `record` (without a version on disk \"written by a build this\n * one does not read\" cannot be told from \"damaged\"), the same pretty-printed JSON the stores always wrote.\n */\nexport function envelopeText(record: IInteractiveSessionRecord): string {\n return JSON.stringify({ schemaVersion: SESSION_RECORD_ENVELOPE_VERSION, record }, null, 2);\n}\n\n/** A stored record: the bytes that go on disk and the plaintext envelope they were made from. */\nexport interface IEncodedSession {\n readonly stored: Uint8Array;\n readonly text: string;\n}\n\n/** The bytes to store for `record`: its envelope, sealed when a sealer is given. */\nexport function encodeStoredSession(\n record: IInteractiveSessionRecord,\n sealer: IArtifactSealer | undefined,\n): IEncodedSession {\n const text = envelopeText(record);\n const bytes = Buffer.from(text, 'utf8');\n return {\n stored: sealer === undefined ? bytes : sealer.sealBytes(bytes, recordContext(record.id)),\n text,\n };\n}\n\n/** What a record's stored bytes are to a migration that seals what is still plain. */\nexport type TStoredSessionMigrationState =\n /** Not sealed framing: whatever it holds — a valid, corrupt, unsupported or bare record. */\n | 'plain'\n /** Sealed in this build's framing, and it opens for this id. */\n | 'sealed'\n /** Sealed in this build's framing and it does not open for this id: damaged, or other keys. */\n | 'does-not-open'\n /** Sealed in a framing a newer build writes. */\n | 'newer-format';\n\n/**\n * Where the bytes of session `id` stand for a migration: decided by whether they open for this id,\n * never by framing alone. Opening needs the keys unlocked; with them locked it throws\n * `ArtifactLockedError`, since \"intact but locked\" decides nothing.\n */\nexport function storedSessionMigrationState(\n id: string,\n stored: Uint8Array,\n sealer: IArtifactSealer,\n): TStoredSessionMigrationState {\n if (!isSealedArtifactBytes(stored)) return 'plain';\n if (!isCurrentSealedArtifactBytes(stored)) return 'newer-format';\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, recordContext(id));\n } catch (error) {\n if (isArtifactLockedError(error)) throw error;\n return 'does-not-open';\n }\n plain.fill(0);\n return 'sealed';\n}\n\n/**\n * `raw` — a plain record's bytes exactly as stored, whatever they hold — sealed to session `id`, so a\n * store that opens it reports the same status the plain bytes gave: a corrupt or unsupported record\n * stays one, only no longer readable without the keys.\n */\nexport function sealStoredSessionBytes(\n id: string,\n raw: Uint8Array,\n sealer: IArtifactSealer,\n): Uint8Array {\n return sealer.sealBytes(raw, recordContext(id));\n}\n\n/** Decode envelope text into an outcome, keeping \"not JSON\" and \"not a record\" the same answer. */\nexport function decodeStoredSessionText(raw: string): TSessionLoadOutcome {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw) as unknown;\n } catch {\n return {\n status: 'corrupt',\n issues: [{ path: '', message: 'the session file is not JSON' }],\n };\n }\n const outcome = decodeVersionedInteractiveSessionRecord(parsed);\n if (outcome.status === 'valid') return { status: 'valid', record: outcome.record };\n if (outcome.status === 'unsupported') {\n return {\n status: 'unsupported',\n schemaVersion: outcome.schemaVersion,\n ...(outcome.cwd !== undefined ? { cwd: outcome.cwd } : {}),\n };\n }\n return {\n status: 'corrupt',\n issues: outcome.issues,\n ...(outcome.cwd !== undefined ? { cwd: outcome.cwd } : {}),\n };\n}\n\n/**\n * {@link decodeStoredSessionText} for the record stored under `id`. A record that names another\n * session is not this session's, wherever it was copied from, so it is `corrupt` — the same for a\n * plain file, which anyone can copy, and a sealed one, which opens here only when sealed to this id.\n */\nexport function decodeRecordText(id: string, raw: string): TSessionLoadOutcome {\n const outcome = decodeStoredSessionText(raw);\n if (outcome.status !== 'valid' || outcome.record.id === id) return outcome;\n return { status: 'corrupt', issues: [{ path: 'id', message: OTHER_ID_MESSAGE }] };\n}\n\n/** Whether plain `bytes` hold a valid record of session `id`: what a migration may offer to mark as the owner's. */\nexport function isPlainSessionRecordOf(id: string, bytes: Uint8Array): boolean {\n return decodeRecordText(id, utf8Text(bytes)).status === 'valid';\n}\n\n/** What a store found at a record's path before it writes over it. */\nexport type TStoredSessionFile =\n /** Nothing is there: \"no such file\" and nothing else. */\n | { readonly kind: 'absent' }\n /**\n * Something is there and it is not sealed framing; its bytes when the store read them whole (a store\n * that seals does, so it can tell whether they are exactly what it holds).\n */\n | { readonly kind: 'plain'; readonly bytes?: Uint8Array }\n /** A sealed file, read whole. */\n | { readonly kind: 'sealed'; readonly bytes: Uint8Array }\n /** Something is there and it could not be read: it may be sealed, so it is protected like one. */\n | { readonly kind: 'unreadable' }\n /** Something is there, too large for this store to read back at all. */\n | { readonly kind: 'oversized' };\n\n/** What `bytes` (or their absence) are, for a store that read the whole file. */\nexport function classifyStoredSessionBytes(bytes: Uint8Array | undefined): TStoredSessionFile {\n if (bytes === undefined) return { kind: 'absent' };\n return isSealedArtifactBytes(bytes) ? { kind: 'sealed', bytes } : { kind: 'plain', bytes };\n}\n\n/** The bytes a file was read with, when it was read whole. */\nexport function bytesOf(file: TStoredSessionFile): Uint8Array | undefined {\n return file.kind === 'sealed' || file.kind === 'plain' ? file.bytes : undefined;\n}\n","/**\n * What one store instance that seals knows of each session id, and the token a held record crosses\n * to another process as.\n *\n * Locking drops the ability to READ, not to keep saving what a process is already running: sealing\n * needs only the public key, and the process holds its session in memory. So a store remembers, per\n * id, the digest of the sealed bytes it last wrote or was asked to hold, the plaintext envelope they\n * hold, and whose they are — the digest proves a file is exactly what this instance put there or\n * read, and the plaintext lets a locked process go on updating a session it could not read back. A\n * fork record written for another process is remembered apart, so a fork never pushes a running\n * session out. Beside them it keeps which ids it handed unverified content out for (never tagged\n * again until the owner adopts them) and which owner bytes wait for a tag. Memory only, never\n * persisted, and a fresh instance (a restart)\n * knows nothing; what it remembers is bounded except for the records a running session needs.\n */\n\nimport { Buffer } from 'node:buffer';\n\nimport { isSha256Hex } from './stored-session-codec.js';\n\n/**\n * Whose a store holds remembered bytes to be. `owner`: bytes it wrote from the owner's content, bytes\n * it opened whose authorship tag verifies, or bytes the owner adopted. `pinned`: a record a write-only\n * child was handed (`importHeld`) and its own later writes of it, judged by exact bytes only, since it\n * holds no keys. `unverified`: anything else.\n */\nexport type THeldRecordKind = 'owner' | 'pinned' | 'unverified';\n\n/** Stored bytes a store remembers for one id: their digest, the envelope they hold, and whose they are. */\nexport interface IRememberedRecord {\n readonly digest: string;\n readonly text: string;\n readonly kind: THeldRecordKind;\n /**\n * A `pinned` record the process that handed it over held as the owner's. That process holds the\n * keys and started this one, so its word is what a write-only child has for whose the record is.\n */\n readonly vouched?: true;\n}\n\n/** Everything one store instance knows of one id. */\nexport interface ISessionRecordState {\n /** The bytes it last wrote or read to hold, for a session it runs or may go on saving. */\n readonly held?: IRememberedRecord;\n /** A record a handoff save left for another process to run, until it is handed over or taken. */\n readonly handoff?: IRememberedRecord;\n /** A read handed a writer content of the id that nobody vouched for while nothing here ran it. */\n readonly tainted: boolean;\n /** Owner bytes the locked keys could not tag: tagged at the next unlock if still in place. */\n readonly pendingDigest?: string;\n}\n\n/** More sessions than one process runs at once; the oldest is forgotten past this, which only means it is refused while locked. */\nconst MAX_HELD_RECORDS = 32;\n/**\n * Fork records written for a handoff and not yet handed over. A fork job is spawned right after its\n * record is written, so a few suffice; the oldest goes first, and its job then needs a fresh fork.\n */\nconst MAX_HANDOFF_RECORDS = 8;\n\ntype TMutableState = { -readonly [K in keyof ISessionRecordState]: ISessionRecordState[K] };\n\n/** The per-id state of one store that seals; see the module documentation. */\nexport class SealedSessionMemory {\n readonly #states = new Map<string, TMutableState>();\n /** Held ids, oldest first, for the bound. */\n readonly #heldOrder = new Set<string>();\n readonly #handoffOrder = new Set<string>();\n readonly #keepsHeld: (id: string) => boolean;\n\n /** @param keepsHeld whether an id's held record must outlast the bound: a session here runs it. */\n constructor(keepsHeld: (id: string) => boolean = () => false) {\n this.#keepsHeld = keepsHeld;\n }\n\n /** How many held records it remembers now. */\n get size(): number {\n return this.#heldOrder.size;\n }\n\n state(id: string): ISessionRecordState | undefined {\n return this.#states.get(id);\n }\n\n /** Remembers `record` as what it holds for `id`, the newest; the oldest it may forget goes past the bound. */\n hold(id: string, record: IRememberedRecord): void {\n this.#edit(id).held = record;\n this.#heldOrder.delete(id);\n this.#heldOrder.add(id);\n for (const oldest of this.#heldOrder) {\n if (this.#heldOrder.size <= MAX_HELD_RECORDS) break;\n if (this.#states.get(oldest)?.held?.kind === 'pinned' || this.#keepsHeld(oldest)) continue;\n this.dropHeld(oldest);\n }\n }\n\n /** Remembers `record` as the record a handoff save left for `id`. */\n holdHandoff(id: string, record: IRememberedRecord): void {\n this.#edit(id).handoff = record;\n this.#handoffOrder.delete(id);\n this.#handoffOrder.add(id);\n for (const oldest of this.#handoffOrder) {\n if (this.#handoffOrder.size <= MAX_HANDOFF_RECORDS) break;\n this.dropHandoff(oldest);\n }\n }\n\n dropHeld(id: string): void {\n this.#heldOrder.delete(id);\n this.#settle(id, (state) => delete state.held);\n }\n\n dropHandoff(id: string): void {\n this.#handoffOrder.delete(id);\n this.#settle(id, (state) => delete state.handoff);\n }\n\n taint(id: string): void {\n this.#edit(id).tainted = true;\n }\n\n untaint(id: string): void {\n this.#settle(id, (state) => (state.tainted = false));\n }\n\n setPending(id: string, digest: string): void {\n this.#edit(id).pendingDigest = digest;\n }\n\n clearPending(id: string): void {\n this.#settle(id, (state) => delete state.pendingDigest);\n }\n\n /** Every id with owner bytes that wait for a tag, and those bytes' digest. */\n pending(): readonly (readonly [string, string])[] {\n const pending: [string, string][] = [];\n for (const [id, state] of this.#states) {\n if (state.pendingDigest !== undefined) pending.push([id, state.pendingDigest]);\n }\n return pending;\n }\n\n /** Forgets the id's records and pending tag: the session is deleted. A taint outlives it. */\n forget(id: string): void {\n this.#heldOrder.delete(id);\n this.#handoffOrder.delete(id);\n this.#settle(id, (state) => {\n delete state.held;\n delete state.handoff;\n delete state.pendingDigest;\n });\n }\n\n #edit(id: string): TMutableState {\n let state = this.#states.get(id);\n if (state === undefined) {\n state = { tainted: false };\n this.#states.set(id, state);\n }\n return state;\n }\n\n /** Applies `change` to the id's state, and drops a state that is left knowing nothing. */\n #settle(id: string, change: (state: TMutableState) => unknown): void {\n const state = this.#states.get(id);\n if (state === undefined) return;\n change(state);\n if (\n state.held === undefined &&\n state.handoff === undefined &&\n state.pendingDigest === undefined &&\n !state.tainted\n ) {\n this.#states.delete(id);\n }\n }\n}\n\n/**\n * A held record as it crosses to another process: `rsh2.`, whose the record was to the store that\n * handed it over (`owner` or `unverified`), the SHA-256 of the sealed bytes it came from, and the\n * canonical unpadded base64url of the plaintext envelope. The digest is what the receiving store\n * compares the file with before it trusts the record over it; whose it was is the only word a\n * receiver without keys has for it.\n */\nconst HELD_TOKEN_PREFIX = 'rsh2.';\n/** Whose the handed record was to the store that handed it over, as the token says it. */\nconst HELD_TOKEN_KINDS = { owner: true, unverified: false } as const;\nconst BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true });\n\nexport function formatHeldToken(digest: string, text: string, owner: boolean): string {\n const kind = owner ? 'owner' : 'unverified';\n return `${HELD_TOKEN_PREFIX}${kind}.${digest}.${Buffer.from(text, 'utf8').toString('base64url')}`;\n}\n\n/**\n * The digest and envelope a token names, and whether the store that issued it held the record as the\n * owner's; `undefined` for anything this build does not issue.\n */\nexport function parseHeldToken(\n token: unknown,\n): { readonly digest: string; readonly text: string; readonly owner: boolean } | undefined {\n if (typeof token !== 'string' || !token.startsWith(HELD_TOKEN_PREFIX)) return undefined;\n const [kind, digest, encoded, ...extra] = token.slice(HELD_TOKEN_PREFIX.length).split('.');\n if (kind !== 'owner' && kind !== 'unverified') return undefined;\n if (extra.length > 0 || digest === undefined || encoded === undefined) return undefined;\n if (!isSha256Hex(digest) || !BASE64URL_PATTERN.test(encoded)) return undefined;\n const bytes = Buffer.from(encoded, 'base64url');\n if (bytes.toString('base64url') !== encoded) return undefined;\n try {\n return { digest, text: STRICT_UTF8.decode(bytes), owner: HELD_TOKEN_KINDS[kind] };\n } catch {\n return undefined;\n }\n}\n","/**\n * Where a sealed session record carries its authorship tag: a byte header in front of\n * the exact envelope bytes, inside the sealed plaintext —\n *\n * robota-authorship/v1 <canonical unpadded base64url of the 32-byte tag>\\n<envelope bytes>\n *\n * The header is authenticated with the record, since it is sealed with it, and the envelope is never\n * re-serialised, so a change to how the codec formats an envelope never invalidates an earlier tag.\n * Plain records never carry it: a plain file that begins with it is just a plain file. A token handed\n * to another process carries the envelope only.\n */\n\nimport { Buffer } from 'node:buffer';\n\n/** Versioned: a later framing is a new prefix, which this build reads as no tag at all. */\nexport const AUTHORSHIP_HEADER_PREFIX = 'robota-authorship/v1 ';\n\nconst PREFIX_BYTES = Buffer.from(AUTHORSHIP_HEADER_PREFIX, 'ascii');\nconst NEWLINE = 0x0a;\nconst TAG_BYTES = 32;\nconst BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/** `envelope` with `tag` in front of it, as a tagged record's sealed plaintext. */\nexport function withAuthorshipHeader(tag: Uint8Array, envelope: Uint8Array): Buffer {\n const encoded = Buffer.from(tag.buffer, tag.byteOffset, tag.byteLength).toString('base64url');\n return Buffer.concat([Buffer.from(`${AUTHORSHIP_HEADER_PREFIX}${encoded}\\n`, 'ascii'), envelope]);\n}\n\n/** A record's sealed plaintext split into its envelope and the tag in front of it, if any. */\nexport interface ISplitRecordPlaintext {\n readonly envelope: Uint8Array;\n /** The tag the header carries; `malformed` for a header that is not one this build writes. */\n readonly tag: Uint8Array | 'malformed' | undefined;\n}\n\n/**\n * Splits a sealed record's plaintext. Without the header the whole plaintext is the envelope, an\n * untagged record. A header whose tag is not canonical base64url of 32 bytes is `malformed`, which a\n * reader treats as a tag that does not verify; one with no line end leaves the plaintext whole, which\n * then does not decode as a record.\n */\nexport function splitAuthorshipHeader(plain: Uint8Array): ISplitRecordPlaintext {\n const bytes = Buffer.from(plain.buffer, plain.byteOffset, plain.byteLength);\n if (\n bytes.length < PREFIX_BYTES.length ||\n !bytes.subarray(0, PREFIX_BYTES.length).equals(PREFIX_BYTES)\n ) {\n return { envelope: plain, tag: undefined };\n }\n const end = bytes.indexOf(NEWLINE, PREFIX_BYTES.length);\n if (end < 0) return { envelope: plain, tag: 'malformed' };\n const encoded = bytes.subarray(PREFIX_BYTES.length, end).toString('latin1');\n const envelope = plain.subarray(end + 1);\n if (!BASE64URL_PATTERN.test(encoded)) return { envelope, tag: 'malformed' };\n const tag = Buffer.from(encoded, 'base64url');\n if (tag.length !== TAG_BYTES || tag.toString('base64url') !== encoded) {\n return { envelope, tag: 'malformed' };\n }\n return { envelope, tag };\n}\n","/**\n * Where a store that seals decides whose each record is, and keeps it so.\n *\n * A file the same OS user can write is never a trust anchor. A sealed file that opens says it is\n * bound to this session's id and unaltered since it was sealed; it says nothing about who sealed it,\n * since anyone with the public key can. So whose a record is is decided when its bytes are opened —\n * an authorship tag only unlocked keys make (`session-record-authorship.ts`), bytes this instance\n * wrote from the owner's content, or bytes the owner adopted by their digest — and every `valid`\n * outcome says which. The ids a session in this process runs are claimed, and a claimed id is\n * answered only from content the owner wrote: unverified content from disk is never handed to it, so\n * nothing unverified can be merged into it, and a file something unverifiable changed is never\n * overwritten by it either. A save tags only content the store can vouch for; an owner save the\n * locked keys could not tag is tagged at the next unlock, only while this store holds the id's writer\n * marker or can take it, so a process the id is lent to or another writer keeps the bytes it holds.\n * Adopting or tagging in place never replaces bytes another writer put there since.\n */\n\nimport { Buffer } from 'node:buffer';\n\nimport {\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isSealedArtifactBytes,\n} from '@robota-sdk/agent-core';\n\nimport { splitAuthorshipHeader, withAuthorshipHeader } from './session-record-authorship.js';\nimport { SealedSessionUnreadableError } from './sealed-session-refusal.js';\nimport { formatHeldToken, parseHeldToken } from './sealed-session-memory.js';\nimport {\n DOES_NOT_OPEN_MESSAGE,\n bytesOf,\n decodeRecordText,\n digestOf,\n isSha256Hex,\n recordContext,\n sameBytes,\n utf8Text,\n} from './stored-session-codec.js';\n\nimport type { SessionClaims } from './session-claims.js';\nimport type {\n IRememberedRecord,\n ISessionRecordState,\n SealedSessionMemory,\n THeldRecordKind,\n} from './sealed-session-memory.js';\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\nimport type {\n ISessionExportOptions,\n ISessionSaveOptions,\n TSessionAdoptResult,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/** Every read a store makes of a record, and the check before a save. */\nexport type TReadPurpose =\n /** A load that only looks: a report, an existence check, an attach. */\n | 'probe'\n /** A load that holds: a resume or a writer about to save. */\n | 'hold'\n /** One entry of a listing; no writer merges it. */\n | 'list'\n /** The held copy a writer asks for once `load` says `locked`. */\n | 'held'\n /** A record handed to another process as a token. */\n | 'export'\n /** The read a claim makes, which the session restores from. */\n | 'claim'\n /** The check before a save replaces the file. */\n | 'save';\n\n/** What a read of each purpose may do. */\ninterface IReadPurpose {\n /** It feeds a writer, so a claimed id is never answered with content it cannot vouch for. */\n readonly writer: boolean;\n /** It takes a handoff record into the memory of the sessions the store runs. */\n readonly takes: boolean;\n /** It remembers what it opened. */\n readonly holds: boolean;\n /** Unverified content it hands out taints the id: a writer could merge from it. */\n readonly taints: boolean;\n}\n\nconst READ_PURPOSES: Readonly<Record<TReadPurpose, IReadPurpose>> = {\n probe: { writer: false, takes: false, holds: false, taints: true },\n hold: { writer: true, takes: true, holds: true, taints: true },\n list: { writer: false, takes: false, holds: false, taints: false },\n held: { writer: true, takes: true, holds: false, taints: true },\n export: { writer: true, takes: false, holds: false, taints: true },\n claim: { writer: true, takes: true, holds: true, taints: true },\n save: { writer: true, takes: false, holds: false, taints: false },\n};\n\n/** How the store reads and replaces a record's file; see `IStoredSessionFiles`. */\nexport interface IAuthorshipFiles {\n current(id: string): TStoredSessionFile;\n replace(id: string, stored: Uint8Array): void;\n}\n\n/** How a write of an id that carries no claim holds the id's writer marker (`StoredSessionRecords`). */\nexport type TUnderWriteMarker = <T>(id: string, write: () => T) => T | 'has_writer';\n\n/** The bytes a save writes, and what the store holds them as once written. */\nexport interface IPreparedSave {\n readonly stored: Uint8Array;\n readonly text: string;\n readonly heldAs: THeldRecordKind;\n /** An owner write the locked keys could not tag, to be tagged at the next unlock if still in place. */\n readonly tagPending: boolean;\n}\n\n/** The envelope a sealed file holds and whether its authorship tag verifies, or why this process cannot get it. */\ntype TOpened =\n | { readonly kind: 'text'; readonly text: string; readonly verified: boolean }\n | { readonly kind: 'locked' }\n | { readonly kind: 'no-keys' }\n | { readonly kind: 'does-not-open' };\n\n/** A sealed file a process without keys meets: intact, and no key in this process opens it. */\nexport const NO_KEYS_OUTCOME: TSessionLoadOutcome = Object.freeze({\n status: 'locked',\n reason: 'no-keys',\n});\n/** A session this instance holds whose file is no longer the bytes it wrote or read. */\nconst CHANGED_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'locked', reason: 'changed' });\n/** A sealed file the locked keys cannot open: whether it is the owner's is decided at unlock. */\nconst LOCKED_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'locked' });\nexport const MISSING_OUTCOME: TSessionLoadOutcome = Object.freeze({ status: 'missing' });\n\ntype TValid = Extract<TSessionLoadOutcome, { status: 'valid' }>;\n\n/** `outcome` with whose it is, which bytes it came from, and whether it is a held copy. */\nfunction annotated(\n outcome: TValid,\n verified: boolean,\n digest: string | undefined,\n diverged: boolean,\n): TSessionLoadOutcome {\n return {\n status: 'valid',\n record: outcome.record,\n authorship: verified ? 'verified' : 'unverified',\n ...(digest === undefined ? {} : { digest }),\n ...(diverged ? { diverged: true as const } : {}),\n };\n}\n\n/** Opens a current-framing sealed file, wiping the plaintext buffer once it is turned into text. */\nfunction openSealed(sealer: IArtifactSealer, id: string, stored: Uint8Array): TOpened {\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, recordContext(id));\n } catch (error) {\n // A write-only sealer (a process handed only a write grant) holds no keys.\n if (isArtifactLockedError(error)) {\n return (error as { readonly reason?: unknown }).reason === 'no-keys'\n ? { kind: 'no-keys' }\n : { kind: 'locked' };\n }\n // Any other failure of the sealer is no evidence the file is fine, and it must neither take a\n // listing down nor read as an absent file: it is reported as a file that does not open.\n return { kind: 'does-not-open' };\n }\n try {\n // The one place a record's authorship header is taken off: what is held, decoded or handed\n // over from here on is the envelope alone.\n const { envelope, tag } = splitAuthorshipHeader(plain);\n let verified = false;\n if (tag instanceof Uint8Array && sealer.verifyAuthorship !== undefined) {\n try {\n verified = sealer.verifyAuthorship(envelope, tag, recordContext(id)) === true;\n } catch {\n // allow-fallback: a check that cannot run shows nothing, and nothing shown is unverified.\n verified = false;\n }\n }\n return { kind: 'text', text: utf8Text(envelope), verified };\n } finally {\n plain.fill(0);\n }\n}\n\n/**\n * What the bytes of session `id` say to a store that seals with `sealer` and holds nothing yet: the\n * outcome, owner's only when the tag verifies. Stateless; a migration reads with it.\n */\nexport function openStoredSessionRecord(\n id: string,\n stored: Uint8Array,\n sealer: IArtifactSealer,\n): TSessionLoadOutcome {\n let text: string;\n let verified = false;\n if (!isSealedArtifactBytes(stored)) {\n text = utf8Text(stored);\n } else {\n if (!isCurrentSealedArtifactBytes(stored))\n return { status: 'unsupported', schemaVersion: undefined };\n const opened = openSealed(sealer, id, stored);\n if (opened.kind === 'locked') return LOCKED_OUTCOME;\n if (opened.kind === 'no-keys') return NO_KEYS_OUTCOME;\n if (opened.kind === 'does-not-open') {\n return { status: 'corrupt', issues: [{ path: '', message: DOES_NOT_OPEN_MESSAGE }] };\n }\n text = opened.text;\n verified = opened.verified;\n }\n const outcome = decodeRecordText(id, text);\n return outcome.status === 'valid'\n ? annotated(outcome, verified, digestOf(stored), false)\n : outcome;\n}\n\n/** The authorship of one store that seals; see the module documentation. */\nexport class StoredSessionAuthorship {\n readonly #sealer: IArtifactSealer;\n readonly #files: IAuthorshipFiles;\n readonly #memory: SealedSessionMemory;\n readonly #claims: SessionClaims;\n /** Runs a write of an id holding its writer marker; `has_writer` when another writer holds it. */\n readonly #underWriteMarker: TUnderWriteMarker;\n /** Held here so the sealer may hold it weakly: it lives exactly as long as this object. */\n readonly #onUnlock: (() => void) | undefined;\n\n constructor(\n sealer: IArtifactSealer,\n files: IAuthorshipFiles,\n memory: SealedSessionMemory,\n claims: SessionClaims,\n underWriteMarker: TUnderWriteMarker,\n ) {\n this.#sealer = sealer;\n this.#files = files;\n this.#memory = memory;\n this.#claims = claims;\n this.#underWriteMarker = underWriteMarker;\n if (sealer.tagAuthorship !== undefined && sealer.onUnlock !== undefined) {\n this.#onUnlock = () => this.#tagPendingAtUnlock();\n sealer.onUnlock(this.#onUnlock);\n }\n }\n\n /**\n * One read of session `id` for `purpose`. A claimed, handed-off or pinned id is answered only from\n * content the owner wrote (see {@link guarded}), before any rebuild an absent file would otherwise\n * get; any other id is what its bytes say, and a rebuild from a log is never the owner's.\n */\n read(\n id: string,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n rebuild: () => TSessionLoadOutcome,\n purpose: TReadPurpose,\n ): TSessionLoadOutcome {\n const guarded = this.guarded(id, file, purpose);\n if (guarded !== undefined) return guarded;\n if (file.kind === 'absent') {\n const rebuilt = rebuild();\n // A rebuild from a log carries no tag and comes from no single stored file.\n return rebuilt.status === 'valid'\n ? this.#handOut(id, rebuilt, 'unverified', undefined, purpose, false)\n : rebuilt;\n }\n const bytes = bytesOf(file);\n return bytes === undefined ? unreadable : this.decode(id, bytes, purpose);\n }\n\n /** What the stored bytes of session `id` say, for `purpose`; whose they are by tag or by what is held. */\n decode(id: string, stored: Uint8Array, purpose: TReadPurpose): TSessionLoadOutcome {\n let text: string;\n let verified = false;\n if (!isSealedArtifactBytes(stored)) {\n text = utf8Text(stored);\n } else {\n if (!isCurrentSealedArtifactBytes(stored)) {\n return { status: 'unsupported', schemaVersion: undefined };\n }\n const opened = openSealed(this.#sealer, id, stored);\n if (opened.kind === 'locked') return LOCKED_OUTCOME;\n if (opened.kind === 'no-keys') return NO_KEYS_OUTCOME;\n if (opened.kind === 'does-not-open') {\n return { status: 'corrupt', issues: [{ path: '', message: DOES_NOT_OPEN_MESSAGE }] };\n }\n text = opened.text;\n verified = opened.verified;\n }\n const outcome = decodeRecordText(id, text);\n if (outcome.status !== 'valid') return outcome;\n const digest = digestOf(stored);\n const state = this.#memory.state(id);\n const kind = verified || this.#isOwners(state, state?.held, digest) ? 'owner' : 'unverified';\n if (READ_PURPOSES[purpose].holds) this.#memory.hold(id, { digest, text, kind });\n return this.#handOut(id, outcome, kind, digest, purpose, false);\n }\n\n /**\n * The claim's own read: a resumed id this instance already holds exactly the bytes of is answered\n * from them; anything else is read as {@link read} reads a claim.\n */\n claimRead(\n id: string,\n fresh: boolean,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n rebuild: () => TSessionLoadOutcome,\n ): TSessionLoadOutcome {\n const held = this.#memory.state(id)?.held;\n const bytes = bytesOf(file);\n if (!fresh && held !== undefined && bytes !== undefined && digestOf(bytes) === held.digest) {\n return this.#heldOutcome(id, held, 'claim', false);\n }\n return this.read(id, file, unreadable, rebuild, 'claim');\n }\n\n /**\n * Readies `id` for a claim: a record a write-only child holds as handed to it is claimed only by the\n * child's own claim under its lease, so no other session here becomes its second writer; for a new\n * session whatever this instance remembered under its id is not that session's.\n */\n beforeClaim(id: string, fresh: boolean, leased: boolean): void {\n const state = this.#memory.state(id);\n if (state?.held?.kind === 'pinned' && !leased) throw this.#claims.refusal(id, 'has_writer');\n if (fresh) {\n this.#memory.dropHeld(id);\n this.#memory.dropHandoff(id);\n }\n }\n\n /**\n * The answer for an id this store holds for a session it runs — claimed, pinned, or a handoff record\n * not yet handed over — or `undefined` for any other id. Every read of such an id answers the same\n * way, before any rebuild an absent file would otherwise get:\n *\n * - the file is exactly the held bytes, or (claimed only) opens with an authorship tag that\n * verifies, as another of the owner's unlocked processes wrote it: that content;\n * - the file is gone: `missing` when nothing is held yet (a fresh claim), otherwise the held copy —\n * a re-create writes only the owner's content;\n * - anything else — plain, untagged, a tag that does not verify, a sealed file the locked keys cannot\n * open: a writer's read is `locked` (with the reason `changed`, or none when only the locked keys\n * stand in the way and an unlock may show the owner wrote it), so the save is refused and the file\n * left as it is; a probe or listing gets the held copy marked `diverged`, for display only.\n *\n * A handoff record is answered from its copy or as `changed`, and a writer's read takes it over.\n */\n guarded(\n id: string,\n file: TStoredSessionFile,\n purpose: TReadPurpose,\n ): TSessionLoadOutcome | undefined {\n const state = this.#memory.state(id);\n const runHere = this.#isRunHere(id, state);\n const handoff = runHere ? undefined : state?.handoff;\n if (!runHere && handoff === undefined) return undefined;\n const use = READ_PURPOSES[purpose];\n const bytes = bytesOf(file);\n const digest = bytes === undefined ? undefined : digestOf(bytes);\n if (handoff !== undefined) {\n if (use.takes) this.#memory.dropHandoff(id);\n if (bytes === undefined || digest !== handoff.digest) return CHANGED_OUTCOME;\n if (use.takes) this.#memory.hold(id, handoff);\n return this.#heldOutcome(id, handoff, purpose, false);\n }\n const entry = state?.held;\n if (entry !== undefined && digest === entry.digest) {\n return this.#heldOutcome(id, entry, purpose, false);\n }\n let lockedOnly = false;\n if (\n entry?.kind !== 'pinned' &&\n file.kind === 'sealed' &&\n isCurrentSealedArtifactBytes(file.bytes)\n ) {\n const opened = openSealed(this.#sealer, id, file.bytes);\n if (opened.kind === 'text' && opened.verified && digest !== undefined) {\n const outcome = decodeRecordText(id, opened.text);\n if (outcome.status === 'valid') {\n if (purpose !== 'save')\n this.#memory.hold(id, { digest, text: opened.text, kind: 'owner' });\n return annotated(outcome, true, digest, false);\n }\n }\n lockedOnly = opened.kind === 'locked';\n }\n if (file.kind === 'absent') {\n return entry === undefined ? MISSING_OUTCOME : this.#heldOutcome(id, entry, purpose, true);\n }\n if (use.writer || entry === undefined) return lockedOnly ? LOCKED_OUTCOME : CHANGED_OUTCOME;\n return this.#heldOutcome(id, entry, purpose, true);\n }\n\n /**\n * Throws {@link SealedSessionUnreadableError} unless `existing` may be replaced by a save: a run-here\n * id's file when it is exactly what is held, verifies or is gone (see {@link guarded}); any other\n * id's when it is absent, plain, exactly the bytes this instance last wrote or holds, or a sealed\n * file this store opens.\n */\n assertReplaceable(id: string, existing: TStoredSessionFile): void {\n const guarded = this.guarded(id, existing, 'save');\n if (guarded !== undefined) {\n if (guarded.status !== 'locked') return;\n throw new SealedSessionUnreadableError(\n id,\n guarded.reason === 'changed' ? 'changed' : 'locked',\n );\n }\n switch (existing.kind) {\n case 'absent':\n case 'plain':\n return;\n case 'unreadable':\n case 'oversized':\n // Nothing past the read limit can be shown to be anything, and it may be a sealed session.\n throw new SealedSessionUnreadableError(id, 'unreadable');\n case 'sealed':\n break;\n }\n const bytes = existing.bytes;\n if (!isCurrentSealedArtifactBytes(bytes)) {\n throw new SealedSessionUnreadableError(id, 'newer-format');\n }\n // Checked first, since it needs no keys and no decryption.\n const held = this.#memory.state(id)?.held;\n if (held !== undefined && held.digest === digestOf(bytes)) return;\n const opened = openSealed(this.#sealer, id, bytes);\n if (opened.kind === 'text') return;\n throw new SealedSessionUnreadableError(\n id,\n held !== undefined\n ? 'changed'\n : opened.kind === 'locked' || opened.kind === 'no-keys'\n ? opened.kind\n : 'does-not-open',\n );\n }\n\n /**\n * The sealed bytes a save of envelope `text` for `id` writes. Tagged only when the store can vouch\n * for the content and the keys are unlocked: the id is held as the owner's, or it is held as\n * nothing, was never handed out unverified, and the save says its content is the owner's\n * (`provenance`). An owner save while locked is written untagged and tagged at the next unlock if it\n * is still in place and this store can hold the id's marker then — unless it is a handoff save, which\n * another process runs. Every other save is untagged, and a pinned record is never tagged.\n */\n prepare(id: string, text: string, options: ISessionSaveOptions): IPreparedSave {\n const envelope = Buffer.from(text, 'utf8');\n const heldAs = this.#kindOfSave(id, options);\n let plaintext: Buffer = envelope;\n let tagPending = false;\n if (heldAs === 'owner') {\n const tag = this.#tag(id, envelope);\n if (tag === 'locked') {\n tagPending = options.handoff !== true;\n } else if (tag !== undefined) {\n plaintext = withAuthorshipHeader(tag, envelope);\n }\n }\n try {\n return {\n stored: this.#sealer.sealBytes(plaintext, recordContext(id)),\n text,\n heldAs,\n tagPending,\n };\n } finally {\n if (plaintext !== envelope) plaintext.fill(0);\n }\n }\n\n /**\n * Records that `prepared` is now at session `id`'s path, so the next save recognises it as its own.\n * A handoff write is kept apart, for one export: the record is another process's to run.\n */\n wrote(id: string, prepared: IPreparedSave, handoff: boolean): void {\n const prior = this.#memory.state(id)?.held;\n // A write-only child's own saves continue the record it was handed, on the same word.\n const vouched = prepared.heldAs === 'pinned' && prior?.kind === 'pinned' && prior.vouched;\n const record: IRememberedRecord = {\n digest: digestOf(prepared.stored),\n text: prepared.text,\n kind: prepared.heldAs,\n ...(vouched === true ? { vouched } : {}),\n };\n if (handoff && !this.#claims.has(id) && prepared.heldAs !== 'pinned') {\n this.#memory.dropHeld(id);\n this.#memory.holdHandoff(id, record);\n } else {\n this.#memory.dropHandoff(id);\n this.#memory.hold(id, record);\n }\n if (prepared.tagPending) this.#memory.setPending(id, record.digest);\n else this.#memory.clearPending(id);\n }\n\n /** {@link wrote} after a write that threw, only when `existing` is exactly its bytes. */\n wroteIfPresent(\n id: string,\n prepared: IPreparedSave,\n existing: TStoredSessionFile,\n handoff: boolean,\n ): void {\n if (existing.kind === 'sealed' && sameBytes(existing.bytes, prepared.stored)) {\n this.wrote(id, prepared, handoff);\n }\n }\n\n /**\n * The record this instance last wrote or holds, for a writer whose `load` said `locked`: offered only\n * while `existing` is exactly those bytes; `changed` when it holds the id and the file moved on. A\n * run-here id is answered as every read of it is, and a handoff record a writer asks for is taken over.\n */\n held(id: string, existing: TStoredSessionFile): TSessionLoadOutcome | undefined {\n const guarded = this.guarded(id, existing, 'held');\n if (guarded !== undefined) return guarded;\n const entry = this.#memory.state(id)?.held;\n if (entry === undefined) return undefined;\n const bytes = bytesOf(existing);\n if (bytes === undefined || digestOf(bytes) !== entry.digest) return CHANGED_OUTCOME;\n return this.#heldOutcome(id, entry, 'held', false);\n }\n\n /**\n * The record of session `id` as a token for another store over the same files, carrying the\n * envelope only: a handoff record while `existing` is still its bytes (handed over once, or left in\n * place with `keep`), or else — unless `handoffOnly` — the record opened from `existing`, a probe that\n * holds nothing. A run-here id's token is only what the owner wrote. The process a token goes to\n * runs the id under a lease ({@link IInteractiveSessionStore.lend}), whose marker keeps this store\n * from tagging or adopting it meanwhile.\n */\n exportHeld(\n id: string,\n existing: TStoredSessionFile,\n options: ISessionExportOptions,\n ): string | undefined {\n if (existing.kind !== 'sealed' || !isCurrentSealedArtifactBytes(existing.bytes))\n return undefined;\n const digest = digestOf(existing.bytes);\n const handOver = (text: string, kind: THeldRecordKind): string =>\n formatHeldToken(digest, text, kind === 'owner');\n const state = this.#memory.state(id);\n if (this.#isRunHere(id, state)) {\n if (options.handoffOnly === true) return undefined;\n const outcome = this.guarded(id, existing, 'export');\n const entry = this.#memory.state(id)?.held;\n if (outcome?.status !== 'valid' || outcome.diverged === true || entry?.digest !== digest) {\n return undefined;\n }\n return handOver(entry.text, outcome.authorship === 'verified' ? 'owner' : 'unverified');\n }\n const handoff = state?.handoff;\n if (handoff !== undefined) {\n if (options.keep !== true) this.#memory.dropHandoff(id);\n if (handoff.digest !== digest) return undefined;\n this.#handedOut(id, handoff.kind, 'export');\n return handOver(handoff.text, handoff.kind);\n }\n if (options.handoffOnly === true) return undefined;\n const opened = openSealed(this.#sealer, id, existing.bytes);\n if (opened.kind !== 'text' || decodeRecordText(id, opened.text).status !== 'valid') {\n return undefined;\n }\n const kind =\n opened.verified || this.#isOwners(state, state?.held, digest) ? 'owner' : 'unverified';\n this.#handedOut(id, kind, 'export');\n return handOver(opened.text, kind);\n }\n\n /**\n * Holds the record a token carries as pinned: every read of the id answers only from it and this\n * instance's own later writes, and a save may replace exactly the bytes the token names. Throws\n * `TypeError` (fixed text) for a token this build does not issue or one that does not hold a valid\n * record of session `id`, and {@link SessionClaimedError} for an id a session here has claimed.\n */\n importHeld(id: string, token: string): void {\n const parsed = parseHeldToken(token);\n if (parsed === undefined) {\n throw new TypeError('The held record is not a token this build issues.');\n }\n if (decodeRecordText(id, parsed.text).status !== 'valid') {\n throw new TypeError('The held record is not a valid record of this session.');\n }\n this.#claims.assertUnclaimed(id);\n this.#memory.dropHandoff(id);\n this.#memory.hold(id, {\n digest: parsed.digest,\n text: parsed.text,\n kind: 'pinned',\n ...(parsed.owner ? { vouched: true as const } : {}),\n });\n }\n\n /**\n * Raises the record of session `id` whose stored bytes have SHA-256 `digest` to the owner's, and\n * clears the id's taint:\n *\n * 1. This instance holds those bytes (a resume the owner accepted): they are held as the owner's\n * from now on, and tagged in place while the file is still them — at the next unlock when the keys\n * are locked (`pending`). A file that changed since is not touched; a claimed id is answered with\n * the adopted copy and its next save writes it.\n * 2. It holds nothing for them and the file is exactly them (a fork record a child wrote): they are\n * tagged in place, or at the next unlock (`pending`), and nothing is held for them here.\n * 3. Otherwise nothing is raised (`changed`).\n *\n * The caller holds the id's writer marker for it, so nothing is raised under another writer.\n */\n adopt(id: string, digest: string): TSessionAdoptResult {\n if (!isSha256Hex(digest)) return 'changed';\n const state = this.#memory.state(id);\n const file = this.#files.current(id);\n const bytes = bytesOf(file);\n const onDisk = bytes === undefined ? undefined : digestOf(bytes);\n const entry = state?.held;\n if (entry !== undefined && entry.digest === digest && entry.kind !== 'pinned') {\n this.#memory.hold(id, { ...entry, kind: 'owner' });\n this.#memory.untaint(id);\n if (onDisk !== digest) return 'adopted';\n // The bytes are raised in memory whether or not the file is still them when the tag is written.\n return this.#tagInPlace(id, digest, entry.text, true) === 'pending' ? 'pending' : 'adopted';\n }\n if (bytes === undefined || onDisk !== digest || this.#sealer.tagAuthorship === undefined) {\n return 'changed';\n }\n const text = this.#textOfFile(id, file, bytes);\n if (text === 'locked') {\n this.#memory.untaint(id);\n this.#memory.setPending(id, digest);\n return 'pending';\n }\n if (text === undefined || decodeRecordText(id, text).status !== 'valid') return 'changed';\n const result = this.#tagInPlace(id, digest, text, false);\n if (result !== 'changed') this.#memory.untaint(id);\n return result;\n }\n\n /** The digest of the bytes this instance holds for `id`, or `undefined`. */\n heldDigest(id: string): string | undefined {\n return this.#memory.state(id)?.held?.digest;\n }\n\n /** Forgets the id's records and pending tag: the session was deleted. Its taint stays. */\n forget(id: string): void {\n this.#memory.forget(id);\n }\n\n /** Drops only a claim's held plaintext; pending tags, taint and handoffs remain intact. */\n discardHeld(id: string): void {\n this.#memory.dropHeld(id);\n }\n\n /** Subscribes `listener` to the unlocks of this store's keys (held as weakly as the sealer holds it). */\n onUnlock(listener: () => void): () => void {\n return this.#sealer.onUnlock?.(listener) ?? (() => undefined);\n }\n\n /** Whether a session in this process runs `id`: claimed, or held pinned by a write-only child. */\n #isRunHere(id: string, state: ISessionRecordState | undefined): boolean {\n return this.#claims.has(id) || state?.held?.kind === 'pinned';\n }\n\n /**\n * Whether bytes `digest` of an id are the owner's by what this store holds for it: `record` is held\n * as the owner's with those bytes, or those are owner bytes waiting for a tag. The one place that\n * says so; a tag that verifies says so on its own.\n */\n #isOwners(\n state: ISessionRecordState | undefined,\n record: IRememberedRecord | undefined,\n digest: string,\n ): boolean {\n return (\n state?.pendingDigest === digest || (record?.kind === 'owner' && record.digest === digest)\n );\n }\n\n /** The record `entry` holds, as an outcome of a read for `purpose`. */\n #heldOutcome(\n id: string,\n entry: IRememberedRecord,\n purpose: TReadPurpose,\n diverged: boolean,\n ): TSessionLoadOutcome {\n const outcome = decodeRecordText(id, entry.text);\n if (outcome.status !== 'valid') return outcome;\n if (entry.kind === 'pinned') {\n // A write-only child's record is the owner's to it only on the word of the process that\n // handed it over; it is run here, so handing it out taints nothing.\n return annotated(outcome, entry.vouched === true, entry.digest, diverged);\n }\n const kind = this.#isOwners(this.#memory.state(id), entry, entry.digest)\n ? 'owner'\n : 'unverified';\n return this.#handOut(id, outcome, kind, entry.digest, purpose, diverged);\n }\n\n /** `outcome` annotated, and the id tainted when unverified content leaves for a writer of an id not run here. */\n #handOut(\n id: string,\n outcome: TValid,\n kind: THeldRecordKind,\n digest: string | undefined,\n purpose: TReadPurpose,\n diverged: boolean,\n ): TSessionLoadOutcome {\n this.#handedOut(id, kind, purpose);\n return annotated(outcome, kind === 'owner', digest, diverged);\n }\n\n #handedOut(id: string, kind: THeldRecordKind, purpose: TReadPurpose): void {\n if (\n kind !== 'owner' &&\n READ_PURPOSES[purpose].taints &&\n !this.#isRunHere(id, this.#memory.state(id))\n ) {\n this.#memory.taint(id);\n }\n }\n\n /** Whose a save of `id` writes (see {@link prepare}). */\n #kindOfSave(id: string, options: ISessionSaveOptions): THeldRecordKind {\n const state = this.#memory.state(id);\n const entry = state?.held ?? state?.handoff;\n if (entry?.kind === 'pinned') return 'pinned';\n if (options.provenance === 'unverified') return 'unverified';\n // A claimed id's saves come only from the session that claimed it, whose content no read of the\n // id ever mixed with unverified content; any other writer may have merged what a read handed out.\n if (!this.#claims.has(id) && state?.tainted === true) return 'unverified';\n if (entry !== undefined) return entry.kind;\n return options.provenance === 'owner' ? 'owner' : 'unverified';\n }\n\n /** The authorship tag of `envelope` for `id`; `locked` while the keys are; `undefined` when this sealer cannot tag. */\n #tag(id: string, envelope: Uint8Array): Uint8Array | 'locked' | undefined {\n if (this.#sealer.tagAuthorship === undefined) return undefined;\n try {\n return this.#sealer.tagAuthorship(envelope, recordContext(id));\n } catch (error) {\n if (isArtifactLockedError(error)) return 'locked';\n throw error;\n }\n }\n\n /** The envelope a file of `id` holds; `locked` while the keys are; `undefined` when it does not open. */\n #textOfFile(\n id: string,\n file: TStoredSessionFile,\n bytes: Uint8Array,\n ): string | 'locked' | undefined {\n if (file.kind !== 'sealed') return utf8Text(bytes);\n if (!isCurrentSealedArtifactBytes(bytes)) return undefined;\n const opened = openSealed(this.#sealer, id, bytes);\n if (opened.kind === 'locked') return 'locked';\n return opened.kind === 'text' ? opened.text : undefined;\n }\n\n /**\n * Re-seals `text` with a tag over the file of `id`, which held the owner's bytes `digest`; while\n * locked, it is tagged at the next unlock instead (`pending`); `changed` when the file is no longer\n * those bytes by the time the tag is written. With `hold`, this instance holds the new bytes.\n */\n #tagInPlace(id: string, digest: string, text: string, hold: boolean): TSessionAdoptResult {\n const envelope = Buffer.from(text, 'utf8');\n const tag = this.#tag(id, envelope);\n if (tag === 'locked') {\n this.#memory.setPending(id, digest);\n return 'pending';\n }\n if (tag === undefined) return 'adopted';\n return this.#writeTagged(id, envelope, text, tag, digest, hold) ? 'adopted' : 'changed';\n }\n\n /**\n * Writes `text` sealed with `tag` over the file of `id` — only while the file is still the bytes\n * `expected`, read again immediately before the write, so a newer write by another of the owner's\n * processes since the check is never replaced with these older bytes. Returns whether it wrote.\n */\n #writeTagged(\n id: string,\n envelope: Uint8Array,\n text: string,\n tag: Uint8Array,\n expected: string,\n hold: boolean,\n ): boolean {\n const plaintext = withAuthorshipHeader(tag, envelope);\n let stored: Uint8Array;\n try {\n stored = this.#sealer.sealBytes(plaintext, recordContext(id));\n } finally {\n plaintext.fill(0);\n }\n const now = bytesOf(this.#files.current(id));\n if (now === undefined || digestOf(now) !== expected) return false;\n const prepared: IPreparedSave = { stored, text, heldAs: 'owner', tagPending: false };\n try {\n this.#files.replace(id, stored);\n } catch (error) {\n if (hold) this.wroteIfPresent(id, prepared, this.#files.current(id), false);\n throw error;\n }\n this.#memory.clearPending(id);\n if (hold) this.wrote(id, prepared, false);\n return true;\n }\n\n /**\n * At unlock: tags, synchronously, every owner write and deferred adopt that waited for the keys,\n * while its file is still exactly those bytes and this store holds the id's writer marker or can take\n * it, and drops each either way. An id another writer holds — a process it is lent to, another\n * session — is left untagged, since tagging would change the bytes that writer holds and refuse its\n * saves; the record then stays unverified.\n */\n #tagPendingAtUnlock(): void {\n for (const [id, digest] of this.#memory.pending()) {\n this.#memory.clearPending(id);\n try {\n this.#underWriteMarker(id, () => this.#tagPendingOne(id, digest));\n } catch {\n // allow-fallback: one record's failure must not stop the others; its next owner save tags it.\n }\n }\n }\n\n /** {@link #tagPendingAtUnlock} for one id, holding its marker. */\n #tagPendingOne(id: string, digest: string): void {\n const file = this.#files.current(id);\n const bytes = bytesOf(file);\n if (bytes === undefined || digestOf(bytes) !== digest) return;\n const entry = this.#memory.state(id)?.held;\n const hold = entry?.digest === digest;\n const text = hold ? entry.text : this.#textOfFile(id, file, bytes);\n if (text === undefined || text === 'locked') return;\n if (decodeRecordText(id, text).status !== 'valid') return;\n const envelope = Buffer.from(text, 'utf8');\n const tag = this.#tag(id, envelope);\n if (!(tag instanceof Uint8Array)) return;\n this.#writeTagged(id, envelope, text, tag, digest, hold);\n }\n}\n","/**\n * The one place both session stores (the user's directory and the trusted project's) keep their\n * records through, so they cannot disagree about what a file means. A record is stored in one of two\n * forms:\n *\n * - plain: the versioned JSON envelope, or\n * - sealed: the same JSON bytes sealed by an {@link IArtifactSealer}, bound to the record's own id.\n *\n * A store that seals WRITES sealed and reads both, so a directory part-way through a migration lists\n * correctly and a plain record turns sealed the next time its session is saved. A store that seals\n * nothing writes plain and meets a sealed file as `locked`: this process cannot read it, which is a\n * different fact from \"damaged\" and from \"not there\".\n *\n * A save over a sealed file this store cannot open goes ahead only when the file is provably exactly\n * the bytes it last wrote or holds (`sealed-session-memory.ts`); anything else on disk is a session\n * this instance never held, or one that changed since, and is not replaced. Every store claims the\n * ids its sessions run (`session-claims.ts`), exclusively across processes and fencing their saves,\n * and every write of an id holds the id's writer marker, the claim's own or one taken for the write;\n * a store that seals also decides and keeps whose each record is (`stored-session-authorship.ts`).\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\n\nimport { isCurrentSealedArtifactBytes, isSealedArtifactBytes } from '@robota-sdk/agent-core';\n\nimport { SealedSessionUnreadableError } from './sealed-session-refusal.js';\nimport { SealedSessionMemory } from './sealed-session-memory.js';\nimport { SessionClaims, isSessionClaimedError, writerHolderOf } from './session-claims.js';\nimport { assertSafeSessionId } from './session-id.js';\nimport {\n MISSING_OUTCOME,\n NO_KEYS_OUTCOME,\n StoredSessionAuthorship,\n} from './stored-session-authorship.js';\nimport {\n bytesOf,\n decodeRecordText,\n encodeStoredSession,\n envelopeText,\n utf8Text,\n} from './stored-session-codec.js';\n\nimport type { ISessionClaimRefusalWords } from './session-claims.js';\nimport type { TReadPurpose } from './stored-session-authorship.js';\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type {\n IArtifactSealer,\n IProcessMarkerHandle,\n IProcessMarkers,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\nimport type {\n IInteractiveSessionRecord,\n ISessionAuthorship,\n ISessionClaim,\n ISessionClaimOptions,\n ISessionExportOptions,\n ISessionLease,\n ISessionLoadOptions,\n ISessionSaveOptions,\n TSessionAdoptResult,\n TSessionLoadOutcome,\n} from '@robota-sdk/agent-interface-session';\n\n/**\n * How a session store is built. `sealing` is required: every store states whether it keeps its\n * records sealed or plain, because a store that silently kept them plain beside sealed ones would\n * write plaintext next to them. A host that seals states it for every store it builds.\n * `releaseCommand` is the host's command that releases a writer marker whose holder is not running,\n * for a `has_writer` refusal to name; without it the refusal names none.\n */\nexport interface ISessionStoreOptions extends ISessionClaimRefusalWords {\n readonly sealing: TArtifactSealing;\n}\n\n/** The writer markers of a store's `claims` directory, which every store is built with. */\nfunction assertProcessMarkers(value: unknown): asserts value is IProcessMarkers {\n const markers = value as Partial<IProcessMarkers> | null | undefined;\n if (\n typeof markers?.acquire !== 'function' ||\n typeof markers.takeOver !== 'function' ||\n typeof markers.endLease !== 'function' ||\n typeof markers.inspect !== 'function'\n ) {\n throw new TypeError(\n 'A session store holds a writer marker for every id it writes: build it with the markers of its claims directory.',\n );\n }\n}\n\n/** The sealing `options` state; one that is not a {@link TArtifactSealing} (a call that ignores the types) is refused. */\nexport function sessionSealingOf(options: ISessionStoreOptions): TArtifactSealing {\n if (typeof options !== 'object' || options === null) {\n throw new TypeError('Session store options must be an object, such as { sealing }.');\n }\n assertSessionSealing(options.sealing);\n return options.sealing;\n}\n\n/** Refuses anything that is not a {@link TArtifactSealing}. */\nexport function assertSessionSealing(value: unknown): asserts value is TArtifactSealing {\n if (value === 'plain') return;\n const sealer =\n typeof value === 'object' && value !== null\n ? (value as { readonly sealer?: unknown }).sealer\n : undefined;\n if (\n typeof sealer === 'object' &&\n sealer !== null &&\n typeof (sealer as Partial<IArtifactSealer>).sealBytes === 'function' &&\n typeof (sealer as Partial<IArtifactSealer>).openBytes === 'function'\n ) {\n return;\n }\n throw new TypeError(\"A session store states how it seals: { sealer }, or 'plain'.\");\n}\n\n/**\n * How a store reaches the file of each record — the one thing it gives {@link StoredSessionRecords}.\n * Each method validates the id the way the store addresses its files.\n */\nexport interface IStoredSessionFiles {\n /**\n * What is at session `id`'s path for a load or a listing, read whole, and what such a read reports\n * when it could not read the file whole.\n */\n load(id: string): { readonly file: TStoredSessionFile; readonly unreadable: TSessionLoadOutcome };\n /** What is at session `id`'s path now, read whole (plain files too in a store that seals), before it is replaced. */\n current(id: string): TStoredSessionFile;\n /** Replaces session `id`'s file with `stored`, whole. */\n replace(id: string, stored: Uint8Array): void;\n /** What an absent record is rebuilt as (a project session's replay log); without it, `missing`. */\n rebuild?(id: string): TSessionLoadOutcome;\n}\n\n/**\n * One store's stored form of its records: how it encodes, decodes, guards a replace, claims and\n * remembers what it holds, over the files {@link IStoredSessionFiles} reaches. A store routes every\n * read and write of a record through one of these.\n */\nexport class StoredSessionRecords {\n readonly #sealer: IArtifactSealer | undefined;\n readonly #files: IStoredSessionFiles;\n readonly #markers: IProcessMarkers;\n readonly #claims: SessionClaims;\n readonly #authorship: StoredSessionAuthorship | undefined;\n /** What a store that seals offers as `IInteractiveSessionStore.authorship`; absent from one that never does. */\n readonly authorship: ISessionAuthorship | undefined;\n\n /**\n * @param markers the writer markers of the store's `claims` directory, beside its records: every\n * claim, lend and write of an id holds the id's marker, so a store without them does not exist.\n * @param words how refusals are worded (`ISessionStoreOptions.releaseCommand`).\n */\n constructor(\n sealing: TArtifactSealing,\n files: IStoredSessionFiles,\n markers: IProcessMarkers,\n words: ISessionClaimRefusalWords = {},\n ) {\n assertSessionSealing(sealing);\n assertProcessMarkers(markers);\n this.#sealer = sealing === 'plain' ? undefined : sealing.sealer;\n this.#files = files;\n this.#markers = markers;\n this.#claims = new SessionClaims(\n words.releaseCommand === undefined ? {} : { releaseCommand: words.releaseCommand },\n { pid: process.pid, windows: process.platform === 'win32' },\n );\n if (this.#sealer === undefined) {\n this.#authorship = undefined;\n this.authorship = undefined;\n return;\n }\n const engine = new StoredSessionAuthorship(\n this.#sealer,\n files,\n new SealedSessionMemory((id) => this.#claims.has(id)),\n this.#claims,\n (id, write) => this.#underWriteMarker(id, write),\n );\n this.#authorship = engine;\n this.authorship = Object.freeze({\n adopt: (id: string, digest: string): TSessionAdoptResult => {\n assertSafeSessionId(id);\n // The raise may tag the file in place: a write of the id, so it holds the id's marker.\n const result = this.#underWriteMarker(id, () => engine.adopt(id, digest));\n return result === 'has_writer' ? 'has_writer' : result;\n },\n heldDigest: (id: string) => {\n assertSafeSessionId(id);\n return engine.heldDigest(id);\n },\n onUnlock: (listener: () => void) => engine.onUnlock(listener),\n });\n }\n\n /** Whether this store writes its records sealed. */\n get seals(): boolean {\n return this.#sealer !== undefined;\n }\n\n /**\n * What session `id` holds, saying which of the five things happened. Only a load that asks to `hold`\n * remembers a record it read, so that this instance can go on saving the session after the keys\n * lock; a probe remembers nothing, and so can never push a held session out of the bounded memory\n * or keep plaintext it was only looking at.\n */\n load(id: string, options: ISessionLoadOptions = {}): TSessionLoadOutcome {\n return this.#read(id, options.hold === true ? 'hold' : 'probe');\n }\n\n /** {@link load} for one entry of a listing, which no writer merges. */\n listed(id: string): TSessionLoadOutcome {\n return this.#read(id, 'list');\n }\n\n /**\n * Claims session `id` for a session this process runs and reads it in the same call (see\n * `IInteractiveSessionStore.claim`): the id's writer marker is taken first — published, or taken\n * over under `lease` — and the read follows, so nothing another writer saves can come between them.\n * An in-process runner's claim of a record a handoff save left takes it over; a copy's read takes\n * no marker. Throws `SessionClaimedError`: `has_writer` for an id claimed or lent here or held by\n * another writer, naming it, and `released` for a lease that has ended.\n */\n claim(id: string, options: ISessionClaimOptions = {}): ISessionClaim {\n this.#claims.assertUnclaimed(id);\n const fresh = options.fresh === true;\n this.#authorship?.beforeClaim(id, fresh, options.lease !== undefined);\n const marker = options.copy === true ? undefined : this.#takeMarker(id, options.lease);\n const open = (): ISessionClaim =>\n this.#claims.open(id, fresh, marker, () => {\n const { file, unreadable } = this.#files.load(id);\n return this.#authorship === undefined\n ? this.#readPlain(id, file, unreadable)\n : this.#authorship.claimRead(id, fresh, file, unreadable, () => this.#rebuild(id));\n }, () => this.#authorship?.discardHeld(id));\n if (marker === undefined) return open();\n try {\n return this.#guardWrite(id, marker, open);\n } catch (error) {\n marker.release();\n throw error;\n }\n }\n\n /**\n * Lends session `id` to another process (see `IInteractiveSessionStore.lend`): its marker is\n * published for this process with the digest of a fresh nonce, which the borrowing process claims\n * with, and the id counts as written here until the end, which removes the marker whoever holds it.\n */\n lend(id: string): ISessionLease {\n this.#claims.assertUnclaimed(id);\n const nonce = randomBytes(32).toString('hex');\n const digest = createHash('sha256').update(nonce, 'utf8').digest('hex');\n const published = this.#markers.acquire(id, { leaseDigest: digest });\n if (published.status !== 'acquired') {\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n const unlend = this.#claims.lend(id, () => {\n const marker = this.#markers.inspect(id);\n return marker.status === 'held' ? marker.holder : undefined;\n });\n let ended = false;\n return Object.freeze({\n nonce,\n end: (): void => {\n if (ended) return;\n ended = true;\n try {\n this.#markers.endLease(id, digest);\n } finally {\n unlend();\n }\n },\n });\n }\n\n /**\n * Whether session `id` may have a writer now: a claim or a lend of this store, or the writer marker\n * of any other holder whose owner is not provably gone. A surface that would open the id as another\n * session asks this first.\n */\n hasWriter(id: string): boolean {\n if (this.#claims.has(id) || this.#claims.isLent(id)) return true;\n const marker = this.#markers.inspect(id);\n return marker.status === 'held' && marker.holder.liveness !== 'gone';\n }\n\n /**\n * Saves `record` after the checks a save must pass: a claimed id is saved only under its live claim,\n * and a claim that ended or lost its marker is refused (the write fence); a save without a claim\n * takes the id's marker for the write and is refused while another writer holds it. Then the file\n * there must be replaceable — absent, plain, exactly what this instance last wrote or holds, or\n * sealed and openable here (a claimed id's only when it is what the owner wrote). What the check\n * stops is a store replacing a sealed session nobody in this process can read. A write that fails\n * with its bytes already in place is remembered as this store's own.\n */\n save(record: IInteractiveSessionRecord, options: ISessionSaveOptions = {}): void {\n const id = record.id;\n const marker = this.#claims.markerForSave(id, options.claim);\n if (marker !== undefined) {\n this.#guardWrite(id, marker, () => this.#write(record, options));\n return;\n }\n this.#withTransientMarker(id, () => this.#write(record, options));\n }\n\n /** Removes session `id`'s record with `remove`, holding the id's marker; refused while another writer holds it. */\n delete(id: string, remove: () => void): void {\n this.#claims.markerForSave(id, undefined);\n this.#withTransientMarker(id, () => {\n this.forget(id);\n remove();\n });\n }\n\n #write(record: IInteractiveSessionRecord, options: ISessionSaveOptions): void {\n const id = record.id;\n const existing = this.#files.current(id);\n const engine = this.#authorship;\n if (engine === undefined) {\n this.#assertPlainReplaceable(id, existing);\n this.#files.replace(id, encodeStoredSession(record, undefined).stored);\n return;\n }\n engine.assertReplaceable(id, existing);\n const prepared = engine.prepare(id, envelopeText(record), options);\n const handoff = options.handoff === true;\n try {\n this.#files.replace(id, prepared.stored);\n } catch (error) {\n // A step after the rename (the directory flush) can fail with the new bytes already in place;\n // they are this store's own, and the next save must know them, or one unlucky write would block\n // this session while the keys are locked. Only bytes provably at the path are remembered.\n engine.wroteIfPresent(id, prepared, this.#currentQuietly(id), handoff);\n throw error;\n }\n engine.wrote(id, prepared, handoff);\n }\n\n /**\n * The record this instance itself last wrote or holds, for a writer whose `load` said `locked`:\n * offered only while the file is exactly the bytes it remembers. `undefined` when it holds nothing\n * for `id` (and always in a store that seals nothing); `locked` with the reason `changed` when it\n * does and the file is no longer those bytes, so the writer can say which of the two stopped it.\n */\n loadHeld(id: string): TSessionLoadOutcome | undefined {\n return this.#authorship?.held(id, this.#files.current(id));\n }\n\n /**\n * The sealed record of session `id` as a token another store over the same files can take with\n * {@link importHeld} (see `StoredSessionAuthorship.exportHeld`); `undefined` from a store that seals\n * nothing, whose records anyone with the store reads from disk.\n */\n exportHeld(id: string, options: ISessionExportOptions = {}): string | undefined {\n return this.#authorship?.exportHeld(id, this.#files.current(id), options);\n }\n\n /**\n * Holds the record a token from {@link exportHeld} carries, pinned, so a store that cannot open the\n * file (a write-only subagent process) can go on with the session. Throws `TypeError` in a store\n * that seals nothing: it would write the record in plaintext over the sealed one.\n */\n importHeld(id: string, token: string): void {\n if (this.#authorship === undefined) {\n throw new TypeError(\n 'A store that seals nothing does not take a held record: it would write it in plaintext.',\n );\n }\n this.#authorship.importHeld(id, token);\n }\n\n /** Forgets what this instance holds of session `id`, which was deleted. */\n forget(id: string): void {\n this.#authorship?.forget(id);\n }\n\n /**\n * The marker a claim of `id` holds: published for this process, or taken over from the process that\n * lent it under `lease`. Throws `has_writer` naming the holder while another writer holds it, and\n * `released` when no marker carries the lease any more.\n */\n #takeMarker(id: string, lease: string | undefined): IProcessMarkerHandle {\n if (lease === undefined) {\n const published = this.#markers.acquire(id);\n if (published.status === 'acquired') return published.handle;\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n const taken = this.#markers.takeOver(id, lease);\n if (taken.status === 'acquired') return taken.handle;\n if (taken.status === 'absent') throw this.#claims.refusal(id, 'released', { lease: true });\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(taken.holder) });\n }\n\n /** Runs `write` holding the id's marker for the write alone, refused while another writer holds it. */\n #withTransientMarker<T>(id: string, write: () => T): T {\n const published = this.#markers.acquire(id);\n if (published.status !== 'acquired') {\n throw this.#claims.refusal(id, 'has_writer', { holder: writerHolderOf(published.holder) });\n }\n try {\n return this.#guardWrite(id, published.handle, write);\n } finally {\n published.handle.release();\n }\n }\n\n #guardWrite<T>(id: string, marker: IProcessMarkerHandle, write: () => T): T {\n const result = marker.withGuard(write);\n if (result.status === 'ran') return result.value;\n throw this.#claims.refusal(id, result.status === 'lost' ? 'released' : 'has_writer');\n }\n\n /**\n * Runs a write of `id` that carries no claim (an adopt, a tag at the unlock) holding the id's marker:\n * the live claim's when this store claims the id, else one taken for the write. `has_writer` when\n * another writer holds it, and nothing is written.\n */\n #underWriteMarker<T>(id: string, write: () => T): T | 'has_writer' {\n try {\n const marker = this.#claims.markerForWrite(id);\n if (marker !== undefined) return this.#guardWrite(id, marker, write);\n } catch (error) {\n if (isSessionClaimedError(error)) return 'has_writer';\n throw error;\n }\n const published = this.#markers.acquire(id);\n if (published.status !== 'acquired') return 'has_writer';\n try {\n return this.#guardWrite(id, published.handle, write);\n } catch (error) {\n if (isSessionClaimedError(error)) return 'has_writer';\n throw error;\n } finally {\n published.handle.release();\n }\n }\n\n #read(id: string, purpose: TReadPurpose): TSessionLoadOutcome {\n const { file, unreadable } = this.#files.load(id);\n return this.#authorship === undefined\n ? this.#readPlain(id, file, unreadable)\n : this.#authorship.read(id, file, unreadable, () => this.#rebuild(id), purpose);\n }\n\n /** A read by a store that seals nothing: what the bytes say, and a sealed file is `locked` with no keys. */\n #readPlain(\n id: string,\n file: TStoredSessionFile,\n unreadable: TSessionLoadOutcome,\n ): TSessionLoadOutcome {\n if (file.kind === 'absent') return this.#rebuild(id);\n const bytes = bytesOf(file);\n if (bytes === undefined) return unreadable;\n if (!isSealedArtifactBytes(bytes)) return decodeRecordText(id, utf8Text(bytes));\n return isCurrentSealedArtifactBytes(bytes)\n ? NO_KEYS_OUTCOME\n : { status: 'unsupported', schemaVersion: undefined };\n }\n\n /**\n * A store that seals nothing replaces absence, a plain file and one past its read limit; a sealed\n * file is a session no key in this process opens, and an unreadable one may be one.\n */\n #assertPlainReplaceable(id: string, existing: TStoredSessionFile): void {\n switch (existing.kind) {\n case 'absent':\n case 'plain':\n case 'oversized':\n return;\n case 'unreadable':\n throw new SealedSessionUnreadableError(id, 'unreadable');\n case 'sealed':\n throw new SealedSessionUnreadableError(\n id,\n isCurrentSealedArtifactBytes(existing.bytes) ? 'no-keys' : 'newer-format',\n );\n }\n }\n\n #rebuild(id: string): TSessionLoadOutcome {\n return this.#files.rebuild?.(id) ?? MISSING_OUTCOME;\n }\n\n /** What is at the path after a failed write, which may have left it in any state: a read that fails proves nothing was written. */\n #currentQuietly(id: string): TStoredSessionFile {\n try {\n return this.#files.current(id);\n } catch {\n // allow-fallback: an unreadable path proves nothing was written, so nothing is remembered.\n return { kind: 'unreadable' };\n }\n }\n}\n","/**\n * NodeSessionStore — persists conversation sessions as JSON files.\n *\n * The caller explicitly supplies a host-owned base directory.\n * This adapter does not interpret that directory as a trusted project root.\n * The store directory is created on first write if it does not exist.\n *\n * Sealed records or plain ones, as the caller states when it builds the store (see\n * `stored-session-record.ts`); plain files are the JSON they always were. Only a store that seals\n * offers `authorship`, and says whose each record it reads is. The writer markers of its claims and\n * lends are kept in `claims/` beside the records, so every process over these files sees them.\n */\n\nimport { createProcessOperationLock } from '@robota-sdk/agent-node-lock';\n\nimport {\n closeSync,\n existsSync,\n fstatSync,\n openSync,\n readFileSync,\n readSync,\n readdirSync,\n unlinkSync,\n} from 'fs';\nimport { join, resolve, sep } from 'path';\n\nimport { SEALED_ARTIFACT_MAGIC, isSealedArtifactBytes } from '@robota-sdk/agent-core';\nimport {\n createProcessMarkers,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n writeOwnerOnlyFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { assertSafeSessionId, isSafeSessionId } from './session-id.js';\nimport { classifyStoredSessionBytes } from './stored-session-codec.js';\nimport { StoredSessionRecords, sessionSealingOf } from './stored-session-record.js';\n\nimport type { TStoredSessionFile } from './stored-session-codec.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\nimport type {\n IInteractiveSessionRecord,\n IInteractiveSessionStore,\n ISessionAuthorship,\n ISessionClaim,\n ISessionClaimOptions,\n ISessionExportOptions,\n ISessionLease,\n ISessionListEntry,\n ISessionLoadOptions,\n ISessionSaveOptions,\n ISessionStoreScan,\n TSessionLoadOutcome,\n TSessionRestoreCommitOutcome,\n} from '@robota-sdk/agent-interface-session';\n\nfunction errnoCode(error: unknown): unknown {\n return typeof error === 'object' && error !== null\n ? (error as { readonly code?: unknown }).code\n : undefined;\n}\n\n/** What a load reports for a record it could not read: fixed text, and the errno code when there is one. */\nfunction unreadableMessage(error: unknown): string {\n const code = errnoCode(error);\n return typeof code === 'string' && /^E[A-Z0-9]+$/.test(code)\n ? `could not read the session file (${code})`\n : 'could not read the session file';\n}\n\n/**\n * The rest of a file whose first bytes `head` holds, read through the same descriptor, so the bytes\n * judged are the bytes of one file. Reads to the size the descriptor reported: a file that grew since\n * is then not the bytes any store remembers, which fails closed.\n */\nfunction readRest(descriptor: number, head: Buffer): Buffer {\n const bytes = Buffer.alloc(Math.max(fstatSync(descriptor).size, head.length));\n head.copy(bytes, 0);\n let offset = head.length;\n while (offset < bytes.length) {\n const read = readSync(descriptor, bytes, offset, bytes.length - offset, offset);\n if (read === 0) break;\n offset += read;\n }\n return bytes.subarray(0, offset);\n}\n\nconst MISSING: TSessionLoadOutcome = Object.freeze({ status: 'missing' });\n\n/**\n * Where a store keeps the writer markers of its claims and lends: a directory beside the records that\n * is not a `.json` file, so a listing never reads it as a session.\n */\nexport const SESSION_CLAIMS_DIRECTORY = 'claims';\n\n/**\n * Most recent first, and an unreadable entry sorts last rather than being dropped.\n *\n * An entry with no record has no `updatedAt` to sort by; giving it the epoch keeps it present and\n * out of the way, which is the whole point of listing it at all.\n */\n/** How many of `entries` are `locked`, by why: the keys are locked, or this store holds none. */\nfunction countLeftOut(entries: readonly ISessionListEntry[]): Omit<ISessionStoreScan, 'entries'> {\n let locked = 0;\n let noKeys = 0;\n for (const { outcome } of entries) {\n if (outcome.status !== 'locked') continue;\n if (outcome.reason === 'no-keys') noKeys += 1;\n else locked += 1;\n }\n return noKeys === 0 ? { locked } : { locked, noKeys };\n}\n\nfunction compareListEntriesByRecency(left: ISessionListEntry, right: ISessionListEntry): number {\n const at = (entry: ISessionListEntry): number =>\n entry.outcome.status === 'valid' ? new Date(entry.outcome.record.updatedAt).getTime() : 0;\n return at(right) - at(left);\n}\n\n/** How a {@link NodeSessionStore} is built. */\nexport interface INodeSessionStoreOptions extends ISessionStoreOptions {\n /**\n * An ancestor of the store's directory the HOST also owns, tightened along with it.\n * Optional because this adapter does not interpret its base directory as a trusted root and must\n * not guess that a parent belongs to the product — which of them do is composition's knowledge.\n * Left out, a store root that already exists keeps whatever mode it was given: the leaf is made\n * 0700 and the directory above it is not touched.\n */\n readonly ownedRoot?: string;\n}\n\n/**\n * Persistent session store backed by individual JSON files.\n *\n * Construct with a host-owned `baseDir`; framework project composition uses a separate\n * authority-backed adapter over the same neutral port.\n */\nexport class NodeSessionStore implements IInteractiveSessionStore {\n readonly supportsClaimDiscard = true as const;\n private readonly baseDir: string;\n private readonly ownedRoot: string | undefined;\n /** How records are stored, and what this instance claims, wrote or holds. */\n private readonly records: StoredSessionRecords;\n /** What only a store that seals offers (`IInteractiveSessionStore.authorship`); absent from one that never does. */\n declare readonly authorship?: ISessionAuthorship;\n\n /**\n * @param baseDir the directory holding the records.\n * @param options `sealing` (required): `{ sealer }` seals what `save` writes and opens what `load`\n * reads; `'plain'` writes plain JSON and reports a sealed file it finds as `locked`.\n * `ownedRoot`: see {@link INodeSessionStoreOptions}. `releaseCommand`: see `ISessionStoreOptions`.\n */\n constructor(baseDir: string, options: INodeSessionStoreOptions) {\n const sealing = sessionSealingOf(options);\n this.records = new StoredSessionRecords(\n sealing,\n {\n load: (id) => this.loadFile(id),\n current: (id) => this.readExisting(this.filePath(id)),\n replace: (id, stored) => {\n this.ensureDir();\n writeOwnerOnlyFile(this.filePath(id), stored);\n },\n },\n createProcessMarkers(join(resolve(baseDir), SESSION_CLAIMS_DIRECTORY), {\n operationLock: createProcessOperationLock(join(resolve(baseDir), SESSION_CLAIMS_DIRECTORY)),\n }),\n options.releaseCommand === undefined ? {} : { releaseCommand: options.releaseCommand },\n );\n this.baseDir = baseDir;\n this.ownedRoot = options.ownedRoot;\n // Only on a store that seals: a store whose records carry no authorship has nothing to adopt.\n const authorship = this.records.authorship;\n if (authorship !== undefined) this.authorship = authorship;\n }\n\n /**\n * Claims `id` for a session this process runs, reading it in the same call\n * (`IInteractiveSessionStore.claim`): exclusive across processes and store instances, and every\n * later save of the id must carry the claim until it is released.\n */\n claim(id: string, options?: ISessionClaimOptions): ISessionClaim {\n this.filePath(id);\n return this.records.claim(id, options);\n }\n\n /** Lends `id` to another process that will write it (`IInteractiveSessionStore.lend`). */\n lend(id: string): ISessionLease {\n this.filePath(id);\n return this.records.lend(id);\n }\n\n /** Whether session `id` may have a writer now (`IInteractiveSessionStore.hasWriter`). */\n hasWriter(id: string): boolean {\n this.filePath(id);\n return this.records.hasWriter(id);\n }\n\n /**\n * Ensure the storage directory exists AND that only its owner can enter it.\n *\n * The `existsSync` guard this replaces is the whole defect. It skipped the case that matters: a\n * directory some earlier version, a shared CI checkout, or another local user left at a wider\n * mode was adopted as ours with no signal. Measured under umask 022 before this change, a fresh\n * sessions directory came out 0755 and its records 0644 — and a directory pre-created at 0777\n * stayed 0777.\n */\n private ensureDir(): void {\n ensureOwnerOnlyDirectory(\n this.baseDir,\n this.ownedRoot === undefined ? {} : { withinRoot: this.ownedRoot },\n );\n }\n\n /**\n * Absolute path to a session's JSON file.\n *\n * Every public method routes through here, so validating the id at this one point covers\n * `save` (write), `load` (read), and `delete` (unlink) at once.\n *\n * `assertSafeSessionId` is the guard, and it is sound — no separator, no `.`/`..`\n * can pass it. But it is a regex reject behind a helper, which static analysis does not model as\n * a path sanitizer, so `js/path-injection` re-opened on `load` every time this function changed\n * length. The containment check below is the shape such tools DO recognise: the resolved path\n * must stay inside the resolved base directory. It is unreachable after the assertion and costs\n * one `resolve`; it exists so the guard is visible where the sink is, not to replace the guard.\n */\n private filePath(id: string): string {\n assertSafeSessionId(id);\n const base = resolve(this.baseDir);\n const candidate = resolve(base, `${id}.json`);\n if (!candidate.startsWith(base + sep)) {\n throw new Error(\n `Invalid session id: ${JSON.stringify(id)} resolves outside the session store.`,\n );\n }\n return candidate;\n }\n\n /**\n * Persist a session record to disk atomically.\n * Creates the storage directory if needed.\n *\n * Bytes go to a same-directory temp file first, then move into place with rename —\n * a crash mid-write can therefore never leave a truncated JSON where the previous\n * record used to be. Same-directory is load-bearing: cross-device rename is a copy.\n *\n * The atomic write now comes from `writeOwnerOnlyFile`, which carries the mode from the\n * moment the temp file is created. The hand-rolled version here wrote it at the umask's default\n * and let `rename` carry that mode to the final path, so every record was 0644 — and even setting\n * the mode after the write would leave a window in which the full transcript was world-readable\n * on disk.\n */\n save(session: IInteractiveSessionRecord, options: ISessionSaveOptions = {}): void {\n this.ensureDir();\n this.filePath(session.id);\n // A sealed file this store cannot open is a session nobody here has read; replacing it would\n // destroy the only copy — unless it is provably exactly what this instance last wrote or holds,\n // which is how a locked process goes on saving the sessions it is running. A file it could not\n // read at all may be one of those, so it is protected the same way. A plain or absent file is\n // replaced.\n // The versioned envelope, not the bare record. Without a version on disk there is\n // nothing to compare, so \"written by a build this one does not read\" cannot be told from\n // \"damaged\" even in principle — and those two need different things from the user.\n // The owner-only atomic write carries it: the envelope is WHAT is written, the mode is HOW.\n // A store that seals writes the same envelope sealed to this session's id, tagged when it can\n // vouch that the content is the owner's and the keys are unlocked.\n this.records.save(session, options);\n }\n\n saveRestoreTransaction(\n session: IInteractiveSessionRecord,\n options?: ISessionSaveOptions,\n ): TSessionRestoreCommitOutcome {\n if (!options?.claim)\n throw new TypeError('A live session claim is required for restore commit.');\n try {\n this.save(session, options);\n return { status: 'committed' };\n } catch (error) {\n return { status: 'unknown', error };\n }\n }\n\n /**\n * What is at `path` now, for the guard before a save. Only \"no such file\" means there is nothing\n * to protect; a file that cannot be opened or read is `unreadable`. The first bytes tell plain from\n * sealed, so the common plain case costs one small read in a store that seals nothing, and the rest\n * of the file is read through the same descriptor, so the bytes judged are those of one file: a\n * sealed file's always, a plain one's in a store that seals, which must tell whether it is exactly\n * what the store holds.\n */\n private readExisting(path: string): TStoredSessionFile {\n let descriptor: number;\n try {\n descriptor = openSync(path, 'r');\n } catch (error) {\n return errnoCode(error) === 'ENOENT' ? { kind: 'absent' } : { kind: 'unreadable' };\n }\n try {\n const head = Buffer.alloc(SEALED_ARTIFACT_MAGIC.length);\n const length = readSync(descriptor, head, 0, head.length, 0);\n if (!isSealedArtifactBytes(head.subarray(0, length))) {\n return this.records.seals\n ? { kind: 'plain', bytes: readRest(descriptor, head.subarray(0, length)) }\n : { kind: 'plain' };\n }\n return { kind: 'sealed', bytes: readRest(descriptor, head) };\n } catch {\n return { kind: 'unreadable' };\n } finally {\n closeSync(descriptor);\n }\n }\n\n /**\n * Load a session by its ID, saying WHICH of the five things happened.\n *\n * `undefined` used to answer the first four — never saved, damaged, written by a build this one\n * cannot read, and the read failed — and a caller that meant to preserve fields it does not own\n * then treated \"damaged\" as \"no prior record\" and overwrote the file. The outcome type is what\n * removes that, by making the caller answer the question it was not asking. The fifth, `locked`,\n * is a sealed file this process cannot open right now: present and intact, so neither of the two.\n *\n * Only a load that asks to `hold` the session remembers a sealed record it opens (see\n * {@link StoredSessionRecords.load}); a probe and the listing remember nothing.\n */\n load(id: string, options: ISessionLoadOptions = {}): TSessionLoadOutcome {\n this.filePath(id);\n return this.records.load(id, options);\n }\n\n /** What is at session `id`'s path, read whole, for one load or listing of it. */\n private loadFile(id: string): {\n readonly file: TStoredSessionFile;\n readonly unreadable: TSessionLoadOutcome;\n } {\n const path = this.filePath(id);\n let raw: Buffer;\n try {\n raw = readFileSync(path);\n } catch (error) {\n if (errnoCode(error) === 'ENOENT') return { file: { kind: 'absent' }, unreadable: MISSING };\n // Only \"no such file\" is missing. A file that cannot be read — or a path that cannot even be\n // looked at — may be a session that is still there; reporting it as missing is what let a\n // recovery path run over one. Fixed text and the errno code: the error's own message names the\n // path and says nothing the code does not.\n return {\n file: { kind: 'unreadable' },\n unreadable: {\n status: 'corrupt',\n issues: [{ path: '', message: unreadableMessage(error) }],\n },\n };\n }\n // `save` tightens a record it REWRITES, but a session that is only resumed or\n // listed keeps whatever mode its writer left — so a record from an older build stays 0644.\n // Repair it on read: `list` routes through here, so browsing or resuming a legacy session makes\n // it owner-only. A no-op once the mode is already tight, and on non-POSIX hosts.\n //\n // Best-effort on this READ path, unlike the write callers of `tightenExistingFile`: a record\n // owned by another account (EPERM) or on a chmod-ignoring mount makes it throw, and this is the\n // one place `tightenExistingFile` runs from `list` — where a single unrepairable file must not\n // take the whole listing down (the invariant `outcomeForListedId` upholds). A failed repair\n // leaves the mode as it was and the record is still returned.\n try {\n tightenExistingFile(path);\n } catch {\n // allow-fallback: repairing the mode is opportunistic; reading the session is the job.\n }\n return { file: classifyStoredSessionBytes(raw), unreadable: MISSING };\n }\n\n /**\n * The record this instance itself last wrote or holds, offered only while the file on disk is\n * still exactly those bytes. It is what a writer needs when `load` says `locked` for a session it\n * is running: the keys that read it back are locked, the copy it holds is not. `undefined` for\n * a session this instance never held, or whose file has changed since.\n */\n loadHeld(id: string): TSessionLoadOutcome | undefined {\n this.filePath(id);\n return this.records.loadHeld(id);\n }\n\n /** The sealed record of `id` as a token for another store over these files (see {@link StoredSessionRecords.exportHeld}). */\n exportHeld(id: string, options?: ISessionExportOptions): string | undefined {\n this.filePath(id);\n return this.records.exportHeld(id, options);\n }\n\n /** Holds the record a token from another store's `exportHeld` carries (see {@link StoredSessionRecords.importHeld}). */\n importHeld(id: string, token: string): void {\n this.filePath(id);\n this.records.importHeld(id, token);\n }\n\n /**\n * Every session this directory holds, each with what the store concluded about it.\n *\n * Unreadable entries are REPORTED rather than skipped. A store that distinguishes four outcomes on\n * `load` and then hides two of them from the surface a person browses has moved the defect rather\n * than removed it: the difference a user experiences is between \"my session vanished\" and \"my\n * session needs a different build\".\n *\n * The one exception is a sealed session this process cannot open: there is no record to describe\n * (no title, no time, no workspace), so it is not an entry, and {@link scan} is how a surface\n * still says that some sessions are there.\n */\n list(): readonly ISessionListEntry[] {\n return this.scan().entries;\n }\n\n /**\n * The listing and how many sealed sessions it left out because they cannot be opened right now,\n * from one read of each file, counted by why: locked keys, or no keys here at all. A record that\n * cannot be read has nothing to show, so the counts are what keep those sessions from looking gone.\n */\n scan(): ISessionStoreScan {\n const all = this.readAll();\n return {\n entries: all\n .filter((entry) => entry.outcome.status !== 'locked')\n .sort(compareListEntriesByRecency),\n ...countLeftOut(all),\n };\n }\n\n private readAll(): ISessionListEntry[] {\n if (!existsSync(this.baseDir)) {\n return [];\n }\n return readdirSync(this.baseDir)\n .filter((file) => file.endsWith('.json'))\n .map((file) => file.slice(0, -'.json'.length))\n .map((id) => ({ id, outcome: this.outcomeForListedId(id) }));\n }\n\n /**\n * The outcome for one directory entry, without letting a bad NAME throw out of `list`.\n *\n * `load` validates the id, because an id reaching it is a caller's value and a malformed one is a\n * bug or an attack. A name read out of the directory is neither: the store did not\n * choose it, and one file it cannot use as an id must not take the whole listing down with it.\n * Routing `list` through `load` made exactly that happen — a single `my session.json` in the\n * sessions directory threw, and the resume picker went with it.\n *\n * Reporting it is the same answer `list` gives for every other file it cannot read, which is the\n * property this work exists to establish.\n */\n private outcomeForListedId(id: string): TSessionLoadOutcome {\n if (!isSafeSessionId(id)) {\n return {\n status: 'corrupt',\n issues: [{ path: '', message: 'the file name is not a usable session id' }],\n };\n }\n return this.records.listed(id);\n }\n\n /**\n * Delete a session by its ID, holding its writer marker: refused while another writer holds it.\n * No-ops silently if the session does not exist.\n */\n delete(id: string): void {\n const path = this.filePath(id);\n this.records.delete(id, () => {\n if (existsSync(path)) unlinkSync(path);\n });\n }\n}\n","/**\n * What a line in an append-only store means, and what a sidecar beside a session log holds.\n *\n * The prompt-history file, both session-log sinks and both session-log sources route every append\n * and every read through this one module — the pattern `StoredSessionRecords` set for records — so\n * none of them can disagree about a sealed line, a torn one, or one written by a newer build.\n *\n * A store that seals appends each call as ONE sealed line that opens on its own: no line waits for\n * another, so a reader walking a file backwards, or paging it, opens exactly the lines it shows.\n * Every sealed append starts with a newline as well as ending with one. Appends are not atomic\n * (Node loops on a short write), and a writer can die part-way; the leading newline puts whatever\n * fragment it left on a line of its own, so the next record is never glued to it. A reader skips\n * empty lines, and the final fragment of a file, if it carries sealed framing (or the start of it)\n * and no newline yet, is an append still in progress rather than a line.\n *\n * A store that seals nothing writes plain lines, and meets a sealed line as one no key in its\n * process opens. Plain and sealed lines may sit in one file, each read by its\n * own framing.\n *\n * An externalized payload of a sealing log is sealed under a RANDOM name. A content-addressed name\n * would let any program of the same user confirm a guessed payload, or see which payloads are equal;\n * the digest now lives only inside the sealed line, where the resolver still checks it against the\n * bytes it opens, so two sealed sidecars swapped with each other are caught. Dedupe is in-process.\n */\nimport { randomBytes } from 'node:crypto';\n\nimport {\n ArtifactCorruptError,\n ArtifactLockedError,\n SEALED_ARTIFACT_MAX_OVERHEAD_BYTES,\n isArtifactCorruptError,\n isArtifactLockedError,\n isCurrentSealedArtifactBytes,\n isCurrentSealedArtifactLine,\n isSealedArtifactBytes,\n isSealedArtifactLine,\n} from '@robota-sdk/agent-core';\n\nimport { SessionLogPayloadResolutionError } from './external-payload-resolution-contracts.js';\nimport { SessionLogDecodeError } from './session-log-codec/index.js';\nimport { createSessionLogExternalPayloadReference } from './session-log-sinks.js';\nimport { assertSessionSealing } from './stored-session-record.js';\n\nimport type { IExternalPayloadReference } from './session-log-reference-types.js';\nimport type {\n IArtifactContext,\n IArtifactSealer,\n TArtifactKind,\n TArtifactLockedReason,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\n\n/** The kinds of artifact an append store's lines are sealed as. */\nexport type TAppendLineKind = Extract<TArtifactKind, 'prompt-history-line' | 'session-log-line'>;\n\n/** One physical line of an append store, as this process can read it. Never carries key material. */\nexport type TStoredLine =\n /** A plain line (one element, verbatim), or a sealed record opened to the lines it holds. */\n | { readonly kind: 'lines'; readonly lines: readonly string[] }\n /** Sealed and intact as far as anyone can tell; `reason` says whether an unlock here would read it. */\n | { readonly kind: 'locked'; readonly reason: TArtifactLockedReason }\n /** Sealed in this build's framing and it does not open for this owner: damaged, torn, or sealed with other keys. */\n | { readonly kind: 'does-not-open' }\n /** Sealed in a framing version a newer build writes: never opened, never counted as damage. */\n | { readonly kind: 'newer-format' };\n\nconst NEWLINE = '\\n';\nconst DOES_NOT_OPEN: TStoredLine = Object.freeze({ kind: 'does-not-open' });\nconst NEWER_FORMAT: TStoredLine = Object.freeze({ kind: 'newer-format' });\nconst NO_KEYS: TStoredLine = Object.freeze({ kind: 'locked', reason: 'no-keys' });\n/** The start of a sealed-line prefix, too short to carry its colon yet: `R`, `RS`, `RSL`, `RSL` and a version digit. */\nconst PARTIAL_SEALED_LINE_PREFIX = /^R(?:S(?:L[0-9]?)?)?$/;\n\nfunction sealerOf(sealing: TArtifactSealing): IArtifactSealer | undefined {\n assertSessionSealing(sealing);\n return sealing === 'plain' ? undefined : sealing.sealer;\n}\n\nfunction lockedReasonOf(error: unknown): TArtifactLockedReason {\n return (error as { readonly reason?: unknown }).reason === 'no-keys' ? 'no-keys' : 'locked';\n}\n\n/** How one append store keeps its lines: sealed as `kind`, bound to the owner id each call names. */\nexport class StoredLines {\n readonly #sealer: IArtifactSealer | undefined;\n readonly #kind: TAppendLineKind;\n\n constructor(sealing: TArtifactSealing, kind: TAppendLineKind) {\n this.#sealer = sealerOf(sealing);\n this.#kind = kind;\n }\n\n /**\n * What to append for `text` — one or more `\\n`-terminated lines, exactly what a plain store\n * appends in that call. Plain: `text` itself. Sealing: one sealed line with a newline before and\n * after it. Throws whatever sealing throws; it never falls back to plaintext.\n */\n encode(text: string, ownerId: string): string {\n if (this.#sealer === undefined) return text;\n return `${NEWLINE}${this.#sealer.sealLine(text, { kind: this.#kind, id: ownerId })}${NEWLINE}`;\n }\n\n /** One physical line (no `\\n` in it), as this process can read it. Never throws. */\n decode(line: string, ownerId: string): TStoredLine {\n try {\n return this.#decode(line, ownerId);\n } catch {\n // A failure of the sealer that says neither \"locked\" nor \"does not open\" is no evidence the\n // line is fine, and must not take a tolerant reader down: it is a line that does not open.\n return DOES_NOT_OPEN;\n }\n }\n\n /**\n * Strict: the plain-equivalent text of a whole store's `text`, for decoders that fail on any bad\n * line (see {@link openedText}). A failure of the sealer that is neither `ArtifactLockedError` nor\n * `ArtifactCorruptError` is a defect (a refused context, a misused key), not a damaged line, so it\n * is thrown as it was rather than reported as corruption.\n */\n openText(text: string, ownerId: string): string {\n const lines = splitStoredLines(text);\n const decode = (line: string): TStoredLine => this.#decode(line, ownerId);\n return openedText(\n (function* decodeInOrder() {\n for (const line of lines) yield decode(line);\n })(),\n );\n }\n\n /** One physical line; throws only what the sealer threw that is neither locked nor corrupt. */\n #decode(line: string, ownerId: string): TStoredLine {\n if (!isSealedArtifactLine(line)) return { kind: 'lines', lines: [line] };\n if (!isCurrentSealedArtifactLine(line)) return NEWER_FORMAT;\n // A sealed line and no sealer: no unlock in this process would open it.\n if (this.#sealer === undefined) return NO_KEYS;\n let text: string;\n try {\n text = this.#sealer.openLine(line, { kind: this.#kind, id: ownerId });\n } catch (error) {\n if (isArtifactLockedError(error)) return { kind: 'locked', reason: lockedReasonOf(error) };\n if (isArtifactCorruptError(error)) return DOES_NOT_OPEN;\n throw error;\n }\n return { kind: 'lines', lines: text.split(NEWLINE).filter((opened) => opened.length > 0) };\n }\n}\n\n/**\n * The physical lines of a store's text. An UNTERMINATED final fragment with sealed framing, or so\n * short that it is still only the start of it (`RSL` and a version digit, cut anywhere), is left\n * out: every sealed append ends with a newline, so such a fragment is an append in progress (or one\n * whose writer died), not a record. A plain fragment is kept as it is.\n */\nexport function splitStoredLines(text: string): string[] {\n const lines = text.split(NEWLINE);\n const tail = lines[lines.length - 1] as string;\n if (tail.length > 0 && (isSealedArtifactLine(tail) || PARTIAL_SEALED_LINE_PREFIX.test(tail))) {\n lines.pop();\n }\n return lines;\n}\n\n/**\n * The plain-equivalent text of a whole store, for decoders that fail on any bad line: what a plain\n * store would hold, so a file of plain lines comes back byte for byte. The first line that cannot be\n * read decides the failure — `ArtifactLockedError` (with its reason) for a locked one,\n * `ArtifactCorruptError` for one that does not open, `SessionLogDecodeError` (`UNSUPPORTED_VERSION`)\n * for one a newer build sealed.\n */\nexport function openedText(stored: Iterable<TStoredLine>): string {\n const parts: string[] = [];\n let lineNumber = 0;\n for (const line of stored) {\n lineNumber += 1;\n switch (line.kind) {\n case 'lines':\n parts.push(line.lines.join(NEWLINE));\n break;\n case 'locked':\n throw new ArtifactLockedError(line.reason);\n case 'does-not-open':\n throw new ArtifactCorruptError();\n case 'newer-format':\n throw new SessionLogDecodeError('UNSUPPORTED_VERSION', [\n {\n path: `line ${lineNumber}`,\n message: 'the line is sealed in a framing version this build does not open',\n },\n ]);\n }\n }\n return parts.join(NEWLINE);\n}\n\n/** 16 random bytes: a name no one can derive from the payload or guess. */\nconst SEALED_PAYLOAD_NAME_BYTES = 16;\nconst SEALED_PAYLOAD_EXTENSION = '.sealed';\n\n/** One sidecar a sink is asked to write. */\nexport interface IStoredPayloadWrite {\n readonly relativePath: string;\n readonly data: string | Uint8Array;\n /**\n * The name is the content's own digest, so a file already at it holds these exact bytes and need\n * not be written again. A sealed sidecar's name is random: a file already there is not this one.\n */\n readonly contentAddressed: boolean;\n}\n\nfunction payloadError(\n code: 'PAYLOAD_UNREADABLE' | 'MAX_TOTAL_BYTES_EXCEEDED',\n relativePath: string,\n maxBytes: number,\n): SessionLogPayloadResolutionError {\n return code === 'PAYLOAD_UNREADABLE'\n ? new SessionLogPayloadResolutionError(\n code,\n `External payload is sealed and does not open here: ${relativePath}.`,\n { relativePath },\n )\n : new SessionLogPayloadResolutionError(\n code,\n `External payload exceeds the remaining byte budget of ${maxBytes}.`,\n { relativePath, expected: maxBytes },\n );\n}\n\n/**\n * How a session-log sink places externalized JSON values beside its log, and how a source opens what\n * it reads back. Plain: the content-addressed `<id>.payloads/<sha256>.json`. Sealing:\n * `<id>.payloads/<22 random base64url characters>.sealed`, holding the payload sealed as\n * `session-log-payload` for the session, written once per payload per instance.\n */\nexport class StoredPayloads {\n readonly #sealer: IArtifactSealer | undefined;\n /** Per session: plaintext digest → the sealed sidecar this instance already wrote for it. */\n readonly #written = new Map<string, Map<string, string>>();\n\n constructor(sealing: TArtifactSealing) {\n this.#sealer = sealerOf(sealing);\n }\n\n /**\n * The reference a log line carries for `serialized`, after `write` has put its sidecar in place.\n * `write` is called at most once; a sealed name is remembered only once `write` returned, so a\n * failed write is attempted again for the next reference to the same payload.\n */\n place(\n sessionId: string,\n sha256: string,\n serialized: string,\n write: (target: IStoredPayloadWrite) => void,\n ): IExternalPayloadReference {\n const reference = createSessionLogExternalPayloadReference(sessionId, sha256, serialized);\n if (this.#sealer === undefined) {\n write({ relativePath: reference.relativePath, data: serialized, contentAddressed: true });\n return reference;\n }\n const written = this.#written.get(sessionId) ?? new Map<string, string>();\n const known = written.get(sha256);\n if (known !== undefined) return { ...reference, relativePath: known };\n const name = randomBytes(SEALED_PAYLOAD_NAME_BYTES).toString('base64url');\n // Always `/`, like a plain reference: every reader accepts either slash.\n const relativePath = `${sessionId}.payloads/${name}${SEALED_PAYLOAD_EXTENSION}`;\n const plain = Buffer.from(serialized, 'utf8');\n let data: Uint8Array;\n try {\n data = this.#sealer.sealBytes(plain, { kind: 'session-log-payload', id: sessionId });\n } finally {\n plain.fill(0);\n }\n write({ relativePath, data, contentAddressed: false });\n written.set(sha256, relativePath);\n this.#written.set(sessionId, written);\n return { ...reference, relativePath };\n }\n\n /** How many bytes to read for a payload whose plaintext budget is `maxBytes`. */\n rawBudget(maxBytes: number): number {\n if (this.#sealer === undefined) return maxBytes;\n return Math.min(maxBytes + SEALED_ARTIFACT_MAX_OVERHEAD_BYTES, Number.MAX_SAFE_INTEGER);\n }\n\n /**\n * The payload bytes in `stored` (what was read at `relativePath`), held to the plaintext budget\n * exactly. A plain store reads every payload as bytes: only a sealed line can\n * reference a sealed payload, and a plain reader never gets past the line. A sealing store passes\n * plain bytes through too, so a directory written before sealing still resolves — a JSON payload\n * never starts with the sealed magic — and opens sealed bytes bound to `ownerId`:\n * `ArtifactLockedError` while they cannot be opened here, and `PAYLOAD_UNREADABLE` when they do\n * not open, were sealed by a newer build, or have no owner to open them for. Any other failure of\n * the sealer is a defect, thrown as it was. The caller then checks the result against the\n * reference's digest and length.\n */\n open(\n stored: Uint8Array,\n maxBytes: number,\n ownerId: string | undefined,\n relativePath: string,\n ): Uint8Array {\n if (this.#sealer === undefined) return stored;\n if (!isSealedArtifactBytes(stored)) {\n if (stored.byteLength > maxBytes) {\n throw payloadError('MAX_TOTAL_BYTES_EXCEEDED', relativePath, maxBytes);\n }\n return stored;\n }\n if (!isCurrentSealedArtifactBytes(stored)) {\n throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n }\n if (ownerId === undefined) throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n let plain: Uint8Array;\n try {\n plain = this.#sealer.openBytes(stored, { kind: 'session-log-payload', id: ownerId });\n } catch (error) {\n if (isArtifactLockedError(error)) throw new ArtifactLockedError(lockedReasonOf(error));\n if (isArtifactCorruptError(error)) {\n throw payloadError('PAYLOAD_UNREADABLE', relativePath, maxBytes);\n }\n throw error;\n }\n if (plain.byteLength > maxBytes) {\n plain.fill(0);\n throw payloadError('MAX_TOTAL_BYTES_EXCEEDED', relativePath, maxBytes);\n }\n return plain;\n }\n}\n\n/** What a stored sidecar is to a migration that seals what is still plain in place. */\nexport type TStoredPayloadMigrationState = 'plain' | 'sealed' | 'does-not-open' | 'newer-format';\n\nfunction payloadContext(ownerId: string): IArtifactContext {\n return { kind: 'session-log-payload', id: ownerId };\n}\n\n/**\n * Where a sidecar of session `ownerId` stands: decided by whether it opens for that session, never by\n * framing alone. With the keys locked it throws `ArtifactLockedError`.\n */\nexport function storedPayloadMigrationState(\n stored: Uint8Array,\n ownerId: string,\n sealer: IArtifactSealer,\n): TStoredPayloadMigrationState {\n if (!isSealedArtifactBytes(stored)) return 'plain';\n if (!isCurrentSealedArtifactBytes(stored)) return 'newer-format';\n let plain: Uint8Array;\n try {\n plain = sealer.openBytes(stored, payloadContext(ownerId));\n } catch (error) {\n if (isArtifactLockedError(error)) throw error;\n return 'does-not-open';\n }\n plain.fill(0);\n return 'sealed';\n}\n\n/**\n * A plain sidecar's bytes sealed for session `ownerId`, to be put back under the SAME name: a source\n * opens sealed bytes whatever the name ({@link StoredPayloads.open}), and the digest and length every\n * reference — nested ones included — checks are the plaintext's, so no reference changes.\n */\nexport function sealStoredPayloadBytes(\n plain: Uint8Array,\n ownerId: string,\n sealer: IArtifactSealer,\n): Uint8Array {\n return sealer.sealBytes(plain, payloadContext(ownerId));\n}\n\n/** Where a rewrite of an append store puts the lines it produces. */\nexport interface IStoredLineSink {\n write(text: string): void;\n}\n\n/** One physical line of a session log as both transcript pages read it: a trailing `\\r` is not content. */\nfunction logLineContent(physical: string): string {\n return physical.endsWith('\\r') ? physical.slice(0, -1) : physical;\n}\n\n/**\n * Seals the plain lines of a session log as a migration does: one sealed record per non-empty plain\n * line, a sealed line kept as it is, an empty line dropped — exactly the records both transcript\n * pages count, so a stored-record offset names the same line before and after.\n */\nexport class SessionLogLineSealer {\n sealedLines = 0;\n carriedLines = 0;\n readonly #lines: StoredLines;\n\n constructor(\n sealer: IArtifactSealer,\n private readonly ownerId: string,\n ) {\n this.#lines = new StoredLines({ sealer }, 'session-log-line');\n }\n\n /** `leadingNewline` frames each line `\\n<line>\\n`, for a file other writers append to meanwhile. */\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void {\n for (const physical of lines) {\n const line = logLineContent(physical);\n if (line.length === 0) continue;\n if (isSealedArtifactLine(line)) {\n out.write(`${leadingNewline ? NEWLINE : ''}${line}${NEWLINE}`);\n this.carriedLines += 1;\n continue;\n }\n const stored = this.#lines.encode(`${line}${NEWLINE}`, this.ownerId);\n out.write(leadingNewline ? stored : stored.slice(NEWLINE.length));\n this.sealedLines += 1;\n }\n }\n}\n\n/** The plain records a session log's text holds, as a migration would seal them. */\nexport function countPlainSessionLogLines(text: string): number {\n return splitStoredLines(text).filter((physical) => {\n const line = logLineContent(physical);\n return line.length > 0 && !isSealedArtifactLine(line);\n }).length;\n}\n\n/** A whole session log's text with every plain line sealed, for a store that replaces the file whole. */\nexport interface ISealedSessionLogText {\n readonly text: string;\n readonly sealedLines: number;\n readonly carriedLines: number;\n}\n\n/**\n * `text` (a whole log, nothing appending to it) with every plain record sealed for session\n * `ownerId` as {@link SessionLogLineSealer} seals it. An unfinished sealed append at the end, which\n * no reader shows, is dropped; an unfinished plain line is a record, and is sealed.\n */\nexport function sealSessionLogText(\n text: string,\n ownerId: string,\n sealer: IArtifactSealer,\n): ISealedSessionLogText {\n const sealing = new SessionLogLineSealer(sealer, ownerId);\n const parts: string[] = [];\n sealing.rewrite(splitStoredLines(text), { write: (part) => parts.push(part) }, false);\n return {\n text: parts.join(''),\n sealedLines: sealing.sealedLines,\n carriedLines: sealing.carriedLines,\n };\n}\n","import { appendFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createLogger } from '@robota-sdk/agent-core';\nimport {\n OWNER_ONLY_FILE_MODE,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { assertSafeSessionId } from './session-id.js';\nimport { StoredLines, StoredPayloads } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type { IExternalPayloadReference } from './session-log-reference-types.js';\nimport type { IExternalPayloadSink, ISessionLogSink } from './session-log-sinks.js';\nimport type { IStoredPayloadWrite } from './stored-append-lines.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\n\nconst logger = createLogger('NodeSessionLogSink');\n\n/** Explicit host-filesystem sink for JSONL logs and their content-addressed sidecars. */\nexport class NodeSessionLogSink implements ISessionLogSink, IExternalPayloadSink {\n readonly externalPayloadSink: IExternalPayloadSink = this;\n private readonly enabled: boolean;\n private readonly lines: StoredLines;\n private readonly payloads: StoredPayloads;\n\n /**\n * @param options `sealing` (required): `{ sealer }` seals every append (one sealed line per call)\n * and every sidecar; `'plain'` writes plain lines and content-addressed sidecars.\n */\n constructor(\n private readonly logDirectory: string,\n options: ISessionStoreOptions,\n ) {\n const sealing = sessionSealingOf(options);\n this.lines = new StoredLines(sealing, 'session-log-line');\n this.payloads = new StoredPayloads(sealing);\n try {\n // `mkdirSync(dir, { recursive: true, mode })` does NOT set the mode of a directory\n // that already exists — it returns successfully and adopts whatever is there. Measured: a log\n // directory pre-created at 0777 stayed 0777 while its records were written 0600, which means\n // another local account could not read a record but could unlink and replace one, and could\n // enumerate every session id.\n ensureOwnerOnlyDirectory(logDirectory);\n this.enabled = true;\n } catch (error) {\n // allow-fallback: session logging is diagnostic and must not disable the session. It now also\n // disables on a directory that cannot be made owner-only, which is the correct direction: no\n // log is better than a log any account can read or replace.\n this.enabled = false;\n logger.warn('session log directory could not be created — session logging is disabled', {\n logDirectory,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n append(sessionId: string, text: string): void {\n assertSafeSessionId(sessionId);\n if (!this.enabled) return;\n const path = join(this.logDirectory, `${sessionId}.jsonl`);\n // Sealed before the file is touched, so a sealing failure writes nothing at all.\n const stored = this.lines.encode(text, sessionId);\n // `mode` on a write applies only when the file is CREATED, so a log an older version\n // left at 0644 keeps 0644 for its whole life however many times it is appended to. Tightening\n // first is the repair path for a store written before this change.\n tightenExistingFile(path);\n appendFileSync(path, stored, { mode: OWNER_ONLY_FILE_MODE });\n }\n\n writeJson(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference {\n return this.payloads.place(sessionId, sha256, serialized, (target) =>\n this.writePayload(sessionId, target),\n );\n }\n\n private writePayload(sessionId: string, target: IStoredPayloadWrite): void {\n if (!this.enabled) return;\n ensureOwnerOnlyDirectory(join(this.logDirectory, `${sessionId}.payloads`));\n const payloadPath = join(this.logDirectory, target.relativePath);\n try {\n writeFileSync(payloadPath, target.data, {\n ...(typeof target.data === 'string' ? { encoding: 'utf8' as const } : {}),\n mode: OWNER_ONLY_FILE_MODE,\n flag: 'wx',\n });\n } catch (error) {\n // A random sealed name that already exists is not this payload, so only a content-addressed\n // one may be taken as already written.\n if (!target.contentAddressed || (error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw error;\n }\n // The payload is content-addressed, so an existing file already holds these exact\n // bytes and `wx` correctly declines to rewrite it. Its MODE is a different question — one\n // written by an older version is 0644 and nothing else would ever repair it.\n tightenExistingFile(payloadPath);\n }\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, dirname, extname, isAbsolute, resolve, win32 } from 'node:path';\n\nimport {\n createStableRootedFileReader,\n StableFileAuthorityError,\n} from '@robota-sdk/agent-file-authority';\n\nimport { SessionLogPayloadResolutionError } from './external-payload-resolution-contracts.js';\nimport { StoredLines, StoredPayloads, splitStoredLines } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type {\n IExternalPayloadSource,\n ISessionLogSource,\n} from './external-payload-source-types.js';\nimport type { TStoredLine } from './stored-append-lines.js';\nimport type { ISessionStoreOptions } from './stored-session-record.js';\n\nfunction payloadPathSegments(relativePath: string): readonly string[] {\n if (\n relativePath.trim().length === 0 ||\n relativePath.includes('\\0') ||\n isAbsolute(relativePath) ||\n win32.isAbsolute(relativePath)\n ) {\n throw outsideRootError(relativePath);\n }\n const segments = relativePath.split(/[\\\\/]+/u);\n if (segments.some((segment) => segment === '.' || segment === '..')) {\n throw outsideRootError(relativePath);\n }\n return segments;\n}\n\nfunction validateMaxBytes(maxBytes: number): void {\n if (!Number.isFinite(maxBytes) || !Number.isSafeInteger(maxBytes) || maxBytes < 0) {\n throw new SessionLogPayloadResolutionError(\n 'INVALID_LIMIT',\n 'External-payload maxBytes must be a finite, non-negative safe integer.',\n { actual: String(maxBytes) },\n );\n }\n}\n\nfunction outsideRootError(relativePath: string, cause?: Error): SessionLogPayloadResolutionError {\n return new SessionLogPayloadResolutionError(\n 'OUTSIDE_ROOT',\n `External payload path escapes its base directory or contains a link: ${relativePath}.`,\n { relativePath },\n cause,\n );\n}\n\nfunction mapStableFileAuthorityError(\n error: StableFileAuthorityError,\n relativePath: string,\n maxBytes: number,\n): SessionLogPayloadResolutionError {\n if (error.code === 'INVALID_PATH' || error.code === 'UNSAFE_ENTRY') {\n return outsideRootError(relativePath, error);\n }\n if (error.code === 'UNSUPPORTED_BACKEND') {\n return new SessionLogPayloadResolutionError(\n 'STABLE_PAYLOAD_READ_UNAVAILABLE',\n 'Stable root-relative external-payload reads are unavailable on this host.',\n { relativePath },\n error,\n );\n }\n if (error.code === 'OVER_BUDGET') {\n return new SessionLogPayloadResolutionError(\n 'MAX_TOTAL_BYTES_EXCEEDED',\n `External payload exceeds the remaining byte budget of ${maxBytes}.`,\n { relativePath, expected: maxBytes },\n error,\n );\n }\n return new SessionLogPayloadResolutionError(\n 'PAYLOAD_UNREADABLE',\n `External payload could not be read: ${relativePath}.`,\n { relativePath },\n error,\n );\n}\n\nexport interface INodeExternalPayloadSourceOptions extends ISessionStoreOptions {\n /**\n * The session the payloads belong to, which a sealed one is bound to. Left out, it is the session\n * whose `<id>.payloads` directory the reference names, the layout every sink writes.\n */\n readonly ownerId?: string;\n}\n\nconst PAYLOAD_DIRECTORY_SUFFIX = '.payloads';\n\n/** The session whose `<id>.payloads` directory holds a payload, or `undefined` for any other layout. */\nfunction payloadDirectoryOwner(segments: readonly string[]): string | undefined {\n const directory = segments.length === 2 ? segments[0] : undefined;\n if (directory === undefined || !directory.endsWith(PAYLOAD_DIRECTORY_SUFFIX)) return undefined;\n return directory.slice(0, -PAYLOAD_DIRECTORY_SUFFIX.length) || undefined;\n}\n\n/** Explicit host-filesystem adapter. A file path is never accepted by the neutral parser itself. */\nexport class NodeExternalPayloadSource implements IExternalPayloadSource {\n private readonly baseDirectory: string;\n private readonly payloads: StoredPayloads;\n private readonly ownerId: string | undefined;\n\n /**\n * @param options `sealing` (required): `{ sealer }` opens sealed payloads (plain ones still\n * resolve); `'plain'` meets a sealed payload as one no key in this process opens.\n */\n constructor(baseDirectory: string, options: INodeExternalPayloadSourceOptions) {\n if (baseDirectory.trim().length === 0) {\n throw new Error('External-payload base directory must not be empty.');\n }\n this.baseDirectory = resolve(baseDirectory);\n this.payloads = new StoredPayloads(sessionSealingOf(options));\n this.ownerId = options.ownerId;\n }\n\n readBytes(relativePath: string, maxBytes: number): Uint8Array | undefined {\n validateMaxBytes(maxBytes);\n const segments = payloadPathSegments(relativePath);\n const stored = this.readStored(relativePath, segments, maxBytes);\n if (stored === undefined) return undefined;\n return this.payloads.open(\n stored,\n maxBytes,\n this.ownerId ?? payloadDirectoryOwner(segments),\n relativePath,\n );\n }\n\n /** The bytes at the path, within the raw budget a sealed payload of `maxBytes` needs. */\n private readStored(\n relativePath: string,\n segments: readonly string[],\n maxBytes: number,\n ): Uint8Array | undefined {\n try {\n const reader = createStableRootedFileReader(this.baseDirectory);\n try {\n return reader.readBytes(segments, this.payloads.rawBudget(maxBytes));\n } finally {\n reader.close();\n }\n } catch (error) {\n if (error instanceof SessionLogPayloadResolutionError) throw error;\n if (error instanceof StableFileAuthorityError) {\n throw mapStableFileAuthorityError(error, relativePath, maxBytes);\n }\n throw new SessionLogPayloadResolutionError(\n 'PAYLOAD_UNREADABLE',\n `External payload could not be read: ${relativePath}.`,\n { relativePath },\n error,\n );\n }\n }\n}\n\n/**\n * Explicit host-filesystem adapter for a JSONL session log. The log's owner — the session id its\n * sealed lines and payloads are bound to — is the file's stem, `<id>.jsonl`, the name every sink\n * writes; a sealed log copied under another session's name does not open as that session's.\n */\nexport class NodeSessionLogSource implements ISessionLogSource {\n readonly externalPayloadSource: IExternalPayloadSource;\n private readonly lines: StoredLines;\n private readonly ownerId: string;\n\n /**\n * @param options `sealing` (required): `{ sealer }` opens sealed lines and payloads (plain ones\n * still read); `'plain'` meets a sealed line as one no key in this process opens.\n */\n constructor(\n private readonly logFile: string,\n options: ISessionStoreOptions,\n ) {\n if (logFile.trim().length === 0) {\n throw new Error('Session log-file path must not be empty.');\n }\n const sealing = sessionSealingOf(options);\n this.lines = new StoredLines(sealing, 'session-log-line');\n this.ownerId = basename(logFile, extname(logFile));\n this.externalPayloadSource = new NodeExternalPayloadSource(dirname(logFile), {\n sealing,\n ownerId: this.ownerId,\n });\n }\n\n /** Strict: the plain-equivalent text, failing on the first line it cannot read (see {@link StoredLines.openText}). */\n readText(): string | undefined {\n if (!existsSync(this.logFile)) return undefined;\n return this.lines.openText(readFileSync(this.logFile, 'utf8'), this.ownerId);\n }\n\n /** Tolerant: every physical line in file order, each as this process can read it; `undefined` when there is no log. */\n readStoredLines(): readonly TStoredLine[] | undefined {\n if (!existsSync(this.logFile)) return undefined;\n return splitStoredLines(readFileSync(this.logFile, 'utf8')).map((line) =>\n this.lines.decode(line, this.ownerId),\n );\n }\n}\n","/**\n * Rewriting a file other processes append to by path, without losing what they append meanwhile\n * (#3679): the prompt history's compaction and the sealing of a plain session log in place both go\n * through here, so the two agree on what a snapshot is and how a late append is caught up.\n *\n * Such a file has no lock, so it cannot be rewritten in place. The new file is written beside it and\n * renamed over it, and what reached the old one meanwhile is caught up:\n *\n * 1. The snapshot is the old file up to just after its last newline; anything after that is an\n * append still in progress and is left to the catch-up.\n * 2. Every line of the snapshot goes through the caller's rewriter into a temporary file, which is\n * flushed and renamed over the original, and the directory is flushed.\n * 3. An append that opened the old file before the rename still lands in it, so the old descriptor\n * is read from the snapshot's end until it stays quiet, and what it finds is rewritten onto the new\n * file. A catch-up that fails part-way is reported with how much of the old file it left unread,\n * since what was appended there is lost with the old file.\n * 4. The new file is read once more. A plain line there is plaintext an older build appended by path\n * after the rename; it is counted for the caller to report, never hidden.\n *\n * Just before the rename the path must still name the file that was opened: another rewrite that\n * renamed its own file in meanwhile holds what was appended since, and renaming over it would lose\n * that, so the rewrite is abandoned instead (a rewrite that renames in the instant between that look\n * and the rename is not caught; cooperating callers keep two rewrites from running at all). A file\n * deleted meanwhile is not put back either.\n *\n * A file that is not a regular file of its own — a symbolic link or a second hard link, as a dotfile\n * sync makes — is refused: the rename would replace the link and leave the plaintext where it points.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n closeSync,\n constants,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n readSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\n\nimport { isSealedArtifactLine } from '@robota-sdk/agent-core';\nimport { OWNER_ONLY_FILE_MODE, ensureOwnerOnlyDirectory } from '@robota-sdk/agent-core/node';\n\nimport { splitStoredLines } from './stored-append-lines.js';\n\nimport type { IStoredLineSink } from './stored-append-lines.js';\n\nconst NEWLINE_BYTE = 0x0a;\n/** Writes are batched up to this many bytes. */\nconst WRITE_BATCH_BYTES = 1024 * 1024;\n/** Quiet passes, each after a pause, before the old file counts as stable. */\nconst QUIET_PASSES = 2;\nconst DEFAULT_PAUSE_MS = 20;\nconst TEMPORARY_SUFFIX_BYTES = 6;\n\n/** Why a file was left as it is. */\nexport type TAppendFileRefusal =\n /** A symbolic link, a directory, or anything else that is not a regular file. */\n | 'not-a-regular-file'\n /** A regular file with another name too, which a rename would leave holding the old bytes. */\n | 'hard-linked';\n\nexport type TOpenedAppendFile =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TAppendFileRefusal }\n | { readonly status: 'open'; readonly descriptor: number };\n\nfunction errorCode(error: unknown): string | undefined {\n return (error as NodeJS.ErrnoException | undefined)?.code;\n}\n\n/** The file opened for reading, after the checks that make it safe to replace by rename. */\nexport function openAppendFile(path: string): TOpenedAppendFile {\n let linked;\n try {\n linked = lstatSync(path);\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return { status: 'missing' };\n throw error;\n }\n if (!linked.isFile()) return { status: 'refused', reason: 'not-a-regular-file' };\n let descriptor: number;\n try {\n descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return { status: 'missing' };\n if (errorCode(error) === 'ELOOP') return { status: 'refused', reason: 'not-a-regular-file' };\n throw error;\n }\n const opened = fstatSync(descriptor);\n // Replaced between the two looks: what was opened is not the file that was checked.\n const sameFile = opened.isFile() && opened.ino === linked.ino && opened.dev === linked.dev;\n if (!sameFile || opened.nlink > 1) {\n closeSync(descriptor);\n return { status: 'refused', reason: sameFile ? 'hard-linked' : 'not-a-regular-file' };\n }\n return { status: 'open', descriptor };\n}\n\nfunction readRange(descriptor: number, from: number, to: number): Buffer {\n const buffer = Buffer.alloc(Math.max(0, to - from));\n let filled = 0;\n while (filled < buffer.length) {\n const bytesRead = readSync(descriptor, buffer, filled, buffer.length - filled, from + filled);\n if (bytesRead === 0) break;\n filled += bytesRead;\n }\n return buffer.subarray(0, filled);\n}\n\n/** The whole of an opened file, as its descriptor reports it now. */\nexport function readWhole(descriptor: number): Buffer {\n return readRange(descriptor, 0, fstatSync(descriptor).size);\n}\n\n/** The physical lines of `buffer`, which ends with a newline or is empty. */\nfunction* linesOf(buffer: Buffer): Generator<string> {\n let start = 0;\n while (start < buffer.length) {\n const end = buffer.indexOf(NEWLINE_BYTE, start);\n const stop = end === -1 ? buffer.length : end;\n yield buffer.toString('utf8', start, stop);\n start = stop + 1;\n }\n}\n\nfunction writeAll(descriptor: number, text: string): void {\n const bytes = Buffer.from(text, 'utf8');\n let written = 0;\n while (written < bytes.length) {\n written += writeSync(descriptor, bytes, written, bytes.length - written);\n }\n}\n\n/** Batches writes to one descriptor. */\nclass BatchedWriter {\n #parts: string[] = [];\n #length = 0;\n\n constructor(private readonly descriptor: number) {}\n\n write(text: string): void {\n this.#parts.push(text);\n this.#length += text.length;\n if (this.#length >= WRITE_BATCH_BYTES) this.flush();\n }\n\n flush(): void {\n if (this.#parts.length === 0) return;\n writeAll(this.descriptor, this.#parts.join(''));\n this.#parts = [];\n this.#length = 0;\n }\n}\n\n/** Flushes a directory's entries where the platform can (not on Windows). */\nfunction flushDirectory(directory: string): void {\n let descriptor: number | undefined;\n try {\n descriptor = openSync(directory, constants.O_RDONLY);\n fsyncSync(descriptor);\n } catch {\n // allow-fallback: a directory that cannot be opened or flushed (Windows) still holds the rename.\n } finally {\n if (descriptor !== undefined) closeSync(descriptor);\n }\n}\n\nfunction removeQuietly(path: string): void {\n try {\n unlinkSync(path);\n } catch {\n // allow-fallback: a temporary file that cannot be removed holds rewritten lines only.\n }\n}\n\n/** Plain lines in the file now, read by path as every reader reads it. */\nfunction countPlainLines(path: string): number {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return 0;\n try {\n return splitStoredLines(readWhole(opened.descriptor).toString('utf8')).filter(\n (line) => line.length > 0 && !isSealedArtifactLine(line),\n ).length;\n } finally {\n closeSync(opened.descriptor);\n }\n}\n\n/** What turns a file's physical lines into the lines its replacement holds. */\nexport interface IAppendFileRewriter {\n /**\n * Writes what `lines` become to `out`. `leadingNewline` is set for the catch-up, which appends to\n * a file other writers append to at the same time and so frames each line `\\n<line>\\n`.\n */\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void;\n}\n\nexport interface IRewriteAppendFileOptions {\n /** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */\n readonly ownedRoot?: string;\n /**\n * Runs once the snapshot is rewritten and flushed, just before the new file replaces the old.\n * Throwing abandons the rewrite with nothing replaced and no temporary file left.\n */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over the replaced file; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n}\n\n/** A catch-up that failed after the rename: what of the replaced file it never carried over. */\nexport interface IAppendFileCatchUpFailure {\n /** Bytes of the replaced file past the last line carried over (at most what was lost). */\n readonly unreadBytes: number;\n /** The failure's name; never its message, which may quote what was being rewritten. */\n readonly reason: string;\n}\n\nexport type TAppendFileRewrite =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TAppendFileRefusal }\n /** Another rewrite replaced the file before this one's rename; nothing was replaced. */\n | { readonly status: 'replaced' }\n /** The file was deleted before this one's rename; nothing was put in its place. */\n | { readonly status: 'removed' }\n | {\n readonly status: 'rewritten';\n /** The replaced file ended in an unfinished sealed append, which no reader shows; it was dropped. */\n readonly droppedTail: boolean;\n /** Plain lines in the new file afterwards: plaintext an older build appended meanwhile. */\n readonly plainLinesAfter: number;\n /** Set when the catch-up failed part-way. */\n readonly catchUpFailed?: IAppendFileCatchUpFailure;\n };\n\n/**\n * Reads the replaced file from `progress.read` until it stays quiet, rewriting what it finds onto\n * `out` and moving `progress.read` past each chunk carried over. Returns whether it ended in an\n * unfinished sealed append, which was dropped.\n */\nasync function catchUp(\n oldDescriptor: number,\n progress: { read: number },\n rewriter: IAppendFileRewriter,\n out: BatchedWriter,\n pause: () => Promise<void>,\n): Promise<boolean> {\n let quiet = 0;\n for (;;) {\n const size = fstatSync(oldDescriptor).size;\n if (size > progress.read) {\n const chunk = readRange(oldDescriptor, progress.read, size);\n const end = chunk.lastIndexOf(NEWLINE_BYTE);\n if (end !== -1) {\n rewriter.rewrite(linesOf(chunk.subarray(0, end + 1)), out, true);\n out.flush();\n progress.read += end + 1;\n quiet = 0;\n continue;\n }\n }\n if (quiet >= QUIET_PASSES) break;\n quiet += 1;\n await pause();\n }\n const end = fstatSync(oldDescriptor).size;\n const tail = readRange(oldDescriptor, progress.read, end).toString('utf8');\n if (tail.length === 0) return false;\n // A writer that stayed quiet this long is gone: a plain fragment is still a line to rewrite, while\n // a sealed one is what no reader shows (`splitStoredLines`).\n const kept = splitStoredLines(tail);\n rewriter.rewrite(kept, out, true);\n out.flush();\n progress.read = end;\n return kept.length === 0;\n}\n\n/** What `path` names now, by `lstat`: the file open at `descriptor`, another one, or nothing. */\nfunction namedNow(path: string, descriptor: number): 'same' | 'replaced' | 'removed' {\n let linked;\n try {\n linked = lstatSync(path);\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return 'removed';\n throw error;\n }\n const opened = fstatSync(descriptor);\n return linked.isFile() && linked.ino === opened.ino && linked.dev === opened.dev\n ? 'same'\n : 'replaced';\n}\n\nfunction waitBriefly(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, DEFAULT_PAUSE_MS));\n}\n\n/** Rewrites the file at `path` through `rewriter`, as the module documentation describes. */\nexport async function rewriteAppendFile(\n path: string,\n rewriter: IAppendFileRewriter,\n options: IRewriteAppendFileOptions = {},\n): Promise<TAppendFileRewrite> {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return opened;\n const oldDescriptor = opened.descriptor;\n const directory = dirname(path);\n const temporary = join(\n directory,\n `.${basename(path)}.${process.pid}.${randomBytes(TEMPORARY_SUFFIX_BYTES).toString('hex')}.tmp`,\n );\n let newDescriptor: number | undefined;\n let committed = false;\n try {\n const buffer = readWhole(oldDescriptor);\n const snapshotEnd = buffer.lastIndexOf(NEWLINE_BYTE) + 1;\n ensureOwnerOnlyDirectory(\n directory,\n options.ownedRoot === undefined ? {} : { withinRoot: options.ownedRoot },\n );\n newDescriptor = openSync(\n temporary,\n constants.O_WRONLY |\n constants.O_CREAT |\n constants.O_EXCL |\n constants.O_APPEND |\n (constants.O_NOFOLLOW ?? 0),\n OWNER_ONLY_FILE_MODE,\n );\n const out = new BatchedWriter(newDescriptor);\n rewriter.rewrite(linesOf(buffer.subarray(0, snapshotEnd)), out, false);\n out.flush();\n fsyncSync(newDescriptor);\n options.beforeCommit?.();\n const named = namedNow(path, oldDescriptor);\n if (named !== 'same') return { status: named };\n renameSync(temporary, path);\n committed = true;\n flushDirectory(directory);\n const progress = { read: snapshotEnd };\n let droppedTail = false;\n let catchUpFailed: IAppendFileCatchUpFailure | undefined;\n try {\n droppedTail = await catchUp(\n oldDescriptor,\n progress,\n rewriter,\n out,\n options.pause ?? waitBriefly,\n );\n } catch (error) {\n catchUpFailed = {\n unreadBytes: Math.max(0, fstatSync(oldDescriptor).size - progress.read),\n reason: error instanceof Error ? error.name : 'unknown',\n };\n }\n fsyncSync(newDescriptor);\n return {\n status: 'rewritten',\n droppedTail,\n plainLinesAfter: countPlainLines(path),\n ...(catchUpFailed === undefined ? {} : { catchUpFailed }),\n };\n } finally {\n if (newDescriptor !== undefined) closeSync(newDescriptor);\n closeSync(oldDescriptor);\n if (!committed) removeQuietly(temporary);\n }\n}\n","/**\n * Temporary files a crashed write left in a store directory of the user state: an atomic\n * owner-only write's `<name>.<pid>.tmp`, and an append-file rewrite's `.<name>.<pid>.<hex>.tmp`. Such\n * a file may hold a whole record in plaintext, and nothing ever reads or removes it.\n *\n * A write takes milliseconds, so a temp file five minutes older than its directory's last change is\n * a crash's; the age is judged by the filesystem's clock, never this process's. A directory nothing\n * changed in since the crash would keep its temp file forever by that rule, so a migration also\n * judges it against when the migration started, as its marker's time on the filesystem says. A temp\n * file of this process is never one. Only regular files are considered, and links are never followed.\n */\nimport { lstatSync, readdirSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst STALE_TEMPORARY_AGE_MS = 5 * 60 * 1000;\nconst OWNER_ONLY_TEMPORARY = /^.+\\.(\\d{1,10})\\.tmp$/u;\nconst REWRITE_TEMPORARY = /^\\..+\\.(\\d{1,10})\\.[0-9a-f]{12}\\.tmp$/u;\n\nfunction ownerPidOf(name: string): number | undefined {\n const digits = (REWRITE_TEMPORARY.exec(name) ?? OWNER_ONLY_TEMPORARY.exec(name))?.[1];\n return digits === undefined ? undefined : Number(digits);\n}\n\n/**\n * The stale temp files in `directory`, removed when `apply` is set; returns how many there were.\n * `migrationStartedAt` is when a migration started, in milliseconds by the filesystem's clock; a temp\n * file older than it or than the directory's last change by the stale age is stale. A directory that\n * is missing or is not a real directory has none.\n */\nexport function sweepStaleStoreTemporaries(\n directory: string,\n apply: boolean,\n migrationStartedAt?: number,\n): number {\n let now: number;\n let names: string[];\n try {\n const metadata = lstatSync(directory);\n if (!metadata.isDirectory()) return 0;\n now = Math.max(metadata.mtimeMs, migrationStartedAt ?? Number.NEGATIVE_INFINITY);\n names = readdirSync(directory);\n } catch {\n return 0; // allow-fallback: a directory that cannot be listed has nothing to sweep.\n }\n let swept = 0;\n for (const name of names) {\n const pid = ownerPidOf(name);\n if (pid === undefined || pid === process.pid) continue;\n const path = join(directory, name);\n try {\n const metadata = lstatSync(path);\n if (!metadata.isFile() || now - metadata.mtimeMs <= STALE_TEMPORARY_AGE_MS) continue;\n if (apply) unlinkSync(path);\n swept += 1;\n } catch {\n // allow-fallback: one removed meanwhile, or one that cannot be removed, is retried next time.\n }\n }\n return swept;\n}\n","/**\n * Replacing many files of a store directory at once, for the converters that seal session records and\n * session-log sidecars in place. A file is read without following a link; every replacement\n * is written and flushed beside its file before any is put in place, so no rename is published ahead\n * of content not yet flushed; and each replace is atomic, made only while the file still holds the\n * bytes that were read under no other name, so a file a running process saved meanwhile is left.\n */\nimport { createHash } from 'node:crypto';\nimport { closeSync, constants, fstatSync, openSync } from 'node:fs';\n\nimport { stageOwnerOnlyFile } from '@robota-sdk/agent-core/node';\n\nimport { readWhole } from './append-file-rewrite.js';\n\nimport type { IStagedOwnerOnlyFile } from '@robota-sdk/agent-core/node';\n\n/** A regular file's bytes and how many names it has. */\nexport interface IRegularFile {\n readonly bytes: Buffer;\n readonly links: number;\n}\n\n/** The regular file at `path`, never through a link; `undefined` when it is gone or not regular. */\nexport function readRegularFile(path: string): IRegularFile | undefined {\n let descriptor: number;\n try {\n descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n try {\n const metadata = fstatSync(descriptor);\n return metadata.isFile() ? { bytes: readWhole(descriptor), links: metadata.nlink } : undefined;\n } finally {\n closeSync(descriptor);\n }\n}\n\nconst digestOf = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');\n\n/** Stages one replacement: the file at `path`, read as `read`, is to hold `content`. */\nexport type TStageReplacement = (path: string, read: Uint8Array, content: Uint8Array) => void;\n\n/** What {@link replaceStagedFiles} put in place, and what it left because it changed meanwhile. */\nexport interface IStagedReplacements {\n readonly replaced: number;\n readonly changed: readonly string[];\n}\n\n/** A file changed between the read and its replace; that replace is abandoned. */\nclass FileChangedError extends Error {\n constructor() {\n super('The file changed while its replacement was staged.');\n this.name = 'FileChangedError';\n }\n}\n\n/**\n * Runs `collect`, which stages the replacements, then puts each in place, as the module\n * documentation describes; `beforeCommit` runs first each time, and a throw stops there with that\n * file as it was. Whatever is not put in place is removed, however this ends.\n */\nexport function replaceStagedFiles(\n collect: (stage: TStageReplacement) => void,\n beforeCommit?: () => void,\n aroundCommit: (path: string, commit: () => void) => boolean = (_path, commit) => {\n commit();\n return true;\n },\n): IStagedReplacements {\n const staged: { path: string; file: IStagedOwnerOnlyFile; digest: string }[] = [];\n const changed: string[] = [];\n let replaced = 0;\n try {\n collect((path, read, content) => {\n staged.push({ path, file: stageOwnerOnlyFile(path, content), digest: digestOf(read) });\n });\n for (const { path, file, digest } of staged) {\n try {\n const committed = aroundCommit(path, () =>\n file.commit(() => {\n beforeCommit?.();\n const now = readRegularFile(path);\n if (now === undefined || now.links > 1 || digestOf(now.bytes) !== digest) {\n throw new FileChangedError();\n }\n }),\n );\n if (committed) replaced += 1;\n else changed.push(path);\n } catch (error) {\n if (!(error instanceof FileChangedError)) throw error;\n changed.push(path);\n }\n }\n } finally {\n for (const { file } of staged) file.discard();\n }\n return { replaced, changed };\n}\n","/**\n * Sealing in place the session logs a host-filesystem sink (`NodeSessionLogSink`) wrote in plaintext,\n * for a host migrating its state once it seals: every `<id>.jsonl` in one directory, with its\n * `<id>.payloads/` sidecars.\n *\n * - A log becomes one sealed record per non-empty plain line, its sealed lines kept as they are and\n * its empty lines dropped, so a stored-record offset (`/agent read`) names the same line after.\n * - A sidecar is sealed in place under its existing name: a source opens sealed bytes whatever the\n * name, and the digest and length a reference checks are the plaintext's, so no reference — nested\n * ones inside other payloads included — changes. A migrated sidecar keeps its content-derived name.\n * - Order: every sidecar of a session sealed beside it and flushed, then each put in place (atomic,\n * made only while the file still holds what was read), then its log, which is the commit. A log is\n * rewritten as the prompt history is compacted (`append-file-rewrite.ts`), so a line a writer\n * appends meanwhile is caught up; a plain line that still reaches it afterwards, a log another\n * rewrite replaced first, or a plain sidecar that appears, is reported for a rerun, and a catch-up\n * that failed with how much it left unread. A log deleted meanwhile stays deleted, and uncounted.\n *\n * Whether something is done is decided by whether it opens for its session, never by framing alone; a\n * sealed sidecar that does not open here is left as it is and reported. Links, anything that is not a\n * regular file or directory, and a file with another hard link (replacing it would leave the\n * plaintext under the other name) are never followed or touched.\n */\nimport { closeSync, lstatSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { openAppendFile, readWhole, rewriteAppendFile } from './append-file-rewrite.js';\nimport { isSafeSessionId } from './session-id.js';\nimport { sweepStaleStoreTemporaries } from './stale-temporaries.js';\nimport { readRegularFile, replaceStagedFiles } from './staged-replacements.js';\nimport {\n SessionLogLineSealer,\n countPlainSessionLogLines,\n sealStoredPayloadBytes,\n storedPayloadMigrationState,\n} from './stored-append-lines.js';\n\nimport type { IAppendFileCatchUpFailure } from './append-file-rewrite.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst LOG_EXTENSION = '.jsonl';\nconst PAYLOAD_DIRECTORY_SUFFIX = '.payloads';\nconst TEMPORARY_SUFFIX = '.tmp';\n\nexport interface IMigrateSessionLogsOptions {\n /** Seals what is plain and opens what is sealed; the keys must be unlocked. */\n readonly sealer: IArtifactSealer;\n /** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */\n readonly apply: boolean;\n /** An ancestor of the directory the host also owns, tightened along with it. */\n readonly ownedRoot?: string;\n /** Runs before each sidecar replace and each log replace; throwing stops the migration there. */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over a replaced log; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n /**\n * Also removes the temp files crashed writes left; leave it off while another writer may be running.\n * Off, they are still counted in `staleTemporaries` and left where they are.\n */\n readonly sweepTemporaries?: boolean;\n /**\n * When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp\n * file is also judged stale against it, so one in a directory nothing changed in since is found.\n */\n readonly migrationStartedAt?: number;\n}\n\n/** A log whose catch-up failed after its rename, and what of the replaced file it left unread. */\nexport interface ISessionLogCatchUpFailure extends IAppendFileCatchUpFailure {\n readonly path: string;\n}\n\n/** What a migration of one directory's logs did, or (planning) would do. */\nexport interface ISessionLogMigration {\n /** Logs that held plain lines: sealed (or to seal). */\n readonly logs: number;\n /** The plain lines among them. */\n readonly lines: number;\n /** Plain sidecars sealed in place (or to seal). */\n readonly payloads: number;\n /** Logs and sidecars that hold no plaintext. */\n readonly alreadySealed: number;\n /** Sealed sidecars that do not open for their session, or that a newer build sealed: left as they are. */\n readonly unopenable: readonly string[];\n /**\n * Links, entries that are not regular, logs and sidecars with another hard link, names that are not\n * session ids.\n */\n readonly refused: readonly string[];\n /**\n * Logs and sidecars that changed while being sealed, that plaintext reached meanwhile, or that\n * another rewrite replaced first.\n */\n readonly changed: readonly string[];\n /** Logs sealed whose catch-up failed: lines appended to the replaced file meanwhile are lost. */\n readonly catchUpFailed: readonly ISessionLogCatchUpFailure[];\n /** Temp files crashed writes left: removed (or to remove) with `sweepTemporaries`, else left. */\n readonly staleTemporaries: number;\n}\n\ninterface IMutableLogMigration {\n logs: number;\n lines: number;\n payloads: number;\n alreadySealed: number;\n unopenable: string[];\n refused: string[];\n changed: string[];\n catchUpFailed: ISessionLogCatchUpFailure[];\n staleTemporaries: number;\n}\n\n/** The entries of `directory` by kind as `lstat` sees them; none when it is missing. */\nfunction entriesOf(directory: string): ReadonlyMap<string, 'file' | 'directory' | 'other'> {\n const kinds = new Map<string, 'file' | 'directory' | 'other'>();\n let names: string[];\n try {\n names = readdirSync(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return kinds;\n throw error;\n }\n for (const name of names.sort()) {\n try {\n const metadata = lstatSync(join(directory, name));\n kinds.set(name, metadata.isFile() ? 'file' : metadata.isDirectory() ? 'directory' : 'other');\n } catch (error) {\n // allow-fallback: an entry removed since the listing is simply not there.\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n return kinds;\n}\n\n/** The sessions a directory holds logs or sidecars for, each with which of the two it has. */\nfunction sessionsOf(\n directory: string,\n result: IMutableLogMigration,\n): Map<string, { log: boolean; payloads: boolean }> {\n const sessions = new Map<string, { log: boolean; payloads: boolean }>();\n for (const [name, kind] of entriesOf(directory)) {\n const isLog = name.endsWith(LOG_EXTENSION);\n const isPayloads = name.endsWith(PAYLOAD_DIRECTORY_SUFFIX);\n if (!isLog && !isPayloads) continue;\n const id = name.slice(0, -(isLog ? LOG_EXTENSION : PAYLOAD_DIRECTORY_SUFFIX).length);\n if ((isLog ? kind !== 'file' : kind !== 'directory') || !isSafeSessionId(id)) {\n result.refused.push(join(directory, name));\n continue;\n }\n const session = sessions.get(id) ?? { log: false, payloads: false };\n if (isLog) session.log = true;\n else session.payloads = true;\n sessions.set(id, session);\n }\n return sessions;\n}\n\n/** Seals one session's plain sidecars in place; returns the names it saw, for the check after the log. */\nfunction migratePayloads(\n directory: string,\n ownerId: string,\n options: IMigrateSessionLogsOptions,\n result: IMutableLogMigration,\n): ReadonlySet<string> {\n const seen = new Set<string>();\n const replacements = replaceStagedFiles((stage) => {\n for (const [name, kind] of entriesOf(directory)) {\n if (name.endsWith(TEMPORARY_SUFFIX)) continue;\n seen.add(name);\n const path = join(directory, name);\n if (kind !== 'file') {\n result.refused.push(path);\n continue;\n }\n const read = readRegularFile(path);\n if (read === undefined) continue;\n const state = storedPayloadMigrationState(read.bytes, ownerId, options.sealer);\n if (state === 'sealed') result.alreadySealed += 1;\n else if (state !== 'plain') result.unopenable.push(path);\n if (state !== 'plain') continue;\n if (read.links > 1) {\n result.refused.push(path);\n continue;\n }\n if (!options.apply) {\n result.payloads += 1;\n continue;\n }\n stage(path, read.bytes, sealStoredPayloadBytes(read.bytes, ownerId, options.sealer));\n }\n }, options.beforeCommit);\n result.payloads += replacements.replaced;\n result.changed.push(...replacements.changed);\n result.staleTemporaries += sweepStaleStoreTemporaries(\n directory,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return seen;\n}\n\n/** Plain sidecars that appeared in `directory` since `seen` was taken: what a writer added meanwhile. */\nfunction plainPayloadsSince(\n directory: string,\n ownerId: string,\n seen: ReadonlySet<string>,\n sealer: IArtifactSealer,\n): string[] {\n const appeared: string[] = [];\n for (const [name, kind] of entriesOf(directory)) {\n if (seen.has(name) || name.endsWith(TEMPORARY_SUFFIX) || kind !== 'file') continue;\n const read = readRegularFile(join(directory, name));\n if (\n read !== undefined &&\n storedPayloadMigrationState(read.bytes, ownerId, sealer) === 'plain'\n ) {\n appeared.push(join(directory, name));\n }\n }\n return appeared;\n}\n\n/** The plain lines in the log at `path`, or why it is not a log this migration rewrites. */\nfunction plainLinesOf(path: string): number | 'missing' | 'refused' {\n const opened = openAppendFile(path);\n if (opened.status === 'missing') return 'missing';\n if (opened.status === 'refused') return 'refused';\n try {\n return countPlainSessionLogLines(readWhole(opened.descriptor).toString('utf8'));\n } finally {\n closeSync(opened.descriptor);\n }\n}\n\nasync function migrateLog(\n path: string,\n ownerId: string,\n options: IMigrateSessionLogsOptions,\n result: IMutableLogMigration,\n): Promise<void> {\n const plain = plainLinesOf(path);\n if (plain === 'missing') return;\n if (plain === 'refused') {\n result.refused.push(path);\n return;\n }\n if (plain === 0) {\n result.alreadySealed += 1;\n return;\n }\n const rewritten = !options.apply\n ? undefined\n : await rewriteAppendFile(path, new SessionLogLineSealer(options.sealer, ownerId), {\n ...(options.ownedRoot === undefined ? {} : { ownedRoot: options.ownedRoot }),\n ...(options.beforeCommit === undefined ? {} : { beforeCommit: options.beforeCommit }),\n ...(options.pause === undefined ? {} : { pause: options.pause }),\n });\n // Deleted since it was counted: nothing of it is left to seal, and nothing is put back.\n if (rewritten?.status === 'missing' || rewritten?.status === 'removed') return;\n result.logs += 1;\n result.lines += plain;\n if (rewritten === undefined) return;\n if (rewritten.status === 'refused') result.refused.push(path);\n else if (rewritten.status === 'replaced') result.changed.push(path);\n else if (rewritten.status === 'rewritten') {\n if (rewritten.plainLinesAfter > 0) result.changed.push(path);\n if (rewritten.catchUpFailed !== undefined) {\n result.catchUpFailed.push({ path, ...rewritten.catchUpFailed });\n }\n }\n}\n\n/** Seals the plain logs and sidecars of `directory` as the module documentation describes. */\nexport async function migrateSessionLogDirectory(\n directory: string,\n options: IMigrateSessionLogsOptions,\n): Promise<ISessionLogMigration> {\n const result: IMutableLogMigration = {\n logs: 0,\n lines: 0,\n payloads: 0,\n alreadySealed: 0,\n unopenable: [],\n refused: [],\n changed: [],\n catchUpFailed: [],\n staleTemporaries: 0,\n };\n try {\n if (!lstatSync(directory).isDirectory()) {\n result.refused.push(directory);\n return result;\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return result;\n throw error;\n }\n for (const [id, session] of sessionsOf(directory, result)) {\n const payloadDirectory = join(directory, `${id}${PAYLOAD_DIRECTORY_SUFFIX}`);\n const seen = session.payloads\n ? migratePayloads(payloadDirectory, id, options, result)\n : new Set<string>();\n if (session.log)\n await migrateLog(join(directory, `${id}${LOG_EXTENSION}`), id, options, result);\n if (options.apply && session.payloads) {\n result.changed.push(...plainPayloadsSince(payloadDirectory, id, seen, options.sealer));\n }\n }\n result.staleTemporaries += sweepStaleStoreTemporaries(\n directory,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return result;\n}\n","/**\n * Sealing in place the records a host-filesystem session store (`NodeSessionStore`) kept in\n * plaintext, for a host migrating its state once it seals.\n *\n * Every plain record is sealed as the raw bytes it holds — valid, corrupt, unsupported or bare alike —\n * under its own file stem, so the store reports the same status after opening it, and the plaintext is\n * gone. Whether a record is already done is decided by whether it opens for its id, never by framing\n * alone: a sealed record that does not open here, or that a newer build sealed, is left as it is and\n * reported. A file whose name is not a session id, anything that is not a regular file, and a file\n * with another hard link (replacing it would leave the plaintext under the other name) is never\n * touched, and neither is a record whose writer marker stands for a process not provably gone: it may\n * be saving it now. Every sealed record is written beside its file and flushed before any is put in\n * place; each replace is then atomic, and is made only if the file still holds the bytes that were\n * read, so a record a running process saved meanwhile is left for the next run.\n */\nimport { createProcessOperationLock } from '@robota-sdk/agent-node-lock';\n\nimport { lstatSync, readdirSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\nimport { createProcessMarkers } from '@robota-sdk/agent-core/node';\n\nimport { isSafeSessionId } from './session-id.js';\nimport { SESSION_CLAIMS_DIRECTORY } from './session-store.js';\nimport { sweepStaleStoreTemporaries } from './stale-temporaries.js';\nimport { readRegularFile, replaceStagedFiles } from './staged-replacements.js';\nimport { openStoredSessionRecord } from './stored-session-authorship.js';\nimport {\n digestOf,\n isPlainSessionRecordOf,\n sealStoredSessionBytes,\n storedSessionMigrationState,\n} from './stored-session-codec.js';\n\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst RECORD_EXTENSION = '.json';\n\nexport interface IMigrateSessionRecordsOptions {\n /** Seals what is plain and opens what is sealed; the keys must be unlocked. */\n readonly sealer: IArtifactSealer;\n /** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */\n readonly apply: boolean;\n /**\n * Runs before each replace, after the sealed record is written and flushed beside it; throwing\n * stops the migration there with that record as it was.\n */\n readonly beforeCommit?: () => void;\n /**\n * Also removes the temp files crashed writes left; leave it off while another writer may be running.\n * Off, they are still counted in `staleTemporaries` and left where they are.\n */\n readonly sweepTemporaries?: boolean;\n /**\n * When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp\n * file is also judged stale against it, so one in a directory nothing changed in since is found.\n */\n readonly migrationStartedAt?: number;\n}\n\n/** What a migration of a store's records did, or (planning) would do. */\nexport interface ISessionRecordMigration {\n /** Plain records sealed (or to seal). */\n readonly sealed: number;\n /** Records that already open sealed. */\n readonly alreadySealed: number;\n /** Sealed records that do not open here, or that a newer build sealed: left as they are. */\n readonly unopenable: readonly string[];\n /** `.json` files whose name is not a session id: left as they are. */\n readonly unsafeNames: readonly string[];\n /**\n * `.json` entries that are links, not regular files, or regular files with another hard link:\n * never followed or replaced, left as they are.\n */\n readonly refused: readonly string[];\n /** Records that changed while being sealed: left as they are, for the next run. */\n readonly changed: readonly string[];\n /**\n * The ids of records open in another process — their writer marker stands for an owner not\n * provably gone — left as they are, for a run after it has closed.\n */\n readonly held: readonly string[];\n /** Temp files crashed writes left with a record in them: removed (or to remove) with `sweepTemporaries`, else left. */\n readonly staleTemporaries: number;\n /**\n * Each record this run sealed, by its id and the SHA-256 of the sealed bytes it put in place — the\n * bytes the owner may then vouch for (`ISessionAuthorship.adopt`), and no others.\n * Empty when planning.\n */\n readonly sealedRecords: readonly ISealedSessionRecord[];\n}\n\n/** One record a migration sealed: its id and the SHA-256 (lowercase hex) of the bytes it wrote. */\nexport interface ISealedSessionRecord {\n readonly id: string;\n readonly digest: string;\n}\n\n/** The `.json` entries of `baseDir`, each a regular file or refused; none when the directory is missing. */\nfunction recordEntries(baseDir: string): { readonly files: string[]; readonly refused: string[] } {\n const files: string[] = [];\n const refused: string[] = [];\n let names: string[];\n try {\n if (!lstatSync(baseDir).isDirectory()) return { files, refused: [baseDir] };\n names = readdirSync(baseDir);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { files, refused };\n throw error;\n }\n for (const name of names.sort()) {\n if (!name.endsWith(RECORD_EXTENSION)) continue;\n try {\n (lstatSync(join(baseDir, name)).isFile() ? files : refused).push(name);\n } catch (error) {\n // allow-fallback: an entry removed since the listing is simply not there.\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n return { files, refused: refused.map((name) => join(baseDir, name)) };\n}\n\n/** Seals the plain records of the store at `baseDir` as the module documentation describes. */\nexport function migrateNodeSessionStore(\n baseDir: string,\n options: IMigrateSessionRecordsOptions,\n): ISessionRecordMigration {\n const { files, refused } = recordEntries(baseDir);\n const result = {\n sealed: 0,\n alreadySealed: 0,\n unopenable: [] as string[],\n unsafeNames: [] as string[],\n refused,\n changed: [] as string[],\n held: [] as string[],\n staleTemporaries: 0,\n sealedRecords: [] as ISealedSessionRecord[],\n };\n const markers = createProcessMarkers(join(baseDir, SESSION_CLAIMS_DIRECTORY), {\n operationLock: createProcessOperationLock(join(baseDir, SESSION_CLAIMS_DIRECTORY)),\n });\n const staged: (ISealedSessionRecord & { readonly path: string })[] = [];\n const replacements = replaceStagedFiles(\n (stage) => {\n for (const name of files) {\n const path = join(baseDir, name);\n const id = name.slice(0, -RECORD_EXTENSION.length);\n if (!isSafeSessionId(id)) {\n result.unsafeNames.push(path);\n continue;\n }\n const writer = markers.inspect(id);\n if (writer.status === 'held' && writer.holder.liveness !== 'gone') {\n result.held.push(id);\n continue;\n }\n const read = readRegularFile(path);\n if (read === undefined) continue;\n const state = storedSessionMigrationState(id, read.bytes, options.sealer);\n if (state === 'sealed') result.alreadySealed += 1;\n else if (state !== 'plain') result.unopenable.push(path);\n if (state !== 'plain') continue;\n if (read.links > 1) {\n result.refused.push(path);\n continue;\n }\n if (!options.apply) {\n result.sealed += 1;\n continue;\n }\n const sealed = sealStoredSessionBytes(id, read.bytes, options.sealer);\n // Only a record that reads as this session's is a session the owner could mark as theirs; a\n // corrupt or unsupported one is sealed as it is and stays what it was.\n if (isPlainSessionRecordOf(id, read.bytes)) {\n staged.push({ path, id, digest: digestOf(sealed) });\n }\n stage(path, read.bytes, sealed);\n }\n },\n options.beforeCommit,\n (path, commit) => {\n const id = basename(path).slice(0, -RECORD_EXTENSION.length);\n const acquired = markers.acquire(id);\n if (acquired.status !== 'acquired') {\n result.held.push(id);\n return false;\n }\n try {\n const guarded = acquired.handle.withGuard(commit);\n if (guarded.status !== 'ran') {\n result.held.push(id);\n return false;\n }\n return true;\n } finally {\n acquired.handle.release();\n }\n },\n );\n result.sealed += replacements.replaced;\n result.changed.push(...replacements.changed);\n // Every staged record was put in place but those that changed meanwhile, since anything else\n // that stops a replacement throws out of it.\n for (const { path, id, digest } of staged) {\n if (!replacements.changed.includes(path)) result.sealedRecords.push({ id, digest });\n }\n result.staleTemporaries = sweepStaleStoreTemporaries(\n baseDir,\n options.apply && options.sweepTemporaries === true,\n options.migrationStartedAt,\n );\n return result;\n}\n\n/**\n * The working directories the store's records name — a valid record's `cwd`, and the `cwd` a corrupt\n * or unsupported one still carries — opened with `sealer`, distinct, in file order. Writes nothing;\n * a record that does not open names none.\n */\nexport function readSessionStoreWorkingDirectories(\n baseDir: string,\n options: { readonly sealer: IArtifactSealer },\n): readonly string[] {\n const directories = new Set<string>();\n for (const name of recordEntries(baseDir).files) {\n const id = name.slice(0, -RECORD_EXTENSION.length);\n if (!isSafeSessionId(id)) continue;\n const read = readRegularFile(join(baseDir, name));\n if (read === undefined) continue;\n const outcome = openStoredSessionRecord(id, read.bytes, options.sealer);\n const cwd =\n outcome.status === 'valid'\n ? outcome.record.cwd\n : outcome.status === 'corrupt' || outcome.status === 'unsupported'\n ? outcome.cwd\n : undefined;\n if (typeof cwd === 'string' && cwd.length > 0) directories.add(cwd);\n }\n return [...directories];\n}\n","import { randomBytes } from 'node:crypto';\nimport {\n closeSync,\n constants,\n fsyncSync,\n linkSync,\n lstatSync,\n openSync,\n readdirSync,\n rmSync,\n rmdirSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\nimport { SEALED_ARTIFACT_MAX_OVERHEAD_BYTES } from '@robota-sdk/agent-core';\nimport {\n PROCESS_OWNER_RECORD_NAME,\n createProcessScopedDirectory,\n removeDirectoryOnProcessExit,\n sweepStaleProcessScopedDirectories,\n} from '@robota-sdk/agent-core/node';\n\nimport { NodeExternalPayloadSource } from './session-log-sources.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type {\n IArtifactContext,\n IArtifactSealer,\n IToolResultSpillStore,\n TArtifactSealing,\n} from '@robota-sdk/agent-core';\n\nconst REFERENCE = /^tool-result:([A-Za-z0-9_-]{22,64})$/u;\n/**\n * The product-neutral name every spill directory starts with; one in a host's parent adds its owner's\n * pid before the random suffix.\n */\nconst TOOL_RESULT_SPILL_DIRECTORY_PREFIX = 'agent-tool-results-';\nconst DEFAULT_RETENTION_MS = 60 * 60 * 1000;\nconst MAX_SPILL_BYTES = 8 * 1024 * 1024;\nconst FILE_FLAGS =\n constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);\n\nexport type TToolResultSpillErrorCode =\n | 'invalid-options'\n | 'unsafe-root'\n | 'write-failed'\n | 'missing'\n | 'expired'\n | 'invalid-reference'\n | 'read-failed'\n | 'cleanup-failed'\n | 'closed';\n\nexport class ToolResultSpillError extends Error {\n constructor(readonly code: TToolResultSpillErrorCode) {\n super(`Tool result spill failed (${code})`);\n this.name = 'ToolResultSpillError';\n }\n}\n\n/**\n * Remove the spill directories under `parent` whose recorded owner can no longer be alive (a process\n * killed before `shutdown()` or its exit removal could run). Returns the names removed.\n */\nexport function sweepStaleToolResultSpillDirectories(parent: string): readonly string[] {\n return sweepStaleProcessScopedDirectories(parent, TOOL_RESULT_SPILL_DIRECTORY_PREFIX);\n}\n\nexport interface INodeToolResultSpillStoreOptions {\n /**\n * Host-owned, owner-only parent that already exists; it is used as it is. Spilled results are\n * conversation content, so they never go to a directory the store picks: the store's directory in\n * the parent records its owner, is removed when the process ends without reaching `shutdown()`,\n * and is removed after a kill by {@link sweepStaleToolResultSpillDirectories}.\n */\n readonly parentDirectory: string;\n readonly retentionMs?: number;\n readonly now?: () => number;\n /** Receives a fixed, payload-free reason if an idle expiry timer cannot delete a file. */\n readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;\n /**\n * Required. `{ sealer }` keeps every result sealed on disk, bound to its reference, as\n * `<token>.sealed`, and opens it with the same sealer to read it back; a result that does not open\n * reads as `read-failed`. `'plain'` writes each result as it is to `<token>.txt`.\n */\n readonly sealing: TArtifactSealing;\n}\n\ninterface IStoredResult {\n readonly fileName: string;\n /** The random part of the reference: what a sealed result is bound to. */\n readonly token: string;\n readonly expiresAt: number;\n}\n\n/**\n * The raw bytes a sealed result may take: the plaintext budget and what any sealer adds to it, so a\n * result of exactly the largest size a spill takes still reads back once sealed.\n */\nconst MAX_SEALED_SPILL_BYTES = MAX_SPILL_BYTES + SEALED_ARTIFACT_MAX_OVERHEAD_BYTES;\n\nfunction spillContext(token: string): IArtifactContext {\n return { kind: 'tool-result-spill', id: token };\n}\n\n/**\n * Node host implementation of core's opaque, session-lifetime spill port. A sealing store keeps the\n * last result it opened in memory, so paging through a large result opens it once.\n */\nexport class NodeToolResultSpillStore implements IToolResultSpillStore {\n private readonly directory: string;\n private readonly retentionMs: number;\n private readonly now: () => number;\n private readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;\n private readonly sealer: IArtifactSealer | undefined;\n private readonly entries = new Map<string, IStoredResult>();\n /** The result a sealing store opened last; forgotten with its entry. */\n private opened?: { readonly reference: string; readonly content: string };\n private expiryTimer?: ReturnType<typeof setTimeout>;\n private cancelExitRemoval?: () => void;\n private closed = false;\n\n constructor(options: INodeToolResultSpillStoreOptions) {\n // A caller that ignores the types gets the same refusal as one that passes a bad value.\n const parent = options?.parentDirectory;\n if (typeof parent !== 'string' || parent.length === 0) {\n throw new ToolResultSpillError('invalid-options');\n }\n this.retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;\n this.now = options.now ?? Date.now;\n this.onCleanupFailure = options.onCleanupFailure;\n if (!Number.isSafeInteger(this.retentionMs) || this.retentionMs <= 0) {\n throw new ToolResultSpillError('invalid-options');\n }\n let sealing: TArtifactSealing;\n try {\n sealing = sessionSealingOf(options);\n } catch {\n throw new ToolResultSpillError('invalid-options');\n }\n this.sealer = sealing === 'plain' ? undefined : sealing.sealer;\n try {\n const stat = lstatSync(parent);\n if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('unsafe parent');\n // The host's parent is used as it is; the directory made in it is owner-only and records its\n // owner before any result goes in.\n this.directory = createProcessScopedDirectory(parent, TOOL_RESULT_SPILL_DIRECTORY_PREFIX, {\n prepareParent: false,\n });\n this.assertRoot();\n } catch {\n throw new ToolResultSpillError('unsafe-root');\n }\n // `shutdown()` is the normal removal; a process that ends without reaching it (an exit, or a\n // signal nothing else handles) still leaves nothing, and one killed outright is swept at the next\n // start by its owner record.\n this.cancelExitRemoval = removeDirectoryOnProcessExit(this.directory);\n }\n\n private assertRoot(): void {\n try {\n const stat = lstatSync(this.directory);\n if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('unsafe directory');\n if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {\n throw new Error('directory is not owner-only');\n }\n } catch {\n throw new ToolResultSpillError('unsafe-root');\n }\n }\n\n private scheduleExpiry(minimumDelayMs = 1): void {\n if (this.expiryTimer !== undefined) clearTimeout(this.expiryTimer);\n this.expiryTimer = undefined;\n if (this.closed || this.entries.size === 0) return;\n let firstExpiry = Number.POSITIVE_INFINITY;\n for (const entry of this.entries.values()) {\n if (entry.expiresAt < firstExpiry) firstExpiry = entry.expiresAt;\n }\n const delay = Math.min(2_147_483_647, Math.max(minimumDelayMs, firstExpiry - this.now()));\n this.expiryTimer = setTimeout(() => {\n this.expiryTimer = undefined;\n void this.cleanupExpired().catch(() => {\n try {\n if (this.onCleanupFailure) this.onCleanupFailure('cleanup-failed');\n else process.emitWarning('Tool result spill expiry cleanup failed (cleanup-failed)');\n } catch {\n process.emitWarning('Tool result spill expiry cleanup failed (cleanup-failed)');\n }\n this.scheduleExpiry(60_000);\n });\n }, delay);\n this.expiryTimer.unref?.();\n }\n\n async write(content: string): Promise<{ readonly reference: string }> {\n if (this.closed) throw new ToolResultSpillError('closed');\n await this.cleanupExpired();\n this.assertRoot();\n if (Buffer.byteLength(content, 'utf8') > MAX_SPILL_BYTES) {\n throw new ToolResultSpillError('write-failed');\n }\n const token = randomBytes(18).toString('base64url');\n const tempName = `${randomBytes(18).toString('base64url')}.partial`;\n const fileName = `${token}${this.sealer === undefined ? '.txt' : '.sealed'}`;\n const tempPath = join(this.directory, tempName);\n const finalPath = join(this.directory, fileName);\n let data: string | Uint8Array;\n try {\n data = this.storedForm(content, token);\n } catch {\n throw new ToolResultSpillError('write-failed');\n }\n let fd: number | undefined;\n let linked = false;\n try {\n fd = openSync(tempPath, FILE_FLAGS, 0o600);\n writeFileSync(fd, data, 'utf8');\n fsyncSync(fd);\n closeSync(fd);\n fd = undefined;\n linkSync(tempPath, finalPath);\n linked = true;\n unlinkSync(tempPath);\n const reference = `tool-result:${token}`;\n this.entries.set(reference, { fileName, token, expiresAt: this.now() + this.retentionMs });\n this.scheduleExpiry();\n return { reference };\n } catch {\n let cleanupFailed = false;\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n cleanupFailed = true;\n }\n }\n try {\n unlinkSync(tempPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') cleanupFailed = true;\n }\n if (linked) {\n try {\n unlinkSync(finalPath);\n } catch {\n cleanupFailed = true;\n }\n }\n throw new ToolResultSpillError(cleanupFailed ? 'cleanup-failed' : 'write-failed');\n }\n }\n\n async read(reference: string): Promise<string> {\n if (this.closed) throw new ToolResultSpillError('closed');\n if (!REFERENCE.test(reference)) throw new ToolResultSpillError('invalid-reference');\n const entry = this.entries.get(reference);\n if (!entry) throw new ToolResultSpillError('missing');\n if (this.now() >= entry.expiresAt) {\n this.removeEntry(reference, entry);\n this.scheduleExpiry();\n throw new ToolResultSpillError('expired');\n }\n if (this.opened?.reference === reference) return this.opened.content;\n this.assertRoot();\n try {\n // The raw stored bytes: this store opens a sealed result itself, bound to its token, so the\n // payload source only reads the file.\n const bytes = new NodeExternalPayloadSource(this.directory, { sealing: 'plain' }).readBytes(\n entry.fileName,\n this.sealer === undefined ? MAX_SPILL_BYTES : MAX_SEALED_SPILL_BYTES,\n );\n if (bytes === undefined) throw new ToolResultSpillError('missing');\n if (this.sealer === undefined) return Buffer.from(bytes).toString('utf8');\n const content = this.openStored(this.sealer, bytes, entry.token);\n this.opened = { reference, content };\n return content;\n } catch (error) {\n if (error instanceof ToolResultSpillError) throw error;\n throw new ToolResultSpillError('read-failed');\n }\n }\n\n /** What goes on disk for `content`: the text as it is, or sealed to the result's token. */\n private storedForm(content: string, token: string): string | Uint8Array {\n if (this.sealer === undefined) return content;\n const plain = Buffer.from(content, 'utf8');\n try {\n return this.sealer.sealBytes(plain, spillContext(token));\n } finally {\n plain.fill(0);\n }\n }\n\n /**\n * The text a sealed result holds, held to the plaintext budget exactly. Any failure to open it —\n * damaged, moved from another reference, sealed with other keys — is the caller's `read-failed`.\n */\n private openStored(sealer: IArtifactSealer, stored: Uint8Array, token: string): string {\n const plain = sealer.openBytes(stored, spillContext(token));\n try {\n if (plain.byteLength > MAX_SPILL_BYTES) throw new ToolResultSpillError('read-failed');\n return Buffer.from(plain.buffer, plain.byteOffset, plain.byteLength).toString('utf8');\n } finally {\n plain.fill(0);\n }\n }\n\n /**\n * Removes a result. What a sealing store had open of it is forgotten first, so a removal that fails\n * does not keep it in memory; a file already gone counts as removed, or one result deleted from\n * outside would fail every later cleanup, and with it every write.\n */\n private removeEntry(reference: string, entry: IStoredResult): void {\n this.assertRoot();\n if (this.opened?.reference === reference) this.opened = undefined;\n try {\n unlinkSync(join(this.directory, entry.fileName));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new ToolResultSpillError('cleanup-failed');\n }\n }\n this.entries.delete(reference);\n }\n\n async cleanupExpired(): Promise<void> {\n if (this.closed) throw new ToolResultSpillError('closed');\n for (const [reference, entry] of this.entries) {\n if (this.now() >= entry.expiresAt) this.removeEntry(reference, entry);\n }\n this.scheduleExpiry();\n }\n\n async shutdown(): Promise<void> {\n if (this.closed) return;\n if (this.expiryTimer !== undefined) clearTimeout(this.expiryTimer);\n this.expiryTimer = undefined;\n this.opened = undefined;\n for (const [reference, entry] of this.entries) this.removeEntry(reference, entry);\n this.assertRoot();\n try {\n // The owner record goes last, so a directory a failed shutdown leaves can still be swept.\n const left = readdirSync(this.directory).filter((name) => name !== PROCESS_OWNER_RECORD_NAME);\n if (left.length !== 0) throw new Error('unexpected files');\n rmSync(join(this.directory, PROCESS_OWNER_RECORD_NAME), { force: true });\n rmdirSync(this.directory);\n this.closed = true;\n this.cancelExitRemoval?.();\n } catch {\n throw new ToolResultSpillError('cleanup-failed');\n }\n }\n}\n","/**\n * The prompt-history file at the path the host names (its user-level `history.jsonl`):\n * one JSON object per line, append-only, owner-only. A derived projection of what the session record\n * already holds, kept so the terminal UI can search prompts across sessions and projects without\n * decoding a record.\n *\n * Writes follow the `NodeSessionLogSink` regime: the directory is made owner-only when the\n * file is constructed, and every append tightens the file before writing with the owner-only mode.\n * Reads walk the file BACKWARDS in fixed blocks so the newest prompts are yielded first and the UI\n * can render them before the rest is read; the file is opened no-follow like every other session\n * file this package reads. A file built with a sealer appends each entry as one sealed line that\n * opens on its own (`stored-append-lines.ts`), so no line waits for another and the first block\n * still renders before the rest is read.\n */\nimport { appendFileSync, closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport {\n OWNER_ONLY_FILE_MODE,\n ensureOwnerOnlyDirectory,\n tightenExistingFile,\n} from '@robota-sdk/agent-core/node';\n\nimport { StoredLines, splitStoredLines } from './stored-append-lines.js';\nimport { sessionSealingOf } from './stored-session-record.js';\n\nimport type { ISessionStoreOptions } from './stored-session-record.js';\nimport type { TUniversalValue } from '@robota-sdk/agent-core';\nimport type {\n IPromptHistoryBlock,\n IPromptHistoryEntry,\n IPromptHistoryReadOptions,\n IPromptHistorySource,\n IPromptHistoryWriter,\n} from '@robota-sdk/agent-interface-session';\n\nconst KIB = 1024;\nconst DEFAULT_BLOCK_KIB = 64;\n/** 64 KiB: a few hundred prompts per block — enough for the first frame, small enough to yield often. */\nexport const DEFAULT_PROMPT_HISTORY_BLOCK_BYTES = DEFAULT_BLOCK_KIB * KIB;\nconst NEWLINE = 0x0a;\n\n/** The owner id the one prompt history of a user state binds its sealed lines to. */\nexport const PROMPT_HISTORY_OWNER_ID = 'user';\n\nexport interface INodePromptHistoryFileOptions extends ISessionStoreOptions {\n /** An ancestor of the file the host also owns, tightened along with the directory. */\n readonly ownedRoot?: string;\n /** Test seam: the read block size in bytes. */\n readonly blockBytes?: number;\n}\n\nfunction isEntry(value: TUniversalValue): value is IPromptHistoryEntry & TUniversalValue {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const record = value as Record<string, TUniversalValue>;\n return (\n typeof record.at === 'string' &&\n typeof record.sessionId === 'string' &&\n typeof record.project === 'string' &&\n typeof record.text === 'string'\n );\n}\n\n/** One line → an entry, or `undefined` when the line is not a well-formed entry. */\nexport function parsePromptHistoryLine(line: string): IPromptHistoryEntry | undefined {\n if (line.trim().length === 0) return undefined;\n let parsed: TUniversalValue;\n try {\n parsed = JSON.parse(line) as TUniversalValue;\n } catch {\n // allow-fallback: a line JSON cannot parse is exactly what `skippedLines` counts for the caller.\n return undefined;\n }\n if (!isEntry(parsed)) return undefined;\n return { at: parsed.at, sessionId: parsed.sessionId, project: parsed.project, text: parsed.text };\n}\n\n/**\n * One block's physical lines, newest first, as entries. Each line is read on its own, so nothing in\n * a block waits for another block. Damage and lines a newer build sealed are `skippedLines`; sealed\n * lines this process cannot open now are `unreadable`, reported only when there are some.\n */\nfunction blockOf(lines: readonly string[], stored: StoredLines): IPromptHistoryBlock {\n const entries: IPromptHistoryEntry[] = [];\n let skippedLines = 0;\n let locked = 0;\n let noKeys = 0;\n for (const line of lines) {\n if (line.length === 0) continue;\n const read = stored.decode(line, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') {\n if (read.reason === 'no-keys') noKeys += 1;\n else locked += 1;\n continue;\n }\n if (read.kind !== 'lines') {\n skippedLines += 1;\n continue;\n }\n // A record holds its lines oldest first; the block is newest first.\n for (let index = read.lines.length - 1; index >= 0; index -= 1) {\n const entry = parsePromptHistoryLine(read.lines[index] as string);\n if (entry === undefined) skippedLines += 1;\n else entries.push(entry);\n }\n }\n return locked + noKeys === 0\n ? { entries, skippedLines }\n : { entries, skippedLines, unreadable: { locked, noKeys } };\n}\n\nfunction isMissingFile(error: Error): boolean {\n return (error as NodeJS.ErrnoException).code === 'ENOENT';\n}\n\nexport class NodePromptHistoryFile implements IPromptHistoryWriter, IPromptHistorySource {\n private readonly blockBytes: number;\n private readonly ownedRoot: string | undefined;\n private readonly lines: StoredLines;\n\n /**\n * @param options `sealing` (required): `{ sealer }` appends each entry as one sealed line and\n * opens sealed lines it reads (plain ones still read); `'plain'` appends plain lines and counts\n * a sealed line as one no key in this process opens.\n */\n constructor(\n private readonly path: string,\n options: INodePromptHistoryFileOptions,\n ) {\n this.lines = new StoredLines(sessionSealingOf(options), 'prompt-history-line');\n this.blockBytes = options.blockBytes ?? DEFAULT_PROMPT_HISTORY_BLOCK_BYTES;\n this.ownedRoot = options.ownedRoot;\n }\n\n append(entry: IPromptHistoryEntry): void {\n // Sealed before the file is touched, so a sealing failure writes nothing at all.\n const stored = this.lines.encode(`${JSON.stringify(entry)}\\n`, PROMPT_HISTORY_OWNER_ID);\n const directory = dirname(this.path);\n ensureOwnerOnlyDirectory(\n directory,\n this.ownedRoot === undefined ? {} : { withinRoot: this.ownedRoot },\n );\n // `mode` applies only when the file is created; tightening first repairs an older file.\n tightenExistingFile(this.path);\n appendFileSync(this.path, stored, { mode: OWNER_ONLY_FILE_MODE });\n }\n\n /**\n * Newest-first blocks. Only a missing file is the empty state (a fresh install, or history off);\n * any other open or read failure is thrown so the surface renders it instead of an empty list.\n */\n async *read(options: IPromptHistoryReadOptions): AsyncIterable<IPromptHistoryBlock> {\n let descriptor: number;\n try {\n descriptor = openSync(this.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n } catch (error) {\n if (error instanceof Error && isMissingFile(error)) return;\n throw error;\n }\n try {\n let position = fstatSync(descriptor).size;\n // The bytes after the last newline seen so far: a line the previous block cut in half.\n let carry = Buffer.alloc(0);\n // The first block yielded holds the end of the file, and so any append still in progress.\n let holdsEnd = true;\n while (position > 0 && !options.signal.aborted) {\n const length = Math.min(this.blockBytes, position);\n position -= length;\n const chunk = Buffer.alloc(length);\n const bytesRead = readSync(descriptor, chunk, 0, length, position);\n const buffer = Buffer.concat([chunk.subarray(0, bytesRead), carry]);\n const firstNewline = position === 0 ? -1 : buffer.indexOf(NEWLINE);\n // Everything before the first newline belongs to a line that continues in the block before.\n carry = firstNewline === -1 && position > 0 ? buffer : buffer.subarray(0, firstNewline + 1);\n const complete =\n position === 0\n ? buffer\n : firstNewline === -1\n ? Buffer.alloc(0)\n : buffer.subarray(firstNewline + 1);\n if (position > 0 && firstNewline === -1) continue;\n const text = complete.toString('utf8');\n const lines = (holdsEnd ? splitStoredLines(text) : text.split('\\n')).reverse();\n holdsEnd = false;\n yield blockOf(lines, this.lines);\n }\n } finally {\n closeSync(descriptor);\n }\n }\n}\n","/**\n * Compaction of the prompt-history file (secure conversation): every entry, plain or sealed,\n * rewritten one per line under the one sealer the caller hands over, so the file holds no plaintext\n * and — given a sealer of its own — its sealed lines share one writer epoch, read back with one key\n * opening instead of one per epoch the file collected.\n *\n * The file has no lock: every terminal appends to it by path whenever a prompt is sent. So it is\n * rewritten beside itself, renamed over, and what terminals appended meanwhile is caught up under the\n * same sealer (`append-file-rewrite.ts`, which also refuses a history that is a link, and abandons a\n * compaction another one overtook or whose history was deleted meanwhile). A line that does not open\n * here, or that a newer build sealed, is carried over byte for byte. A crash inside the catch-up\n * window, or a catch-up that fails, can lose what was appended in it: milliseconds, and the session\n * records hold those prompts too, since this file is only a derived copy of them.\n */\nimport { closeSync } from 'node:fs';\n\nimport { ArtifactLockedError, isSealedArtifactLine } from '@robota-sdk/agent-core';\n\nimport { openAppendFile, readWhole, rewriteAppendFile } from './append-file-rewrite.js';\nimport { PROMPT_HISTORY_OWNER_ID, parsePromptHistoryLine } from './prompt-history-file.js';\nimport { StoredLines, splitStoredLines } from './stored-append-lines.js';\n\nimport type {\n IAppendFileCatchUpFailure,\n IAppendFileRewriter,\n TAppendFileRefusal,\n} from './append-file-rewrite.js';\nimport type { IStoredLineSink } from './stored-append-lines.js';\nimport type { IArtifactSealer } from '@robota-sdk/agent-core';\n\nconst NEWLINE = '\\n';\n\n/** Why a history was left as it is. */\nexport type TPromptHistoryRefusal = TAppendFileRefusal;\n\nexport interface IInspectPromptHistoryOptions {\n /** Opens sealed lines; with the keys locked, inspecting a sealed line throws `ArtifactLockedError`. */\n readonly sealer: IArtifactSealer;\n}\n\nexport interface ICompactPromptHistoryOptions {\n /**\n * Seals every entry and opens the sealed ones. Give the compaction a sealer of its own so all of\n * its lines share one writer epoch. With the keys locked it stops with `ArtifactLockedError` and\n * replaces nothing.\n */\n readonly sealer: IArtifactSealer;\n /** An ancestor of the file the host also owns, tightened along with the directory. */\n readonly ownedRoot?: string;\n /**\n * Runs once the new file is written and flushed, just before it replaces the history. Throwing\n * abandons the compaction with nothing replaced and no temporary file left.\n */\n readonly beforeCommit?: () => void;\n /** Waits between catch-up passes over the replaced file; defaults to a short timer. */\n readonly pause?: () => Promise<void>;\n}\n\nexport type TPromptHistoryInspection =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TPromptHistoryRefusal }\n | {\n readonly status: 'present';\n /** Physical lines that are plaintext. */\n readonly plainLines: number;\n /** Physical lines sealed in this build's framing that open. */\n readonly sealedLines: number;\n /** Physical lines that do not open here or that a newer build sealed. */\n readonly unreadableLines: number;\n };\n\nexport type TPromptHistoryCompaction =\n | { readonly status: 'missing' }\n | { readonly status: 'refused'; readonly reason: TPromptHistoryRefusal }\n /** Another compaction replaced the history before this one's rename; nothing was replaced. */\n | { readonly status: 'replaced' }\n /** The history was deleted before this compaction's rename; nothing was put in its place. */\n | { readonly status: 'removed' }\n | {\n readonly status: 'compacted';\n /** Entries written sealed, one per line. */\n readonly entries: number;\n /** Of those, entries that were plain lines. */\n readonly fromPlain: number;\n /** Of those, entries appended after the snapshot and caught up from the replaced file. */\n readonly caughtUp: number;\n /** Lines that do not open here, or that a newer build sealed, carried over unchanged. */\n readonly carriedOver: number;\n /** The replaced file ended in an unfinished sealed append, which no reader shows; it was dropped. */\n readonly droppedTail: boolean;\n /** Plain lines in the new file afterwards: plaintext an older build appended meanwhile. */\n readonly plainLinesAfter: number;\n /**\n * Set when the catch-up failed after the rename: entries appended to the replaced file from\n * then on are not in the history (their session records hold them).\n */\n readonly catchUpFailed?: IAppendFileCatchUpFailure;\n };\n\ninterface IRewriteCounts {\n entries: number;\n fromPlain: number;\n carriedOver: number;\n}\n\n/**\n * Rewrites physical lines: every entry a line holds becomes one sealed line, a line that does not\n * open or that a newer build sealed is carried over as it is, and empty lines go (readers skip them).\n * `leadingNewline` keeps the append framing of a file other writers append to at the same time.\n */\nclass EntryRewriter implements IAppendFileRewriter {\n readonly counts: IRewriteCounts = { entries: 0, fromPlain: 0, carriedOver: 0 };\n readonly #lines: StoredLines;\n\n constructor(sealer: IArtifactSealer) {\n this.#lines = new StoredLines({ sealer }, 'prompt-history-line');\n }\n\n rewrite(lines: Iterable<string>, out: IStoredLineSink, leadingNewline: boolean): void {\n for (const physical of lines) {\n if (physical.length === 0) continue;\n const read = this.#lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') {\n out.write(`${physical}${NEWLINE}`);\n this.counts.carriedOver += 1;\n continue;\n }\n if (!isSealedArtifactLine(physical)) this.counts.fromPlain += read.lines.length;\n for (const text of read.lines) {\n // A sealed append is framed `\\n<line>\\n`; a file this rewrite writes alone needs no leading one.\n const stored = this.#lines.encode(`${text}${NEWLINE}`, PROMPT_HISTORY_OWNER_ID);\n out.write(leadingNewline ? stored : stored.slice(NEWLINE.length));\n this.counts.entries += 1;\n }\n }\n }\n}\n\n/** What the history holds, line by line, as {@link compactPromptHistoryFile} would find it. */\nexport function inspectPromptHistoryFile(\n path: string,\n options: IInspectPromptHistoryOptions,\n): TPromptHistoryInspection {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return opened;\n const lines = new StoredLines({ sealer: options.sealer }, 'prompt-history-line');\n let plainLines = 0;\n let sealedLines = 0;\n let unreadableLines = 0;\n try {\n for (const physical of splitStoredLines(readWhole(opened.descriptor).toString('utf8'))) {\n if (physical.length === 0) continue;\n const read = lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') unreadableLines += 1;\n else if (isSealedArtifactLine(physical)) sealedLines += 1;\n else plainLines += 1;\n }\n } finally {\n closeSync(opened.descriptor);\n }\n return { status: 'present', plainLines, sealedLines, unreadableLines };\n}\n\n/**\n * The distinct `project` values the history's entries name, plain or sealed, in the order first met;\n * lines that do not open or do not parse are passed over. A history that is missing or refused names\n * none. Writes nothing.\n */\nexport function readPromptHistoryProjects(\n path: string,\n options: IInspectPromptHistoryOptions,\n): readonly string[] {\n const opened = openAppendFile(path);\n if (opened.status !== 'open') return [];\n const lines = new StoredLines({ sealer: options.sealer }, 'prompt-history-line');\n const projects = new Set<string>();\n try {\n for (const physical of splitStoredLines(readWhole(opened.descriptor).toString('utf8'))) {\n if (physical.length === 0) continue;\n const read = lines.decode(physical, PROMPT_HISTORY_OWNER_ID);\n if (read.kind === 'locked') throw new ArtifactLockedError(read.reason);\n if (read.kind !== 'lines') continue;\n for (const text of read.lines) {\n const entry = parsePromptHistoryLine(text);\n if (entry !== undefined && entry.project.length > 0) projects.add(entry.project);\n }\n }\n } finally {\n closeSync(opened.descriptor);\n }\n return [...projects];\n}\n\n/**\n * Compacts the history at `path` as the module documentation describes. Rerunning it gives the same\n * entries under a new writer epoch.\n */\nexport async function compactPromptHistoryFile(\n path: string,\n options: ICompactPromptHistoryOptions,\n): Promise<TPromptHistoryCompaction> {\n const rewriter = new EntryRewriter(options.sealer);\n let entriesInSnapshot = 0;\n const result = await rewriteAppendFile(path, rewriter, {\n ...(options.ownedRoot === undefined ? {} : { ownedRoot: options.ownedRoot }),\n ...(options.pause === undefined ? {} : { pause: options.pause }),\n beforeCommit: () => {\n entriesInSnapshot = rewriter.counts.entries;\n options.beforeCommit?.();\n },\n });\n if (result.status !== 'rewritten') return result;\n return {\n status: 'compacted',\n ...rewriter.counts,\n caughtUp: rewriter.counts.entries - entriesInSnapshot,\n droppedTail: result.droppedTail,\n plainLinesAfter: result.plainLinesAfter,\n ...(result.catchUpFailed === undefined ? {} : { catchUpFailed: result.catchUpFailed }),\n };\n}\n","/**\n * Where a path a shell command names really is, for the gate's read-only command check.\n * Symlinks are followed, as `Read`'s own containment check does: a link committed inside\n * the workspace can point anywhere, so a string check alone cannot say a command stays inside.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, isAbsolute, relative, resolve } from 'node:path';\n\nimport type { TResolveInWorkspace } from '@robota-sdk/agent-core';\n\n/** The real path of `path`, or of its nearest existing ancestor with the rest appended. */\nfunction realOrNearest(path: string): string {\n let existing = path;\n const rest: string[] = [];\n for (;;) {\n try {\n const real = realpathSync(existing);\n return rest.length === 0 ? real : resolve(real, ...rest.reverse());\n } catch {\n const parent = dirname(existing);\n if (parent === existing) return path;\n rest.push(existing.slice(parent.length).replace(/^[\\\\/]/, ''));\n existing = parent;\n }\n }\n}\n\nfunction isInside(root: string, path: string): boolean {\n const fromRoot = relative(root, path);\n return fromRoot === '' || (!fromRoot.startsWith('..') && !isAbsolute(fromRoot));\n}\n\nexport function createNodeWorkspacePathResolver(cwd: string): TResolveInWorkspace {\n return (base, path) => {\n const root = realOrNearest(cwd);\n const real = realOrNearest(resolve(base ?? root, path));\n return isInside(root, real) ? real : undefined;\n };\n}\n","/**\n * The Node host's answer to `IFileSessionLoggerOptions.registerExitFlush`: run `flush` when the\n * process is about to end. `exit` listeners may only do synchronous work, which is what a logger's\n * flush does. Pass this very function to every logger — the logger registers once per distinct\n * function, so one process gets one listener however many loggers it creates.\n */\nexport function registerProcessExitFlush(flush: () => void): void {\n if (typeof process === 'undefined' || typeof process.on !== 'function') return;\n process.on('exit', flush);\n}\n"],"mappings":"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"}