@theokit/sdk 5.9.2 → 5.10.0
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/CHANGELOG.md +45 -0
- package/dist/{agent-enXXu447.d.ts → agent-CmKwSyyD.d.ts} +27 -0
- package/dist/{agent-WQ4LLTSY.cjs → agent-TLRURB7W.cjs} +9 -8
- package/dist/{agent-WQ4LLTSY.cjs.map → agent-TLRURB7W.cjs.map} +1 -1
- package/dist/{agent-Dh2YHSLj.d.cts → agent-XMkGzff_.d.cts} +27 -0
- package/dist/{agent-GZVZXKME.js → agent-XTLAVVYC.js} +8 -7
- package/dist/{agent-GZVZXKME.js.map → agent-XTLAVVYC.js.map} +1 -1
- package/dist/{chunk-M2CCHC66.js → chunk-523O5UYB.js} +3 -3
- package/dist/{chunk-M2CCHC66.js.map → chunk-523O5UYB.js.map} +1 -1
- package/dist/chunk-7KIN3L2S.js +103 -0
- package/dist/chunk-7KIN3L2S.js.map +1 -0
- package/dist/chunk-ASK2OQM5.cjs +84 -0
- package/dist/chunk-ASK2OQM5.cjs.map +1 -0
- package/dist/{chunk-JU76WCW7.cjs → chunk-CVEQ5366.cjs} +37 -24
- package/dist/chunk-CVEQ5366.cjs.map +1 -0
- package/dist/{chunk-ZDF2IJKC.js → chunk-DPY2M74T.js} +3 -3
- package/dist/{chunk-ZDF2IJKC.js.map → chunk-DPY2M74T.js.map} +1 -1
- package/dist/{chunk-X7Z2FEI7.cjs → chunk-FYEHYQ7T.cjs} +8 -8
- package/dist/{chunk-X7Z2FEI7.cjs.map → chunk-FYEHYQ7T.cjs.map} +1 -1
- package/dist/{chunk-OZLQ6ZFM.js → chunk-IZDDSLL2.js} +4 -3
- package/dist/chunk-IZDDSLL2.js.map +1 -0
- package/dist/{chunk-DLUCTZ56.cjs → chunk-L3C6CCBG.cjs} +15 -15
- package/dist/{chunk-DLUCTZ56.cjs.map → chunk-L3C6CCBG.cjs.map} +1 -1
- package/dist/{chunk-RX3CFWQ2.cjs → chunk-NQVWSSX2.cjs} +6 -5
- package/dist/chunk-NQVWSSX2.cjs.map +1 -0
- package/dist/{chunk-KXKT3HSD.cjs → chunk-PCLRIQPX.cjs} +4 -4
- package/dist/{chunk-KXKT3HSD.cjs.map → chunk-PCLRIQPX.cjs.map} +1 -1
- package/dist/chunk-R5YPDAKD.cjs +108 -0
- package/dist/chunk-R5YPDAKD.cjs.map +1 -0
- package/dist/chunk-RGQICWFU.js +82 -0
- package/dist/chunk-RGQICWFU.js.map +1 -0
- package/dist/{chunk-PEIV7LOU.js → chunk-RTE2DD4S.js} +20 -7
- package/dist/chunk-RTE2DD4S.js.map +1 -0
- package/dist/{chunk-N7KYUNFO.js → chunk-W2ZVTEOD.js} +3 -3
- package/dist/{chunk-N7KYUNFO.js.map → chunk-W2ZVTEOD.js.map} +1 -1
- package/dist/{chunk-47Z6LP3I.js → chunk-X3LOJJMO.js} +88 -28
- package/dist/chunk-X3LOJJMO.js.map +1 -0
- package/dist/{chunk-VZ2JZ2EH.cjs → chunk-ZD66GFH5.cjs} +89 -27
- package/dist/chunk-ZD66GFH5.cjs.map +1 -0
- package/dist/{cron-DTuaPMVI.d.ts → cron-BPccQCO8.d.ts} +1 -1
- package/dist/{cron-QTgcwcZt.d.cts → cron-Blj6wMYg.d.cts} +1 -1
- package/dist/cron.cjs +8 -7
- package/dist/cron.d.cts +2 -2
- package/dist/cron.d.ts +2 -2
- package/dist/cron.js +7 -6
- package/dist/eval.cjs +11 -10
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +7 -6
- package/dist/eval.js.map +1 -1
- package/dist/fs-session-store-3VI4CPTX.js +11 -0
- package/dist/{fs-session-store-LCZBM3LT.js.map → fs-session-store-3VI4CPTX.js.map} +1 -1
- package/dist/fs-session-store-IQ6PEHYF.cjs +20 -0
- package/dist/{fs-session-store-SVEMRE4K.cjs.map → fs-session-store-IQ6PEHYF.cjs.map} +1 -1
- package/dist/{index-manager-6B2UF5S3.js → index-manager-JVYIVTZG.js} +5 -4
- package/dist/{index-manager-6B2UF5S3.js.map → index-manager-JVYIVTZG.js.map} +1 -1
- package/dist/{index-manager-M3FHJCHQ.cjs → index-manager-YGXRFJMZ.cjs} +6 -5
- package/dist/{index-manager-M3FHJCHQ.cjs.map → index-manager-YGXRFJMZ.cjs.map} +1 -1
- package/dist/index.cjs +44 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +22 -15
- package/dist/index.js.map +1 -1
- package/dist/internal/memory/dreaming/phases.d.ts +2 -0
- package/dist/internal/memory/dreaming/run.d.ts +5 -0
- package/dist/internal/memory/sdk-memory-peer-loader.d.ts +2 -0
- package/dist/internal/memory/storage/fact-ceiling.d.cts +19 -0
- package/dist/internal/memory/storage/fact-ceiling.d.ts +19 -0
- package/dist/internal/memory/storage/index.cjs +40 -31
- package/dist/internal/memory/storage/index.d.cts +1 -0
- package/dist/internal/memory/storage/index.d.ts +1 -0
- package/dist/internal/memory/storage/index.js +3 -2
- package/dist/internal/memory/storage/markdown-store.d.cts +3 -1
- package/dist/internal/memory/storage/markdown-store.d.ts +3 -1
- package/dist/internal/memory/types.d.ts +6 -0
- package/dist/internal/persistence/index.cjs +7 -6
- package/dist/internal/persistence/index.cjs.map +1 -1
- package/dist/internal/persistence/index.js +2 -1
- package/dist/internal/persistence/index.js.map +1 -1
- package/dist/internal/runtime/registry/agent-registry-store.d.ts +2 -0
- package/dist/persistence.cjs +7 -6
- package/dist/persistence.cjs.map +1 -1
- package/dist/persistence.js +2 -1
- package/dist/persistence.js.map +1 -1
- package/dist/subagents-loader.d.cts +1 -1
- package/dist/subagents-loader.d.ts +1 -1
- package/dist/types/agent.d.ts +27 -0
- package/docs/error-codes.md +27 -25
- package/docs/harness-capability-map.md +6 -1
- package/package.json +1 -1
- package/dist/chunk-47Z6LP3I.js.map +0 -1
- package/dist/chunk-BKMDRBJ7.js +0 -180
- package/dist/chunk-BKMDRBJ7.js.map +0 -1
- package/dist/chunk-JU76WCW7.cjs.map +0 -1
- package/dist/chunk-OF2CLCTH.cjs +0 -186
- package/dist/chunk-OF2CLCTH.cjs.map +0 -1
- package/dist/chunk-OZLQ6ZFM.js.map +0 -1
- package/dist/chunk-PEIV7LOU.js.map +0 -1
- package/dist/chunk-RX3CFWQ2.cjs.map +0 -1
- package/dist/chunk-VZ2JZ2EH.cjs.map +0 -1
- package/dist/fs-session-store-LCZBM3LT.js +0 -10
- package/dist/fs-session-store-SVEMRE4K.cjs +0 -19
package/dist/persistence.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/persistence/list-sessions.ts","../src/internal/persistence/session-artifacts.ts","../src/internal/persistence/transcript-ops.ts"],"names":["join","transcriptRoot","encodeProjectDir","readdir","stat","createReadStream","atomicWriteTempTarget","TheokitAgentError","readFileSync","openSync","writeSync","closeSync","fstatSync","readSync"],"mappings":"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","file":"persistence.cjs","sourcesContent":["/**\n * #598 — enumerate the sessions on disk, saying where each id came from.\n *\n * ## Why this exists\n *\n * Every transcript helper this package published mapped FORWARD — `sessionUuidFor`,\n * `transcriptPath`, `legacyTranscriptPath`, `encodeProjectDir`, `transcriptRoot`. **None enumerated.**\n * So a consumer that needed the list rebuilt it, and two independent ones did, in opposite\n * directions, and both got it wrong on the first attempt:\n *\n * | consumer | direction | what broke |\n * |---|---|---|\n * | `@theokit/agents` | file → id | derived the id from the file STEM |\n * | a downstream agent runtime | id → file | compared session ids against filenames |\n *\n * The second measured, against 5.0.1: the protected set never matched, **so neither the registered\n * sessions nor the live one were protected and everything classified as an orphan** — a garbage\n * collector that would delete the session in use.\n *\n * ## Why documentation was not the fix\n *\n * `b85dab4` documented the rename thoroughly. One of those two consumers had READ it and broke\n * anyway; the other had not and broke identically. Two samples, one informed and one not, same\n * defect. If reading were sufficient the informed sample would have survived — so the cause is the\n * shape of the surface, not the reader.\n *\n * And it cannot be closed by publishing an inverse: the filename is a UUIDv8 over SHA-256, which has\n * none. Whoever holds the file must read the id from INSIDE it, and that the id lives in the first\n * record is this package's knowledge. Both consumers had to discover it by reading bytes.\n *\n * ## Why the id is not just a string\n *\n * An `id: string` that is sometimes read from the transcript and sometimes inferred from the\n * filename is the same defect one layer up: a value that reads as authoritative and occasionally is\n * not. That is precisely what produced the garbage-collector failure above.\n *\n * So every entry carries {@link SessionListing.idSource}, and an entry whose id could not be\n * determined is `undefined` rather than guessed. A caller deciding what to DELETE needs to tell\n * \"this session is not registered\" from \"I could not read this file\" — those look identical in a\n * plain list and mean opposite things.\n *\n * @public\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { encodeProjectDir, transcriptRoot } from \"./session-transcript.js\";\n\n/** How the `id` on a {@link SessionListing} was obtained. */\nexport type SessionIdSource =\n /** Read from the `sessionId` of the transcript's first well-formed record. Authoritative. */\n | \"transcript\"\n /**\n * Not determined: the file was unreadable, empty, or its first records carried no `sessionId`.\n * `id` is `undefined` — deliberately not the filename, which is a hash and not an id.\n */\n | \"unavailable\";\n\n/** One session found on disk. */\nexport interface SessionListing {\n /**\n * The session id, or `undefined` when {@link idSource} is `\"unavailable\"`.\n *\n * Never derived from the filename. The filename is a UUIDv8 over SHA-256 of the id, so treating\n * it as the id is the exact defect this function exists to prevent.\n */\n readonly id: string | undefined;\n /** Where {@link id} came from. Check this before acting on `id`. */\n readonly idSource: SessionIdSource;\n /** Absolute path to the `.jsonl` transcript. */\n readonly transcript: string;\n /** Last modification time of the transcript. */\n readonly modifiedAt: Date;\n}\n\n/** Options for {@link listSessions}. */\nexport interface ListSessionsOptions {\n /**\n * Root under which `projects/<encoded-cwd>/` lives. Defaults to {@link transcriptRoot}, which\n * honours `THEOKIT_HOME`.\n */\n readonly baseDir?: string;\n /**\n * How many bytes of each transcript to read looking for the id. Default 65536.\n *\n * A cap rather than a full read because a transcript grows without bound and the id is in the\n * first record. A session whose id is not in the first 64KB reports `\"unavailable\"` rather than\n * being read to the end — bounded work with a declared outcome beats an unbounded read.\n */\n readonly idScanBytes?: number;\n}\n\nconst DEFAULT_ID_SCAN_BYTES = 64 * 1024;\n\n/**\n * Every session transcript under `<baseDir>/projects/<encoded-cwd>/`, with the id read from inside\n * each file.\n *\n * An absent directory yields `[]` — a cwd with no sessions is the common case, not an error.\n *\n * ```ts\n * for (const s of await listSessions(process.cwd())) {\n * if (s.idSource === \"unavailable\") continue; // do not guess, and do not delete\n * …\n * }\n * ```\n */\nexport async function listSessions(\n cwd: string,\n options: ListSessionsOptions = {},\n): Promise<readonly SessionListing[]> {\n const dir = join(options.baseDir ?? transcriptRoot(), \"projects\", encodeProjectDir(cwd));\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n\n const out: SessionListing[] = [];\n for (const name of names) {\n if (!name.endsWith(\".jsonl\")) continue;\n const transcript = join(dir, name);\n let modifiedAt: Date;\n try {\n modifiedAt = (await stat(transcript)).mtime;\n } catch {\n // Vanished between readdir and stat — a live session being rotated. Skipping is correct:\n // reporting a file that no longer exists would be worse than omitting it.\n continue;\n }\n const id = await readSessionId(transcript, options.idScanBytes ?? DEFAULT_ID_SCAN_BYTES);\n out.push({\n id,\n idSource: id === undefined ? \"unavailable\" : \"transcript\",\n transcript,\n modifiedAt,\n });\n }\n return out;\n}\n\n/**\n * The `sessionId` of the first well-formed record, or `undefined`.\n *\n * Reads at most `maxBytes` and stops at the first record that yields one. Tolerant of malformed\n * lines for the same reason `readTranscript` is: a truncated final line in a live transcript is\n * normal, and failing the whole listing over it would make the function useless exactly when it\n * matters.\n */\nasync function readSessionId(path: string, maxBytes: number): Promise<string | undefined> {\n let buffered = \"\";\n try {\n const stream = createReadStream(path, { encoding: \"utf8\", end: maxBytes - 1 });\n for await (const chunk of stream) {\n buffered += chunk as string;\n const lines = buffered.split(\"\\n\");\n buffered = lines.pop() ?? \"\";\n for (const line of lines) {\n const id = sessionIdOfLine(line);\n if (id !== undefined) {\n stream.destroy();\n return id;\n }\n }\n }\n } catch {\n return undefined;\n }\n return sessionIdOfLine(buffered);\n}\n\nfunction sessionIdOfLine(line: string): string | undefined {\n if (line.trim() === \"\") return undefined;\n try {\n const parsed: unknown = JSON.parse(line);\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const id = (parsed as { sessionId?: unknown }).sessionId;\n return typeof id === \"string\" && id !== \"\" ? id : undefined;\n } catch {\n return undefined;\n }\n}\n","import { atomicWriteTempTarget } from \"./atomic-write.js\";\n\n/**\n * The kinds of file this SDK leaves in a project's transcript directory.\n *\n * - `transcript` — the session itself (`transcriptPath`).\n * - `writer-lock` — the cross-process writer lease (`session-writer.ts`, `<file>.writer.lock`).\n * - `lock-directory` — `withFileLock`'s companion, taken by `mkdir`, so it is a DIRECTORY.\n * - `temp` — what `replaceFileAtomic` leaves when a process dies between the open and the rename.\n */\nexport type SessionArtifact = \"transcript\" | \"writer-lock\" | \"lock-directory\" | \"temp\";\n\n/**\n * U-1 — what is this entry, if it is one of ours?\n *\n * The SDK writes four kinds of file into a project directory and reasons about none of them\n * afterwards: there is no retention, no collector, and there was no way even to ask what an entry\n * IS. A consumer wanting to reclaim disk had to re-derive the suffixes by reading this source — and\n * one did, which means a suffix changing here would have left its classifier silently mislabelling\n * files on a path that deletes them.\n *\n * This is deliberately NOT a garbage collector. Retention is policy — how many days, how many to\n * keep, which session is live, whether to delete at all — and policy belongs to the application,\n * which is the only one that can know. What belongs here is the half only the SDK can answer.\n *\n * `undefined` means \"not written by this SDK\", and it is the answer that matters most: a caller\n * deleting what it does not recognise is how an editor's swap file gets collected. The `temp` case\n * defers to {@link atomicWriteTempTarget} rather than matching `.tmp`, for exactly that reason.\n */\nexport function classifySessionArtifact(\n name: string,\n isDirectory: boolean,\n): SessionArtifact | undefined {\n // `withFileLock` takes its lock by `mkdir`, so the same name as a plain file is not ours.\n if (name.endsWith(\".jsonl.lock\")) return isDirectory ? \"lock-directory\" : undefined;\n if (isDirectory) return undefined;\n if (name.endsWith(\".jsonl\")) return \"transcript\";\n if (name.endsWith(\".writer.lock\")) return \"writer-lock\";\n if (atomicWriteTempTarget(name) !== undefined) return \"temp\";\n return undefined;\n}\n","/**\n * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.\n *\n * ## What this replaces\n *\n * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a\n * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and\n * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.\n *\n * ## The rule that travels WITH the operation\n *\n * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live\n * pointer, the most recent transcript, and any active registry entry. That rule lived in the\n * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy\n * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:\n * critical knowledge outside the module that needs it, applied by convention.\n *\n * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of\n * them. The caller supplies the list because only the caller knows which session is live; the\n * enforcement lives here because that is where the write happens.\n *\n * @internal\n */\n\nimport { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).\n *\n * Typed rather than a bare `Error` because the caller must distinguish \"this session is protected\"\n * from \"the disk is full\": the first is a correct refusal, the second is an incident.\n *\n * RENAMED from `LiveSessionError` (2026-09-01). `session-guard.ts` exports a DIFFERENT class of that\n * name from the root barrel, with an incompatible shape — `(sessionId, reason)` and a `reason`\n * field, against this one's `(path)` and `code: \"live_session_protected\"` — and `.` and\n * `./persistence` are both declared subpaths, so one consumer can hold both. `instanceof` never\n * crossed the pair, so a `catch` checking the root import silently missed this one and ran its\n * fallback for a condition it believed it handled; `err.name` matched BOTH, so a name check looked\n * right and then read `err.reason`, which only the other one has. The names now say what each\n * refusal is about: destroying a SESSION, versus overwriting a TRANSCRIPT file.\n */\nexport class LiveTranscriptError extends TheokitAgentError {\n override readonly name = \"LiveTranscriptError\";\n\n constructor(readonly path: string) {\n super(\n `refusing to write over a live session transcript: ${path}. ` +\n \"Fork to a new id instead — the live pointer, the most recent transcript and any active \" +\n \"registry entry are never overwritten (audit-trail rotation, M60).\",\n { code: \"live_session_protected\", isRetryable: false },\n );\n }\n}\n\n/** Options for {@link forkTranscript}. */\nexport interface ForkTranscriptOptions {\n /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */\n readonly beforeRecordIndex?: number;\n /**\n * Paths that must never be written over — the live pointer, the most recent transcript, any active\n * registry entry. The caller supplies them because only the caller knows which session is live.\n */\n readonly liveSessionPaths?: readonly string[];\n /**\n * M107 — permission bits for the created destination. Default: `0o600`.\n *\n * A transcript carries the conversation. Before M107 no mode was passed at all, so the file was\n * born `0o666 & ~umask` — measured `0o664` (group-WRITABLE) on a `umask 002` machine, `0o644` on\n * `umask 022`, `0o466` on `umask 0200`. This is a DEFAULT and not a required knob on purpose: a\n * knob would reach zero consumers by omission, which is the failure mode that matters.\n *\n * As with any `open` mode, the `umask` may still CLEAR bits — under `umask 0200` the result is\n * `0o400`. That is accepted: the invariant bought here is \"neither group nor others\", and `0o400`\n * satisfies it more strictly. The SDK deliberately does not `fchmod` the default back, because\n * that would hand back a bit the operator asked to remove.\n */\n readonly mode?: number;\n}\n\n/**\n * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.\n *\n * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot\n * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also\n * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an\n * error is the worst failure mode for an operation that touches user sessions.\n */\nexport function forkTranscript(\n src: string,\n dst: string,\n options: ForkTranscriptOptions = {},\n): void {\n for (const live of options.liveSessionPaths ?? []) {\n if (live === dst) throw new LiveTranscriptError(dst);\n }\n\n const lines = readFileSync(src, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim().length > 0);\n const kept =\n options.beforeRecordIndex === undefined ? lines : lines.slice(0, options.beforeRecordIndex);\n const body = kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\";\n\n // `wx` — fails with EEXIST instead of truncating. The exclusivity IS the concurrency guarantee,\n // and M107 only added the third argument: the mode. See `ForkTranscriptOptions.mode`.\n const fd = openSync(dst, \"wx\", options.mode ?? 0o600);\n try {\n writeSync(fd, body);\n } finally {\n closeSync(fd);\n }\n}\n\n/** Options for {@link readJsonlTail}. */\nexport interface ReadJsonlTailOptions {\n /** Maximum records to return, counted from the END. */\n readonly maxRecords?: number;\n /**\n * Start the window AFTER the last record whose `subtype` (or `type`) equals this.\n *\n * Matched STRUCTURALLY since T2.5. It used to be `line.includes(marker)`, so any message\n * mentioning the marker in its text truncated the read — silently, with a successful return.\n */\n readonly sinceMarker?: string;\n /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */\n readonly _stats?: boolean;\n}\n\nconst TAIL_CHUNK = 64 * 1024;\n\n/**\n * Reads chunks backwards until enough complete lines have accumulated.\n *\n * Extracted from `readJsonlTail` because the read loop and the record selection are two\n * responsibilities — and together they exceeded the complexity ceiling. The buffer's first line may\n * be cut in half when the read stopped before the start of the file; that is why it is discarded.\n */\nfunction readRawTail(path: string, want: number): { lines: string[]; bytesRead: number } {\n // Opened FIRST, then sized through the descriptor. `statSync(path)` followed by\n // `openSync(path)` resolves the name twice, and `size` is what drives every read offset below —\n // so a path that changed between the two calls would have the loop seeking by one file's length\n // inside another (CodeQL js/file-system-race #19). `fstat` on the open fd describes the file\n // being read, by construction.\n const fd = openSync(path, \"r\");\n const size = fstatSync(fd).size;\n let bytesRead = 0;\n let tail = \"\";\n let pos = size;\n try {\n while (pos > 0) {\n const len = Math.min(TAIL_CHUNK, pos);\n pos -= len;\n const buf = Buffer.alloc(len);\n readSync(fd, buf, 0, len, pos);\n bytesRead += len;\n tail = buf.toString(\"utf8\") + tail;\n if (nonEmptyLines(tail).length > want) break;\n }\n } finally {\n closeSync(fd);\n }\n const lines = nonEmptyLines(tail);\n return { lines: pos > 0 ? lines.slice(1) : lines, bytesRead };\n}\n\n/** Non-empty lines, in file order. */\nfunction nonEmptyLines(text: string): string[] {\n return text.split(\"\\n\").filter((l) => l.trim().length > 0);\n}\n\n/**\n * Whether a raw JSONL line IS the marker record, rather than a line that talks about it.\n *\n * Matches on the record's own discriminants (`subtype`, then `type`) — the fields that identify\n * what a record *is*. Free text is never consulted, which is the whole point: content is the user's\n * and must not steer the reader.\n *\n * A line that does not parse is not a marker. Deciding a window boundary from bytes that are not a\n * record would be guessing, and this function exists because guessing is what it replaced.\n */\nfunction isMarkerRecord(line: string, marker: string): boolean {\n let record: { type?: unknown; subtype?: unknown };\n try {\n record = JSON.parse(line) as { type?: unknown; subtype?: unknown };\n } catch {\n return false;\n }\n return record.subtype === marker || record.type === marker;\n}\n\n/**\n * Read the LAST records of a JSONL file without loading the whole thing.\n *\n * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session\n * transcript grows without bound; loading megabytes to show the last three turns is the cost this\n * exists to avoid — and a `slice` over a full read would be that same cost with a better name.\n */\nexport function readJsonlTail<T = Record<string, unknown>>(\n path: string,\n options: ReadJsonlTailOptions = {},\n): T[] {\n const want = options.maxRecords ?? Number.POSITIVE_INFINITY;\n const { lines, bytesRead } = readRawTail(path, want);\n\n let sel = lines;\n if (options.sinceMarker !== undefined) {\n const marker = options.sinceMarker;\n // STRUCTURAL, not `line.includes(marker)`.\n //\n // A raw substring match is true for any line that merely MENTIONS the marker — a user asking\n // \"how does compact_boundary work?\" silently truncated the window to start at their question.\n // The read then succeeded, returned fewer records than exist, and said nothing. That is the\n // measured reason the only would-be consumer kept its own reader instead of this one.\n const idx = sel.findLastIndex((l) => isMarkerRecord(l, marker));\n if (idx >= 0) sel = sel.slice(idx + 1);\n }\n if (Number.isFinite(want)) sel = sel.slice(-want);\n\n const out = sel.map((l) => JSON.parse(l) as T);\n return options._stats === true ? (Object.assign(out, { bytesRead }) as T[]) : out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/list-sessions.ts","../src/internal/persistence/session-artifacts.ts","../src/internal/persistence/transcript-ops.ts"],"names":["join","transcriptRoot","encodeProjectDir","readdir","stat","createReadStream","atomicWriteTempTarget","TheokitAgentError","readFileSync","openSync","writeSync","closeSync","fstatSync","readSync"],"mappings":"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","file":"persistence.cjs","sourcesContent":["/**\n * #598 — enumerate the sessions on disk, saying where each id came from.\n *\n * ## Why this exists\n *\n * Every transcript helper this package published mapped FORWARD — `sessionUuidFor`,\n * `transcriptPath`, `legacyTranscriptPath`, `encodeProjectDir`, `transcriptRoot`. **None enumerated.**\n * So a consumer that needed the list rebuilt it, and two independent ones did, in opposite\n * directions, and both got it wrong on the first attempt:\n *\n * | consumer | direction | what broke |\n * |---|---|---|\n * | `@theokit/agents` | file → id | derived the id from the file STEM |\n * | a downstream agent runtime | id → file | compared session ids against filenames |\n *\n * The second measured, against 5.0.1: the protected set never matched, **so neither the registered\n * sessions nor the live one were protected and everything classified as an orphan** — a garbage\n * collector that would delete the session in use.\n *\n * ## Why documentation was not the fix\n *\n * `b85dab4` documented the rename thoroughly. One of those two consumers had READ it and broke\n * anyway; the other had not and broke identically. Two samples, one informed and one not, same\n * defect. If reading were sufficient the informed sample would have survived — so the cause is the\n * shape of the surface, not the reader.\n *\n * And it cannot be closed by publishing an inverse: the filename is a UUIDv8 over SHA-256, which has\n * none. Whoever holds the file must read the id from INSIDE it, and that the id lives in the first\n * record is this package's knowledge. Both consumers had to discover it by reading bytes.\n *\n * ## Why the id is not just a string\n *\n * An `id: string` that is sometimes read from the transcript and sometimes inferred from the\n * filename is the same defect one layer up: a value that reads as authoritative and occasionally is\n * not. That is precisely what produced the garbage-collector failure above.\n *\n * So every entry carries {@link SessionListing.idSource}, and an entry whose id could not be\n * determined is `undefined` rather than guessed. A caller deciding what to DELETE needs to tell\n * \"this session is not registered\" from \"I could not read this file\" — those look identical in a\n * plain list and mean opposite things.\n *\n * @public\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { encodeProjectDir, transcriptRoot } from \"./session-transcript.js\";\n\n/** How the `id` on a {@link SessionListing} was obtained. */\nexport type SessionIdSource =\n /** Read from the `sessionId` of the transcript's first well-formed record. Authoritative. */\n | \"transcript\"\n /**\n * Not determined: the file was unreadable, empty, or its first records carried no `sessionId`.\n * `id` is `undefined` — deliberately not the filename, which is a hash and not an id.\n */\n | \"unavailable\";\n\n/** One session found on disk. */\nexport interface SessionListing {\n /**\n * The session id, or `undefined` when {@link idSource} is `\"unavailable\"`.\n *\n * Never derived from the filename. The filename is a UUIDv8 over SHA-256 of the id, so treating\n * it as the id is the exact defect this function exists to prevent.\n */\n readonly id: string | undefined;\n /** Where {@link id} came from. Check this before acting on `id`. */\n readonly idSource: SessionIdSource;\n /** Absolute path to the `.jsonl` transcript. */\n readonly transcript: string;\n /** Last modification time of the transcript. */\n readonly modifiedAt: Date;\n}\n\n/** Options for {@link listSessions}. */\nexport interface ListSessionsOptions {\n /**\n * Root under which `projects/<encoded-cwd>/` lives. Defaults to {@link transcriptRoot}, which\n * honours `THEOKIT_HOME`.\n */\n readonly baseDir?: string;\n /**\n * How many bytes of each transcript to read looking for the id. Default 65536.\n *\n * A cap rather than a full read because a transcript grows without bound and the id is in the\n * first record. A session whose id is not in the first 64KB reports `\"unavailable\"` rather than\n * being read to the end — bounded work with a declared outcome beats an unbounded read.\n */\n readonly idScanBytes?: number;\n}\n\nconst DEFAULT_ID_SCAN_BYTES = 64 * 1024;\n\n/**\n * Every session transcript under `<baseDir>/projects/<encoded-cwd>/`, with the id read from inside\n * each file.\n *\n * An absent directory yields `[]` — a cwd with no sessions is the common case, not an error.\n *\n * ```ts\n * for (const s of await listSessions(process.cwd())) {\n * if (s.idSource === \"unavailable\") continue; // do not guess, and do not delete\n * …\n * }\n * ```\n */\nexport async function listSessions(\n cwd: string,\n options: ListSessionsOptions = {},\n): Promise<readonly SessionListing[]> {\n const dir = join(options.baseDir ?? transcriptRoot(), \"projects\", encodeProjectDir(cwd));\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n\n const out: SessionListing[] = [];\n for (const name of names) {\n if (!name.endsWith(\".jsonl\")) continue;\n const transcript = join(dir, name);\n let modifiedAt: Date;\n try {\n modifiedAt = (await stat(transcript)).mtime;\n } catch {\n // Vanished between readdir and stat — a live session being rotated. Skipping is correct:\n // reporting a file that no longer exists would be worse than omitting it.\n continue;\n }\n const id = await readSessionId(transcript, options.idScanBytes ?? DEFAULT_ID_SCAN_BYTES);\n out.push({\n id,\n idSource: id === undefined ? \"unavailable\" : \"transcript\",\n transcript,\n modifiedAt,\n });\n }\n return out;\n}\n\n/**\n * The `sessionId` of the first well-formed record, or `undefined`.\n *\n * Reads at most `maxBytes` and stops at the first record that yields one. Tolerant of malformed\n * lines for the same reason `readTranscript` is: a truncated final line in a live transcript is\n * normal, and failing the whole listing over it would make the function useless exactly when it\n * matters.\n */\nasync function readSessionId(path: string, maxBytes: number): Promise<string | undefined> {\n let buffered = \"\";\n try {\n const stream = createReadStream(path, { encoding: \"utf8\", end: maxBytes - 1 });\n for await (const chunk of stream) {\n buffered += chunk as string;\n const lines = buffered.split(\"\\n\");\n buffered = lines.pop() ?? \"\";\n for (const line of lines) {\n const id = sessionIdOfLine(line);\n if (id !== undefined) {\n stream.destroy();\n return id;\n }\n }\n }\n } catch {\n return undefined;\n }\n return sessionIdOfLine(buffered);\n}\n\nfunction sessionIdOfLine(line: string): string | undefined {\n if (line.trim() === \"\") return undefined;\n try {\n const parsed: unknown = JSON.parse(line);\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const id = (parsed as { sessionId?: unknown }).sessionId;\n return typeof id === \"string\" && id !== \"\" ? id : undefined;\n } catch {\n return undefined;\n }\n}\n","import { atomicWriteTempTarget } from \"./atomic-write.js\";\n\n/**\n * The kinds of file this SDK leaves in a project's transcript directory.\n *\n * - `transcript` — the session itself (`transcriptPath`).\n * - `writer-lock` — the cross-process writer lease (`session-writer.ts`, `<file>.writer.lock`).\n * - `lock-directory` — `withFileLock`'s companion, taken by `mkdir`, so it is a DIRECTORY.\n * - `temp` — what `replaceFileAtomic` leaves when a process dies between the open and the rename.\n */\nexport type SessionArtifact = \"transcript\" | \"writer-lock\" | \"lock-directory\" | \"temp\";\n\n/**\n * U-1 — what is this entry, if it is one of ours?\n *\n * The SDK writes four kinds of file into a project directory and reasons about none of them\n * afterwards: there is no retention, no collector, and there was no way even to ask what an entry\n * IS. A consumer wanting to reclaim disk had to re-derive the suffixes by reading this source — and\n * one did, which means a suffix changing here would have left its classifier silently mislabelling\n * files on a path that deletes them.\n *\n * This is deliberately NOT a garbage collector. Retention is policy — how many days, how many to\n * keep, which session is live, whether to delete at all — and policy belongs to the application,\n * which is the only one that can know. What belongs here is the half only the SDK can answer.\n *\n * `undefined` means \"not written by this SDK\", and it is the answer that matters most: a caller\n * deleting what it does not recognise is how an editor's swap file gets collected. The `temp` case\n * defers to {@link atomicWriteTempTarget} rather than matching `.tmp`, for exactly that reason.\n */\nexport function classifySessionArtifact(\n name: string,\n isDirectory: boolean,\n): SessionArtifact | undefined {\n // `withFileLock` takes its lock by `mkdir`, so the same name as a plain file is not ours.\n if (name.endsWith(\".jsonl.lock\")) return isDirectory ? \"lock-directory\" : undefined;\n if (isDirectory) return undefined;\n if (name.endsWith(\".jsonl\")) return \"transcript\";\n if (name.endsWith(\".writer.lock\")) return \"writer-lock\";\n if (atomicWriteTempTarget(name) !== undefined) return \"temp\";\n return undefined;\n}\n","/**\n * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.\n *\n * ## What this replaces\n *\n * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a\n * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and\n * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.\n *\n * ## The rule that travels WITH the operation\n *\n * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live\n * pointer, the most recent transcript, and any active registry entry. That rule lived in the\n * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy\n * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:\n * critical knowledge outside the module that needs it, applied by convention.\n *\n * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of\n * them. The caller supplies the list because only the caller knows which session is live; the\n * enforcement lives here because that is where the write happens.\n *\n * @internal\n */\n\nimport { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).\n *\n * Typed rather than a bare `Error` because the caller must distinguish \"this session is protected\"\n * from \"the disk is full\": the first is a correct refusal, the second is an incident.\n *\n * RENAMED from `LiveSessionError` (2026-09-01). `session-guard.ts` exports a DIFFERENT class of that\n * name from the root barrel, with an incompatible shape — `(sessionId, reason)` and a `reason`\n * field, against this one's `(path)` and `code: \"live_session_protected\"` — and `.` and\n * `./persistence` are both declared subpaths, so one consumer can hold both. `instanceof` never\n * crossed the pair, so a `catch` checking the root import silently missed this one and ran its\n * fallback for a condition it believed it handled; `err.name` matched BOTH, so a name check looked\n * right and then read `err.reason`, which only the other one has. The names now say what each\n * refusal is about: destroying a SESSION, versus overwriting a TRANSCRIPT file.\n */\nexport class LiveTranscriptError extends TheokitAgentError {\n override readonly name = \"LiveTranscriptError\";\n\n constructor(readonly path: string) {\n super(\n `refusing to write over a live session transcript: ${path}. ` +\n \"Fork to a new id instead — the live pointer, the most recent transcript and any active \" +\n \"registry entry are never overwritten (audit-trail rotation, M60).\",\n { code: \"live_session_protected\", isRetryable: false },\n );\n }\n}\n\n/** Options for {@link forkTranscript}. */\nexport interface ForkTranscriptOptions {\n /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */\n readonly beforeRecordIndex?: number;\n /**\n * Paths that must never be written over — the live pointer, the most recent transcript, any active\n * registry entry. The caller supplies them because only the caller knows which session is live.\n */\n readonly liveSessionPaths?: readonly string[];\n /**\n * M107 — permission bits for the created destination. Default: `0o600`.\n *\n * A transcript carries the conversation. Before M107 no mode was passed at all, so the file was\n * born `0o666 & ~umask` — measured `0o664` (group-WRITABLE) on a `umask 002` machine, `0o644` on\n * `umask 022`, `0o466` on `umask 0200`. This is a DEFAULT and not a required knob on purpose: a\n * knob would reach zero consumers by omission, which is the failure mode that matters.\n *\n * As with any `open` mode, the `umask` may still CLEAR bits — under `umask 0200` the result is\n * `0o400`. That is accepted: the invariant bought here is \"neither group nor others\", and `0o400`\n * satisfies it more strictly. The SDK deliberately does not `fchmod` the default back, because\n * that would hand back a bit the operator asked to remove.\n */\n readonly mode?: number;\n}\n\n/**\n * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.\n *\n * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot\n * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also\n * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an\n * error is the worst failure mode for an operation that touches user sessions.\n */\nexport function forkTranscript(\n src: string,\n dst: string,\n options: ForkTranscriptOptions = {},\n): void {\n for (const live of options.liveSessionPaths ?? []) {\n if (live === dst) throw new LiveTranscriptError(dst);\n }\n\n const lines = readFileSync(src, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim().length > 0);\n const kept =\n options.beforeRecordIndex === undefined ? lines : lines.slice(0, options.beforeRecordIndex);\n const body = kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\";\n\n // `wx` — fails with EEXIST instead of truncating. The exclusivity IS the concurrency guarantee,\n // and M107 only added the third argument: the mode. See `ForkTranscriptOptions.mode`.\n const fd = openSync(dst, \"wx\", options.mode ?? 0o600);\n try {\n writeSync(fd, body);\n } finally {\n closeSync(fd);\n }\n}\n\n/** Options for {@link readJsonlTail}. */\nexport interface ReadJsonlTailOptions {\n /** Maximum records to return, counted from the END. */\n readonly maxRecords?: number;\n /**\n * Start the window AFTER the last record whose `subtype` (or `type`) equals this.\n *\n * Matched STRUCTURALLY since T2.5. It used to be `line.includes(marker)`, so any message\n * mentioning the marker in its text truncated the read — silently, with a successful return.\n */\n readonly sinceMarker?: string;\n /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */\n readonly _stats?: boolean;\n}\n\nconst TAIL_CHUNK = 64 * 1024;\n\n/**\n * Reads chunks backwards until enough complete lines have accumulated.\n *\n * Extracted from `readJsonlTail` because the read loop and the record selection are two\n * responsibilities — and together they exceeded the complexity ceiling. The buffer's first line may\n * be cut in half when the read stopped before the start of the file; that is why it is discarded.\n */\nfunction readRawTail(path: string, want: number): { lines: string[]; bytesRead: number } {\n // Opened FIRST, then sized through the descriptor. `statSync(path)` followed by\n // `openSync(path)` resolves the name twice, and `size` is what drives every read offset below —\n // so a path that changed between the two calls would have the loop seeking by one file's length\n // inside another (CodeQL js/file-system-race #19). `fstat` on the open fd describes the file\n // being read, by construction.\n const fd = openSync(path, \"r\");\n const size = fstatSync(fd).size;\n let bytesRead = 0;\n let tail = \"\";\n let pos = size;\n try {\n while (pos > 0) {\n const len = Math.min(TAIL_CHUNK, pos);\n pos -= len;\n const buf = Buffer.alloc(len);\n readSync(fd, buf, 0, len, pos);\n bytesRead += len;\n tail = buf.toString(\"utf8\") + tail;\n if (nonEmptyLines(tail).length > want) break;\n }\n } finally {\n closeSync(fd);\n }\n const lines = nonEmptyLines(tail);\n return { lines: pos > 0 ? lines.slice(1) : lines, bytesRead };\n}\n\n/** Non-empty lines, in file order. */\nfunction nonEmptyLines(text: string): string[] {\n return text.split(\"\\n\").filter((l) => l.trim().length > 0);\n}\n\n/**\n * Whether a raw JSONL line IS the marker record, rather than a line that talks about it.\n *\n * Matches on the record's own discriminants (`subtype`, then `type`) — the fields that identify\n * what a record *is*. Free text is never consulted, which is the whole point: content is the user's\n * and must not steer the reader.\n *\n * A line that does not parse is not a marker. Deciding a window boundary from bytes that are not a\n * record would be guessing, and this function exists because guessing is what it replaced.\n */\nfunction isMarkerRecord(line: string, marker: string): boolean {\n let record: { type?: unknown; subtype?: unknown };\n try {\n record = JSON.parse(line) as { type?: unknown; subtype?: unknown };\n } catch {\n return false;\n }\n return record.subtype === marker || record.type === marker;\n}\n\n/**\n * Read the LAST records of a JSONL file without loading the whole thing.\n *\n * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session\n * transcript grows without bound; loading megabytes to show the last three turns is the cost this\n * exists to avoid — and a `slice` over a full read would be that same cost with a better name.\n */\nexport function readJsonlTail<T = Record<string, unknown>>(\n path: string,\n options: ReadJsonlTailOptions = {},\n): T[] {\n const want = options.maxRecords ?? Number.POSITIVE_INFINITY;\n const { lines, bytesRead } = readRawTail(path, want);\n\n let sel = lines;\n if (options.sinceMarker !== undefined) {\n const marker = options.sinceMarker;\n // STRUCTURAL, not `line.includes(marker)`.\n //\n // A raw substring match is true for any line that merely MENTIONS the marker — a user asking\n // \"how does compact_boundary work?\" silently truncated the window to start at their question.\n // The read then succeeded, returned fewer records than exist, and said nothing. That is the\n // measured reason the only would-be consumer kept its own reader instead of this one.\n const idx = sel.findLastIndex((l) => isMarkerRecord(l, marker));\n if (idx >= 0) sel = sel.slice(idx + 1);\n }\n if (Number.isFinite(want)) sel = sel.slice(-want);\n\n const out = sel.map((l) => JSON.parse(l) as T);\n return options._stats === true ? (Object.assign(out, { bytesRead }) as T[]) : out;\n}\n"]}
|
package/dist/persistence.js
CHANGED
|
@@ -4,7 +4,8 @@ import { transcriptRoot, encodeProjectDir } from './chunk-FTZTZBCU.js';
|
|
|
4
4
|
export { encodeProjectDir, legacyTranscriptPath, sessionUuidFor, transcriptPath, transcriptRoot } from './chunk-FTZTZBCU.js';
|
|
5
5
|
export { sanitizeFts5Query } from './chunk-WE22OXQA.js';
|
|
6
6
|
export { applyWalWithFallback, isCorruptionError, openSqliteResilient } from './chunk-XZHJYSPB.js';
|
|
7
|
-
export { JsonlParseError, appendJsonl, loadJsonl, readJsonlIds
|
|
7
|
+
export { JsonlParseError, appendJsonl, loadJsonl, readJsonlIds } from './chunk-7KIN3L2S.js';
|
|
8
|
+
export { withFileLock } from './chunk-RGQICWFU.js';
|
|
8
9
|
export { withCwdMutex } from './chunk-Q5EWJPRY.js';
|
|
9
10
|
import { atomicWriteTempTarget } from './chunk-3JHIFQ4I.js';
|
|
10
11
|
export { atomicWriteJson, atomicWriteTempTarget, atomicWriteText, replaceFileAtomic } from './chunk-3JHIFQ4I.js';
|
package/dist/persistence.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/persistence/list-sessions.ts","../src/internal/persistence/session-artifacts.ts","../src/internal/persistence/transcript-ops.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA8FA,IAAM,wBAAwB,EAAA,GAAK,IAAA;AAenC,eAAsB,YAAA,CACpB,GAAA,EACA,OAAA,GAA+B,EAAC,EACI;AACpC,EAAA,MAAM,GAAA,GAAM,KAAK,OAAA,CAAQ,OAAA,IAAW,gBAAe,EAAG,UAAA,EAAY,gBAAA,CAAiB,GAAG,CAAC,CAAA;AACvF,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI;AACF,IAAA,KAAA,GAAQ,MAAM,QAAQ,GAAG,CAAA;AAAA,EAC3B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,MAAwB,EAAC;AAC/B,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,EAAG;AAC9B,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAK,IAAI,CAAA;AACjC,IAAA,IAAI,UAAA;AACJ,IAAA,IAAI;AACF,MAAA,UAAA,GAAA,CAAc,MAAM,IAAA,CAAK,UAAU,CAAA,EAAG,KAAA;AAAA,IACxC,CAAA,CAAA,MAAQ;AAGN,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAK,MAAM,aAAA,CAAc,UAAA,EAAY,OAAA,CAAQ,eAAe,qBAAqB,CAAA;AACvF,IAAA,GAAA,CAAI,IAAA,CAAK;AAAA,MACP,EAAA;AAAA,MACA,QAAA,EAAU,EAAA,KAAO,MAAA,GAAY,aAAA,GAAgB,YAAA;AAAA,MAC7C,UAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AACA,EAAA,OAAO,GAAA;AACT;AAUA,eAAe,aAAA,CAAc,MAAc,QAAA,EAA+C;AACxF,EAAA,IAAI,QAAA,GAAW,EAAA;AACf,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,iBAAiB,IAAA,EAAM,EAAE,UAAU,MAAA,EAAQ,GAAA,EAAK,QAAA,GAAW,CAAA,EAAG,CAAA;AAC7E,IAAA,WAAA,MAAiB,SAAS,MAAA,EAAQ;AAChC,MAAA,QAAA,IAAY,KAAA;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AACjC,MAAA,QAAA,GAAW,KAAA,CAAM,KAAI,IAAK,EAAA;AAC1B,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,MAAM,EAAA,GAAK,gBAAgB,IAAI,CAAA;AAC/B,QAAA,IAAI,OAAO,KAAA,CAAA,EAAW;AACpB,UAAA,MAAA,CAAO,OAAA,EAAQ;AACf,UAAA,OAAO,EAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,OAAO,gBAAgB,QAAQ,CAAA;AACjC;AAEA,SAAS,gBAAgB,IAAA,EAAkC;AACzD,EAAA,IAAI,IAAA,CAAK,IAAA,EAAK,KAAM,EAAA,EAAI,OAAO,MAAA;AAC/B,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACvC,IAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,MAAM,OAAO,KAAA,CAAA;AAC1D,IAAA,MAAM,KAAM,MAAA,CAAmC,SAAA;AAC/C,IAAA,OAAO,OAAO,EAAA,KAAO,QAAA,IAAY,EAAA,KAAO,KAAK,EAAA,GAAK,KAAA,CAAA;AAAA,EACpD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;AC3JO,SAAS,uBAAA,CACd,MACA,WAAA,EAC6B;AAE7B,EAAA,IAAI,KAAK,QAAA,CAAS,aAAa,CAAA,EAAG,OAAO,cAAc,gBAAA,GAAmB,MAAA;AAC1E,EAAA,IAAI,aAAa,OAAO,MAAA;AACxB,EAAA,IAAI,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,EAAG,OAAO,YAAA;AACpC,EAAA,IAAI,IAAA,CAAK,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,aAAA;AAC1C,EAAA,IAAI,qBAAA,CAAsB,IAAI,CAAA,KAAM,MAAA,EAAW,OAAO,MAAA;AACtD,EAAA,OAAO,MAAA;AACT;ACGO,IAAM,mBAAA,GAAN,cAAkC,iBAAA,CAAkB;AAAA,EAGzD,YAAqB,IAAA,EAAc;AACjC,IAAA,KAAA;AAAA,MACE,qDAAqD,IAAI,CAAA,+JAAA,CAAA;AAAA,MAGzD,EAAE,IAAA,EAAM,wBAAA,EAA0B,WAAA,EAAa,KAAA;AAAM,KACvD;AANmB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAOrB;AAAA,EAPqB,IAAA;AAAA,EAFH,IAAA,GAAO,qBAAA;AAU3B;AAmCO,SAAS,cAAA,CACd,GAAA,EACA,GAAA,EACA,OAAA,GAAiC,EAAC,EAC5B;AACN,EAAA,KAAA,MAAW,IAAA,IAAQ,OAAA,CAAQ,gBAAA,IAAoB,EAAC,EAAG;AACjD,IAAA,IAAI,IAAA,KAAS,GAAA,EAAK,MAAM,IAAI,oBAAoB,GAAG,CAAA;AAAA,EACrD;AAEA,EAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,GAAA,EAAK,MAAM,EACnC,KAAA,CAAM,IAAI,CAAA,CACV,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAK,CAAE,SAAS,CAAC,CAAA;AACpC,EAAA,MAAM,IAAA,GACJ,QAAQ,iBAAA,KAAsB,MAAA,GAAY,QAAQ,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,OAAA,CAAQ,iBAAiB,CAAA;AAC5F,EAAA,MAAM,IAAA,GAAO,KAAK,MAAA,GAAS,CAAA,GAAI,GAAG,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC;AAAA,CAAA,GAAO,EAAA;AAIxD,EAAA,MAAM,KAAK,QAAA,CAAS,GAAA,EAAK,IAAA,EAAM,OAAA,CAAQ,QAAQ,GAAK,CAAA;AACpD,EAAA,IAAI;AACF,IAAA,SAAA,CAAU,IAAI,IAAI,CAAA;AAAA,EACpB,CAAA,SAAE;AACA,IAAA,SAAA,CAAU,EAAE,CAAA;AAAA,EACd;AACF;AAiBA,IAAM,aAAa,EAAA,GAAK,IAAA;AASxB,SAAS,WAAA,CAAY,MAAc,IAAA,EAAsD;AAMvF,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,IAAA,EAAM,GAAG,CAAA;AAC7B,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,EAAE,CAAA,CAAE,IAAA;AAC3B,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,IAAI,IAAA,GAAO,EAAA;AACX,EAAA,IAAI,GAAA,GAAM,IAAA;AACV,EAAA,IAAI;AACF,IAAA,OAAO,MAAM,CAAA,EAAG;AACd,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,UAAA,EAAY,GAAG,CAAA;AACpC,MAAA,GAAA,IAAO,GAAA;AACP,MAAA,MAAM,GAAA,GAAM,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA;AAC5B,MAAA,QAAA,CAAS,EAAA,EAAI,GAAA,EAAK,CAAA,EAAG,GAAA,EAAK,GAAG,CAAA;AAC7B,MAAA,SAAA,IAAa,GAAA;AACb,MAAA,IAAA,GAAO,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA,GAAI,IAAA;AAC9B,MAAA,IAAI,aAAA,CAAc,IAAI,CAAA,CAAE,MAAA,GAAS,IAAA,EAAM;AAAA,IACzC;AAAA,EACF,CAAA,SAAE;AACA,IAAA,SAAA,CAAU,EAAE,CAAA;AAAA,EACd;AACA,EAAA,MAAM,KAAA,GAAQ,cAAc,IAAI,CAAA;AAChC,EAAA,OAAO,EAAE,OAAO,GAAA,GAAM,CAAA,GAAI,MAAM,KAAA,CAAM,CAAC,CAAA,GAAI,KAAA,EAAO,SAAA,EAAU;AAC9D;AAGA,SAAS,cAAc,IAAA,EAAwB;AAC7C,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAK,CAAE,MAAA,GAAS,CAAC,CAAA;AAC3D;AAYA,SAAS,cAAA,CAAe,MAAc,MAAA,EAAyB;AAC7D,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA,CAAO,OAAA,KAAY,MAAA,IAAU,MAAA,CAAO,IAAA,KAAS,MAAA;AACtD;AASO,SAAS,aAAA,CACd,IAAA,EACA,OAAA,GAAgC,EAAC,EAC5B;AACL,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,UAAA,IAAc,MAAA,CAAO,iBAAA;AAC1C,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,WAAA,CAAY,MAAM,IAAI,CAAA;AAEnD,EAAA,IAAI,GAAA,GAAM,KAAA;AACV,EAAA,IAAI,OAAA,CAAQ,gBAAgB,MAAA,EAAW;AACrC,IAAA,MAAM,SAAS,OAAA,CAAQ,WAAA;AAOvB,IAAA,MAAM,GAAA,GAAM,IAAI,aAAA,CAAc,CAAC,MAAM,cAAA,CAAe,CAAA,EAAG,MAAM,CAAC,CAAA;AAC9D,IAAA,IAAI,OAAO,CAAA,EAAG,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,MAAM,CAAC,CAAA;AAAA,EACvC;AACA,EAAA,IAAI,MAAA,CAAO,SAAS,IAAI,CAAA,QAAS,GAAA,CAAI,KAAA,CAAM,CAAC,IAAI,CAAA;AAEhD,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,CAAC,MAAM,IAAA,CAAK,KAAA,CAAM,CAAC,CAAM,CAAA;AAC7C,EAAA,OAAO,OAAA,CAAQ,WAAW,IAAA,GAAQ,MAAA,CAAO,OAAO,GAAA,EAAK,EAAE,SAAA,EAAW,CAAA,GAAY,GAAA;AAChF","file":"persistence.js","sourcesContent":["/**\n * #598 — enumerate the sessions on disk, saying where each id came from.\n *\n * ## Why this exists\n *\n * Every transcript helper this package published mapped FORWARD — `sessionUuidFor`,\n * `transcriptPath`, `legacyTranscriptPath`, `encodeProjectDir`, `transcriptRoot`. **None enumerated.**\n * So a consumer that needed the list rebuilt it, and two independent ones did, in opposite\n * directions, and both got it wrong on the first attempt:\n *\n * | consumer | direction | what broke |\n * |---|---|---|\n * | `@theokit/agents` | file → id | derived the id from the file STEM |\n * | a downstream agent runtime | id → file | compared session ids against filenames |\n *\n * The second measured, against 5.0.1: the protected set never matched, **so neither the registered\n * sessions nor the live one were protected and everything classified as an orphan** — a garbage\n * collector that would delete the session in use.\n *\n * ## Why documentation was not the fix\n *\n * `b85dab4` documented the rename thoroughly. One of those two consumers had READ it and broke\n * anyway; the other had not and broke identically. Two samples, one informed and one not, same\n * defect. If reading were sufficient the informed sample would have survived — so the cause is the\n * shape of the surface, not the reader.\n *\n * And it cannot be closed by publishing an inverse: the filename is a UUIDv8 over SHA-256, which has\n * none. Whoever holds the file must read the id from INSIDE it, and that the id lives in the first\n * record is this package's knowledge. Both consumers had to discover it by reading bytes.\n *\n * ## Why the id is not just a string\n *\n * An `id: string` that is sometimes read from the transcript and sometimes inferred from the\n * filename is the same defect one layer up: a value that reads as authoritative and occasionally is\n * not. That is precisely what produced the garbage-collector failure above.\n *\n * So every entry carries {@link SessionListing.idSource}, and an entry whose id could not be\n * determined is `undefined` rather than guessed. A caller deciding what to DELETE needs to tell\n * \"this session is not registered\" from \"I could not read this file\" — those look identical in a\n * plain list and mean opposite things.\n *\n * @public\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { encodeProjectDir, transcriptRoot } from \"./session-transcript.js\";\n\n/** How the `id` on a {@link SessionListing} was obtained. */\nexport type SessionIdSource =\n /** Read from the `sessionId` of the transcript's first well-formed record. Authoritative. */\n | \"transcript\"\n /**\n * Not determined: the file was unreadable, empty, or its first records carried no `sessionId`.\n * `id` is `undefined` — deliberately not the filename, which is a hash and not an id.\n */\n | \"unavailable\";\n\n/** One session found on disk. */\nexport interface SessionListing {\n /**\n * The session id, or `undefined` when {@link idSource} is `\"unavailable\"`.\n *\n * Never derived from the filename. The filename is a UUIDv8 over SHA-256 of the id, so treating\n * it as the id is the exact defect this function exists to prevent.\n */\n readonly id: string | undefined;\n /** Where {@link id} came from. Check this before acting on `id`. */\n readonly idSource: SessionIdSource;\n /** Absolute path to the `.jsonl` transcript. */\n readonly transcript: string;\n /** Last modification time of the transcript. */\n readonly modifiedAt: Date;\n}\n\n/** Options for {@link listSessions}. */\nexport interface ListSessionsOptions {\n /**\n * Root under which `projects/<encoded-cwd>/` lives. Defaults to {@link transcriptRoot}, which\n * honours `THEOKIT_HOME`.\n */\n readonly baseDir?: string;\n /**\n * How many bytes of each transcript to read looking for the id. Default 65536.\n *\n * A cap rather than a full read because a transcript grows without bound and the id is in the\n * first record. A session whose id is not in the first 64KB reports `\"unavailable\"` rather than\n * being read to the end — bounded work with a declared outcome beats an unbounded read.\n */\n readonly idScanBytes?: number;\n}\n\nconst DEFAULT_ID_SCAN_BYTES = 64 * 1024;\n\n/**\n * Every session transcript under `<baseDir>/projects/<encoded-cwd>/`, with the id read from inside\n * each file.\n *\n * An absent directory yields `[]` — a cwd with no sessions is the common case, not an error.\n *\n * ```ts\n * for (const s of await listSessions(process.cwd())) {\n * if (s.idSource === \"unavailable\") continue; // do not guess, and do not delete\n * …\n * }\n * ```\n */\nexport async function listSessions(\n cwd: string,\n options: ListSessionsOptions = {},\n): Promise<readonly SessionListing[]> {\n const dir = join(options.baseDir ?? transcriptRoot(), \"projects\", encodeProjectDir(cwd));\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n\n const out: SessionListing[] = [];\n for (const name of names) {\n if (!name.endsWith(\".jsonl\")) continue;\n const transcript = join(dir, name);\n let modifiedAt: Date;\n try {\n modifiedAt = (await stat(transcript)).mtime;\n } catch {\n // Vanished between readdir and stat — a live session being rotated. Skipping is correct:\n // reporting a file that no longer exists would be worse than omitting it.\n continue;\n }\n const id = await readSessionId(transcript, options.idScanBytes ?? DEFAULT_ID_SCAN_BYTES);\n out.push({\n id,\n idSource: id === undefined ? \"unavailable\" : \"transcript\",\n transcript,\n modifiedAt,\n });\n }\n return out;\n}\n\n/**\n * The `sessionId` of the first well-formed record, or `undefined`.\n *\n * Reads at most `maxBytes` and stops at the first record that yields one. Tolerant of malformed\n * lines for the same reason `readTranscript` is: a truncated final line in a live transcript is\n * normal, and failing the whole listing over it would make the function useless exactly when it\n * matters.\n */\nasync function readSessionId(path: string, maxBytes: number): Promise<string | undefined> {\n let buffered = \"\";\n try {\n const stream = createReadStream(path, { encoding: \"utf8\", end: maxBytes - 1 });\n for await (const chunk of stream) {\n buffered += chunk as string;\n const lines = buffered.split(\"\\n\");\n buffered = lines.pop() ?? \"\";\n for (const line of lines) {\n const id = sessionIdOfLine(line);\n if (id !== undefined) {\n stream.destroy();\n return id;\n }\n }\n }\n } catch {\n return undefined;\n }\n return sessionIdOfLine(buffered);\n}\n\nfunction sessionIdOfLine(line: string): string | undefined {\n if (line.trim() === \"\") return undefined;\n try {\n const parsed: unknown = JSON.parse(line);\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const id = (parsed as { sessionId?: unknown }).sessionId;\n return typeof id === \"string\" && id !== \"\" ? id : undefined;\n } catch {\n return undefined;\n }\n}\n","import { atomicWriteTempTarget } from \"./atomic-write.js\";\n\n/**\n * The kinds of file this SDK leaves in a project's transcript directory.\n *\n * - `transcript` — the session itself (`transcriptPath`).\n * - `writer-lock` — the cross-process writer lease (`session-writer.ts`, `<file>.writer.lock`).\n * - `lock-directory` — `withFileLock`'s companion, taken by `mkdir`, so it is a DIRECTORY.\n * - `temp` — what `replaceFileAtomic` leaves when a process dies between the open and the rename.\n */\nexport type SessionArtifact = \"transcript\" | \"writer-lock\" | \"lock-directory\" | \"temp\";\n\n/**\n * U-1 — what is this entry, if it is one of ours?\n *\n * The SDK writes four kinds of file into a project directory and reasons about none of them\n * afterwards: there is no retention, no collector, and there was no way even to ask what an entry\n * IS. A consumer wanting to reclaim disk had to re-derive the suffixes by reading this source — and\n * one did, which means a suffix changing here would have left its classifier silently mislabelling\n * files on a path that deletes them.\n *\n * This is deliberately NOT a garbage collector. Retention is policy — how many days, how many to\n * keep, which session is live, whether to delete at all — and policy belongs to the application,\n * which is the only one that can know. What belongs here is the half only the SDK can answer.\n *\n * `undefined` means \"not written by this SDK\", and it is the answer that matters most: a caller\n * deleting what it does not recognise is how an editor's swap file gets collected. The `temp` case\n * defers to {@link atomicWriteTempTarget} rather than matching `.tmp`, for exactly that reason.\n */\nexport function classifySessionArtifact(\n name: string,\n isDirectory: boolean,\n): SessionArtifact | undefined {\n // `withFileLock` takes its lock by `mkdir`, so the same name as a plain file is not ours.\n if (name.endsWith(\".jsonl.lock\")) return isDirectory ? \"lock-directory\" : undefined;\n if (isDirectory) return undefined;\n if (name.endsWith(\".jsonl\")) return \"transcript\";\n if (name.endsWith(\".writer.lock\")) return \"writer-lock\";\n if (atomicWriteTempTarget(name) !== undefined) return \"temp\";\n return undefined;\n}\n","/**\n * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.\n *\n * ## What this replaces\n *\n * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a\n * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and\n * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.\n *\n * ## The rule that travels WITH the operation\n *\n * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live\n * pointer, the most recent transcript, and any active registry entry. That rule lived in the\n * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy\n * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:\n * critical knowledge outside the module that needs it, applied by convention.\n *\n * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of\n * them. The caller supplies the list because only the caller knows which session is live; the\n * enforcement lives here because that is where the write happens.\n *\n * @internal\n */\n\nimport { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).\n *\n * Typed rather than a bare `Error` because the caller must distinguish \"this session is protected\"\n * from \"the disk is full\": the first is a correct refusal, the second is an incident.\n *\n * RENAMED from `LiveSessionError` (2026-09-01). `session-guard.ts` exports a DIFFERENT class of that\n * name from the root barrel, with an incompatible shape — `(sessionId, reason)` and a `reason`\n * field, against this one's `(path)` and `code: \"live_session_protected\"` — and `.` and\n * `./persistence` are both declared subpaths, so one consumer can hold both. `instanceof` never\n * crossed the pair, so a `catch` checking the root import silently missed this one and ran its\n * fallback for a condition it believed it handled; `err.name` matched BOTH, so a name check looked\n * right and then read `err.reason`, which only the other one has. The names now say what each\n * refusal is about: destroying a SESSION, versus overwriting a TRANSCRIPT file.\n */\nexport class LiveTranscriptError extends TheokitAgentError {\n override readonly name = \"LiveTranscriptError\";\n\n constructor(readonly path: string) {\n super(\n `refusing to write over a live session transcript: ${path}. ` +\n \"Fork to a new id instead — the live pointer, the most recent transcript and any active \" +\n \"registry entry are never overwritten (audit-trail rotation, M60).\",\n { code: \"live_session_protected\", isRetryable: false },\n );\n }\n}\n\n/** Options for {@link forkTranscript}. */\nexport interface ForkTranscriptOptions {\n /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */\n readonly beforeRecordIndex?: number;\n /**\n * Paths that must never be written over — the live pointer, the most recent transcript, any active\n * registry entry. The caller supplies them because only the caller knows which session is live.\n */\n readonly liveSessionPaths?: readonly string[];\n /**\n * M107 — permission bits for the created destination. Default: `0o600`.\n *\n * A transcript carries the conversation. Before M107 no mode was passed at all, so the file was\n * born `0o666 & ~umask` — measured `0o664` (group-WRITABLE) on a `umask 002` machine, `0o644` on\n * `umask 022`, `0o466` on `umask 0200`. This is a DEFAULT and not a required knob on purpose: a\n * knob would reach zero consumers by omission, which is the failure mode that matters.\n *\n * As with any `open` mode, the `umask` may still CLEAR bits — under `umask 0200` the result is\n * `0o400`. That is accepted: the invariant bought here is \"neither group nor others\", and `0o400`\n * satisfies it more strictly. The SDK deliberately does not `fchmod` the default back, because\n * that would hand back a bit the operator asked to remove.\n */\n readonly mode?: number;\n}\n\n/**\n * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.\n *\n * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot\n * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also\n * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an\n * error is the worst failure mode for an operation that touches user sessions.\n */\nexport function forkTranscript(\n src: string,\n dst: string,\n options: ForkTranscriptOptions = {},\n): void {\n for (const live of options.liveSessionPaths ?? []) {\n if (live === dst) throw new LiveTranscriptError(dst);\n }\n\n const lines = readFileSync(src, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim().length > 0);\n const kept =\n options.beforeRecordIndex === undefined ? lines : lines.slice(0, options.beforeRecordIndex);\n const body = kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\";\n\n // `wx` — fails with EEXIST instead of truncating. The exclusivity IS the concurrency guarantee,\n // and M107 only added the third argument: the mode. See `ForkTranscriptOptions.mode`.\n const fd = openSync(dst, \"wx\", options.mode ?? 0o600);\n try {\n writeSync(fd, body);\n } finally {\n closeSync(fd);\n }\n}\n\n/** Options for {@link readJsonlTail}. */\nexport interface ReadJsonlTailOptions {\n /** Maximum records to return, counted from the END. */\n readonly maxRecords?: number;\n /**\n * Start the window AFTER the last record whose `subtype` (or `type`) equals this.\n *\n * Matched STRUCTURALLY since T2.5. It used to be `line.includes(marker)`, so any message\n * mentioning the marker in its text truncated the read — silently, with a successful return.\n */\n readonly sinceMarker?: string;\n /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */\n readonly _stats?: boolean;\n}\n\nconst TAIL_CHUNK = 64 * 1024;\n\n/**\n * Reads chunks backwards until enough complete lines have accumulated.\n *\n * Extracted from `readJsonlTail` because the read loop and the record selection are two\n * responsibilities — and together they exceeded the complexity ceiling. The buffer's first line may\n * be cut in half when the read stopped before the start of the file; that is why it is discarded.\n */\nfunction readRawTail(path: string, want: number): { lines: string[]; bytesRead: number } {\n // Opened FIRST, then sized through the descriptor. `statSync(path)` followed by\n // `openSync(path)` resolves the name twice, and `size` is what drives every read offset below —\n // so a path that changed between the two calls would have the loop seeking by one file's length\n // inside another (CodeQL js/file-system-race #19). `fstat` on the open fd describes the file\n // being read, by construction.\n const fd = openSync(path, \"r\");\n const size = fstatSync(fd).size;\n let bytesRead = 0;\n let tail = \"\";\n let pos = size;\n try {\n while (pos > 0) {\n const len = Math.min(TAIL_CHUNK, pos);\n pos -= len;\n const buf = Buffer.alloc(len);\n readSync(fd, buf, 0, len, pos);\n bytesRead += len;\n tail = buf.toString(\"utf8\") + tail;\n if (nonEmptyLines(tail).length > want) break;\n }\n } finally {\n closeSync(fd);\n }\n const lines = nonEmptyLines(tail);\n return { lines: pos > 0 ? lines.slice(1) : lines, bytesRead };\n}\n\n/** Non-empty lines, in file order. */\nfunction nonEmptyLines(text: string): string[] {\n return text.split(\"\\n\").filter((l) => l.trim().length > 0);\n}\n\n/**\n * Whether a raw JSONL line IS the marker record, rather than a line that talks about it.\n *\n * Matches on the record's own discriminants (`subtype`, then `type`) — the fields that identify\n * what a record *is*. Free text is never consulted, which is the whole point: content is the user's\n * and must not steer the reader.\n *\n * A line that does not parse is not a marker. Deciding a window boundary from bytes that are not a\n * record would be guessing, and this function exists because guessing is what it replaced.\n */\nfunction isMarkerRecord(line: string, marker: string): boolean {\n let record: { type?: unknown; subtype?: unknown };\n try {\n record = JSON.parse(line) as { type?: unknown; subtype?: unknown };\n } catch {\n return false;\n }\n return record.subtype === marker || record.type === marker;\n}\n\n/**\n * Read the LAST records of a JSONL file without loading the whole thing.\n *\n * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session\n * transcript grows without bound; loading megabytes to show the last three turns is the cost this\n * exists to avoid — and a `slice` over a full read would be that same cost with a better name.\n */\nexport function readJsonlTail<T = Record<string, unknown>>(\n path: string,\n options: ReadJsonlTailOptions = {},\n): T[] {\n const want = options.maxRecords ?? Number.POSITIVE_INFINITY;\n const { lines, bytesRead } = readRawTail(path, want);\n\n let sel = lines;\n if (options.sinceMarker !== undefined) {\n const marker = options.sinceMarker;\n // STRUCTURAL, not `line.includes(marker)`.\n //\n // A raw substring match is true for any line that merely MENTIONS the marker — a user asking\n // \"how does compact_boundary work?\" silently truncated the window to start at their question.\n // The read then succeeded, returned fewer records than exist, and said nothing. That is the\n // measured reason the only would-be consumer kept its own reader instead of this one.\n const idx = sel.findLastIndex((l) => isMarkerRecord(l, marker));\n if (idx >= 0) sel = sel.slice(idx + 1);\n }\n if (Number.isFinite(want)) sel = sel.slice(-want);\n\n const out = sel.map((l) => JSON.parse(l) as T);\n return options._stats === true ? (Object.assign(out, { bytesRead }) as T[]) : out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/list-sessions.ts","../src/internal/persistence/session-artifacts.ts","../src/internal/persistence/transcript-ops.ts"],"names":[],"mappings":"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","file":"persistence.js","sourcesContent":["/**\n * #598 — enumerate the sessions on disk, saying where each id came from.\n *\n * ## Why this exists\n *\n * Every transcript helper this package published mapped FORWARD — `sessionUuidFor`,\n * `transcriptPath`, `legacyTranscriptPath`, `encodeProjectDir`, `transcriptRoot`. **None enumerated.**\n * So a consumer that needed the list rebuilt it, and two independent ones did, in opposite\n * directions, and both got it wrong on the first attempt:\n *\n * | consumer | direction | what broke |\n * |---|---|---|\n * | `@theokit/agents` | file → id | derived the id from the file STEM |\n * | a downstream agent runtime | id → file | compared session ids against filenames |\n *\n * The second measured, against 5.0.1: the protected set never matched, **so neither the registered\n * sessions nor the live one were protected and everything classified as an orphan** — a garbage\n * collector that would delete the session in use.\n *\n * ## Why documentation was not the fix\n *\n * `b85dab4` documented the rename thoroughly. One of those two consumers had READ it and broke\n * anyway; the other had not and broke identically. Two samples, one informed and one not, same\n * defect. If reading were sufficient the informed sample would have survived — so the cause is the\n * shape of the surface, not the reader.\n *\n * And it cannot be closed by publishing an inverse: the filename is a UUIDv8 over SHA-256, which has\n * none. Whoever holds the file must read the id from INSIDE it, and that the id lives in the first\n * record is this package's knowledge. Both consumers had to discover it by reading bytes.\n *\n * ## Why the id is not just a string\n *\n * An `id: string` that is sometimes read from the transcript and sometimes inferred from the\n * filename is the same defect one layer up: a value that reads as authoritative and occasionally is\n * not. That is precisely what produced the garbage-collector failure above.\n *\n * So every entry carries {@link SessionListing.idSource}, and an entry whose id could not be\n * determined is `undefined` rather than guessed. A caller deciding what to DELETE needs to tell\n * \"this session is not registered\" from \"I could not read this file\" — those look identical in a\n * plain list and mean opposite things.\n *\n * @public\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { encodeProjectDir, transcriptRoot } from \"./session-transcript.js\";\n\n/** How the `id` on a {@link SessionListing} was obtained. */\nexport type SessionIdSource =\n /** Read from the `sessionId` of the transcript's first well-formed record. Authoritative. */\n | \"transcript\"\n /**\n * Not determined: the file was unreadable, empty, or its first records carried no `sessionId`.\n * `id` is `undefined` — deliberately not the filename, which is a hash and not an id.\n */\n | \"unavailable\";\n\n/** One session found on disk. */\nexport interface SessionListing {\n /**\n * The session id, or `undefined` when {@link idSource} is `\"unavailable\"`.\n *\n * Never derived from the filename. The filename is a UUIDv8 over SHA-256 of the id, so treating\n * it as the id is the exact defect this function exists to prevent.\n */\n readonly id: string | undefined;\n /** Where {@link id} came from. Check this before acting on `id`. */\n readonly idSource: SessionIdSource;\n /** Absolute path to the `.jsonl` transcript. */\n readonly transcript: string;\n /** Last modification time of the transcript. */\n readonly modifiedAt: Date;\n}\n\n/** Options for {@link listSessions}. */\nexport interface ListSessionsOptions {\n /**\n * Root under which `projects/<encoded-cwd>/` lives. Defaults to {@link transcriptRoot}, which\n * honours `THEOKIT_HOME`.\n */\n readonly baseDir?: string;\n /**\n * How many bytes of each transcript to read looking for the id. Default 65536.\n *\n * A cap rather than a full read because a transcript grows without bound and the id is in the\n * first record. A session whose id is not in the first 64KB reports `\"unavailable\"` rather than\n * being read to the end — bounded work with a declared outcome beats an unbounded read.\n */\n readonly idScanBytes?: number;\n}\n\nconst DEFAULT_ID_SCAN_BYTES = 64 * 1024;\n\n/**\n * Every session transcript under `<baseDir>/projects/<encoded-cwd>/`, with the id read from inside\n * each file.\n *\n * An absent directory yields `[]` — a cwd with no sessions is the common case, not an error.\n *\n * ```ts\n * for (const s of await listSessions(process.cwd())) {\n * if (s.idSource === \"unavailable\") continue; // do not guess, and do not delete\n * …\n * }\n * ```\n */\nexport async function listSessions(\n cwd: string,\n options: ListSessionsOptions = {},\n): Promise<readonly SessionListing[]> {\n const dir = join(options.baseDir ?? transcriptRoot(), \"projects\", encodeProjectDir(cwd));\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n\n const out: SessionListing[] = [];\n for (const name of names) {\n if (!name.endsWith(\".jsonl\")) continue;\n const transcript = join(dir, name);\n let modifiedAt: Date;\n try {\n modifiedAt = (await stat(transcript)).mtime;\n } catch {\n // Vanished between readdir and stat — a live session being rotated. Skipping is correct:\n // reporting a file that no longer exists would be worse than omitting it.\n continue;\n }\n const id = await readSessionId(transcript, options.idScanBytes ?? DEFAULT_ID_SCAN_BYTES);\n out.push({\n id,\n idSource: id === undefined ? \"unavailable\" : \"transcript\",\n transcript,\n modifiedAt,\n });\n }\n return out;\n}\n\n/**\n * The `sessionId` of the first well-formed record, or `undefined`.\n *\n * Reads at most `maxBytes` and stops at the first record that yields one. Tolerant of malformed\n * lines for the same reason `readTranscript` is: a truncated final line in a live transcript is\n * normal, and failing the whole listing over it would make the function useless exactly when it\n * matters.\n */\nasync function readSessionId(path: string, maxBytes: number): Promise<string | undefined> {\n let buffered = \"\";\n try {\n const stream = createReadStream(path, { encoding: \"utf8\", end: maxBytes - 1 });\n for await (const chunk of stream) {\n buffered += chunk as string;\n const lines = buffered.split(\"\\n\");\n buffered = lines.pop() ?? \"\";\n for (const line of lines) {\n const id = sessionIdOfLine(line);\n if (id !== undefined) {\n stream.destroy();\n return id;\n }\n }\n }\n } catch {\n return undefined;\n }\n return sessionIdOfLine(buffered);\n}\n\nfunction sessionIdOfLine(line: string): string | undefined {\n if (line.trim() === \"\") return undefined;\n try {\n const parsed: unknown = JSON.parse(line);\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const id = (parsed as { sessionId?: unknown }).sessionId;\n return typeof id === \"string\" && id !== \"\" ? id : undefined;\n } catch {\n return undefined;\n }\n}\n","import { atomicWriteTempTarget } from \"./atomic-write.js\";\n\n/**\n * The kinds of file this SDK leaves in a project's transcript directory.\n *\n * - `transcript` — the session itself (`transcriptPath`).\n * - `writer-lock` — the cross-process writer lease (`session-writer.ts`, `<file>.writer.lock`).\n * - `lock-directory` — `withFileLock`'s companion, taken by `mkdir`, so it is a DIRECTORY.\n * - `temp` — what `replaceFileAtomic` leaves when a process dies between the open and the rename.\n */\nexport type SessionArtifact = \"transcript\" | \"writer-lock\" | \"lock-directory\" | \"temp\";\n\n/**\n * U-1 — what is this entry, if it is one of ours?\n *\n * The SDK writes four kinds of file into a project directory and reasons about none of them\n * afterwards: there is no retention, no collector, and there was no way even to ask what an entry\n * IS. A consumer wanting to reclaim disk had to re-derive the suffixes by reading this source — and\n * one did, which means a suffix changing here would have left its classifier silently mislabelling\n * files on a path that deletes them.\n *\n * This is deliberately NOT a garbage collector. Retention is policy — how many days, how many to\n * keep, which session is live, whether to delete at all — and policy belongs to the application,\n * which is the only one that can know. What belongs here is the half only the SDK can answer.\n *\n * `undefined` means \"not written by this SDK\", and it is the answer that matters most: a caller\n * deleting what it does not recognise is how an editor's swap file gets collected. The `temp` case\n * defers to {@link atomicWriteTempTarget} rather than matching `.tmp`, for exactly that reason.\n */\nexport function classifySessionArtifact(\n name: string,\n isDirectory: boolean,\n): SessionArtifact | undefined {\n // `withFileLock` takes its lock by `mkdir`, so the same name as a plain file is not ours.\n if (name.endsWith(\".jsonl.lock\")) return isDirectory ? \"lock-directory\" : undefined;\n if (isDirectory) return undefined;\n if (name.endsWith(\".jsonl\")) return \"transcript\";\n if (name.endsWith(\".writer.lock\")) return \"writer-lock\";\n if (atomicWriteTempTarget(name) !== undefined) return \"temp\";\n return undefined;\n}\n","/**\n * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.\n *\n * ## What this replaces\n *\n * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a\n * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and\n * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.\n *\n * ## The rule that travels WITH the operation\n *\n * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live\n * pointer, the most recent transcript, and any active registry entry. That rule lived in the\n * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy\n * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:\n * critical knowledge outside the module that needs it, applied by convention.\n *\n * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of\n * them. The caller supplies the list because only the caller knows which session is live; the\n * enforcement lives here because that is where the write happens.\n *\n * @internal\n */\n\nimport { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).\n *\n * Typed rather than a bare `Error` because the caller must distinguish \"this session is protected\"\n * from \"the disk is full\": the first is a correct refusal, the second is an incident.\n *\n * RENAMED from `LiveSessionError` (2026-09-01). `session-guard.ts` exports a DIFFERENT class of that\n * name from the root barrel, with an incompatible shape — `(sessionId, reason)` and a `reason`\n * field, against this one's `(path)` and `code: \"live_session_protected\"` — and `.` and\n * `./persistence` are both declared subpaths, so one consumer can hold both. `instanceof` never\n * crossed the pair, so a `catch` checking the root import silently missed this one and ran its\n * fallback for a condition it believed it handled; `err.name` matched BOTH, so a name check looked\n * right and then read `err.reason`, which only the other one has. The names now say what each\n * refusal is about: destroying a SESSION, versus overwriting a TRANSCRIPT file.\n */\nexport class LiveTranscriptError extends TheokitAgentError {\n override readonly name = \"LiveTranscriptError\";\n\n constructor(readonly path: string) {\n super(\n `refusing to write over a live session transcript: ${path}. ` +\n \"Fork to a new id instead — the live pointer, the most recent transcript and any active \" +\n \"registry entry are never overwritten (audit-trail rotation, M60).\",\n { code: \"live_session_protected\", isRetryable: false },\n );\n }\n}\n\n/** Options for {@link forkTranscript}. */\nexport interface ForkTranscriptOptions {\n /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */\n readonly beforeRecordIndex?: number;\n /**\n * Paths that must never be written over — the live pointer, the most recent transcript, any active\n * registry entry. The caller supplies them because only the caller knows which session is live.\n */\n readonly liveSessionPaths?: readonly string[];\n /**\n * M107 — permission bits for the created destination. Default: `0o600`.\n *\n * A transcript carries the conversation. Before M107 no mode was passed at all, so the file was\n * born `0o666 & ~umask` — measured `0o664` (group-WRITABLE) on a `umask 002` machine, `0o644` on\n * `umask 022`, `0o466` on `umask 0200`. This is a DEFAULT and not a required knob on purpose: a\n * knob would reach zero consumers by omission, which is the failure mode that matters.\n *\n * As with any `open` mode, the `umask` may still CLEAR bits — under `umask 0200` the result is\n * `0o400`. That is accepted: the invariant bought here is \"neither group nor others\", and `0o400`\n * satisfies it more strictly. The SDK deliberately does not `fchmod` the default back, because\n * that would hand back a bit the operator asked to remove.\n */\n readonly mode?: number;\n}\n\n/**\n * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.\n *\n * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot\n * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also\n * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an\n * error is the worst failure mode for an operation that touches user sessions.\n */\nexport function forkTranscript(\n src: string,\n dst: string,\n options: ForkTranscriptOptions = {},\n): void {\n for (const live of options.liveSessionPaths ?? []) {\n if (live === dst) throw new LiveTranscriptError(dst);\n }\n\n const lines = readFileSync(src, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim().length > 0);\n const kept =\n options.beforeRecordIndex === undefined ? lines : lines.slice(0, options.beforeRecordIndex);\n const body = kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\";\n\n // `wx` — fails with EEXIST instead of truncating. The exclusivity IS the concurrency guarantee,\n // and M107 only added the third argument: the mode. See `ForkTranscriptOptions.mode`.\n const fd = openSync(dst, \"wx\", options.mode ?? 0o600);\n try {\n writeSync(fd, body);\n } finally {\n closeSync(fd);\n }\n}\n\n/** Options for {@link readJsonlTail}. */\nexport interface ReadJsonlTailOptions {\n /** Maximum records to return, counted from the END. */\n readonly maxRecords?: number;\n /**\n * Start the window AFTER the last record whose `subtype` (or `type`) equals this.\n *\n * Matched STRUCTURALLY since T2.5. It used to be `line.includes(marker)`, so any message\n * mentioning the marker in its text truncated the read — silently, with a successful return.\n */\n readonly sinceMarker?: string;\n /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */\n readonly _stats?: boolean;\n}\n\nconst TAIL_CHUNK = 64 * 1024;\n\n/**\n * Reads chunks backwards until enough complete lines have accumulated.\n *\n * Extracted from `readJsonlTail` because the read loop and the record selection are two\n * responsibilities — and together they exceeded the complexity ceiling. The buffer's first line may\n * be cut in half when the read stopped before the start of the file; that is why it is discarded.\n */\nfunction readRawTail(path: string, want: number): { lines: string[]; bytesRead: number } {\n // Opened FIRST, then sized through the descriptor. `statSync(path)` followed by\n // `openSync(path)` resolves the name twice, and `size` is what drives every read offset below —\n // so a path that changed between the two calls would have the loop seeking by one file's length\n // inside another (CodeQL js/file-system-race #19). `fstat` on the open fd describes the file\n // being read, by construction.\n const fd = openSync(path, \"r\");\n const size = fstatSync(fd).size;\n let bytesRead = 0;\n let tail = \"\";\n let pos = size;\n try {\n while (pos > 0) {\n const len = Math.min(TAIL_CHUNK, pos);\n pos -= len;\n const buf = Buffer.alloc(len);\n readSync(fd, buf, 0, len, pos);\n bytesRead += len;\n tail = buf.toString(\"utf8\") + tail;\n if (nonEmptyLines(tail).length > want) break;\n }\n } finally {\n closeSync(fd);\n }\n const lines = nonEmptyLines(tail);\n return { lines: pos > 0 ? lines.slice(1) : lines, bytesRead };\n}\n\n/** Non-empty lines, in file order. */\nfunction nonEmptyLines(text: string): string[] {\n return text.split(\"\\n\").filter((l) => l.trim().length > 0);\n}\n\n/**\n * Whether a raw JSONL line IS the marker record, rather than a line that talks about it.\n *\n * Matches on the record's own discriminants (`subtype`, then `type`) — the fields that identify\n * what a record *is*. Free text is never consulted, which is the whole point: content is the user's\n * and must not steer the reader.\n *\n * A line that does not parse is not a marker. Deciding a window boundary from bytes that are not a\n * record would be guessing, and this function exists because guessing is what it replaced.\n */\nfunction isMarkerRecord(line: string, marker: string): boolean {\n let record: { type?: unknown; subtype?: unknown };\n try {\n record = JSON.parse(line) as { type?: unknown; subtype?: unknown };\n } catch {\n return false;\n }\n return record.subtype === marker || record.type === marker;\n}\n\n/**\n * Read the LAST records of a JSONL file without loading the whole thing.\n *\n * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session\n * transcript grows without bound; loading megabytes to show the last three turns is the cost this\n * exists to avoid — and a `slice` over a full read would be that same cost with a better name.\n */\nexport function readJsonlTail<T = Record<string, unknown>>(\n path: string,\n options: ReadJsonlTailOptions = {},\n): T[] {\n const want = options.maxRecords ?? Number.POSITIVE_INFINITY;\n const { lines, bytesRead } = readRawTail(path, want);\n\n let sel = lines;\n if (options.sinceMarker !== undefined) {\n const marker = options.sinceMarker;\n // STRUCTURAL, not `line.includes(marker)`.\n //\n // A raw substring match is true for any line that merely MENTIONS the marker — a user asking\n // \"how does compact_boundary work?\" silently truncated the window to start at their question.\n // The read then succeeded, returned fewer records than exist, and said nothing. That is the\n // measured reason the only would-be consumer kept its own reader instead of this one.\n const idx = sel.findLastIndex((l) => isMarkerRecord(l, marker));\n if (idx >= 0) sel = sel.slice(idx + 1);\n }\n if (Number.isFinite(want)) sel = sel.slice(-want);\n\n const out = sel.map((l) => JSON.parse(l) as T);\n return options._stats === true ? (Object.assign(out, { bytesRead }) as T[]) : out;\n}\n"]}
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -404,6 +404,33 @@ export interface MemorySettings {
|
|
|
404
404
|
* share the writes.
|
|
405
405
|
*/
|
|
406
406
|
directory?: string;
|
|
407
|
+
/**
|
|
408
|
+
* The most facts this agent's memory may hold. Default: no ceiling.
|
|
409
|
+
*
|
|
410
|
+
* The ceiling counts the write directory only: the per-memory fact files and any legacy
|
|
411
|
+
* `## Facts` bullets in that directory's `MEMORY.md`. A dreaming sweep's `factsBefore` counts
|
|
412
|
+
* every root it reads, so the two numbers can differ.
|
|
413
|
+
*
|
|
414
|
+
* At the ceiling a new fact is refused with a `ConfigurationError` whose code is
|
|
415
|
+
* `memory_ceiling_reached` and whose message names the directory, the count and what to
|
|
416
|
+
* consolidate; nothing is written. Recording a fact that is already stored is a second
|
|
417
|
+
* observation, not a new fact, and is still admitted. A value that is not a positive integer is
|
|
418
|
+
* refused with `invalid_memory_ceiling` by `Agent.create` and `Agent.resume`, before the agent
|
|
419
|
+
* exists, whether or not memory is enabled, and again by the writer before anything touches disk.
|
|
420
|
+
*
|
|
421
|
+
* The count and the write hold a lock that every SDK process writing this directory with a
|
|
422
|
+
* ceiling takes, so several processes on one directory cannot together pass it. That lock is
|
|
423
|
+
* the companion `<directory>.lock` beside the directory and needs the optional `proper-lockfile`
|
|
424
|
+
* peer; without it the ceiling holds within one process only, and the SDK says so once on its
|
|
425
|
+
* diagnostics channel. A writer that declares no ceiling, such as the Claude Code CLI, does not
|
|
426
|
+
* take the lock and can still add facts past it. When the lock stays busy past its retries the
|
|
427
|
+
* write is refused with `proper-lockfile`'s `ELOCKED` error, and nothing is written.
|
|
428
|
+
*
|
|
429
|
+
* A resumed agent keeps its ceiling and its `directory`: a `memory` object passed to
|
|
430
|
+
* `Agent.resume` replaces only the fields it sets, and a field set to `undefined` keeps its
|
|
431
|
+
* persisted value.
|
|
432
|
+
*/
|
|
433
|
+
maxFacts?: number;
|
|
407
434
|
/**
|
|
408
435
|
* Whether the SDK auto-injects recalled facts as a `<memory>` block in the
|
|
409
436
|
* LLM system prompt. Default `true`.
|
package/docs/error-codes.md
CHANGED
|
@@ -6,7 +6,7 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
6
6
|
|
|
7
7
|
**Transport codes vs the rest.** `ErrorCode` in `errors.ts` is the small canonical union a provider failure maps onto — the codes marked *transport* below. Everything else is raised by a specific subsystem at a specific place, and a `catch` that only handles the union will meet them anyway.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
246 distinct code(s).
|
|
10
10
|
|
|
11
11
|
| Code | Kind | Raised by | Sites |
|
|
12
12
|
|---|---|---|---|
|
|
@@ -14,9 +14,9 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
14
14
|
| `a2a_request_timeout` | domain | A2ARequestTimeoutError | `packages/sdk/src/a2a/message-bus.ts:62` +1 |
|
|
15
15
|
| `aborted` | domain | TheokitAgentError | `packages/sdk/src/internal/agent/batch.ts:358` +1 |
|
|
16
16
|
| `agent_disposed` | domain | — | `packages/sdk/src/errors.ts:544` |
|
|
17
|
-
| `agent_id_already_exists` | domain | ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:
|
|
17
|
+
| `agent_id_already_exists` | domain | ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:59` |
|
|
18
18
|
| `agent_not_registered` | domain | UnknownAgentError | `packages/sdk/src/internal/cron/run-job.ts:61` |
|
|
19
|
-
| `agent_rehydration_failed` | domain | UnknownAgentError | `packages/sdk/src/internal/agent/helpers.ts:
|
|
19
|
+
| `agent_rehydration_failed` | domain | UnknownAgentError | `packages/sdk/src/internal/agent/helpers.ts:362` +1 |
|
|
20
20
|
| `anthropic_auth_failed` | domain | AuthenticationError | `packages/sdk/src/internal/error-mappers/anthropic.ts:56` |
|
|
21
21
|
| `anthropic_rate_limit` | domain | RateLimitError | `packages/sdk/src/internal/error-mappers/anthropic.ts:59` |
|
|
22
22
|
| `anthropic_server_error` | domain | NetworkError | `packages/sdk/src/internal/error-mappers/anthropic.ts:71` |
|
|
@@ -44,7 +44,7 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
44
44
|
| `cloud_run_http_error` | domain | NetworkError | `packages/sdk/src/internal/cloud-agent/real-cloud-run.ts:156` |
|
|
45
45
|
| `cloud_run_unknown_status` | domain | NetworkError | `packages/sdk/src/internal/cloud-agent/real-cloud-run.ts:285` +1 |
|
|
46
46
|
| `cloud_runtime_pre_release` | domain | ConfigurationError | `packages/sdk/src/agent.ts:417` +2 |
|
|
47
|
-
| `cloud_stdio_cwd_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
47
|
+
| `cloud_stdio_cwd_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:123` |
|
|
48
48
|
| `compression_failed` | domain | — | `packages/sdk/src/errors.ts:700` |
|
|
49
49
|
| `compression_model_unresolved` | domain | — | `packages/sdk/src/internal/runtime/compression/compression-model-registry.ts:102` +1 |
|
|
50
50
|
| `context_config_shape` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/context/context-manager.ts:288` |
|
|
@@ -52,14 +52,14 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
52
52
|
| `context_json_invalid` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/context/context-manager.ts:282` |
|
|
53
53
|
| `context_read_error` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/context/context-manager.ts:269` |
|
|
54
54
|
| `context_sources_shape` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/context/context-manager.ts:309` |
|
|
55
|
-
| `credential_pool_ambiguous` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
55
|
+
| `credential_pool_ambiguous` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:70` +1 |
|
|
56
56
|
| `credential_pool_empty` | domain | ConfigurationError | `packages/sdk/src/internal/llm/credential-pool.ts:91` |
|
|
57
57
|
| `cron_ambiguous_target` | domain | ConfigurationError | `packages/sdk/src/cron.ts:215` |
|
|
58
58
|
| `cron_missing_message` | domain | ConfigurationError | `packages/sdk/src/cron.ts:233` +1 |
|
|
59
59
|
| `cron_no_target` | domain | ConfigurationError | `packages/sdk/src/cron.ts:221` +1 |
|
|
60
60
|
| `cron_workflow_message` | domain | ConfigurationError | `packages/sdk/src/cron.ts:227` |
|
|
61
61
|
| `duplicate_skill_name` | domain | ConfigurationError | `packages/sdk/src/define-skill-read-tool.ts:155` |
|
|
62
|
-
| `duplicate_tool_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
62
|
+
| `duplicate_tool_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:203` |
|
|
63
63
|
| `effective_tools_expected_options` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/effective-tools.ts:102` +1 |
|
|
64
64
|
| `embedding_dimension_mismatch` | domain | ConfigurationError | `packages/sdk/src/internal/memory/lance-index.ts:146` +1 |
|
|
65
65
|
| `embedding_invalid_response` | domain | NetworkError | `packages/sdk/src/internal/memory/adapters/openai-compatible.ts:400` |
|
|
@@ -74,7 +74,7 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
74
74
|
| `filesystem_security` | domain | FilesystemSecurityError | `packages/sdk/src/filesystem/types.ts:61` +1 |
|
|
75
75
|
| `filesystem_stale` | domain | StaleFileError | `packages/sdk/src/filesystem/types.ts:115` +2 |
|
|
76
76
|
| `forbidden_path` | domain | — | `packages/sdk/src/internal/security/path-guard.ts:57` +1 |
|
|
77
|
-
| `handoff_package_missing` | domain | ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:
|
|
77
|
+
| `handoff_package_missing` | domain | ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:109` +1 |
|
|
78
78
|
| `handoff_target_invalid` | domain | ConfigurationError | `packages/sdk-handoff/src/handoff.ts:116` |
|
|
79
79
|
| `handoff_target_required` | domain | ConfigurationError | `packages/sdk-handoff/src/handoff.ts:111` |
|
|
80
80
|
| `hitl_timeout` | domain | HitlTimeoutError | `packages/sdk/src/internal/runtime/tools/hitl-middleware.ts:42` +1 |
|
|
@@ -100,8 +100,9 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
100
100
|
| `invalid_input` | domain | MemoryAdapterError | `packages/memory-honcho/src/adapter.ts:98` +9 |
|
|
101
101
|
| `invalid_max_iterations` | domain | ConfigurationError | `packages/sdk/src/internal/local-agent/real-local-run.ts:71` |
|
|
102
102
|
| `invalid_memory_backend` | domain | ConfigurationError | `packages/sdk/src/internal/memory/index-manager-dispatch.ts:24` +1 |
|
|
103
|
+
| `invalid_memory_ceiling` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/fact-ceiling.ts:23` +1 |
|
|
103
104
|
| `invalid_memory_directory` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/memory-root.ts:151` |
|
|
104
|
-
| `invalid_memory_kind` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/markdown-store.ts:
|
|
105
|
+
| `invalid_memory_kind` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/markdown-store.ts:266` |
|
|
105
106
|
| `invalid_model_selection` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/model-selection.ts:21` |
|
|
106
107
|
| `invalid_processor_options` | domain | ConfigurationError | `packages/sdk/src/built-in-processors.ts:86` |
|
|
107
108
|
| `invalid_redaction_pattern` | domain | ConfigurationError | `packages/sdk/src/security.ts:84` |
|
|
@@ -120,7 +121,7 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
120
121
|
| `live_session_protected` | domain | — | `packages/sdk/src/internal/persistence/transcript-ops.ts:48` +1 |
|
|
121
122
|
| `local_provider_http_error` | domain | ConfigurationError | `packages/sdk/src/internal/catalog/local-models.ts:61` |
|
|
122
123
|
| `local_provider_unreachable` | domain | ConfigurationError | `packages/sdk/src/internal/catalog/local-models.ts:46` |
|
|
123
|
-
| `malformed_api_key` | domain | AuthenticationError | `packages/sdk/src/internal/agent/helpers.ts:
|
|
124
|
+
| `malformed_api_key` | domain | AuthenticationError | `packages/sdk/src/internal/agent/helpers.ts:231` +1 |
|
|
124
125
|
| `max_delegation_depth` | domain | MaxDelegationDepthError | `packages/sdk/src/a2a/subagent.ts:239` +1 |
|
|
125
126
|
| `mcp_buffer_overflow` | domain | NetworkError | `packages/sdk/src/internal/mcp/client.ts:332` |
|
|
126
127
|
| `mcp_closed` | domain | NetworkError | `packages/sdk/src/internal/mcp/client.ts:307` |
|
|
@@ -129,16 +130,17 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
129
130
|
| `mcp_http_error` | domain | NetworkError | `packages/sdk/src/internal/mcp/client.ts:497` +1 |
|
|
130
131
|
| `mcp_not_init` | domain | ConfigurationError | `packages/sdk/src/internal/mcp/client.ts:265` +2 |
|
|
131
132
|
| `mcp_timeout` | domain | NetworkError | `packages/sdk/src/internal/mcp/client.ts:69` |
|
|
133
|
+
| `memory_ceiling_reached` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/fact-ceiling.ts:43` +1 |
|
|
132
134
|
| `memory_context_missing_user_id` | domain | ConfigurationError | `packages/sdk/src/internal/local-agent/local-agent-memory-direct.ts:128` |
|
|
133
135
|
| `memory_path_escapes_root` | domain | ConfigurationError | `packages/sdk/src/internal/memory/tools.ts:115` +1 |
|
|
134
|
-
| `memory_path_traversal` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
135
|
-
| `memory_threat_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/markdown-store.ts:
|
|
136
|
+
| `memory_path_traversal` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:285` |
|
|
137
|
+
| `memory_threat_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/memory/storage/markdown-store.ts:283` |
|
|
136
138
|
| `memory_tool_bad_args` | domain | ConfigurationError | `packages/sdk/src/internal/memory/tools.ts:138` +1 |
|
|
137
139
|
| `migration_destination_exists` | domain | ConfigurationError | `packages/sdk/src/internal/memory/migrate-sqlite-to-lance.ts:159` +1 |
|
|
138
|
-
| `missing_api_key` | domain | AuthenticationError, ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:
|
|
140
|
+
| `missing_api_key` | domain | AuthenticationError, ConfigurationError | `packages/sdk/src/internal/agent/helpers.ts:222` +2 |
|
|
139
141
|
| `missing_credential` | domain | AuthenticationError | `packages/sdk/src/internal/providers/builtin/openai-chatgpt.ts:72` |
|
|
140
142
|
| `missing_frontmatter` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/skill-frontmatter.ts:88` |
|
|
141
|
-
| `missing_model` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
143
|
+
| `missing_model` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:37` |
|
|
142
144
|
| `model_unavailable` | domain | buildErrorMetadata | `packages/sdk/src/internal/error-mappers/ollama.ts:75` +1 |
|
|
143
145
|
| `network` | domain | MemoryAdapterError, buildErrorMetadata | `packages/memory-honcho/src/adapter.ts:258` +5 |
|
|
144
146
|
| `network_error` | domain | NetworkError | `packages/sdk/src/internal/http.ts:99` |
|
|
@@ -173,16 +175,16 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
173
175
|
| `plugin_manifest_invalid` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/plugin-loader/plugins-manager.ts:185` |
|
|
174
176
|
| `plugin_manifest_shape` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/plugin-loader/plugins-manager.ts:191` |
|
|
175
177
|
| `plugin_missing_manifest` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/plugin-loader/plugins-manager.ts:176` +2 |
|
|
176
|
-
| `programmatic_hooks_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
178
|
+
| `programmatic_hooks_rejected` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:99` |
|
|
177
179
|
| `provider_unresolved` | domain | ConfigurationError | `packages/sdk/src/internal/llm/router.ts:99` +1 |
|
|
178
180
|
| `rate_limit` | domain | — | `packages/sdk/src/internal/error-mappers/vertex.ts:47` |
|
|
179
181
|
| `rate_limited` | domain | MemoryAdapterError | `packages/memory-honcho/src/adapter.ts:244` +3 |
|
|
180
182
|
| `redirect_blocked` | domain | — | `packages/sdk-tools/src/internal/network-guard.ts:38` |
|
|
181
183
|
| `repo_provision_failed` | domain | — | `packages/sdk/src/sandbox/provision.ts:33` |
|
|
182
|
-
| `reserved_env_prefix` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
184
|
+
| `reserved_env_prefix` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:110` |
|
|
183
185
|
| `run_cancelled` | domain | — | `packages/sdk/src/errors.ts:653` |
|
|
184
186
|
| `run_not_found` | domain | UnknownAgentError | `packages/sdk/src/agent.ts:424` |
|
|
185
|
-
| `runtime_exclusive` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
187
|
+
| `runtime_exclusive` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:90` |
|
|
186
188
|
| `sandbox_derived_helper_failed` | domain | ConfigurationError | `packages/sdk/src/sandbox/types.ts:185` +1 |
|
|
187
189
|
| `sandbox_not_available` | domain | SandboxNotAvailableError | `packages/sdk/src/sandbox/types.ts:92` +1 |
|
|
188
190
|
| `sandbox_security` | domain | SandboxSecurityError | `packages/sdk/src/sandbox/types.ts:73` +1 |
|
|
@@ -200,9 +202,9 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
200
202
|
| `stream_truncated` | domain | NetworkError | `packages/sdk/src/internal/llm/anthropic.ts:185` +1 |
|
|
201
203
|
| `subagent_foreign_runtime_field` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:245` |
|
|
202
204
|
| `subagent_mcp_unsupported_local` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:272` |
|
|
203
|
-
| `subagent_missing_description` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
205
|
+
| `subagent_missing_description` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:144` |
|
|
204
206
|
| `subagent_missing_frontmatter` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:349` |
|
|
205
|
-
| `subagent_missing_prompt` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
207
|
+
| `subagent_missing_prompt` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:149` |
|
|
206
208
|
| `subagent_reasoning_effort_without_model` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:292` |
|
|
207
209
|
| `subagent_sandbox_not_boolean` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:312` |
|
|
208
210
|
| `subagent_unknown_field` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/skills/subagents-loader.ts:249` |
|
|
@@ -217,13 +219,13 @@ Branch on `code`, never on the message: messages carry context (an id, a path, a
|
|
|
217
219
|
| `task_not_found` | domain | — | `packages/sdk/src/task-errors.ts:53` +1 |
|
|
218
220
|
| `task_op_unsupported` | domain | — | `packages/sdk/src/task-errors.ts:73` +1 |
|
|
219
221
|
| `timeout` | domain | — | `packages/sdk/src/internal/error-mappers/vertex.ts:55` |
|
|
220
|
-
| `tool_invalid_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
221
|
-
| `tool_invalid_schema_type` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
222
|
-
| `tool_missing_description` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
223
|
-
| `tool_missing_handler` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
224
|
-
| `tool_missing_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
225
|
-
| `tool_missing_schema` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
226
|
-
| `tool_reserved_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:
|
|
222
|
+
| `tool_invalid_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:241` |
|
|
223
|
+
| `tool_invalid_schema_type` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:270` |
|
|
224
|
+
| `tool_missing_description` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:256` |
|
|
225
|
+
| `tool_missing_handler` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:228` |
|
|
226
|
+
| `tool_missing_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:236` |
|
|
227
|
+
| `tool_missing_schema` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:265` |
|
|
228
|
+
| `tool_reserved_name` | domain | ConfigurationError | `packages/sdk/src/internal/runtime/validation/validate-agent-options.ts:247` |
|
|
227
229
|
| `transport_failure` | domain | NetworkError | `packages/sdk/src/internal/llm/transport-error.ts:48` |
|
|
228
230
|
| `transport_unavailable` | domain | ConfigurationError | `packages/sdk/src/internal/llm/router.ts:525` +1 |
|
|
229
231
|
| `unimplemented_budget_scope` | domain | ConfigurationError | `packages/sdk/src/internal/budget/registry.ts:58` +1 |
|
|
@@ -4,7 +4,7 @@ Every public symbol the TheoKit workspace publishes, and the exact specifier to
|
|
|
4
4
|
|
|
5
5
|
A symbol listed under two specifiers is reachable from both, but that does NOT make the two interchangeable: a class emitted separately into a subpath entry is a distinct nominal type from the one in the root bundle, so passing one where the other is expected fails on a private field. When a symbol appears twice, import it and everything it is passed to from the SAME specifier.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1233 export(s) across 46 entry point(s).
|
|
8
8
|
|
|
9
9
|
## `@theokit/acp`
|
|
10
10
|
|
|
@@ -565,6 +565,7 @@ A symbol listed under two specifiers is reachable from both, but that does NOT m
|
|
|
565
565
|
| `buildErrorMetadata` | function | Build an `ErrorMetadata` object with all optional fields included conditionally (no `undefined` keys in the output). |
|
|
566
566
|
| `CategorizedFact` | interface | A fact stored under a typed category. |
|
|
567
567
|
| `CategorizedMemory` | interface | A typed, category-partitioned markdown memory store. |
|
|
568
|
+
| `ceilingReachedError` | function | The refusal of a new fact at a store's declared ceiling: where the store is, how many facts it holds, the ceiling, and what to consolidate. |
|
|
568
569
|
| `chunkMarkdown` | function | Split markdown into the chunks that get embedded and searched. |
|
|
569
570
|
| `ChunkMarkdownOptions` | interface | Split a markdown document into semantically meaningful chunks (ADR D1 of memory-system-peer-project-parity). |
|
|
570
571
|
| `CircuitBreaker` | class | Stops calling a recall path that keeps timing out. |
|
|
@@ -618,6 +619,7 @@ A symbol listed under two specifiers is reachable from both, but that does NOT m
|
|
|
618
619
|
| `indexBudgetWarning` | function | What to say about an index that the interop partner will truncate, or `undefined` when there is nothing true to say. |
|
|
619
620
|
| `IndexManager` | class | The default memory index: SQLite for storage, FTS5 for text matching, and sqlite-vec for vectors when an embedding runtime is supplied. |
|
|
620
621
|
| `IndexStatus` | interface | A snapshot of index health. |
|
|
622
|
+
| `InMemoryMarkdownProviderOptions` | interface | Options for {@link createInMemoryMarkdownProvider } . |
|
|
621
623
|
| `isLanceAvailable` | function | Test helper for {@link LanceIndex } : indicates whether the Lance module is loadable in the current environment. |
|
|
622
624
|
| `isSqliteVecLoaded` | function | Check whether sqlite-vec is loaded by running a tiny version query. |
|
|
623
625
|
| `jinaMemoryEmbeddingProviderAdapter` | const | Jina AI embeddings, over the standard OpenAI wire. |
|
|
@@ -690,6 +692,7 @@ A symbol listed under two specifiers is reachable from both, but that does NOT m
|
|
|
690
692
|
| `remPhase` | function | REM phase — single-link agglomerative clustering by cosine similarity. |
|
|
691
693
|
| `renderDiaryEntry` | function | One diary entry as the markdown that gets appended: a timestamp heading, the short entry hash, and the counts the sweep produced. |
|
|
692
694
|
| `resetMigrationStateForTests` | function | Test-only — reset the in-process migration flag map. |
|
|
695
|
+
| `resolveMaxFacts` | function | The ceiling a caller declared, or `undefined` when none was declared. |
|
|
693
696
|
| `resolveMemoryRoot` | function | The memory root for this agent: `memory.directory` when set, the project store otherwise. |
|
|
694
697
|
| `runActiveMemory` | function | Run one blocking recall before the system prompt is assembled, and report what happened. |
|
|
695
698
|
| `RunActiveMemoryArgs` | interface | Everything {@link runActiveMemory } needs for one attempt. |
|
|
@@ -1065,6 +1068,7 @@ A symbol listed under two specifiers is reachable from both, but that does NOT m
|
|
|
1065
1068
|
| `appendFact` | function | Record a fact, honouring the `enabled` gate on {@link MemoryConfig } : when memory is disabled the call resolves without touching disk. |
|
|
1066
1069
|
| `appendFactToMarkdown` | function | Write a fact as its own memory file and point the `MEMORY.md` index at it. |
|
|
1067
1070
|
| `asMemoryRoot` | function | Treat a directory as a memory root without resolving one. |
|
|
1071
|
+
| `ceilingReachedError` | function | The refusal of a new fact at a store's declared ceiling: where the store is, how many facts it holds, the ceiling, and what to consolidate. |
|
|
1068
1072
|
| `claudeProjectMemoryDir` | function | Where the Claude Code CLI keeps THIS project's memories. |
|
|
1069
1073
|
| `collectMarkdownFiles` | function | Every markdown file the memory corpus holds: the index, the per-memory files, `notes/`, `wiki/` and `sessions/`, each tagged with the bucket `memory_search`'s `corpus` filters on. |
|
|
1070
1074
|
| `defaultIndexPath` | function | `<index root>/.index/memory.sqlite`. |
|
|
@@ -1090,6 +1094,7 @@ A symbol listed under two specifiers is reachable from both, but that does NOT m
|
|
|
1090
1094
|
| `readFacts` | function | Every memory in the store, honouring the `enabled` gate on {@link MemoryConfig } : when memory is disabled the call resolves to `[]` without touching disk. |
|
|
1091
1095
|
| `readFactsFromMarkdown` | function | Every memory in the store: the per-memory files, plus any legacy `## Facts` bullets still in `MEMORY.md`. |
|
|
1092
1096
|
| `renderDiaryEntry` | function | One diary entry as the markdown that gets appended: a timestamp heading, the short entry hash, and the counts the sweep produced. |
|
|
1097
|
+
| `resolveMaxFacts` | function | The ceiling a caller declared, or `undefined` when none was declared. |
|
|
1093
1098
|
| `resolveMemoryRoot` | function | The memory root for this agent: `memory.directory` when set, the project store otherwise. |
|
|
1094
1099
|
| `SessionFile` | interface | Session summary discovery (ADR D20). |
|
|
1095
1100
|
| `sessionsDir` | function | `<memory root>/sessions`. |
|
package/package.json
CHANGED