@theokit/sdk 5.9.2 → 5.11.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/{agent-WQ4LLTSY.cjs → agent-5DOR5T3E.cjs} +12 -10
  3. package/dist/{agent-WQ4LLTSY.cjs.map → agent-5DOR5T3E.cjs.map} +1 -1
  4. package/dist/{agent-enXXu447.d.ts → agent-CmKwSyyD.d.ts} +27 -0
  5. package/dist/{agent-GZVZXKME.js → agent-GWKRARAL.js} +11 -9
  6. package/dist/{agent-GZVZXKME.js.map → agent-GWKRARAL.js.map} +1 -1
  7. package/dist/{agent-Dh2YHSLj.d.cts → agent-XMkGzff_.d.cts} +27 -0
  8. package/dist/{chunk-PEIV7LOU.js → chunk-4PHSVR2R.js} +68 -37
  9. package/dist/chunk-4PHSVR2R.js.map +1 -0
  10. package/dist/{chunk-KXKT3HSD.cjs → chunk-5H2GJXGC.cjs} +4 -4
  11. package/dist/{chunk-KXKT3HSD.cjs.map → chunk-5H2GJXGC.cjs.map} +1 -1
  12. package/dist/{chunk-JU76WCW7.cjs → chunk-5OHB5HJF.cjs} +123 -92
  13. package/dist/chunk-5OHB5HJF.cjs.map +1 -0
  14. package/dist/{chunk-EVH3OUMY.js → chunk-74OA5DIM.js} +189 -43
  15. package/dist/chunk-74OA5DIM.js.map +1 -0
  16. package/dist/chunk-7KIN3L2S.js +103 -0
  17. package/dist/chunk-7KIN3L2S.js.map +1 -0
  18. package/dist/{chunk-EXI7EHDN.js → chunk-7Y23B7XX.js} +3 -3
  19. package/dist/{chunk-EXI7EHDN.js.map → chunk-7Y23B7XX.js.map} +1 -1
  20. package/dist/chunk-ASK2OQM5.cjs +84 -0
  21. package/dist/chunk-ASK2OQM5.cjs.map +1 -0
  22. package/dist/{chunk-QY3CIMUV.cjs → chunk-BZKF7TXX.cjs} +192 -44
  23. package/dist/chunk-BZKF7TXX.cjs.map +1 -0
  24. package/dist/{chunk-ZDF2IJKC.js → chunk-DPY2M74T.js} +3 -3
  25. package/dist/{chunk-ZDF2IJKC.js.map → chunk-DPY2M74T.js.map} +1 -1
  26. package/dist/{chunk-X7Z2FEI7.cjs → chunk-FYEHYQ7T.cjs} +8 -8
  27. package/dist/{chunk-X7Z2FEI7.cjs.map → chunk-FYEHYQ7T.cjs.map} +1 -1
  28. package/dist/{chunk-OZLQ6ZFM.js → chunk-IZDDSLL2.js} +4 -3
  29. package/dist/chunk-IZDDSLL2.js.map +1 -0
  30. package/dist/{chunk-DLUCTZ56.cjs → chunk-L3C6CCBG.cjs} +15 -15
  31. package/dist/{chunk-DLUCTZ56.cjs.map → chunk-L3C6CCBG.cjs.map} +1 -1
  32. package/dist/chunk-LZJXEPEQ.js +58 -0
  33. package/dist/chunk-LZJXEPEQ.js.map +1 -0
  34. package/dist/{chunk-FR6COQXZ.cjs → chunk-N7ZIPW2N.cjs} +3 -3
  35. package/dist/{chunk-FR6COQXZ.cjs.map → chunk-N7ZIPW2N.cjs.map} +1 -1
  36. package/dist/{chunk-RX3CFWQ2.cjs → chunk-NQVWSSX2.cjs} +6 -5
  37. package/dist/chunk-NQVWSSX2.cjs.map +1 -0
  38. package/dist/chunk-NWS2CS3Y.cjs +61 -0
  39. package/dist/chunk-NWS2CS3Y.cjs.map +1 -0
  40. package/dist/chunk-R5YPDAKD.cjs +108 -0
  41. package/dist/chunk-R5YPDAKD.cjs.map +1 -0
  42. package/dist/chunk-RGQICWFU.js +82 -0
  43. package/dist/chunk-RGQICWFU.js.map +1 -0
  44. package/dist/{chunk-N7KYUNFO.js → chunk-W2ZVTEOD.js} +3 -3
  45. package/dist/{chunk-N7KYUNFO.js.map → chunk-W2ZVTEOD.js.map} +1 -1
  46. package/dist/{chunk-47Z6LP3I.js → chunk-X3LOJJMO.js} +88 -28
  47. package/dist/chunk-X3LOJJMO.js.map +1 -0
  48. package/dist/{chunk-M2CCHC66.js → chunk-Z5BAFHPU.js} +3 -3
  49. package/dist/{chunk-M2CCHC66.js.map → chunk-Z5BAFHPU.js.map} +1 -1
  50. package/dist/{chunk-VZ2JZ2EH.cjs → chunk-ZD66GFH5.cjs} +89 -27
  51. package/dist/chunk-ZD66GFH5.cjs.map +1 -0
  52. package/dist/{compact-session-4QHBBGEY.cjs → compact-session-2AETONOS.cjs} +12 -12
  53. package/dist/{compact-session-4QHBBGEY.cjs.map → compact-session-2AETONOS.cjs.map} +1 -1
  54. package/dist/{compact-session-PVAWTKKL.js → compact-session-F2UDGZCN.js} +3 -3
  55. package/dist/{compact-session-PVAWTKKL.js.map → compact-session-F2UDGZCN.js.map} +1 -1
  56. package/dist/{cron-DTuaPMVI.d.ts → cron-BPccQCO8.d.ts} +1 -1
  57. package/dist/{cron-QTgcwcZt.d.cts → cron-Blj6wMYg.d.cts} +1 -1
  58. package/dist/cron.cjs +11 -9
  59. package/dist/cron.d.cts +2 -2
  60. package/dist/cron.d.ts +2 -2
  61. package/dist/cron.js +10 -8
  62. package/dist/eval.cjs +14 -12
  63. package/dist/eval.cjs.map +1 -1
  64. package/dist/eval.js +10 -8
  65. package/dist/eval.js.map +1 -1
  66. package/dist/fs-session-store-3VI4CPTX.js +11 -0
  67. package/dist/{fs-session-store-LCZBM3LT.js.map → fs-session-store-3VI4CPTX.js.map} +1 -1
  68. package/dist/fs-session-store-IQ6PEHYF.cjs +20 -0
  69. package/dist/{fs-session-store-SVEMRE4K.cjs.map → fs-session-store-IQ6PEHYF.cjs.map} +1 -1
  70. package/dist/{index-manager-6B2UF5S3.js → index-manager-JVYIVTZG.js} +5 -4
  71. package/dist/{index-manager-6B2UF5S3.js.map → index-manager-JVYIVTZG.js.map} +1 -1
  72. package/dist/{index-manager-M3FHJCHQ.cjs → index-manager-YGXRFJMZ.cjs} +6 -5
  73. package/dist/{index-manager-M3FHJCHQ.cjs.map → index-manager-YGXRFJMZ.cjs.map} +1 -1
  74. package/dist/index.cjs +62 -145
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +12 -3
  77. package/dist/index.d.ts +12 -3
  78. package/dist/index.js +27 -115
  79. package/dist/index.js.map +1 -1
  80. package/dist/{inject-session-O24XGRR6.js → inject-session-E5UDFVLI.js} +3 -3
  81. package/dist/{inject-session-O24XGRR6.js.map → inject-session-E5UDFVLI.js.map} +1 -1
  82. package/dist/{inject-session-OMLFL2V3.cjs → inject-session-W4UKACYJ.cjs} +4 -4
  83. package/dist/{inject-session-OMLFL2V3.cjs.map → inject-session-W4UKACYJ.cjs.map} +1 -1
  84. package/dist/internal/budget/normalize-usage.d.ts +81 -0
  85. package/dist/internal/llm/anthropic-shared.d.ts +6 -0
  86. package/dist/internal/llm/openai.d.ts +3 -5
  87. package/dist/internal/llm/openrouter-system.d.ts +1 -0
  88. package/dist/internal/llm/types.d.ts +10 -5
  89. package/dist/internal/memory/dreaming/phases.d.ts +2 -0
  90. package/dist/internal/memory/dreaming/run.d.ts +5 -0
  91. package/dist/internal/memory/sdk-memory-peer-loader.d.ts +2 -0
  92. package/dist/internal/memory/storage/fact-ceiling.d.cts +19 -0
  93. package/dist/internal/memory/storage/fact-ceiling.d.ts +19 -0
  94. package/dist/internal/memory/storage/index.cjs +40 -31
  95. package/dist/internal/memory/storage/index.d.cts +1 -0
  96. package/dist/internal/memory/storage/index.d.ts +1 -0
  97. package/dist/internal/memory/storage/index.js +3 -2
  98. package/dist/internal/memory/storage/markdown-store.d.cts +3 -1
  99. package/dist/internal/memory/storage/markdown-store.d.ts +3 -1
  100. package/dist/internal/memory/types.d.ts +6 -0
  101. package/dist/internal/persistence/index.cjs +7 -6
  102. package/dist/internal/persistence/index.cjs.map +1 -1
  103. package/dist/internal/persistence/index.js +2 -1
  104. package/dist/internal/persistence/index.js.map +1 -1
  105. package/dist/internal/runtime/registry/agent-registry-store.d.ts +2 -0
  106. package/dist/models.cjs +5 -50
  107. package/dist/models.cjs.map +1 -1
  108. package/dist/models.js +3 -51
  109. package/dist/models.js.map +1 -1
  110. package/dist/persistence.cjs +7 -6
  111. package/dist/persistence.cjs.map +1 -1
  112. package/dist/persistence.js +2 -1
  113. package/dist/persistence.js.map +1 -1
  114. package/dist/subagents-loader.d.cts +1 -1
  115. package/dist/subagents-loader.d.ts +1 -1
  116. package/dist/types/agent.d.ts +27 -0
  117. package/docs/error-codes.md +29 -26
  118. package/docs/harness-capability-map.md +6 -1
  119. package/package.json +1 -1
  120. package/dist/chunk-47Z6LP3I.js.map +0 -1
  121. package/dist/chunk-BKMDRBJ7.js +0 -180
  122. package/dist/chunk-BKMDRBJ7.js.map +0 -1
  123. package/dist/chunk-EVH3OUMY.js.map +0 -1
  124. package/dist/chunk-JU76WCW7.cjs.map +0 -1
  125. package/dist/chunk-OF2CLCTH.cjs +0 -186
  126. package/dist/chunk-OF2CLCTH.cjs.map +0 -1
  127. package/dist/chunk-OZLQ6ZFM.js.map +0 -1
  128. package/dist/chunk-PEIV7LOU.js.map +0 -1
  129. package/dist/chunk-QY3CIMUV.cjs.map +0 -1
  130. package/dist/chunk-RX3CFWQ2.cjs.map +0 -1
  131. package/dist/chunk-VZ2JZ2EH.cjs.map +0 -1
  132. package/dist/fs-session-store-LCZBM3LT.js +0 -10
  133. package/dist/fs-session-store-SVEMRE4K.cjs +0 -19
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":["sessions","transcriptKey","hydratedKeys","persistTurn","diag","readSessionMessages"],"mappings":"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","file":"chunk-FR6COQXZ.cjs","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./types.js\";\n\nimport type { SessionMessage } from \"./types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { globalSingleton } from \"../global-singleton.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n // Through the helper, with the SAME key compact-session.ts uses: two files hand-rolling one slot is\n // how two copies of a mechanism drift apart.\n globalSingleton(\n \"theokit-sdk.session.auto-compact-attempts\",\n () => new Map<string, number>(),\n ).clear();\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":["sessions","transcriptKey","hydratedKeys","persistTurn","diag","readSessionMessages"],"mappings":";;;;;;;AAyCA,IAAM,aAAA,uBAAoB,GAAA,EAA2B;AACrD,IAAM,YAAA,uBAAmB,GAAA,EAAoB;AAStC,SAAS,oBAAA,CAAqB,SAAiB,OAAA,EAA+B;AACnF,EAAA,MAAM,QAAA,GAAWA,0BAAA,CAAS,GAAA,CAAI,OAAO,KAAK,EAAC;AAC3C,EAAA,QAAA,CAAS,KAAK,OAAO,CAAA;AAIrB,EAAAA,0BAAA,CAAS,OAAO,OAAO,CAAA;AACvB,EAAAA,0BAAA,CAAS,GAAA,CAAI,SAAS,QAAQ,CAAA;AAC9B,EAAA,cAAA,EAAe;AACjB;AAWO,IAAM,mBAAA,GAAsB,EAAA;AAEnC,SAAS,cAAA,GAAuB;AAC9B,EAAA,OAAOA,0BAAA,CAAS,OAAO,mBAAA,EAAqB;AAC1C,IAAA,MAAM,MAAA,GAASA,0BAAA,CAAS,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AACtC,IAAA,IAAI,WAAW,MAAA,EAAW;AAC1B,IAAAA,0BAAA,CAAS,OAAO,MAAM,CAAA;AACtB,IAAA,iBAAA,CAAkB,MAAM,CAAA;AAAA,EAC1B;AACF;AAeA,SAAS,kBAAkB,OAAA,EAAuB;AAChD,EAAA,MAAM,MAAA,GAASC,gCAAc,EAAA,EAAI,OAAO,EAAE,KAAA,CAAM,CAAA,GAAI,OAAA,CAAQ,MAAA,GAAS,CAAC,CAAA;AACtE,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAGC,8BAAY,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAGA,8BAAA,CAAa,MAAA,CAAO,CAAC,CAAA;AAChF,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,aAAA,CAAc,MAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG,aAAA,CAAc,OAAO,CAAC,CAAA;AACzF,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,YAAA,CAAa,MAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG,YAAA,CAAa,OAAO,CAAC,CAAA;AACzF;AAaO,SAAS,cAAA,CAAe,KAAa,OAAA,EAAyB;AACnE,EAAA,MAAM,GAAA,GAAMD,+BAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AACtC,EAAA,IAAI,OAAA,GAAU,CAAA;AAUd,EAAA,IAAIC,8BAAA,CAAa,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC9B,EAAA,IAAI,aAAA,CAAc,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC/B,EAAA,IAAI,YAAA,CAAa,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC9B,EAAA,OAAO,OAAA;AACT;AAEO,SAAS,mBAAmB,OAAA,EAAmC;AACpE,EAAA,OAAOF,0BAAA,CAAS,GAAA,CAAI,OAAO,CAAA,IAAK,EAAC;AACnC;AAWO,SAAS,uBAAA,CACd,KAAA,EACA,GAAA,EACA,SAAA,EACA,MACA,SAAA,EACM;AACN,EAAA,MAAM,GAAA,GAAMC,+BAAA,CAAc,GAAA,CAAI,GAAA,EAAK,IAAI,OAAO,CAAA;AAM9C,EAAA,MAAM,OAAA,GAAA,CAAW,cAAc,GAAA,CAAI,GAAG,KAAK,OAAA,CAAQ,OAAA,EAAQ,EAAG,IAAA,CAAK,YAAY;AAC7E,IAAA,IAAI;AACF,MAAA,MAAME,6BAAA,CAAY,KAAA,EAAO,GAAA,EAAK,SAAA,EAAW,IAAI,CAAA;AAC7C,MAAA,MAAM,KAAA,GAAA,CAAS,YAAA,CAAa,GAAA,CAAI,GAAG,KAAK,CAAA,IAAK,CAAA;AAC7C,MAAA,YAAA,CAAa,GAAA,CAAI,KAAK,KAAK,CAAA;AAI3B,MAAA,IAAI,IAAA,CAAK,gBAAgB,KAAA,CAAA,EAAW;AAClC,QAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,MAAM,OAAO,gCAAsB,CAAA;AACnE,QAAA,MAAM,KAAA,GAAQ,MAAM,mBAAA,CAAoB;AAAA,UACtC,KAAA;AAAA,UACA,GAAA;AAAA,UACA,SAAA;AAAA,UACA,UAAA,EAAY,KAAK,WAAA,CAAY,UAAA;AAAA,UAC7B,aAAA,EAAe,KAAK,WAAA,CAAY,aAAA;AAAA,UAChC,SAAA,EAAW,KAAA;AAAA,UACX,SAAA,EAAW,KAAK,WAAA,CAAY;AAAA,SAC7B,CAAA;AACD,QAAA,IAAI,OAAO,SAAA,IAAY;AAAA,MACzB;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,MAAM,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACjE,MAAAC,sBAAA,CAAK,CAAA,+CAAA,EAAkD,GAAA,CAAI,OAAO,CAAA,GAAA,EAAM,GAAG;AAAA,CAAI,CAAA;AAAA,IACjF;AAAA,EACF,CAAC,CAAA;AACD,EAAA,aAAA,CAAc,GAAA;AAAA,IACZ,GAAA;AAAA,IACA,OAAA,CAAQ,IAAA;AAAA,MACN,MAAM,MAAA;AAAA,MACN,MAAM;AAAA;AACR,GACF;AACF;AAQA,eAAsB,cAAA,CACpB,SACA,GAAA,EACe;AACf,EAAA,MAAM,GAAA,GAAMH,+BAAA,CAAc,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,IAAIC,8BAAA,CAAa,GAAA,CAAI,GAAG,CAAA,EAAG;AAC3B,EAAAA,8BAAA,CAAa,IAAI,GAAG,CAAA;AAEpB,EAAA,MAAM,SAAA,GAAY,MAAMG,qCAAA,CAAoB,GAAA,CAAI,OAAO,OAAO,CAAA;AAC9D,EAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAM5B,EAAAL,0BAAA,CAAS,GAAA,CAAI,SAAS,SAAS,CAAA;AACjC;AAQA,eAAsB,kBAAA,GAAoC;AACxD,EAAA,OAAO,aAAA,CAAc,OAAO,CAAA,EAAG;AAC7B,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,aAAA,CAAc,QAAQ,CAAA;AAC7C,IAAA,aAAA,CAAc,KAAA,EAAM;AACpB,IAAA,MAAM,OAAA,CAAQ,IAAI,GAAG,CAAA;AAAA,EACvB;AACF;AAkBO,SAAS,aAAa,OAAA,EAAuB;AAClD,EAAAA,0BAAA,CAAS,OAAO,OAAO,CAAA;AACzB;AAOO,SAAS,mBAAA,CACd,GAAA,EACA,OAAA,EACA,EAAA,EACY;AACZ,EAAA,MAAM,GAAA,GAAMC,+BAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AACtC,EAAA,MAAM,QAAQ,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA,IAAK,QAAQ,OAAA,EAAQ;AACxD,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,EAAE,CAAA;AAC5B,EAAA,aAAA,CAAc,GAAA;AAAA,IACZ,GAAA;AAAA,IACA,MAAA,CAAO,IAAA;AAAA,MACL,MAAM,MAAA;AAAA,MACN,MAAM;AAAA;AACR,GACF;AACA,EAAA,OAAO,MAAA;AACT","file":"chunk-N7ZIPW2N.cjs","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./types.js\";\n\nimport type { SessionMessage } from \"./types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { globalSingleton } from \"../global-singleton.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n // Through the helper, with the SAME key compact-session.ts uses: two files hand-rolling one slot is\n // how two copies of a mechanism drift apart.\n globalSingleton(\n \"theokit-sdk.session.auto-compact-attempts\",\n () => new Map<string, number>(),\n ).clear();\n}\n"]}
@@ -2,7 +2,8 @@
2
2
 
3
3
  var chunkNKIQTLFA_cjs = require('./chunk-NKIQTLFA.cjs');
4
4
  var chunkZWACQM5U_cjs = require('./chunk-ZWACQM5U.cjs');
5
- var chunkOF2CLCTH_cjs = require('./chunk-OF2CLCTH.cjs');
5
+ var chunkR5YPDAKD_cjs = require('./chunk-R5YPDAKD.cjs');
6
+ var chunkASK2OQM5_cjs = require('./chunk-ASK2OQM5.cjs');
6
7
  var fs = require('fs');
7
8
  var promises = require('fs/promises');
8
9
  var path = require('path');
@@ -79,8 +80,8 @@ var FsSessionStore = class {
79
80
  if (records.length === 0) return;
80
81
  const path$1 = this.#pathFor(agentId);
81
82
  await promises.mkdir(path.dirname(path$1), { recursive: true });
82
- await chunkOF2CLCTH_cjs.withFileLock(path$1, async () => {
83
- for (const record of records) chunkOF2CLCTH_cjs.appendJsonl(path$1, record);
83
+ await chunkASK2OQM5_cjs.withFileLock(path$1, async () => {
84
+ for (const record of records) chunkR5YPDAKD_cjs.appendJsonl(path$1, record);
84
85
  });
85
86
  }
86
87
  /**
@@ -131,5 +132,5 @@ var FsSessionStore = class {
131
132
  };
132
133
 
133
134
  exports.FsSessionStore = FsSessionStore;
134
- //# sourceMappingURL=chunk-RX3CFWQ2.cjs.map
135
- //# sourceMappingURL=chunk-RX3CFWQ2.cjs.map
135
+ //# sourceMappingURL=chunk-NQVWSSX2.cjs.map
136
+ //# sourceMappingURL=chunk-NQVWSSX2.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/fs-session-store.ts"],"names":["acquireSessionWriter","transcriptPath","legacyTranscriptPath","existsSync","readTranscript","path","mkdir","dirname","withFileLock","appendJsonl"],"mappings":";;;;;;;;;;AA2DA,IAAM,YAAA,uBAAmB,GAAA,EAAyD;AAElF,eAAe,cAAc,IAAA,EAA2C;AACtE,EAAA,MAAM,QAAA,GAAW,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACtC,EAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,IAAA,QAAA,CAAS,IAAA,EAAA;AACT,IAAA,OAAO,YAAY,IAAI,CAAA;AAAA,EACzB;AACA,EAAA,MAAM,KAAA,GAAQ,MAAMA,sCAAA,CAAqB,IAAI,CAAA;AAC7C,EAAA,YAAA,CAAa,IAAI,IAAA,EAAM,EAAE,KAAA,EAAO,IAAA,EAAM,GAAG,CAAA;AACzC,EAAA,OAAO,YAAY,IAAI,CAAA;AACzB;AAGA,SAAS,YAAY,IAAA,EAAkC;AACrD,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,IAAA;AAAA;AAAA;AAAA;AAAA,IAIb,OAAO,MAAY;AACjB,MAAA,IAAI,QAAA,EAAU;AACd,MAAA,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG,KAAA,CAAM,KAAA,EAAM;AAAA,IACtC,CAAA;AAAA,IACA,SAAS,YAA2B;AAClC,MAAA,IAAI,QAAA,EAAU;AACd,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACnC,MAAA,IAAI,UAAU,MAAA,EAAW;AACzB,MAAA,KAAA,CAAM,IAAA,EAAA;AACN,MAAA,IAAI,KAAA,CAAM,OAAO,CAAA,EAAG;AACpB,MAAA,YAAA,CAAa,OAAO,IAAI,CAAA;AACxB,MAAA,MAAM,KAAA,CAAM,MAAM,OAAA,EAAQ;AAAA,IAC5B;AAAA,GACF;AACF;AAKO,IAAM,iBAAN,MAA6C;AAAA,EACzC,QAAA;AAAA,EACA,IAAA;AAAA;AAAA,EAEA,OAAA,uBAAc,GAAA,EAAgC;AAAA;AAAA,EAE9C,MAAA,uBAAa,GAAA,EAAoB;AAAA,EAE1C,YAAY,OAAA,EAAgC;AAC1C,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,OAAA;AACxB,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,GAAA;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,SAAS,OAAA,EAAyB;AAChC,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AACnC,IAAA,MAAM,YAAYC,gCAAA,CAAe,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,MAAM,OAAO,CAAA;AAClE,IAAA,MAAM,SAASC,sCAAA,CAAqB,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,MAAM,OAAO,CAAA;AACrE,IAAA,MAAM,WAAW,MAAA,KAAW,SAAA,IAAaC,aAAA,CAAW,MAAM,IAAI,MAAA,GAAS,SAAA;AACvE,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAA,EAAS,QAAQ,CAAA;AACjC,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,OAAA,EAA2C;AAC3D,IAAA,OAAOC,gCAAA,CAAe,IAAA,CAAK,QAAA,CAAS,OAAO,CAAC,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAM,aAAA,CAAc,OAAA,EAAiB,OAAA,EAAkD;AAErF,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC1B,IAAA,MAAMC,MAAA,GAAO,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA;AAElC,IAAA,MAAMC,eAAMC,YAAA,CAAQF,MAAI,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAE9C,IAAA,MAAMG,8BAAA,CAAaH,QAAM,YAAY;AA0BnC,MAAA,KAAA,MAAW,MAAA,IAAU,OAAA,EAASI,6BAAA,CAAYJ,MAAA,EAAM,MAAM,CAAA;AAAA,IACxD,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,QAAQ,OAAA,EAAgC;AAC5C,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA,EAAG;AAC/B,IAAA,MAAMA,MAAA,GAAO,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA;AAClC,IAAA,MAAMC,eAAMC,YAAA,CAAQF,MAAI,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAC9C,IAAA,IAAA,CAAK,QAAQ,GAAA,CAAI,OAAA,EAAS,MAAM,aAAA,CAAcA,MAAI,CAAC,CAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,QAAQ,OAAA,EAAgC;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AACtC,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,IAAA,CAAK,OAAA,CAAQ,OAAO,OAAO,CAAA;AAC3B,IAAA,MAAM,MAAM,OAAA,EAAQ;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAA,GAAyB;AAC7B,IAAA,MAAM,SAAS,CAAC,GAAG,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA;AACxC,IAAA,IAAA,CAAK,QAAQ,KAAA,EAAM;AACnB,IAAA,KAAA,MAAW,KAAA,IAAS,MAAA,EAAQ,MAAM,KAAA,CAAM,OAAA,EAAQ;AAAA,EAClD;AACF","file":"chunk-NQVWSSX2.cjs","sourcesContent":["/**\n * SE41 — `FsSessionStore`, the DEFAULT reference implementation of the pluggable\n * {@link SessionStore} seam. It reads and append-writes the native Claude-shaped\n * `.jsonl` transcript at `<baseDir>/projects/<encoded-cwd>/<agentId>.jsonl` — the\n * exact on-disk format SE40 introduced (the file the Claude Code CLI can\n * `--continue`). Omitting `local.sessionStore` resolves to this store, so the\n * default persistence path is byte-identical to SE40 behavior.\n *\n * `readRecords` is `readTranscript(transcriptPath(...))` (a missing session →\n * `[]`, not an error — a fresh agent has no history). `appendRecords` is a TRUE\n * append: it reads the prior records, concatenates the new-turn delta, and\n * rewrites the whole line set atomically under the SE40 cross-process file lock\n * (`writeTranscript` never shrinks — the native format is an append-only\n * `parentUuid` DAG). The parent dir is created BEFORE acquiring the lock because\n * the lock's companion `<path>.lock` file needs an existing parent dir (the SE40\n * `mkdir(dirname)`-before-lock fix).\n *\n * @internal\n */\n\nimport { existsSync } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { SessionStore } from \"../../types/session-store.js\";\nimport { withFileLock } from \"./file-lock.js\";\nimport { appendJsonl } from \"./jsonl.js\";\nimport {\n legacyTranscriptPath,\n readTranscript,\n type SessionRecord,\n transcriptPath,\n} from \"./session-transcript.js\";\nimport { acquireSessionWriter, type SessionWriterLease } from \"./session-writer.js\";\n\n/** Options identifying the on-disk transcript location for the FS default store. */\nexport interface FsSessionStoreOptions {\n /** Transcript base dir (`~/.theokit` default, `~/.claude` for CLI interop) — already `~`-expanded. */\n baseDir: string;\n /** The workspace cwd whose encoded form is the transcript project dir. */\n cwd: string;\n}\n\n/**\n * Reference-counted leases, shared per path.\n *\n * `acquireSessionWriter` is STRICT on purpose: two concurrent acquisitions of the same path, and\n * exactly one wins — \"a lease that let both through would be decorative\" (the M81 test says exactly\n * that, and it is right). That is the primitive's contract, and it does not change.\n *\n * But within ONE process it is normal to have more than one store over the same session: the golden\n * compaction tests and the concurrent-send tests do exactly that, and they record real runtime\n * behavior. Applying the raw primitive there would turn a legitimate pattern into a\n * `SessionBusyError`.\n *\n * The reconciliation belongs here, in the primitive's consumer: the process takes ONE lease per path\n * and counts how many stores use it. The last to release actually frees it. Cross-process stays\n * strict — which is the problem M81 states (\"`exec resume --last` can write into the TUI's live\n * session\").\n */\nconst sharedLeases = new Map<string, { lease: SessionWriterLease; refs: number }>();\n\nasync function acquireShared(path: string): Promise<SessionWriterLease> {\n const existing = sharedLeases.get(path);\n if (existing !== undefined) {\n existing.refs++;\n return createProxy(path);\n }\n const lease = await acquireSessionWriter(path);\n sharedLeases.set(path, { lease, refs: 1 });\n return createProxy(path);\n}\n\n/** A handle that decrements the count; the last to release frees the real lease. */\nfunction createProxy(path: string): SessionWriterLease {\n let released = false;\n return {\n sessionPath: path,\n // The proxy forwards the renewal to the ONE real lease this path shares. Renewing through any\n // holder is correct and is the point: the record must say \"someone in this process is still\n // writing\", and the refcount already guarantees they are all the same owner.\n renew: (): void => {\n if (released) return;\n sharedLeases.get(path)?.lease.renew();\n },\n release: async (): Promise<void> => {\n if (released) return;\n released = true;\n const entry = sharedLeases.get(path);\n if (entry === undefined) return;\n entry.refs--;\n if (entry.refs > 0) return;\n sharedLeases.delete(path);\n await entry.lease.release();\n },\n };\n}\n\n/**\n * The default `SessionStore` — reads/append-writes the native `.jsonl` transcript.\n */\nexport class FsSessionStore implements SessionStore {\n readonly #baseDir: string;\n readonly #cwd: string;\n /** One lease per `agentId` — a store serves more than one session over the process lifetime. */\n readonly #leases = new Map<string, SessionWriterLease>();\n /** Memoized per agent so read, append and lease can never disagree about which file is the session. */\n readonly #paths = new Map<string, string>();\n\n constructor(options: FsSessionStoreOptions) {\n this.#baseDir = options.baseDir;\n this.#cwd = options.cwd;\n }\n\n /**\n * The file that IS this agent's session.\n *\n * #400 made transcript filenames UUIDs so `claude --continue` can find them. A transcript written\n * before that lives under the old name, and switching to the new one would not lose the file but\n * would abandon it: the agent would start a second, empty history beside its real one. So an\n * existing legacy file wins, and only a session with no file yet gets the new name.\n *\n * Resolved ONCE per agent. Re-deciding per call would let `acquire` lock one path while\n * `appendRecords` wrote another — the lease would be guarding a file nobody was writing.\n */\n #pathFor(agentId: string): string {\n const memoized = this.#paths.get(agentId);\n if (memoized !== undefined) return memoized;\n const canonical = transcriptPath(this.#baseDir, this.#cwd, agentId);\n const legacy = legacyTranscriptPath(this.#baseDir, this.#cwd, agentId);\n const resolved = legacy !== canonical && existsSync(legacy) ? legacy : canonical;\n this.#paths.set(agentId, resolved);\n return resolved;\n }\n\n async readRecords(agentId: string): Promise<SessionRecord[]> {\n return readTranscript(this.#pathFor(agentId));\n }\n\n async appendRecords(agentId: string, records: readonly SessionRecord[]): Promise<void> {\n // Empty delta → nothing to persist (avoids an unnecessary lock + rewrite).\n if (records.length === 0) return;\n const path = this.#pathFor(agentId);\n // mkdir BEFORE the lock: withFileLock's companion `<path>.lock` needs the parent dir.\n await mkdir(dirname(path), { recursive: true });\n\n await withFileLock(path, async () => {\n // M93 — appends the DELTA instead of rewriting the whole file.\n //\n // Before: `readTranscript` + `writeTranscript` of everything, per turn. O(n) of I/O **and** of\n // parsing on every turn, O(n^2) per session — the consumer note in\n // `agents/lib/session/backtrack.ts` records 1.4 MB / 3000 lines over 200 turns.\n //\n // Correct because the format **is already append-only**: the `parentUuid` DAG does not depend\n // on line order, and every record carries its own parent. `appendJsonl` **already existed in\n // the package** and had a single caller (`eval/runner.ts`) — the primitive was there, it was\n // the store that ignored it (rung 4).\n //\n // `withFileLock` stays — but the earlier claim that \"it is what serializes two concurrent\n // `appendRecords`\" was too strong, and M93's adversarial review measured it: removing it fails\n // no test. The reason is the paragraph above — the `parentUuid` DAG does not depend on line\n // order, so two interleaved batches reconstruct identically. The work the lock was doing\n // (protecting a read-modify-write) disappeared along with the rewrite.\n //\n // What it still covers is the TOCTOU window in `needsLineBreakBefore` (read the last byte,\n // then write): without it, two processes can both conclude \"a \\n is missing\" and produce a\n // blank line — which the reader discards, i.e. benign. It stays as a **declared, not\n // mechanized** defense (the discipline in `error-handling.md` § 4: enumerate the residue\n // rather than let a missing test pass for coverage).\n //\n // `writeTranscript` still exists for **compaction**, the one operation that legitimately\n // rewrites the file.\n for (const record of records) appendJsonl(path, record);\n });\n }\n\n /**\n * Takes the session's writer lease. Throws `SessionBusyError` when another process holds it.\n *\n * **Explicit, and NOT inside `appendRecords`** — M95's adversarial review measured why that\n * matters: the `SessionStore` contract states that \"an `appendRecords` rejection is logged to\n * stderr, NOT thrown to the caller (best-effort write)\". Acquiring there made the\n * `SessionBusyError` get **swallowed**, and the result was worse than the original problem:\n * instead of two writers interleaving lines, the loser **lost the turn silently** — nothing on\n * disk, a stderr warning invisible under the TUI, and no way for the caller to react.\n *\n * At init the error reaches someone who can act: `exec` forks to a new id, which is exactly what\n * the error message itself prescribes. It is the difference between failing where a decision is\n * possible and failing where only loss is.\n */\n async acquire(agentId: string): Promise<void> {\n if (this.#leases.has(agentId)) return;\n const path = this.#pathFor(agentId);\n await mkdir(dirname(path), { recursive: true });\n this.#leases.set(agentId, await acquireShared(path));\n }\n\n /**\n * Releases ONE agent's lease.\n *\n * It exists because `dispose()` releases **every** lease the store holds, and a store injected by\n * the consumer may serve several agents: an init that fails for agent B must not free agent A's\n * lease, which is still live and writing.\n */\n async release(agentId: string): Promise<void> {\n const lease = this.#leases.get(agentId);\n if (lease === undefined) return;\n this.#leases.delete(agentId);\n await lease.release();\n }\n\n /**\n * Releases every lease this store holds.\n *\n * Without it the `.writer.lock` outlives the process and the next open would have to wait out the\n * heartbeat window — recoverable, but 30 s of waiting after a CLEAN shutdown would be an avoidable\n * defect. Idempotent: calling it twice is not an error.\n */\n async dispose(): Promise<void> {\n const leases = [...this.#leases.values()];\n this.#leases.clear();\n for (const lease of leases) await lease.release();\n }\n}\n"]}
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ var chunkHU666T3Q_cjs = require('./chunk-HU666T3Q.cjs');
4
+
5
+ // src/internal/llm/model-capabilities.ts
6
+ var CONSERVATIVE_DEFAULTS = {
7
+ supportsVision: false,
8
+ supportsStructuredOutput: false,
9
+ supportsToolUse: false,
10
+ supportsCacheControl: false,
11
+ maxContextTokens: 4096,
12
+ maxOutputTokens: 4096
13
+ };
14
+ var ROUTING_PREFIXES = ["openrouter/", "vertex/", "bedrock/"];
15
+ function capsFromCatalog(m) {
16
+ return {
17
+ supportsVision: m.modalities?.input?.includes("image") ?? m.attachment ?? false,
18
+ supportsStructuredOutput: m.structured_output ?? false,
19
+ supportsToolUse: m.tool_call ?? false,
20
+ supportsCacheControl: m.cache_control ?? false,
21
+ maxContextTokens: m.limit?.context ?? CONSERVATIVE_DEFAULTS.maxContextTokens,
22
+ maxOutputTokens: m.limit?.output ?? CONSERVATIVE_DEFAULTS.maxOutputTokens
23
+ };
24
+ }
25
+ function resolveModelCapabilities(modelId) {
26
+ const bare = stripVariantSuffix(stripRoutingPrefix(modelId));
27
+ const fromIndex = chunkHU666T3Q_cjs.getCatalogModelInfo(bare);
28
+ if (fromIndex !== void 0) return capsFromCatalog(fromIndex);
29
+ const withVendor = inferVendorPrefix(bare);
30
+ if (withVendor !== bare) {
31
+ const vendored = chunkHU666T3Q_cjs.getCatalogModelInfo(withVendor);
32
+ if (vendored !== void 0) return capsFromCatalog(vendored);
33
+ }
34
+ return CONSERVATIVE_DEFAULTS;
35
+ }
36
+ function isAnthropicFamilyModel(modelId) {
37
+ if (modelId === void 0) return false;
38
+ return /^(anthropic\/|claude)/.test(stripRoutingPrefix(modelId));
39
+ }
40
+ function stripRoutingPrefix(modelId) {
41
+ for (const prefix of ROUTING_PREFIXES) {
42
+ if (modelId.startsWith(prefix)) return modelId.slice(prefix.length);
43
+ }
44
+ return modelId;
45
+ }
46
+ function stripVariantSuffix(modelId) {
47
+ const i = modelId.indexOf(":");
48
+ return i >= 0 ? modelId.slice(0, i) : modelId;
49
+ }
50
+ function inferVendorPrefix(bare) {
51
+ if (bare.startsWith("claude")) return `anthropic/${bare}`;
52
+ if (bare.startsWith("gpt-") || bare.startsWith("o1") || bare.startsWith("o3"))
53
+ return `openai/${bare}`;
54
+ if (bare.startsWith("gemini")) return `google/${bare}`;
55
+ return bare;
56
+ }
57
+
58
+ exports.isAnthropicFamilyModel = isAnthropicFamilyModel;
59
+ exports.resolveModelCapabilities = resolveModelCapabilities;
60
+ //# sourceMappingURL=chunk-NWS2CS3Y.cjs.map
61
+ //# sourceMappingURL=chunk-NWS2CS3Y.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/llm/model-capabilities.ts"],"names":["getCatalogModelInfo"],"mappings":";;;;;AAgCA,IAAM,qBAAA,GAA2C;AAAA,EAC/C,cAAA,EAAgB,KAAA;AAAA,EAChB,wBAAA,EAA0B,KAAA;AAAA,EAC1B,eAAA,EAAiB,KAAA;AAAA,EACjB,oBAAA,EAAsB,KAAA;AAAA,EACtB,gBAAA,EAAkB,IAAA;AAAA,EAClB,eAAA,EAAiB;AACnB,CAAA;AAGA,IAAM,gBAAA,GAAmB,CAAC,aAAA,EAAe,SAAA,EAAW,UAAU,CAAA;AAM9D,SAAS,gBAAgB,CAAA,EAAoC;AAC3D,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,EAAE,UAAA,EAAY,KAAA,EAAO,SAAS,OAAO,CAAA,IAAK,EAAE,UAAA,IAAc,KAAA;AAAA,IAC1E,wBAAA,EAA0B,EAAE,iBAAA,IAAqB,KAAA;AAAA,IACjD,eAAA,EAAiB,EAAE,SAAA,IAAa,KAAA;AAAA,IAChC,oBAAA,EAAuB,EAAkC,aAAA,IAAiB,KAAA;AAAA,IAC1E,gBAAA,EAAkB,CAAA,CAAE,KAAA,EAAO,OAAA,IAAW,qBAAA,CAAsB,gBAAA;AAAA,IAC5D,eAAA,EAAiB,CAAA,CAAE,KAAA,EAAO,MAAA,IAAU,qBAAA,CAAsB;AAAA,GAC5D;AACF;AAWO,SAAS,yBAAyB,OAAA,EAAoC;AAC3E,EAAA,MAAM,IAAA,GAAO,kBAAA,CAAmB,kBAAA,CAAmB,OAAO,CAAC,CAAA;AAG3D,EAAA,MAAM,SAAA,GAAYA,sCAAoB,IAAI,CAAA;AAC1C,EAAA,IAAI,SAAA,KAAc,MAAA,EAAW,OAAO,eAAA,CAAgB,SAAS,CAAA;AAG7D,EAAA,MAAM,UAAA,GAAa,kBAAkB,IAAI,CAAA;AACzC,EAAA,IAAI,eAAe,IAAA,EAAM;AACvB,IAAA,MAAM,QAAA,GAAWA,sCAAoB,UAAU,CAAA;AAC/C,IAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAO,eAAA,CAAgB,QAAQ,CAAA;AAAA,EAC7D;AACA,EAAA,OAAO,qBAAA;AACT;AAUO,SAAS,uBAAuB,OAAA,EAAsC;AAC3E,EAAA,IAAI,OAAA,KAAY,QAAW,OAAO,KAAA;AAClC,EAAA,OAAO,uBAAA,CAAwB,IAAA,CAAK,kBAAA,CAAmB,OAAO,CAAC,CAAA;AACjE;AAEA,SAAS,mBAAmB,OAAA,EAAyB;AACnD,EAAA,KAAA,MAAW,UAAU,gBAAA,EAAkB;AACrC,IAAA,IAAI,OAAA,CAAQ,WAAW,MAAM,CAAA,SAAU,OAAA,CAAQ,KAAA,CAAM,OAAO,MAAM,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA;AACT;AAOA,SAAS,mBAAmB,OAAA,EAAyB;AACnD,EAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC7B,EAAA,OAAO,KAAK,CAAA,GAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,GAAI,OAAA;AACxC;AAGA,SAAS,kBAAkB,IAAA,EAAsB;AAC/C,EAAA,IAAI,KAAK,UAAA,CAAW,QAAQ,CAAA,EAAG,OAAO,aAAa,IAAI,CAAA,CAAA;AACvD,EAAA,IAAI,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA,IAAK,IAAA,CAAK,WAAW,IAAI,CAAA,IAAK,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA;AAC1E,IAAA,OAAO,UAAU,IAAI,CAAA,CAAA;AACvB,EAAA,IAAI,KAAK,UAAA,CAAW,QAAQ,CAAA,EAAG,OAAO,UAAU,IAAI,CAAA,CAAA;AACpD,EAAA,OAAO,IAAA;AACT","file":"chunk-NWS2CS3Y.cjs","sourcesContent":["/**\n * T3.10c — Model capability registry (DR3 #17).\n *\n * Typed per-model flags that let the SDK gate features at the boundary\n * (before hitting the provider) instead of letting opaque 400s surface.\n *\n * Resolution algorithm:\n * 1. Strip routing prefixes (openrouter/, vertex/, bedrock/) AND the\n * OpenRouter `:variant` suffix (:free/:nitro/…) to find the bare vendor id.\n * 2. Exact match in the `EXACT` catalog → return entry.\n * 3. Vendor inference (e.g., `claude-*` → `anthropic/claude-*`) → return entry.\n * 4. No match → conservative defaults (all false, minimum tokens).\n *\n * Module is internal, but `ModelCapabilities` + `resolveModelCapabilities` are\n * re-exported publicly via the `@theokit/sdk/models` subpath (see their @public tags).\n */\n\n/**\n * Per-model capability shape. Consumers use this to gate features at\n * the SDK boundary (before request construction, not after 400).\n *\n * @public\n */\nexport interface ModelCapabilities {\n supportsVision: boolean;\n supportsStructuredOutput: boolean;\n supportsToolUse: boolean;\n supportsCacheControl: boolean;\n maxContextTokens: number;\n maxOutputTokens: number;\n}\n\nconst CONSERVATIVE_DEFAULTS: ModelCapabilities = {\n supportsVision: false,\n supportsStructuredOutput: false,\n supportsToolUse: false,\n supportsCacheControl: false,\n maxContextTokens: 4096,\n maxOutputTokens: 4096,\n};\n\n/** Routing prefixes to strip to find the underlying vendor model. */\nconst ROUTING_PREFIXES = [\"openrouter/\", \"vertex/\", \"bedrock/\"] as const;\n\nimport { getCatalogModelInfo } from \"../providers/catalog-loader.js\";\nimport type { CatalogModel } from \"../providers/catalog-schema.js\";\n\n/** Map a catalog model block (models.dev shape) to the SDK's ModelCapabilities (M44 — catalog-backed). */\nfunction capsFromCatalog(m: CatalogModel): ModelCapabilities {\n return {\n supportsVision: m.modalities?.input?.includes(\"image\") ?? m.attachment ?? false,\n supportsStructuredOutput: m.structured_output ?? false,\n supportsToolUse: m.tool_call ?? false,\n supportsCacheControl: (m as { cache_control?: boolean }).cache_control ?? false,\n maxContextTokens: m.limit?.context ?? CONSERVATIVE_DEFAULTS.maxContextTokens,\n maxOutputTokens: m.limit?.output ?? CONSERVATIVE_DEFAULTS.maxOutputTokens,\n };\n}\n\n/**\n * Resolve per-model capability flags (vision/structured-output/tool-use/cache +\n * `maxContextTokens`/`maxOutputTokens`) for a model id. Pure, sync, offline (a\n * static catalog — no network). Strips routing prefixes (`openrouter/`/`vertex/`/\n * `bedrock/`) and OpenRouter `:variant` suffixes before lookup; unknown models get\n * conservative defaults. Public via `@theokit/sdk/models`.\n *\n * @public\n */\nexport function resolveModelCapabilities(modelId: string): ModelCapabilities {\n const bare = stripVariantSuffix(stripRoutingPrefix(modelId));\n // M44 — the catalog model-info index replaced the hand-curated EXACT map (single source of truth; the\n // vendored provider-catalog.json carries the migrated data — parity-tested against the old map snapshot).\n const fromIndex = getCatalogModelInfo(bare);\n if (fromIndex !== undefined) return capsFromCatalog(fromIndex);\n // Routing-prefixed models (vertex/claude-3-5-sonnet) strip to bare\n // model name without vendor. Try inferring the vendor from the name.\n const withVendor = inferVendorPrefix(bare);\n if (withVendor !== bare) {\n const vendored = getCatalogModelInfo(withVendor);\n if (vendored !== undefined) return capsFromCatalog(vendored);\n }\n return CONSERVATIVE_DEFAULTS;\n}\n\n/**\n * B-412 / B-475 — whether a model id names an Anthropic model the loop sends cacheable system\n * blocks to. Decided by the id, not by the catalog's `supportsCacheControl`, which is false for the\n * Anthropic ids measured on OpenRouter. After the same routing prefixes the capability lookup\n * strips (`openrouter/`, `vertex/`, `bedrock/`), the id must start with `anthropic/` or `claude`,\n * so Vertex Anthropic ids match and Bedrock ids (`anthropic.claude-…`) do not.\n * @internal\n */\nexport function isAnthropicFamilyModel(modelId: string | undefined): boolean {\n if (modelId === undefined) return false;\n return /^(anthropic\\/|claude)/.test(stripRoutingPrefix(modelId));\n}\n\nfunction stripRoutingPrefix(modelId: string): string {\n for (const prefix of ROUTING_PREFIXES) {\n if (modelId.startsWith(prefix)) return modelId.slice(prefix.length);\n }\n return modelId;\n}\n\n/**\n * Strip an OpenRouter variant suffix (`:free`/`:nitro`/`:floor`/`:beta`/…) so the\n * catalog lookup hits the underlying model. Model slugs contain no `:` except the\n * variant separator, so cutting at the first `:` is safe.\n */\nfunction stripVariantSuffix(modelId: string): string {\n const i = modelId.indexOf(\":\");\n return i >= 0 ? modelId.slice(0, i) : modelId;\n}\n\n/** Infer vendor prefix from bare model name for routing-prefixed lookups. */\nfunction inferVendorPrefix(bare: string): string {\n if (bare.startsWith(\"claude\")) return `anthropic/${bare}`;\n if (bare.startsWith(\"gpt-\") || bare.startsWith(\"o1\") || bare.startsWith(\"o3\"))\n return `openai/${bare}`;\n if (bare.startsWith(\"gemini\")) return `google/${bare}`;\n return bare;\n}\n"]}
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ var chunkUNFBHV46_cjs = require('./chunk-UNFBHV46.cjs');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
7
+ var JsonlParseError = class extends chunkUNFBHV46_cjs.TheokitAgentError {
8
+ constructor(message, line) {
9
+ super(message, { code: "jsonl_parse_failed", isRetryable: false });
10
+ this.line = line;
11
+ }
12
+ line;
13
+ name = "JsonlParseError";
14
+ };
15
+ function isPlainObject(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function tryParseObjectLine(line) {
19
+ if (line.length === 0) return void 0;
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(line);
23
+ } catch {
24
+ return void 0;
25
+ }
26
+ return isPlainObject(parsed) ? parsed : void 0;
27
+ }
28
+ function loadJsonl(path, opts = {}) {
29
+ const lines = fs.readFileSync(path, "utf8").split("\n");
30
+ const out = [];
31
+ for (let i = 0; i < lines.length; i += 1) {
32
+ const lineNumber = i + 1;
33
+ const line = (lines[i] ?? "").trim();
34
+ if (line.length === 0) continue;
35
+ const parsed = parseLine(
36
+ line,
37
+ lineNumber,
38
+ opts.tolerateTrailingPartialLine === true && lineNumber === lines.length
39
+ );
40
+ if (parsed === void 0) break;
41
+ out.push(opts.map ? opts.map(parsed, lineNumber) : parsed);
42
+ }
43
+ return out;
44
+ }
45
+ function parseLine(line, lineNumber, tolerate) {
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(line);
49
+ } catch {
50
+ if (tolerate) return void 0;
51
+ throw new JsonlParseError(`line ${lineNumber}: invalid JSON`, lineNumber);
52
+ }
53
+ if (!isPlainObject(parsed)) {
54
+ throw new JsonlParseError(`line ${lineNumber}: not a JSON object`, lineNumber);
55
+ }
56
+ return parsed;
57
+ }
58
+ function appendJsonl(path$1, record) {
59
+ fs.mkdirSync(path.dirname(path$1), { recursive: true, mode: 448 });
60
+ const prefix = needsLineBreakBefore(path$1) ? "\n" : "";
61
+ const fd = fs.openSync(path$1, "a", 384);
62
+ try {
63
+ fs.writeSync(fd, `${prefix}${JSON.stringify(record)}
64
+ `);
65
+ } finally {
66
+ fs.closeSync(fd);
67
+ }
68
+ }
69
+ function needsLineBreakBefore(path) {
70
+ let fd;
71
+ try {
72
+ fd = fs.openSync(path, "r");
73
+ } catch {
74
+ return false;
75
+ }
76
+ try {
77
+ const size = fs.fstatSync(fd).size;
78
+ if (size === 0) return false;
79
+ const buf = Buffer.alloc(1);
80
+ fs.readSync(fd, buf, 0, 1, size - 1);
81
+ return buf[0] !== 10;
82
+ } finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ }
86
+ function readJsonlIds(path, keyFn) {
87
+ const done = /* @__PURE__ */ new Set();
88
+ let text;
89
+ try {
90
+ text = fs.readFileSync(path, "utf8");
91
+ } catch {
92
+ return done;
93
+ }
94
+ for (const rawLine of text.split("\n")) {
95
+ const parsed = tryParseObjectLine(rawLine.trim());
96
+ if (parsed === void 0) continue;
97
+ const key = keyFn(parsed);
98
+ if (typeof key === "string" && key.length > 0) done.add(key);
99
+ }
100
+ return done;
101
+ }
102
+
103
+ exports.JsonlParseError = JsonlParseError;
104
+ exports.appendJsonl = appendJsonl;
105
+ exports.loadJsonl = loadJsonl;
106
+ exports.readJsonlIds = readJsonlIds;
107
+ //# sourceMappingURL=chunk-R5YPDAKD.cjs.map
108
+ //# sourceMappingURL=chunk-R5YPDAKD.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/jsonl.ts"],"names":["TheokitAgentError","readFileSync","path","mkdirSync","dirname","openSync","writeSync","closeSync","fstatSync","readSync"],"mappings":"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","file":"chunk-R5YPDAKD.cjs","sourcesContent":["/**\n * Durable JSONL primitives shared by the eval harness (M6).\n *\n * - `loadJsonl` — generic dataset reader (split/trim/skip-blank/parse) with a\n * line-numbered {@link JsonlParseError}. The dataset SCHEMA is the caller's\n * concern via `map` (M6 ADR D3) — this module owns only the parse.\n * - `appendJsonl` / `readJsonlIds` — crash-durable, resumable batch persistence\n * (M6 ADR D1): each record is appended as one whole `\\n`-terminated line the\n * instant it is produced, and a re-run resumes by skipping already-keyed rows.\n *\n * reference: knowledge-base/references/theocode-eval/lib/swebench-dataset.ts:82\n * (parseJsonl + line-N error) and swebench-batch.ts:113,205 (resume + per-line\n * flush).\n *\n * @internal\n */\nimport {\n closeSync,\n fstatSync,\n mkdirSync,\n openSync,\n readFileSync,\n readSync,\n writeSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/** Raised when a JSONL line is not valid JSON or is not a JSON object. Carries the 1-based line number. */\nexport class JsonlParseError extends TheokitAgentError {\n override readonly name = \"JsonlParseError\";\n constructor(\n message: string,\n readonly line: number,\n ) {\n // Not retryable: the bytes on the line do not change between reads.\n super(message, { code: \"jsonl_parse_failed\", isRetryable: false });\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Parse a single trimmed line to a plain object, or `undefined` if blank / invalid / non-object. */\nfunction tryParseObjectLine(line: string): Record<string, unknown> | undefined {\n if (line.length === 0) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return undefined;\n }\n return isPlainObject(parsed) ? parsed : undefined;\n}\n\n/**\n * Parse a JSONL file into rows. Blank lines are skipped. A malformed or\n * non-object line throws {@link JsonlParseError} naming the 1-based line. When\n * `map` is provided, each raw object is mapped to the typed row (the SWE-bench\n * schema lives in the caller's `map`, per M6 ADR D3).\n */\nexport function loadJsonl<T = Record<string, unknown>>(\n path: string,\n opts: {\n map?: (raw: Record<string, unknown>, lineNumber: number) => T;\n /**\n * M81 — tolerate a truncated LAST line (a crash artifact: the process died mid-write).\n *\n * Opt-in on purpose. As a default it would also swallow corruption in the MIDDLE of the file,\n * turning loud data loss into silent data loss — the wrong trade for a session store.\n */\n tolerateTrailingPartialLine?: boolean;\n } = {},\n): T[] {\n const lines = readFileSync(path, \"utf8\").split(\"\\n\");\n const out: T[] = [];\n for (let i = 0; i < lines.length; i += 1) {\n const lineNumber = i + 1;\n const line = (lines[i] ?? \"\").trim();\n if (line.length === 0) continue;\n // M81 — `undefined` = a tolerated invalid line (the last one, truncated by a crash). Any other\n // invalid line has already thrown inside `parseLine`.\n const parsed = parseLine(\n line,\n lineNumber,\n opts.tolerateTrailingPartialLine === true && lineNumber === lines.length,\n );\n if (parsed === undefined) break;\n out.push(opts.map ? opts.map(parsed, lineNumber) : (parsed as unknown as T));\n }\n return out;\n}\n\n/**\n * Parses ONE line, returning `undefined` when it is a tolerated truncated last line.\n *\n * Extracted because `loadJsonl` went over the cognitive-complexity ceiling once it gained the\n * tolerance — and because parsing a line and iterating a file are two responsibilities that were\n * only together out of habit.\n */\nfunction parseLine(\n line: string,\n lineNumber: number,\n tolerate: boolean,\n): Record<string, unknown> | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n if (tolerate) return undefined;\n throw new JsonlParseError(`line ${lineNumber}: invalid JSON`, lineNumber);\n }\n if (!isPlainObject(parsed)) {\n throw new JsonlParseError(`line ${lineNumber}: not a JSON object`, lineNumber);\n }\n return parsed;\n}\n\n/**\n * Append one record as a whole `\\n`-terminated JSON line. Creates the parent dir\n * if missing. `appendFileSync` is synchronous, so within a single Node process\n * the event loop serializes writes and each call writes its line atomically —\n * interleave-safe for the bounded-concurrency batch runner.\n *\n * reference: swebench-batch.ts:192 (mkdir-before-append), :205 (per-line flush).\n */\nexport function appendJsonl(path: string, record: unknown): void {\n // T2.4 — `0o700` for the same reason the file below is `0o600`, and it is the half M93 missed. A\n // private file inside a group-writable directory can be replaced wholesale, and the replacement's\n // mode is whatever the writer chose. Under `umask 002` this directory was born `0775`.\n //\n // The directory sits under `~/.theokit`, shared with the credential and trust stores, and the\n // framework already wrote the diagnosis while fixing a sibling (`@theokit/agents\n // config/trust-store.ts:157-161`): the mode argument is a no-op on a directory that already\n // exists, \"and this one is shared with the SDK's transcript root — whoever creates it first sets\n // the permissions\". Whoever creates it first is usually THIS function, because a session writes a\n // transcript before it ever touches a credential. So `assertSecureModes` was not wrong to demand a\n // private directory; this path was wrong to produce one that fails it, depending on run order.\n //\n // Creation-time only, deliberately: repairing a pre-existing directory means `stat` + `chmod` on\n // every append, and this is the hot path of every session. The pre-existing case is what\n // `assertSecureModes` is for.\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n const prefix = needsLineBreakBefore(path) ? \"\\n\" : \"\";\n // M93 (adversarial review, H1) — `0o600`. `appendFileSync` takes no `mode`, so the permission came\n // from the umask: under `umask 022` the transcript was born `0664`, world-readable. The previous\n // path (`replaceFileAtomic`) pinned `0o600` on purpose — \"holds the FULL in-flight content\n // (credential snapshots, OAuth tokens)\" (`atomic-write.ts:107`) — and switching to append lost\n // that silently. The same class had already been caught in the consumer (`atomic-sync.ts`, M88\n // HIGH-1).\n //\n // `mode` only applies on CREATION; a pre-existing file keeps whatever permission it already has.\n const fd = openSync(path, \"a\", 0o600);\n try {\n writeSync(fd, `${prefix}${JSON.stringify(record)}\\n`);\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Does the file end without a `\\n`? Then the last line is truncated — a crash mid-append.\n *\n * Without this check the next append **glues** itself onto the middle of the broken line, producing\n * an invalid line the reader discards: the NEW record disappears along with the partial one. The\n * previous path (read-modify-write) self-healed from this because it rewrote the whole file.\n * Measured in M93's adversarial review (H2): after an append over a truncated file, the\n * just-written record was no longer readable.\n *\n * Reads **one byte**, not the file.\n */\nfunction needsLineBreakBefore(path: string): boolean {\n let fd: number;\n try {\n fd = openSync(path, \"r\");\n } catch {\n return false; // the file does not exist yet: nothing to splice onto\n }\n try {\n const size = fstatSync(fd).size;\n if (size === 0) return false;\n const buf = Buffer.alloc(1);\n readSync(fd, buf, 0, 1, size - 1);\n return buf[0] !== 0x0a;\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Read the set of keys from an existing JSONL file for which `keyFn(parsed)`\n * returns a non-empty string. Used to resume a crashed batch by skipping rows\n * already persisted with a successful result. A trailing partial line from an\n * interrupted append is tolerated (skipped, not thrown), and a missing file\n * yields an empty set.\n *\n * reference: swebench-batch.ts:113 (readDoneIds), :129 (success-only),\n * :131 (tolerate partial line).\n */\nexport function readJsonlIds(\n path: string,\n keyFn: (parsed: Record<string, unknown>) => string | undefined,\n): Set<string> {\n const done = new Set<string>();\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n return done; // no file yet → nothing done\n }\n for (const rawLine of text.split(\"\\n\")) {\n // A trailing partial line from an interrupted run parses to undefined → skipped.\n const parsed = tryParseObjectLine(rawLine.trim());\n if (parsed === undefined) continue;\n const key = keyFn(parsed);\n if (typeof key === \"string\" && key.length > 0) done.add(key);\n }\n return done;\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import { withCwdMutex } from './chunk-Q5EWJPRY.js';
2
+ import { diag } from './chunk-CZJ6Q7CW.js';
3
+
4
+ // src/internal/persistence/file-lock.ts
5
+ var cached;
6
+ var warnedMissing = false;
7
+ var warnedStructural = false;
8
+ var loadFailure;
9
+ function isModuleAbsent(err) {
10
+ const code = err?.code;
11
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
12
+ }
13
+ function describeLockLoadFailure(err) {
14
+ const consequence = "cross-process file lock unavailable \u2014 concurrent processes over the same file are NOT serialized.";
15
+ if (isModuleAbsent(err)) {
16
+ return `[theokit-sdk] proper-lockfile not installed; ${consequence} Install with: pnpm add proper-lockfile
17
+ `;
18
+ }
19
+ const code = err?.code;
20
+ const detail = err instanceof Error ? err.message : String(err);
21
+ return `[theokit-sdk] proper-lockfile could not be loaded${code !== void 0 ? ` (${String(code)})` : ""}: ${detail}. ${consequence} The package resolves for the SDK but failed to load \u2014 check bundling and module-format interop before re-checking the install.
22
+ `;
23
+ }
24
+ async function getProperLockfile() {
25
+ if (cached !== void 0) return cached;
26
+ try {
27
+ const mod = await import('proper-lockfile');
28
+ if (!validateLockModule(mod)) {
29
+ if (!warnedStructural) {
30
+ warnedStructural = true;
31
+ diag(
32
+ "[theokit-sdk] proper-lockfile: imported module does NOT expose the expected `lock`/`unlock` API surface. This may indicate a supply-chain compromise or an incompatible major version. Falling back to in-process mutex (no cross-process safety). Reinstall with: pnpm add proper-lockfile@^11\n"
33
+ );
34
+ }
35
+ cached = null;
36
+ return cached;
37
+ }
38
+ cached = mod;
39
+ } catch (err) {
40
+ loadFailure = err;
41
+ cached = null;
42
+ }
43
+ return cached;
44
+ }
45
+ function validateLockModule(mod) {
46
+ if (mod === null || mod === void 0 || typeof mod !== "object") return false;
47
+ const m = mod;
48
+ return typeof m.lock === "function" && typeof m.unlock === "function";
49
+ }
50
+ async function withFileLock(path, fn, options) {
51
+ const lib = await getProperLockfile();
52
+ if (lib === null) {
53
+ if (!warnedMissing && !warnedStructural) {
54
+ warnedMissing = true;
55
+ diag(describeLockLoadFailure(loadFailure));
56
+ }
57
+ return withCwdMutex(`file-lock:${path}`, fn);
58
+ }
59
+ return withCwdMutex(`file-lock:${path}`, async () => {
60
+ const release = await lib.lock(path, {
61
+ // EC-1: companion lockfile, target path may not exist yet.
62
+ lockfilePath: `${path}.lock`,
63
+ realpath: false,
64
+ stale: options?.stale ?? 3e4,
65
+ retries: {
66
+ retries: options?.retries ?? 5,
67
+ factor: options?.retryFactor ?? 1.5,
68
+ minTimeout: 100,
69
+ maxTimeout: 5e3
70
+ }
71
+ });
72
+ try {
73
+ return await fn();
74
+ } finally {
75
+ await release();
76
+ }
77
+ });
78
+ }
79
+
80
+ export { withFileLock };
81
+ //# sourceMappingURL=chunk-RGQICWFU.js.map
82
+ //# sourceMappingURL=chunk-RGQICWFU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/file-lock.ts"],"names":[],"mappings":"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","file":"chunk-RGQICWFU.js","sourcesContent":["/**\n * Cross-process file lock helper (ADR D61).\n *\n * Uses `proper-lockfile` (optional peer dep) for cross-process locks. When\n * the peer dep is absent, falls back to `withCwdMutex` (in-process only)\n * with a one-shot stderr warning.\n *\n * EC-1 fix: uses a companion `<path>.lock` file with `realpath: false` so\n * `withFileLock` works even when the target `path` does not exist yet.\n * Without this, fresh installs that lock-then-create would crash with ENOENT.\n *\n * @internal\n */\n\nimport { diag } from \"../diagnostics.js\";\nimport { withCwdMutex } from \"./cwd-mutex.js\";\n\ninterface ProperLockfileModule {\n lock: (file: string, options: ProperLockfileOptions) => Promise<() => Promise<void>>;\n}\n\ninterface ProperLockfileOptions {\n lockfilePath?: string;\n realpath?: boolean;\n stale?: number;\n retries?: {\n retries: number;\n factor?: number;\n minTimeout?: number;\n maxTimeout?: number;\n };\n}\n\nlet cached: ProperLockfileModule | null | undefined;\nlet warnedMissing = false;\nlet warnedStructural = false;\n/** Why the import failed, kept so the warning can report the observation instead of a guess (#174). */\nlet loadFailure: unknown;\n\n/** Node reports a missing module as `ERR_MODULE_NOT_FOUND` (ESM loader) or `MODULE_NOT_FOUND` (CJS). */\nfunction isModuleAbsent(err: unknown): boolean {\n const code = (err as { code?: unknown } | null | undefined)?.code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n\n/**\n * #174 — build the fallback warning from what actually happened.\n *\n * This used to always read \"proper-lockfile not installed\", because the import was wrapped in a\n * bare `catch` that discarded the error (Unbreakable Rule 8: a swallowed error is the dangerous\n * kind). A consumer whose package WAS installed, declared and resolvable spent a debugging session\n * re-verifying the install, because the message named a cause the code had never checked.\n *\n * Absence is now the only case that claims absence. Every other failure — a broken install, an\n * interop problem, a bundler that rewrote the specifier — reports its own code and message, which\n * is what points at the real cause.\n *\n * @internal\n */\nfunction describeLockLoadFailure(err: unknown): string {\n const consequence =\n \"cross-process file lock unavailable — concurrent processes over the same file are NOT serialized.\";\n if (isModuleAbsent(err)) {\n return `[theokit-sdk] proper-lockfile not installed; ${consequence} Install with: pnpm add proper-lockfile\\n`;\n }\n const code = (err as { code?: unknown } | null | undefined)?.code;\n const detail = err instanceof Error ? err.message : String(err);\n return (\n `[theokit-sdk] proper-lockfile could not be loaded${code !== undefined ? ` (${String(code)})` : \"\"}: ` +\n `${detail}. ${consequence} The package resolves for the SDK but failed to load — check bundling ` +\n \"and module-format interop before re-checking the install.\\n\"\n );\n}\n\n/** Test seam for the message builder — NOT in the public barrel. @internal */\nexport function __TESTING__describeLockLoadFailure(err: unknown): string {\n return describeLockLoadFailure(err);\n}\n\nasync function getProperLockfile(): Promise<ProperLockfileModule | null> {\n if (cached !== undefined) return cached;\n try {\n const mod = await import(\"proper-lockfile\");\n // T5.9 — supply-chain hardening: validate the imported module\n // exposes the API surface we depend on BEFORE caching it. A\n // tampered or incompatible version that lacks `lock`/`unlock`\n // functions gets treated as \"not installed\" with an advisory\n // warning — never silently used.\n if (!validateLockModule(mod)) {\n if (!warnedStructural) {\n warnedStructural = true;\n diag(\n \"[theokit-sdk] proper-lockfile: imported module does NOT expose \" +\n \"the expected `lock`/`unlock` API surface. This may indicate a \" +\n \"supply-chain compromise or an incompatible major version. \" +\n \"Falling back to in-process mutex (no cross-process safety). \" +\n \"Reinstall with: pnpm add proper-lockfile@^11\\n\",\n );\n }\n cached = null;\n return cached;\n }\n cached = mod as ProperLockfileModule;\n } catch (err) {\n // #174 — keep WHY. Discarding it here is what made the fallback warning assert a cause it had\n // never observed, and sent a consumer to re-check an install that was already correct.\n loadFailure = err;\n cached = null;\n }\n return cached;\n}\n\n/**\n * T5.9 — Structural validation of the dynamically-imported\n * `proper-lockfile` module. Verifies the API surface we depend on\n * (`lock` and `unlock` as functions) is present. Pure function —\n * never throws, never mutates, never performs I/O.\n *\n * Exported via `__TESTING__validateLockModule` seam so unit tests\n * can drive the check without spinning up the dynamic import.\n *\n * @internal\n */\nfunction validateLockModule(mod: unknown): boolean {\n if (mod === null || mod === undefined || typeof mod !== \"object\") return false;\n const m = mod as Record<string, unknown>;\n return typeof m.lock === \"function\" && typeof m.unlock === \"function\";\n}\n\n/**\n * T5.9 — Test seam: expose the structural validator for unit tests.\n * NOT included in the public barrel.\n *\n * @internal\n */\nexport function __TESTING__validateLockModule(mod: unknown): boolean {\n return validateLockModule(mod);\n}\n\n/**\n * T5.9 — Test seam: reset the module cache + warning flags between\n * tests so each test starts fresh. NOT included in the public barrel.\n *\n * @internal\n */\nexport function __TESTING__resetFileLockCache(): void {\n cached = undefined;\n warnedMissing = false;\n warnedStructural = false;\n loadFailure = undefined;\n}\n\n/**\n * Retry and staleness tuning for `withFileLock`.\n *\n * Every field is optional and the defaults are applied per call, so `undefined` means the default\n * rather than \"no limit\". They are handed to `proper-lockfile` and have NO effect on the fallback\n * path: when that package is absent, `withFileLock` degrades to the in-process mutex, which\n * queues indefinitely and never goes stale.\n *\n * `stale` (default 30000 ms) is how old a lock file must be before it is treated as abandoned by a\n * dead process and broken. Setting it below the longest a legitimate holder can run is what turns\n * a slow operation into two concurrent ones.\n *\n * `retries` (default 5) is the number of acquisition attempts AFTER the first, and `retryFactor`\n * (default 1.5) the backoff multiplier between them. The per-attempt wait is clamped to\n * 100–5000 ms, which the caller cannot change. Exhausting the retries rejects — it does not fall\n * through to running `fn` unlocked.\n *\n * @public — re-exported from the semver-protected `@theokit/sdk/persistence` barrel, and (for\n * back-compat) from the semver-exempt `@theokit/sdk/internal/persistence` alias.\n */\nexport interface FileLockOptions {\n /** Stale lock timeout in ms. Default 30_000 (30s). */\n stale?: number;\n /** Max retries on busy lock. Default 5. */\n retries?: number;\n /** Backoff factor between retries. Default 1.5. */\n retryFactor?: number;\n}\n\n/**\n * Run `fn` while holding an OS-level cross-process lock on `path`.\n *\n * If `proper-lockfile` is installed, uses it with a companion `<path>.lock`\n * file (`realpath: false`, so target file does NOT need to exist yet).\n * Otherwise falls back to in-process `withCwdMutex` and prints a one-shot\n * stderr warning telling the user to install `proper-lockfile` for\n * cross-process safety.\n *\n * The lock is released even when `fn` throws.\n *\n */\nexport async function withFileLock<T>(\n path: string,\n fn: () => Promise<T>,\n options?: FileLockOptions,\n): Promise<T> {\n const lib = await getProperLockfile();\n\n if (lib === null) {\n // #174 — stay silent when the structural check already explained itself. Emitting \"not\n // installed\" on top of \"does not expose lock/unlock\" would be two contradictory diagnoses of\n // one failure, and the reader has no way to tell which is true.\n if (!warnedMissing && !warnedStructural) {\n warnedMissing = true;\n diag(describeLockLoadFailure(loadFailure));\n }\n return withCwdMutex(`file-lock:${path}`, fn);\n }\n\n // proper-lockfile errors immediately on same-process concurrent acquire\n // (\"Lock file is already being held\"). Wrap with cwd-mutex first so\n // in-process callers queue and only ONE thread at a time enters the\n // cross-process acquire path. Combined: full in-process + cross-process\n // serialization.\n return withCwdMutex(`file-lock:${path}`, async () => {\n const release = await lib.lock(path, {\n // EC-1: companion lockfile, target path may not exist yet.\n lockfilePath: `${path}.lock`,\n realpath: false,\n stale: options?.stale ?? 30_000,\n retries: {\n retries: options?.retries ?? 5,\n factor: options?.retryFactor ?? 1.5,\n minTimeout: 100,\n maxTimeout: 5_000,\n },\n });\n\n try {\n return await fn();\n } finally {\n await release();\n }\n });\n}\n\n/**\n * Test helper — resets the cached proper-lockfile module + warning flag.\n * Allows tests to simulate \"module absent\" by clearing cache then\n * monkey-patching the dynamic import resolution.\n *\n * @internal\n */\nexport function _resetFileLockCacheForTesting(): void {\n cached = undefined;\n warnedMissing = false;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { defaultIndexPath, openMemoryDb, resolveMemoryRoot, lanceStoragePath, LanceIndex } from './chunk-47Z6LP3I.js';
1
+ import { defaultIndexPath, openMemoryDb, resolveMemoryRoot, lanceStoragePath, LanceIndex } from './chunk-X3LOJJMO.js';
2
2
  import { replaceFileAtomic } from './chunk-3JHIFQ4I.js';
3
3
  import { ConfigurationError } from './chunk-B5MHRQJZ.js';
4
4
  import { redactSecrets } from './chunk-CZJ6Q7CW.js';
@@ -200,5 +200,5 @@ async function migrateSqliteToLance(opts) {
200
200
  }
201
201
 
202
202
  export { appendDiaryEntry, diaryPath, entryHash, migrateSqliteToLance, readAllSqliteFacts, renderDiaryEntry };
203
- //# sourceMappingURL=chunk-N7KYUNFO.js.map
204
- //# sourceMappingURL=chunk-N7KYUNFO.js.map
203
+ //# sourceMappingURL=chunk-W2ZVTEOD.js.map
204
+ //# sourceMappingURL=chunk-W2ZVTEOD.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/memory/dreaming/diary.ts","../src/internal/memory/migrate-sqlite-to-lance.ts"],"names":["join"],"mappings":"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","file":"chunk-N7KYUNFO.js","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { replaceFileAtomic } from \"../../persistence/atomic-write.js\";\nimport type { MemoryRoot } from \"../storage/memory-root.js\";\n\n/**\n * Dream-diary append (ADR D7).\n *\n * Diary lives at `.theokit/memory/dream-diary.md` and grows with one entry\n * per sweep. Each entry is content-hashed so the same input produces the\n * same entry — idempotency contract.\n *\n * Writes go through `replaceFileAtomic` (EC-3 of edge-case review) so a\n * crash mid-write can never leave a half-written diary.\n *\n * Shared with `@theokit/sdk-memory`; see the memory-store barrel.\n */\n\nexport interface DiaryEntry {\n timestampMs: number;\n factsBefore: number;\n factsAfter: number;\n duplicatesRemoved: number;\n clustersCreated: number;\n notesWritten: number;\n}\n\n/** `<memory root>/dream-diary.md`. Takes the RESOLVED ROOT — see `storage/memory-root.ts` (#463). */\nexport function diaryPath(root: MemoryRoot): string {\n return join(root, \"dream-diary.md\");\n}\n\n/**\n * One diary entry as the markdown that gets appended: a timestamp heading, the short entry hash,\n * and the counts the sweep produced. The hash is what makes a re-run recognisable as the same\n * sweep rather than a new one.\n */\nexport function renderDiaryEntry(entry: DiaryEntry): string {\n const stamp = new Date(entry.timestampMs).toISOString();\n const hash = entryHash(entry).slice(0, 8);\n return [\n `## ${stamp}`,\n \"\",\n `- entry-hash: ${hash}`,\n `- facts before: ${entry.factsBefore}`,\n `- facts after: ${entry.factsAfter}`,\n `- duplicates removed: ${entry.duplicatesRemoved}`,\n `- clusters created: ${entry.clustersCreated}`,\n `- notes written: ${entry.notesWritten}`,\n \"\",\n ].join(\"\\n\");\n}\n\n/**\n * Append one sweep's entry to `<memory root>/dream-diary.md`, creating the file with its header\n * when this is the first sweep. The diary is a human-readable record of what dreaming changed —\n * consolidations are otherwise invisible, because they alter the notes rather than announce\n * themselves.\n */\nexport async function appendDiaryEntry(root: MemoryRoot, entry: DiaryEntry): Promise<void> {\n const path = diaryPath(root);\n let raw = \"\";\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n raw = \"# Dream Diary\\n\\n\";\n }\n const next = `${raw.endsWith(\"\\n\") ? raw : `${raw}\\n`}${renderDiaryEntry(entry)}`;\n await replaceFileAtomic(path, next);\n}\n\n/**\n * A stable hash of one entry's counts, so two sweeps that did the same work read as the same work.\n * Rendered truncated in the entry; the full value is what callers compare.\n */\nexport function entryHash(entry: DiaryEntry): string {\n return createHash(\"sha256\")\n .update(\n [\n entry.factsBefore,\n entry.factsAfter,\n entry.duplicatesRemoved,\n entry.clustersCreated,\n entry.notesWritten,\n ].join(\"|\"),\n )\n .digest(\"hex\");\n}\n","import { existsSync, mkdirSync, renameSync, rmSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { ConfigurationError } from \"../../errors.js\";\nimport { redactSecrets } from \"../security/index.js\";\nimport { defaultIndexPath, openMemoryDb } from \"./index-db.js\";\nimport { LanceIndex, lanceStoragePath } from \"./lance-index.js\";\nimport { type MemoryRoot, resolveMemoryRoot } from \"./storage/memory-root.js\";\n\n/**\n * Migrate Memory.index from SQLite to LanceDB (ADR D44).\n *\n * EC-3 MUST FIX: validation uses NFC unicode normalization on both sides.\n *\n * Shared with `@theokit/sdk-memory` through the memory-store barrel (#463).\n */\n\nexport interface MigrateOptions {\n cwd: string;\n /**\n * Absolute path (or `~/`-prefixed) of the memory root to migrate, when the agent that wrote it\n * set `memory.directory`. Defaults to `<cwd>/.theokit/memory` (#463).\n */\n directory?: string;\n dryRun?: boolean;\n batchSize?: number;\n /** Inject for tests; defaults to console.log. */\n logger?: (msg: string) => void;\n}\n\nexport interface MigrateResult {\n countSqlite: number;\n countLance: number;\n /** Set when validation succeeded (or dryRun); false on validation failure. */\n validated: boolean;\n /** Per-fact compare results (size = sample count, up to 10). */\n sampleComparisons: ReadonlyArray<{ id: string; match: boolean }>;\n /** Lance storage location (final, after migration). */\n lancePath: string;\n /** Was the migration committed (false for dry-run). */\n committed: boolean;\n}\n\n/**\n * One row of the SQLite memory index, as the Lance migration reads it: the chunk plus the tenant\n * columns the target table needs. Shared with `@theokit/sdk-memory`, which migrates the same table.\n */\nexport interface SqliteFactRow {\n id: string;\n path: string;\n source: \"memory\" | \"sessions\" | \"wiki\";\n start_line: number;\n end_line: number;\n text: string;\n namespace?: string | null;\n scope?: string | null;\n user_id?: string | null;\n}\n\n/**\n * Read all facts from the SQLite memory index. Returns empty array if the\n * SQLite db file does not exist (workspace never used Memory).\n *\n * Shared with `@theokit/sdk-memory` through the memory-store barrel, so it carries no visibility\n * tag — naming that tag in this block would delete the function below it (#463).\n */\nexport async function readAllSqliteFacts(root: MemoryRoot): Promise<SqliteFactRow[]> {\n const dbPath = defaultIndexPath(root);\n if (!existsSync(dbPath)) return [];\n const db = await openMemoryDb({ filePath: dbPath });\n try {\n // SQLite schema: chunks table holds the facts. Schema may vary; we\n // probe column existence and fall back to safe defaults.\n const stmt = db.prepare(\"SELECT id, path, source, start_line, end_line, text FROM chunks\");\n const rows = stmt.all() as Array<{\n id: string;\n path: string;\n source: \"memory\" | \"sessions\" | \"wiki\";\n start_line: number;\n end_line: number;\n text: string;\n }>;\n return rows.map((r) => ({\n ...r,\n namespace: \"default\",\n scope: \"agent\",\n user_id: \"default\",\n }));\n } finally {\n db.close();\n }\n}\n\n/**\n * Compare two strings via NFC normalization (EC-3 MUST FIX). Required\n * because SQLite/Lance native bindings can normalize unicode differently\n * (NFC vs NFD), producing false negatives on facts with accents/emojis.\n *\n * @internal\n */\nfunction nfcEqual(a: string, b: string): boolean {\n return a.normalize(\"NFC\") === b.normalize(\"NFC\");\n}\n\n/**\n * Refuse a `batchSize` the loop below cannot make progress with, before anything is migrated.\n *\n * The loop is `for (let i = 0; i < facts.length; i += batchSize)`, so the three ways this goes wrong\n * are all silent and all different:\n *\n * - 0 or negative: `i` never advances past the guard. The migration SPINS FOREVER, calling\n * `addFacts([])` and writing a \"Migrated x/y\" line every iteration — a hang with unbounded output,\n * which is the worst shape a failure can take because it looks like work.\n * - NaN (`Number(\"abc\")`): `i += NaN` makes the guard false on the first check, zero facts move, and\n * the caller is told \"Validation FAILED. SQLite preserved.\" A typo in an argument is reported as a\n * migration failure.\n * - a fraction: `slice` truncates and the loop still terminates, but the batch boundaries stop\n * matching the number the caller asked for.\n *\n * Validated HERE rather than only in the CLI that motivated the finding, because the CLI is a wrapper\n * over this function and a programmatic caller reaches it with no wrapper at all.\n *\n * @internal\n */\nfunction assertUsableBatchSize(batchSize: number | undefined): void {\n if (batchSize === undefined) return;\n if (!Number.isInteger(batchSize) || batchSize < 1) {\n throw new ConfigurationError(\n `migrateSqliteToLance: batchSize must be a whole number of at least 1; received ${String(batchSize)}.`,\n { code: \"invalid_batch_size\" },\n );\n }\n}\n\n/**\n * Run the migration. Writes Lance to `<cwd>/.theokit/memory/lance-new/`\n * first, validates round-trip, then renames to `lance/` on success.\n * SQLite db is preserved (CLI prompts user to delete).\n *\n * @internal\n */\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: migration is a single transaction (read → write → validate → commit-or-rollback); splitting harms atomicity reasoning.\nexport async function migrateSqliteToLance(opts: MigrateOptions): Promise<MigrateResult> {\n assertUsableBatchSize(opts.batchSize);\n const cwd = opts.cwd;\n // One resolution, reused by every path this migration touches (#463).\n const memoryRoot = resolveMemoryRoot(cwd, { directory: opts.directory });\n const finalPath = lanceStoragePath(memoryRoot);\n const newPath = join(memoryRoot, \"lance-new\");\n // T1.4 (ADR D68): wrap logger so any fact text containing secrets is\n // masked before reaching the destination (console or user-supplied sink).\n // Caller-supplied loggers cannot bypass — by design (D70).\n const rawLog = opts.logger ?? ((m: string) => console.log(m));\n const log = (m: string) => rawLog(redactSecrets(m));\n\n // EC: destination exists → typed error.\n if (existsSync(finalPath)) {\n throw new ConfigurationError(\n `Destination already exists: ${finalPath}. Remove it manually (\\`rm -rf ${finalPath}\\`) and re-run.`,\n { code: \"migration_destination_exists\" },\n );\n }\n if (existsSync(newPath)) {\n // Leftover from a previous failed/interrupted migration. Safe to delete.\n log(`Removing stale ${newPath} from a previous run...`);\n rmSync(newPath, { recursive: true, force: true });\n }\n\n log(`Reading SQLite facts from ${cwd}/.theokit/memory/index.sqlite ...`);\n const sqliteFacts = await readAllSqliteFacts(memoryRoot);\n log(`SQLite has ${sqliteFacts.length} facts.`);\n if (sqliteFacts.length === 0) {\n return {\n countSqlite: 0,\n countLance: 0,\n validated: true,\n sampleComparisons: [],\n lancePath: finalPath,\n committed: false,\n };\n }\n\n // Reuse the embedding runtime configured in the workspace? For v1.2 we\n // require the caller to pass embedding explicitly via env. The migration\n // path is documented as: \"set OPENAI_API_KEY (or similar) before running.\"\n // Here we ship a placeholder embedder that uses a deterministic\n // content-hash — sufficient for migration validation; consumers should\n // re-embed on first real query after migration if needed.\n const placeholderEmbedding = {\n id: \"migration-placeholder\",\n model: \"deterministic-hash\",\n dimension: 8,\n embed: async (texts: ReadonlyArray<string>) => {\n // Simple deterministic hash → 8-dim vector (migration validation only).\n return texts.map((t) => {\n const arr = new Array(8).fill(0);\n for (let i = 0; i < t.length; i += 1) {\n arr[i % 8] += t.charCodeAt(i);\n }\n return arr.map((x) => x / 1000);\n });\n },\n stats: () => ({ cacheHits: 0, cacheMisses: 0, httpCalls: 0, retries: 0 }),\n };\n\n mkdirSync(newPath, { recursive: true });\n log(`Writing Lance index to ${newPath} ...`);\n\n const lance = await LanceIndex.open({\n cwd,\n embedding: placeholderEmbedding,\n storagePath: newPath,\n });\n\n const batchSize = opts.batchSize ?? 100;\n for (let i = 0; i < sqliteFacts.length; i += batchSize) {\n const batch = sqliteFacts.slice(i, i + batchSize).map((f) => ({\n id: f.id,\n text: f.text,\n source: f.source,\n namespace: f.namespace ?? \"default\",\n scope: f.scope ?? \"agent\",\n user_id: f.user_id ?? \"default\",\n timestamp: 0,\n }));\n await lance.addFacts(batch);\n log(` Migrated ${Math.min(i + batchSize, sqliteFacts.length)}/${sqliteFacts.length}`);\n }\n\n // Validation: count + sample compare (10 random) with NFC normalization.\n const lanceCount = await lance.countFacts(\"default\");\n log(`Lance has ${lanceCount} facts (expected ${sqliteFacts.length}).`);\n\n const sampleSize = Math.min(10, sqliteFacts.length);\n const sampleComparisons: { id: string; match: boolean }[] = [];\n for (let i = 0; i < sampleSize; i += 1) {\n const sqliteFact = sqliteFacts[Math.floor((i * sqliteFacts.length) / sampleSize)];\n if (sqliteFact === undefined) continue;\n // Search Lance for this fact's text — best-effort round-trip.\n const hits = await lance.search(sqliteFact.text, {\n namespace: sqliteFact.namespace ?? \"default\",\n limit: 1,\n });\n // EC-3: compare with NFC normalization.\n const hit = hits[0];\n const match = hit !== undefined && nfcEqual(hit.text, sqliteFact.text);\n sampleComparisons.push({ id: sqliteFact.id, match });\n }\n\n const allMatch = sampleComparisons.every((c) => c.match);\n const countMatch = lanceCount === sqliteFacts.length;\n const validated = allMatch && countMatch;\n\n await lance.close();\n\n if (opts.dryRun === true) {\n log(\"Dry-run mode — discarding Lance staging dir.\");\n rmSync(newPath, { recursive: true, force: true });\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated,\n sampleComparisons,\n lancePath: finalPath,\n committed: false,\n };\n }\n\n if (!validated) {\n log(\"Validation FAILED — leaving SQLite intact, removing Lance staging dir.\");\n rmSync(newPath, { recursive: true, force: true });\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated: false,\n sampleComparisons,\n lancePath: finalPath,\n committed: false,\n };\n }\n\n // Atomic commit via rename.\n renameSync(newPath, finalPath);\n log(`Migration committed to ${finalPath}.`);\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated: true,\n sampleComparisons,\n lancePath: finalPath,\n committed: true,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/memory/dreaming/diary.ts","../src/internal/memory/migrate-sqlite-to-lance.ts"],"names":["join"],"mappings":"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","file":"chunk-W2ZVTEOD.js","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { replaceFileAtomic } from \"../../persistence/atomic-write.js\";\nimport type { MemoryRoot } from \"../storage/memory-root.js\";\n\n/**\n * Dream-diary append (ADR D7).\n *\n * Diary lives at `.theokit/memory/dream-diary.md` and grows with one entry\n * per sweep. Each entry is content-hashed so the same input produces the\n * same entry — idempotency contract.\n *\n * Writes go through `replaceFileAtomic` (EC-3 of edge-case review) so a\n * crash mid-write can never leave a half-written diary.\n *\n * Shared with `@theokit/sdk-memory`; see the memory-store barrel.\n */\n\nexport interface DiaryEntry {\n timestampMs: number;\n factsBefore: number;\n factsAfter: number;\n duplicatesRemoved: number;\n clustersCreated: number;\n notesWritten: number;\n}\n\n/** `<memory root>/dream-diary.md`. Takes the RESOLVED ROOT — see `storage/memory-root.ts` (#463). */\nexport function diaryPath(root: MemoryRoot): string {\n return join(root, \"dream-diary.md\");\n}\n\n/**\n * One diary entry as the markdown that gets appended: a timestamp heading, the short entry hash,\n * and the counts the sweep produced. The hash is what makes a re-run recognisable as the same\n * sweep rather than a new one.\n */\nexport function renderDiaryEntry(entry: DiaryEntry): string {\n const stamp = new Date(entry.timestampMs).toISOString();\n const hash = entryHash(entry).slice(0, 8);\n return [\n `## ${stamp}`,\n \"\",\n `- entry-hash: ${hash}`,\n `- facts before: ${entry.factsBefore}`,\n `- facts after: ${entry.factsAfter}`,\n `- duplicates removed: ${entry.duplicatesRemoved}`,\n `- clusters created: ${entry.clustersCreated}`,\n `- notes written: ${entry.notesWritten}`,\n \"\",\n ].join(\"\\n\");\n}\n\n/**\n * Append one sweep's entry to `<memory root>/dream-diary.md`, creating the file with its header\n * when this is the first sweep. The diary is a human-readable record of what dreaming changed —\n * consolidations are otherwise invisible, because they alter the notes rather than announce\n * themselves.\n */\nexport async function appendDiaryEntry(root: MemoryRoot, entry: DiaryEntry): Promise<void> {\n const path = diaryPath(root);\n let raw = \"\";\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n raw = \"# Dream Diary\\n\\n\";\n }\n const next = `${raw.endsWith(\"\\n\") ? raw : `${raw}\\n`}${renderDiaryEntry(entry)}`;\n await replaceFileAtomic(path, next);\n}\n\n/**\n * A stable hash of one entry's counts, so two sweeps that did the same work read as the same work.\n * Rendered truncated in the entry; the full value is what callers compare.\n */\nexport function entryHash(entry: DiaryEntry): string {\n return createHash(\"sha256\")\n .update(\n [\n entry.factsBefore,\n entry.factsAfter,\n entry.duplicatesRemoved,\n entry.clustersCreated,\n entry.notesWritten,\n ].join(\"|\"),\n )\n .digest(\"hex\");\n}\n","import { existsSync, mkdirSync, renameSync, rmSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { ConfigurationError } from \"../../errors.js\";\nimport { redactSecrets } from \"../security/index.js\";\nimport { defaultIndexPath, openMemoryDb } from \"./index-db.js\";\nimport { LanceIndex, lanceStoragePath } from \"./lance-index.js\";\nimport { type MemoryRoot, resolveMemoryRoot } from \"./storage/memory-root.js\";\n\n/**\n * Migrate Memory.index from SQLite to LanceDB (ADR D44).\n *\n * EC-3 MUST FIX: validation uses NFC unicode normalization on both sides.\n *\n * Shared with `@theokit/sdk-memory` through the memory-store barrel (#463).\n */\n\nexport interface MigrateOptions {\n cwd: string;\n /**\n * Absolute path (or `~/`-prefixed) of the memory root to migrate, when the agent that wrote it\n * set `memory.directory`. Defaults to `<cwd>/.theokit/memory` (#463).\n */\n directory?: string;\n dryRun?: boolean;\n batchSize?: number;\n /** Inject for tests; defaults to console.log. */\n logger?: (msg: string) => void;\n}\n\nexport interface MigrateResult {\n countSqlite: number;\n countLance: number;\n /** Set when validation succeeded (or dryRun); false on validation failure. */\n validated: boolean;\n /** Per-fact compare results (size = sample count, up to 10). */\n sampleComparisons: ReadonlyArray<{ id: string; match: boolean }>;\n /** Lance storage location (final, after migration). */\n lancePath: string;\n /** Was the migration committed (false for dry-run). */\n committed: boolean;\n}\n\n/**\n * One row of the SQLite memory index, as the Lance migration reads it: the chunk plus the tenant\n * columns the target table needs. Shared with `@theokit/sdk-memory`, which migrates the same table.\n */\nexport interface SqliteFactRow {\n id: string;\n path: string;\n source: \"memory\" | \"sessions\" | \"wiki\";\n start_line: number;\n end_line: number;\n text: string;\n namespace?: string | null;\n scope?: string | null;\n user_id?: string | null;\n}\n\n/**\n * Read all facts from the SQLite memory index. Returns empty array if the\n * SQLite db file does not exist (workspace never used Memory).\n *\n * Shared with `@theokit/sdk-memory` through the memory-store barrel, so it carries no visibility\n * tag — naming that tag in this block would delete the function below it (#463).\n */\nexport async function readAllSqliteFacts(root: MemoryRoot): Promise<SqliteFactRow[]> {\n const dbPath = defaultIndexPath(root);\n if (!existsSync(dbPath)) return [];\n const db = await openMemoryDb({ filePath: dbPath });\n try {\n // SQLite schema: chunks table holds the facts. Schema may vary; we\n // probe column existence and fall back to safe defaults.\n const stmt = db.prepare(\"SELECT id, path, source, start_line, end_line, text FROM chunks\");\n const rows = stmt.all() as Array<{\n id: string;\n path: string;\n source: \"memory\" | \"sessions\" | \"wiki\";\n start_line: number;\n end_line: number;\n text: string;\n }>;\n return rows.map((r) => ({\n ...r,\n namespace: \"default\",\n scope: \"agent\",\n user_id: \"default\",\n }));\n } finally {\n db.close();\n }\n}\n\n/**\n * Compare two strings via NFC normalization (EC-3 MUST FIX). Required\n * because SQLite/Lance native bindings can normalize unicode differently\n * (NFC vs NFD), producing false negatives on facts with accents/emojis.\n *\n * @internal\n */\nfunction nfcEqual(a: string, b: string): boolean {\n return a.normalize(\"NFC\") === b.normalize(\"NFC\");\n}\n\n/**\n * Refuse a `batchSize` the loop below cannot make progress with, before anything is migrated.\n *\n * The loop is `for (let i = 0; i < facts.length; i += batchSize)`, so the three ways this goes wrong\n * are all silent and all different:\n *\n * - 0 or negative: `i` never advances past the guard. The migration SPINS FOREVER, calling\n * `addFacts([])` and writing a \"Migrated x/y\" line every iteration — a hang with unbounded output,\n * which is the worst shape a failure can take because it looks like work.\n * - NaN (`Number(\"abc\")`): `i += NaN` makes the guard false on the first check, zero facts move, and\n * the caller is told \"Validation FAILED. SQLite preserved.\" A typo in an argument is reported as a\n * migration failure.\n * - a fraction: `slice` truncates and the loop still terminates, but the batch boundaries stop\n * matching the number the caller asked for.\n *\n * Validated HERE rather than only in the CLI that motivated the finding, because the CLI is a wrapper\n * over this function and a programmatic caller reaches it with no wrapper at all.\n *\n * @internal\n */\nfunction assertUsableBatchSize(batchSize: number | undefined): void {\n if (batchSize === undefined) return;\n if (!Number.isInteger(batchSize) || batchSize < 1) {\n throw new ConfigurationError(\n `migrateSqliteToLance: batchSize must be a whole number of at least 1; received ${String(batchSize)}.`,\n { code: \"invalid_batch_size\" },\n );\n }\n}\n\n/**\n * Run the migration. Writes Lance to `<cwd>/.theokit/memory/lance-new/`\n * first, validates round-trip, then renames to `lance/` on success.\n * SQLite db is preserved (CLI prompts user to delete).\n *\n * @internal\n */\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: migration is a single transaction (read → write → validate → commit-or-rollback); splitting harms atomicity reasoning.\nexport async function migrateSqliteToLance(opts: MigrateOptions): Promise<MigrateResult> {\n assertUsableBatchSize(opts.batchSize);\n const cwd = opts.cwd;\n // One resolution, reused by every path this migration touches (#463).\n const memoryRoot = resolveMemoryRoot(cwd, { directory: opts.directory });\n const finalPath = lanceStoragePath(memoryRoot);\n const newPath = join(memoryRoot, \"lance-new\");\n // T1.4 (ADR D68): wrap logger so any fact text containing secrets is\n // masked before reaching the destination (console or user-supplied sink).\n // Caller-supplied loggers cannot bypass — by design (D70).\n const rawLog = opts.logger ?? ((m: string) => console.log(m));\n const log = (m: string) => rawLog(redactSecrets(m));\n\n // EC: destination exists → typed error.\n if (existsSync(finalPath)) {\n throw new ConfigurationError(\n `Destination already exists: ${finalPath}. Remove it manually (\\`rm -rf ${finalPath}\\`) and re-run.`,\n { code: \"migration_destination_exists\" },\n );\n }\n if (existsSync(newPath)) {\n // Leftover from a previous failed/interrupted migration. Safe to delete.\n log(`Removing stale ${newPath} from a previous run...`);\n rmSync(newPath, { recursive: true, force: true });\n }\n\n log(`Reading SQLite facts from ${cwd}/.theokit/memory/index.sqlite ...`);\n const sqliteFacts = await readAllSqliteFacts(memoryRoot);\n log(`SQLite has ${sqliteFacts.length} facts.`);\n if (sqliteFacts.length === 0) {\n return {\n countSqlite: 0,\n countLance: 0,\n validated: true,\n sampleComparisons: [],\n lancePath: finalPath,\n committed: false,\n };\n }\n\n // Reuse the embedding runtime configured in the workspace? For v1.2 we\n // require the caller to pass embedding explicitly via env. The migration\n // path is documented as: \"set OPENAI_API_KEY (or similar) before running.\"\n // Here we ship a placeholder embedder that uses a deterministic\n // content-hash — sufficient for migration validation; consumers should\n // re-embed on first real query after migration if needed.\n const placeholderEmbedding = {\n id: \"migration-placeholder\",\n model: \"deterministic-hash\",\n dimension: 8,\n embed: async (texts: ReadonlyArray<string>) => {\n // Simple deterministic hash → 8-dim vector (migration validation only).\n return texts.map((t) => {\n const arr = new Array(8).fill(0);\n for (let i = 0; i < t.length; i += 1) {\n arr[i % 8] += t.charCodeAt(i);\n }\n return arr.map((x) => x / 1000);\n });\n },\n stats: () => ({ cacheHits: 0, cacheMisses: 0, httpCalls: 0, retries: 0 }),\n };\n\n mkdirSync(newPath, { recursive: true });\n log(`Writing Lance index to ${newPath} ...`);\n\n const lance = await LanceIndex.open({\n cwd,\n embedding: placeholderEmbedding,\n storagePath: newPath,\n });\n\n const batchSize = opts.batchSize ?? 100;\n for (let i = 0; i < sqliteFacts.length; i += batchSize) {\n const batch = sqliteFacts.slice(i, i + batchSize).map((f) => ({\n id: f.id,\n text: f.text,\n source: f.source,\n namespace: f.namespace ?? \"default\",\n scope: f.scope ?? \"agent\",\n user_id: f.user_id ?? \"default\",\n timestamp: 0,\n }));\n await lance.addFacts(batch);\n log(` Migrated ${Math.min(i + batchSize, sqliteFacts.length)}/${sqliteFacts.length}`);\n }\n\n // Validation: count + sample compare (10 random) with NFC normalization.\n const lanceCount = await lance.countFacts(\"default\");\n log(`Lance has ${lanceCount} facts (expected ${sqliteFacts.length}).`);\n\n const sampleSize = Math.min(10, sqliteFacts.length);\n const sampleComparisons: { id: string; match: boolean }[] = [];\n for (let i = 0; i < sampleSize; i += 1) {\n const sqliteFact = sqliteFacts[Math.floor((i * sqliteFacts.length) / sampleSize)];\n if (sqliteFact === undefined) continue;\n // Search Lance for this fact's text — best-effort round-trip.\n const hits = await lance.search(sqliteFact.text, {\n namespace: sqliteFact.namespace ?? \"default\",\n limit: 1,\n });\n // EC-3: compare with NFC normalization.\n const hit = hits[0];\n const match = hit !== undefined && nfcEqual(hit.text, sqliteFact.text);\n sampleComparisons.push({ id: sqliteFact.id, match });\n }\n\n const allMatch = sampleComparisons.every((c) => c.match);\n const countMatch = lanceCount === sqliteFacts.length;\n const validated = allMatch && countMatch;\n\n await lance.close();\n\n if (opts.dryRun === true) {\n log(\"Dry-run mode — discarding Lance staging dir.\");\n rmSync(newPath, { recursive: true, force: true });\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated,\n sampleComparisons,\n lancePath: finalPath,\n committed: false,\n };\n }\n\n if (!validated) {\n log(\"Validation FAILED — leaving SQLite intact, removing Lance staging dir.\");\n rmSync(newPath, { recursive: true, force: true });\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated: false,\n sampleComparisons,\n lancePath: finalPath,\n committed: false,\n };\n }\n\n // Atomic commit via rename.\n renameSync(newPath, finalPath);\n log(`Migration committed to ${finalPath}.`);\n return {\n countSqlite: sqliteFacts.length,\n countLance: lanceCount,\n validated: true,\n sampleComparisons,\n lancePath: finalPath,\n committed: true,\n };\n}\n"]}