chainlesschain 0.162.193 → 0.162.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -6
- package/completions/_cc +2 -2
- package/completions/cc.bash +2 -2
- package/completions/cc.fish +2 -2
- package/completions/cc.ps1 +2 -2
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DBLkaTje.js → AIOps-CSN-D2xb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BAy-Qebo.js → ActionButton-CBSMlGzv.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Dfb1Mlau.js → Analytics-3yr3Nt2t.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BbFkN8m0.js → AppLayout-1b-o3lMH.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-kM4-cEEH.js → Artifacts-Dr8rJi17.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BSQfYWf3.js → Audit-2V689HC2.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CWDbI_fG.js → BackgroundAgents-DIxkNrD_.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BxLPbq_Q.js → Backup-Pg6BcwrE.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DuIbdrp6.js → BaseInput-CLUpqyfH.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-NEmjGahU.js → Chat-qVS8rD4G.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DfJEo-5_.js → ChatBubbleRenderer-ti49X12Q.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CaoeG3Ns.js → Checkbox-Bs9AaEay.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BFf_g3xY.js → Codegen-Dvc1mq8q.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CNOd8mdQ.js → Col-C4_biO7A.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BhpDqz-i.js → Community-al25XQUV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CQ42T7Z6.js → Compact-D-ZDB8M1.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DI5PcLMN.js → Compliance-6lIfDz1W.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-wYYZe0wR.js → Cowork-DdI48XMJ.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-Dlq03DD_.js → Cron-DF5HZd2g.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BzqS8PP3.js → Crosschain-DuxeSvt3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-It55Yem7.js → DID-B9yHkUTd.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-9HkH6oxn.js → Dashboard-Z5EuI3IM.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-00e2YWRB.js → Dropdown-vuRfO3dY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-By-ugRDL.js → EmailListRenderer-QFLCc1uL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-C396yu5m.js → FamilyGuardDashboard-BZ7EXkFJ.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BME5sbZY.js → Federation-DycIEg2s.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B4TEZCr-.js → FormItemContext-Ba0H_7yG.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-_9CX9ieY.js → GenericCardRenderer--JgSTK-C.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BrS6WVSr.js → Git-DEvTbldE.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BMdCWnKF.js → Governance-BJIljy18.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bcp3vzcA.js → Inference-CVyW3G3q.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DlLuqGY5.js → KnowledgeGraph-v8-borge.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-D-W_6x09.js → Logs-BCdS7gGo.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DElGPxCZ.js → MarkdownRenderer-CU-JB2Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Dzn2guvI.js → Marketplace-AN6tGA2B.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-uPict-KL.js → McpTools-DGViXASN.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-BCw43kOb.js → Memory-Bpvmx9X4.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DmV8Wv7F.js → MobileBridge-CTP-DOTC.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-C-HaCmSM.js → MobileProjects-C8qfktxn.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-C6jxJ34Y.js → Mtc-ndYr0M1s.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Za8ExwhQ.js → MtcAudit-Ci0-YK7W.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-KKDbuEgb.js → Multisig-BehSTFW7.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Dj97zbCk.js → NLProgramming-B91_YBLv.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DBXvMraU.js → Notes-BQlJ0-gq.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DOxITLYl.js → NotificationSettings-CDgYMjYK.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-7DVjZwMM.js → OrderTableRenderer-BJkSlKeH.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D1i73mjb.js → Organization-B-b3cWCp.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-kW6Q3rvg.js → Overflow-CILUL4nL.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-C-v_8p2v.js → P2P-BTCFBkF0.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-tUbbY64W.js → PdhVaultBrowser-BAR6AUx5.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DkHxEZ58.js → Permissions-RRf6HbWJ.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DWuMPGrK.js → PersonalDataHub-BGTMiICa.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-30IYXWWm.js → Pipeline-945qY9YF.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DZNQ4RrH.js → Privacy-DPZwI6lJ.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-De2MCElf.js → ProjectInit-BPH2NRKc.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-5uHk9wsC.js → ProjectSettings-BZCRNCvb.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-XjDfrFLb.js → Projects-BTI__tBt.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Bgwj6gcE.js → Providers-DDCf_nIw.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-gKql9umv.js → QrScannerModal-N6rbqt1r.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BWvmwkh7.js → QuickAsk-DrwjPS7M.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Uok4hZPd.js → Recommend-Cog9Wd99.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DLJzTk9a.js → RemoteSession-DC9pynjM.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-ZVwIPMSO.js → Reputation-D4th4gKS.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CN04HxWR.js → Row-ogiWR6v9.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BdSrEG4g.js → RssFeed-uYlk-2Oj.js} +3 -3
- package/src/assets/web-panel/assets/{Search-5PwVd0IR.js → Search-DYzzwvtM.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DdBo4zjN.js → Security-Da7YOyX6.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D3letU7M.js → Services-CLI1VM1X.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton--Jb83f1v.js → Skeleton-DK77dzDC.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Bg-CulHX.js → Skills-C_z7Lmhl.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-7QOa7S-X.js → Sla-D6413Q8L.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DQwppjRw.js → SpeechSettings-C07ydbj2.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-dNPQGL-s.js → SyncSettings-DGOm2W22.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Ggs-Z48q.js → Tasks-Cr_PVyLM.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-78TYHgRO.js → Templates-BhL2mZ0t.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-xHwwv9vO.js → Tenant-CPk5oyUr.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BJXSJfm4.js → Terminal-ClhqUsqV.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-fLbjRhJw.js → TimelineRenderer-BCC7MV5a.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-oMSBSBmB.js → Tokens-DD1Jt4oO.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CfnY_wX_.js → Trigger-BCydFvN2.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-AdyTgYKO.js → Trust-DDHT1noa.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C5K_81dS.js → UkeySign-CWPZC7oI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-vcGVDqki.js → VideoEditing-9Ea0nJR0.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-hhOc3hlY.js → Wallet-BjxhLxTf.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CVloC11s.js → WebAuthn-C6uCye67.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CYeE32R5.js → WorkflowEditor-d2g4reYo.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CNROR5bN.js → chat-frTc6ScX.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Cjx-cnwU.js → colors-B5K6lG3v.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DOIfelvD.js → compact-item-Cdn46r8D.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DfA7UZ8u.js → createContext-DU1jqVQi.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BMSCzcfM.js → hasIn-DdNH-aW_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAnvhwg6.js → index-32ajXmTb.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEMwBBHk.js → index-7ehytj1K.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVL27kXC.js → index-BC4eGkar.js} +1 -1
- package/src/assets/web-panel/assets/{index-FBc_CJBd.js → index-BVLL-mq_.js} +1 -1
- package/src/assets/web-panel/assets/{index-dCd8cSUq.js → index-BXKhhx9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BemJ2u_E.js → index-BcGwGIaR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVm1_i3t.js → index-Be4OtL-N.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs6l7uRu.js → index-BoFak7le.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPAWBrMf.js → index-BwlIxAmX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLmn0BoK.js → index-CGiwfwB9.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7gf77CJ.js → index-CHmkkdfT.js} +1 -1
- package/src/assets/web-panel/assets/{index-m2vG9gG_.js → index-CQorP1Ng.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClNMrqEY.js → index-CUoNAmE8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHtpHZAQ.js → index-Cj6P9Nst.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2K9SNVX.js → index-CtRPi4YC.js} +1 -1
- package/src/assets/web-panel/assets/index-CzrCHrFy.js +1 -0
- package/src/assets/web-panel/assets/{index-BolaOQxx.js → index-D1qIC4GR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM1y_9hM.js → index-D23xGYjM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cyweh0Hy.js → index-D4l82Me1.js} +1 -1
- package/src/assets/web-panel/assets/{index-By61ojUx.js → index-D4miNkWv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbQ2eQ8H.js → index-DIVyTmZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-Icdoqmzt.js → index-DM5FmPZU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjEsxO5h.js → index-DRzBBdQ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4iYzpkL.js → index-Dca_i7H8.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3L5gdiD.js → index-DftZuyA7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdYBye8B.js → index-DtBCCkC4.js} +1 -1
- package/src/assets/web-panel/assets/{index-AWg-0M1S.js → index-HjCvXHf5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEKMcOyU.js → index-LaivuXyF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxOk9vkG.js → index-MM4Zz2OE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNC5onm4.js → index-NmEXVjzt.js} +1 -1
- package/src/assets/web-panel/assets/index-OJsGskZw.js +1 -0
- package/src/assets/web-panel/assets/{index-BUonmKSE.js → index-PjAS0sIZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5Udeb_6.js → index-VDM2KKo9.js} +1 -1
- package/src/assets/web-panel/assets/{index-5-HXgOX5.js → index-cYaWV4Mq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBpWXFed.js → index-gg3QPC3i.js} +3 -3
- package/src/assets/web-panel/assets/{index-C7xn0Nub.js → index-kVO_MDm-.js} +1 -1
- package/src/assets/web-panel/assets/{index-ti_lnTWl.js → index-mgU3Q-lN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CshQbilz.js → index-t_OBzIP4.js} +1 -1
- package/src/assets/web-panel/assets/{index-mTpHoEzr.js → index-yZRuZpwK.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Bu8k7H1Y.js → initDefaultProps-DwLTRNdo.js} +1 -1
- package/src/assets/web-panel/assets/{motion-s3iVK41k.js → motion-BchZDXnh.js} +1 -1
- package/src/assets/web-panel/assets/{move-B6B-E5Jz.js → move-e8M-zZFl.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BiYgTidA.js → mtc-parser-Ci40fQb3.js} +1 -1
- package/src/assets/web-panel/assets/{omit-pU1RdUDA.js → omit-C7110_f2.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Cdoq_SeM.js → pickAttrs-skteMgnM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DIuvAn2K.js → placementArrow-MGZ-lh2B.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-8w16YR2D.js → responsiveObserve-N4Sd7uy-.js} +1 -1
- package/src/assets/web-panel/assets/{slide-VZ01cZe3.js → slide-C7HDGRaa.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-lh8YMN8Z.js → statusUtils-BhRexlgy.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CmDyreec.js → styleChecker-ChEoQkN2.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-hiJWMU5y.js → useFlexGapSupport-CDbG1s2Y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Thnu-Iaj.js → useFs-CUIx0tcJ.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-LBdOxjwY.js → usePersonalDataHub-bkO7L7ec.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Cg6lFY_X.js → vnode-B_ZvABIG.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DVfsR1YO.js → zoom-D5shjpS8.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +4 -4
- package/src/command-manifest.json +214 -50
- package/src/command-surface-policy.js +115 -0
- package/src/commands/agent.js +3 -3
- package/src/commands/background-session.js +145 -0
- package/src/commands/mcp.js +125 -7
- package/src/commands/serve.js +1 -1
- package/src/commands/session-show.js +131 -0
- package/src/commands/session.js +50 -106
- package/src/commands/skill.js +87 -50
- package/src/data/changelog.json +18 -5
- package/src/gateways/ws/ws-agent-handler.js +8 -1
- package/src/harness/jsonl-session-store.js +428 -25
- package/src/harness/mcp-client.js +2652 -404
- package/src/harness/session-list-index.js +65 -7
- package/src/lazy-dispatch.js +76 -8
- package/src/lib/abort-utils.js +68 -0
- package/src/lib/agent-core.js +1 -59
- package/src/lib/chat-core.js +24 -6
- package/src/lib/cowork-mcp-tools.js +47 -2
- package/src/lib/cowork-task-runner.js +11 -3
- package/src/lib/doctor-checkup.js +9 -7
- package/src/lib/learning/skill-improver.js +21 -2
- package/src/lib/mcp-call-ledger-store.js +116 -3
- package/src/lib/mcp-call-ledger.js +59 -14
- package/src/lib/mcp-client.js +1 -0
- package/src/lib/mcp-stdio-executable-identity.js +598 -0
- package/src/lib/mcp-stdio-execution-authority.js +363 -0
- package/src/lib/mcp-tool-metadata.js +436 -0
- package/src/lib/mcp-tool-result.js +367 -0
- package/src/lib/observability/index.js +71 -0
- package/src/lib/pr-link-ledger.js +8 -27
- package/src/lib/pr-link-store.js +26 -0
- package/src/lib/process-execution-broker/index.js +14 -0
- package/src/lib/process-tree-termination.js +253 -0
- package/src/lib/public-ide-capability-manifest.js +6 -0
- package/src/lib/session-host-snapshot.js +9 -5
- package/src/lib/session-projection.js +5 -1
- package/src/lib/session-tail.js +31 -1
- package/src/lib/skill-budget.js +222 -0
- package/src/lib/skill-execution-authority.js +381 -0
- package/src/lib/skill-execution-identity.js +39 -2
- package/src/lib/skill-loader.js +322 -86
- package/src/lib/skill-mcp.js +43 -1
- package/src/repl/agent-repl.js +349 -46
- package/src/repl/bg-dashboard.js +81 -28
- package/src/repl/chat-repl.js +77 -12
- package/src/repl/prompt-interactions.js +67 -7
- package/src/repl/slash-command-registry.js +3 -4
- package/src/runtime/agent-core.js +340 -108
- package/src/runtime/agent-runtime.js +26 -3
- package/src/runtime/headless-runner.js +150 -23
- package/src/runtime/headless-stream.js +309 -59
- package/src/runtime/mcp-config.js +157 -10
- package/src/runtime/mcp-tool-search.js +0 -0
- package/src/runtime/output-backpressure.js +366 -0
- package/src/runtime/pipe-safety.js +56 -21
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +0 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +0 -1
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +0 -1
- package/src/lib/skill-process-broker.js +0 -85
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
recordSessionEvent,
|
|
53
53
|
recordSessionActivity,
|
|
54
54
|
replaceSessionMeta,
|
|
55
|
+
SESSION_TOMBSTONE_MARKER_SUFFIX,
|
|
55
56
|
} from "./session-list-index.js";
|
|
56
57
|
|
|
57
58
|
let securedSessionsDir = null;
|
|
@@ -784,16 +785,56 @@ function appendEventLocked(
|
|
|
784
785
|
filePath,
|
|
785
786
|
() => {
|
|
786
787
|
const existingMeta = readSessionMeta(getSessionsDir(), sessionId);
|
|
788
|
+
const persistedWitness = existingMeta;
|
|
789
|
+
const transcriptExists = existsSync(filePath);
|
|
790
|
+
const startsNewGeneration =
|
|
791
|
+
!transcriptExists &&
|
|
792
|
+
persistedWitness?.deleted === true &&
|
|
793
|
+
type === "session_start";
|
|
787
794
|
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
type !== "session_start"
|
|
795
|
+
persistedWitness?.deleted === true &&
|
|
796
|
+
(transcriptExists || type !== "session_start")
|
|
791
797
|
) {
|
|
792
798
|
const error = new Error(`Session was deleted: ${sessionId}`);
|
|
793
799
|
error.code = "SESSION_DELETED";
|
|
794
800
|
throw error;
|
|
795
801
|
}
|
|
802
|
+
if (
|
|
803
|
+
!transcriptExists &&
|
|
804
|
+
persistedWitness !== null &&
|
|
805
|
+
persistedWitness?.deleted !== true
|
|
806
|
+
) {
|
|
807
|
+
throw unverifiedTranscriptError(
|
|
808
|
+
sessionId,
|
|
809
|
+
missingTranscriptVerification(),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
796
812
|
const { prevHash, recovery } = _resolveChainTail(filePath);
|
|
813
|
+
if (
|
|
814
|
+
transcriptExists &&
|
|
815
|
+
existingMeta !== null &&
|
|
816
|
+
existingMeta?.deleted !== true &&
|
|
817
|
+
(existingMeta.last_hash ?? null) !== prevHash
|
|
818
|
+
) {
|
|
819
|
+
throw unverifiedTranscriptError(
|
|
820
|
+
sessionId,
|
|
821
|
+
transcriptAnchorMismatchVerification(prevHash),
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (transcriptExists && existingMeta === null) {
|
|
825
|
+
const verification = verifyTranscriptFile(filePath);
|
|
826
|
+
if (
|
|
827
|
+
verification.status === TRANSCRIPT_CHAIN_STATUS.TAMPERED ||
|
|
828
|
+
verification.malformedLines > 0 ||
|
|
829
|
+
verification.truncatedTail
|
|
830
|
+
) {
|
|
831
|
+
throw unverifiedTranscriptError(sessionId, verification);
|
|
832
|
+
}
|
|
833
|
+
// A sidecar is rebuildable, but appending before rebuilding it would
|
|
834
|
+
// publish a count of one for a multi-event transcript. Restore the
|
|
835
|
+
// projection under the same writer lock before advancing the chain.
|
|
836
|
+
rebuildSessionMetaUnlocked(getSessionsDir(), sessionId, filePath);
|
|
837
|
+
}
|
|
797
838
|
if (compareHead && prevHash !== (expectedHeadHash || null)) {
|
|
798
839
|
const error = new Error(
|
|
799
840
|
`Session revision changed for ${sessionId}; refresh the checkpoint timeline`,
|
|
@@ -817,7 +858,9 @@ function appendEventLocked(
|
|
|
817
858
|
filePath,
|
|
818
859
|
});
|
|
819
860
|
try {
|
|
820
|
-
recordSessionEvent(getSessionsDir(), sessionId, event, hash
|
|
861
|
+
recordSessionEvent(getSessionsDir(), sessionId, event, hash, {
|
|
862
|
+
resetGeneration: startsNewGeneration,
|
|
863
|
+
});
|
|
821
864
|
} catch (cause) {
|
|
822
865
|
if (requireIndexAnchor) {
|
|
823
866
|
const error = new Error(
|
|
@@ -909,6 +952,112 @@ export function appendAuthorityEventIfHead(
|
|
|
909
952
|
});
|
|
910
953
|
}
|
|
911
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Validate a caller-owned projection and append one authority event while the
|
|
957
|
+
* same transcript writer lock remains held. This closes the read/validate to
|
|
958
|
+
* append race for authorities (such as MCP host fencing) that are derived from
|
|
959
|
+
* the complete verified transcript rather than from the head hash alone.
|
|
960
|
+
*
|
|
961
|
+
* Both callbacks must be synchronous. The projection remains caller-owned and
|
|
962
|
+
* only its validation result controls whether the event is appended.
|
|
963
|
+
*/
|
|
964
|
+
export function appendAuthorityEventWithVerifiedProjection(
|
|
965
|
+
sessionId,
|
|
966
|
+
type,
|
|
967
|
+
data,
|
|
968
|
+
{ createProjection, validateProjection } = {},
|
|
969
|
+
) {
|
|
970
|
+
if (
|
|
971
|
+
typeof createProjection !== "function" ||
|
|
972
|
+
typeof validateProjection !== "function"
|
|
973
|
+
) {
|
|
974
|
+
throw new TypeError(
|
|
975
|
+
"Authority projection append requires projection and validation callbacks",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (
|
|
979
|
+
createProjection.constructor?.name === "AsyncFunction" ||
|
|
980
|
+
validateProjection.constructor?.name === "AsyncFunction"
|
|
981
|
+
) {
|
|
982
|
+
throw new TypeError(
|
|
983
|
+
"Authority projection append callbacks must be synchronous",
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const filePath = sessionPath(sessionId);
|
|
987
|
+
return withFileLock(
|
|
988
|
+
filePath,
|
|
989
|
+
() => {
|
|
990
|
+
const projection = createProjection();
|
|
991
|
+
if (
|
|
992
|
+
!projection ||
|
|
993
|
+
typeof projection.accept !== "function" ||
|
|
994
|
+
typeof projection.finish !== "function"
|
|
995
|
+
) {
|
|
996
|
+
throw new TypeError(
|
|
997
|
+
"Authority projection must provide accept() and finish()",
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (!existsSync(filePath)) {
|
|
1001
|
+
throw unverifiedTranscriptError(sessionId, {
|
|
1002
|
+
status: "missing",
|
|
1003
|
+
reason: "authority transcript does not exist",
|
|
1004
|
+
lastHash: null,
|
|
1005
|
+
chainedEvents: 0,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
_resolveChainTail(filePath);
|
|
1009
|
+
const verification = verifyTranscriptFile(filePath, {
|
|
1010
|
+
onVerifiedEvent: (event) => {
|
|
1011
|
+
const accepted = projection.accept(event);
|
|
1012
|
+
if (accepted && typeof accepted.then === "function") {
|
|
1013
|
+
throw new TypeError(
|
|
1014
|
+
"Authority projection accept() must be synchronous",
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
assertVerifiedTranscriptAnchor(sessionId, verification);
|
|
1020
|
+
const authority = Object.freeze({
|
|
1021
|
+
headHash: verification.lastHash,
|
|
1022
|
+
eventCount: verification.chainedEvents,
|
|
1023
|
+
readMessages: () => rebuildVerifiedMessagesFromFile(filePath),
|
|
1024
|
+
});
|
|
1025
|
+
const projected = projection.finish(authority);
|
|
1026
|
+
if (projected && typeof projected.then === "function") {
|
|
1027
|
+
throw new TypeError(
|
|
1028
|
+
"Authority projection finish() must be synchronous",
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const validation = validateProjection(projected, authority);
|
|
1032
|
+
if (validation && typeof validation.then === "function") {
|
|
1033
|
+
throw new TypeError(
|
|
1034
|
+
"Authority projection validation must be synchronous",
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (validation === false) {
|
|
1038
|
+
throw new Error("Authority projection validation rejected the append");
|
|
1039
|
+
}
|
|
1040
|
+
const persistedData = encodeEventMessageProvenance(type, data);
|
|
1041
|
+
const appended = appendVerifiedWsAuthorityEventLocked(
|
|
1042
|
+
sessionId,
|
|
1043
|
+
filePath,
|
|
1044
|
+
type,
|
|
1045
|
+
persistedData,
|
|
1046
|
+
authority.headHash,
|
|
1047
|
+
);
|
|
1048
|
+
return Object.freeze({ hash: appended.hash });
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
failIfUnavailable: true,
|
|
1052
|
+
timeoutMs: 30_000,
|
|
1053
|
+
retryMs: 1,
|
|
1054
|
+
maxRetryMs: 8,
|
|
1055
|
+
retryJitterMs: 4,
|
|
1056
|
+
yieldAfterReleaseMs: 2,
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
912
1061
|
/**
|
|
913
1062
|
* Run a synchronous authority transaction while holding the canonical
|
|
914
1063
|
* transcript writer lock for its whole lifetime.
|
|
@@ -948,12 +1097,13 @@ export function withSessionAuthorityTransaction(
|
|
|
948
1097
|
filePath,
|
|
949
1098
|
() => {
|
|
950
1099
|
const existingMeta = readSessionMeta(getSessionsDir(), sessionId);
|
|
951
|
-
|
|
1100
|
+
const persistedWitness = existingMeta;
|
|
1101
|
+
if (persistedWitness?.deleted === true) {
|
|
952
1102
|
const error = new Error(`Session was deleted: ${sessionId}`);
|
|
953
1103
|
error.code = "SESSION_DELETED";
|
|
954
1104
|
throw error;
|
|
955
1105
|
}
|
|
956
|
-
if (!existsSync(filePath) &&
|
|
1106
|
+
if (!existsSync(filePath) && persistedWitness !== null) {
|
|
957
1107
|
throw unverifiedTranscriptError(sessionId, {
|
|
958
1108
|
status: "missing",
|
|
959
1109
|
reason: "session sidecar exists without its transcript",
|
|
@@ -1205,7 +1355,18 @@ export function verifySession(sessionId) {
|
|
|
1205
1355
|
return { sessionId, status: "invalid-id", reason: "invalid session id" };
|
|
1206
1356
|
}
|
|
1207
1357
|
const filePath = sessionPath(sessionId);
|
|
1358
|
+
const presence = getSessionPresence(sessionId);
|
|
1359
|
+
if (presence === SESSION_PRESENCE.CONFLICT) {
|
|
1360
|
+
return {
|
|
1361
|
+
sessionId,
|
|
1362
|
+
status: "conflict",
|
|
1363
|
+
reason: "tombstoned session has a restored transcript path",
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1208
1366
|
if (!existsSync(filePath)) {
|
|
1367
|
+
if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
|
|
1368
|
+
return { sessionId, ...missingTranscriptVerification() };
|
|
1369
|
+
}
|
|
1209
1370
|
return { sessionId, status: "not-found", reason: "session file not found" };
|
|
1210
1371
|
}
|
|
1211
1372
|
return {
|
|
@@ -1217,10 +1378,20 @@ export function verifySession(sessionId) {
|
|
|
1217
1378
|
export function verifyAllSessions(options = {}) {
|
|
1218
1379
|
const dir = getSessionsDir();
|
|
1219
1380
|
if (!existsSync(dir)) return [];
|
|
1220
|
-
|
|
1221
|
-
|
|
1381
|
+
const sessionIds = new Set(listSessionIds());
|
|
1382
|
+
for (const file of readdirSync(dir)) {
|
|
1383
|
+
if (!file.endsWith(".meta.json")) continue;
|
|
1384
|
+
const sessionId = file.slice(0, -".meta.json".length);
|
|
1385
|
+
if (
|
|
1386
|
+
!isUnsafeSessionId(sessionId) &&
|
|
1387
|
+
getSessionPresence(sessionId) === SESSION_PRESENCE.MISSING_TRANSCRIPT
|
|
1388
|
+
) {
|
|
1389
|
+
sessionIds.add(sessionId);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return [...sessionIds]
|
|
1222
1393
|
.slice(0, options.limit || 1000)
|
|
1223
|
-
.map((
|
|
1394
|
+
.map((sessionId) => verifySession(sessionId));
|
|
1224
1395
|
}
|
|
1225
1396
|
|
|
1226
1397
|
function indexedProjectionMatchesTranscript(meta, verification, validation) {
|
|
@@ -1267,6 +1438,32 @@ export function repairSession(sessionId, options = {}) {
|
|
|
1267
1438
|
filePath,
|
|
1268
1439
|
() => {
|
|
1269
1440
|
const dir = getSessionsDir();
|
|
1441
|
+
if (getSessionPresence(sessionId) === SESSION_PRESENCE.CONFLICT) {
|
|
1442
|
+
return {
|
|
1443
|
+
sessionId,
|
|
1444
|
+
dryRun,
|
|
1445
|
+
changed: false,
|
|
1446
|
+
physicalChanged: false,
|
|
1447
|
+
indexChanged: false,
|
|
1448
|
+
wouldChange: false,
|
|
1449
|
+
action: "none",
|
|
1450
|
+
physicalAction: "none",
|
|
1451
|
+
indexAction: "none",
|
|
1452
|
+
indexRepairRequired: false,
|
|
1453
|
+
indexRebuilt: false,
|
|
1454
|
+
indexRepairError: null,
|
|
1455
|
+
discardedBytes: 0,
|
|
1456
|
+
discardedRecords: 0,
|
|
1457
|
+
healthy: false,
|
|
1458
|
+
transcriptHealthy: false,
|
|
1459
|
+
status: "conflict",
|
|
1460
|
+
before: null,
|
|
1461
|
+
after: null,
|
|
1462
|
+
beforeValidation: null,
|
|
1463
|
+
afterValidation: null,
|
|
1464
|
+
reason: "tombstoned session has a restored transcript path",
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1270
1467
|
const before = verifyTranscriptFile(filePath);
|
|
1271
1468
|
const beforeValidation = validateJsonlSession(sessionId);
|
|
1272
1469
|
const repair = inspectPhysicalTail(filePath, { dryRun });
|
|
@@ -1367,7 +1564,9 @@ export function listSessionIds() {
|
|
|
1367
1564
|
export function resolveSessionId(input) {
|
|
1368
1565
|
if (isUnsafeSessionId(input)) return null;
|
|
1369
1566
|
if (sessionExists(input)) return input;
|
|
1370
|
-
const matches = listSessionIds().filter(
|
|
1567
|
+
const matches = listSessionIds().filter(
|
|
1568
|
+
(id) => id.startsWith(input) && sessionExists(id),
|
|
1569
|
+
);
|
|
1371
1570
|
if (matches.length > 1) {
|
|
1372
1571
|
const error = new Error(`Ambiguous session id prefix: ${input}`);
|
|
1373
1572
|
error.code = "AMBIGUOUS_SESSION_ID";
|
|
@@ -1757,6 +1956,16 @@ export function appendCompactEventIfMessagesMatch(
|
|
|
1757
1956
|
|
|
1758
1957
|
export function readEvents(sessionId) {
|
|
1759
1958
|
if (isUnsafeSessionId(sessionId)) return []; // traversal id → treat as empty
|
|
1959
|
+
const presence = getSessionPresence(sessionId);
|
|
1960
|
+
if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
|
|
1961
|
+
throw unverifiedTranscriptError(sessionId, missingTranscriptVerification());
|
|
1962
|
+
}
|
|
1963
|
+
if (presence === SESSION_PRESENCE.CONFLICT) {
|
|
1964
|
+
throw unverifiedTranscriptError(
|
|
1965
|
+
sessionId,
|
|
1966
|
+
transcriptAnchorMismatchVerification(),
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1760
1969
|
const filePath = sessionPath(sessionId);
|
|
1761
1970
|
if (!existsSync(filePath)) return [];
|
|
1762
1971
|
|
|
@@ -1782,6 +1991,37 @@ function unverifiedTranscriptError(sessionId, verification) {
|
|
|
1782
1991
|
return error;
|
|
1783
1992
|
}
|
|
1784
1993
|
|
|
1994
|
+
function missingTranscriptVerification() {
|
|
1995
|
+
return Object.freeze({
|
|
1996
|
+
status: "missing",
|
|
1997
|
+
reason: "persisted session witness exists without its transcript",
|
|
1998
|
+
lastHash: null,
|
|
1999
|
+
chainedEvents: 0,
|
|
2000
|
+
malformedLines: 0,
|
|
2001
|
+
truncatedTail: false,
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function transcriptAnchorMismatchVerification(lastHash = null) {
|
|
2006
|
+
return Object.freeze({
|
|
2007
|
+
status: "anchor-mismatch",
|
|
2008
|
+
reason: "persisted session witness does not match its transcript",
|
|
2009
|
+
lastHash,
|
|
2010
|
+
chainedEvents: 0,
|
|
2011
|
+
malformedLines: 0,
|
|
2012
|
+
truncatedTail: false,
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function readSessionPersistenceWitness(sessionId) {
|
|
2017
|
+
return readSessionMeta(getSessionsDir(), sessionId);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function hasLiveSessionWitness(sessionId) {
|
|
2021
|
+
const witness = readSessionPersistenceWitness(sessionId);
|
|
2022
|
+
return witness !== null && witness?.deleted !== true;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1785
2025
|
function assertVerifiedTranscriptAnchor(sessionId, verification) {
|
|
1786
2026
|
const meta = readSessionMeta(getSessionsDir(), sessionId);
|
|
1787
2027
|
const trustedStatus =
|
|
@@ -1847,6 +2087,18 @@ export function readVerifiedProjection(
|
|
|
1847
2087
|
);
|
|
1848
2088
|
}
|
|
1849
2089
|
if (!existsSync(filePath)) {
|
|
2090
|
+
const witness = readSessionPersistenceWitness(sessionId);
|
|
2091
|
+
if (witness?.deleted === true) {
|
|
2092
|
+
const error = new Error(`Session was deleted: ${sessionId}`);
|
|
2093
|
+
error.code = "SESSION_DELETED";
|
|
2094
|
+
throw error;
|
|
2095
|
+
}
|
|
2096
|
+
if (witness !== null) {
|
|
2097
|
+
throw unverifiedTranscriptError(
|
|
2098
|
+
sessionId,
|
|
2099
|
+
missingTranscriptVerification(),
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
1850
2102
|
return projection.finish(
|
|
1851
2103
|
Object.freeze({
|
|
1852
2104
|
headHash: null,
|
|
@@ -2052,6 +2304,16 @@ function rebuildVerifiedMessagesFromFile(filePath, options = {}) {
|
|
|
2052
2304
|
|
|
2053
2305
|
export function rebuildMessages(sessionId, options = {}) {
|
|
2054
2306
|
if (isUnsafeSessionId(sessionId)) return [];
|
|
2307
|
+
const presence = getSessionPresence(sessionId);
|
|
2308
|
+
if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
|
|
2309
|
+
throw unverifiedTranscriptError(sessionId, missingTranscriptVerification());
|
|
2310
|
+
}
|
|
2311
|
+
if (presence === SESSION_PRESENCE.CONFLICT) {
|
|
2312
|
+
throw unverifiedTranscriptError(
|
|
2313
|
+
sessionId,
|
|
2314
|
+
transcriptAnchorMismatchVerification(),
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2055
2317
|
const filePath = sessionPath(sessionId);
|
|
2056
2318
|
if (!existsSync(filePath)) return [];
|
|
2057
2319
|
return rebuildMessagesFromFile(filePath, options);
|
|
@@ -2115,9 +2377,13 @@ export function listJsonlSessions(options = {}) {
|
|
|
2115
2377
|
|
|
2116
2378
|
const limit = options.limit || 20;
|
|
2117
2379
|
const sessionIds = listSessionIds();
|
|
2380
|
+
let indexScan = null;
|
|
2118
2381
|
let indexed = listIndexedSessions(dir, {
|
|
2119
2382
|
limit,
|
|
2120
2383
|
hasSession: (id) => sessionExists(id),
|
|
2384
|
+
onScanComplete: (scan) => {
|
|
2385
|
+
indexScan = scan;
|
|
2386
|
+
},
|
|
2121
2387
|
});
|
|
2122
2388
|
|
|
2123
2389
|
// First use after upgrade (or a lost/corrupt rebuildable index): stream each
|
|
@@ -2126,8 +2392,22 @@ export function listJsonlSessions(options = {}) {
|
|
|
2126
2392
|
if (indexed.length < expected) {
|
|
2127
2393
|
const rebuilt = [];
|
|
2128
2394
|
for (const id of sessionIds) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2395
|
+
// A physical path restored behind a tombstone is a conflict, not a live
|
|
2396
|
+
// list entry. Never re-emit its deleted snapshot as list activity.
|
|
2397
|
+
if (!sessionExists(id)) continue;
|
|
2398
|
+
const meta = readSessionMeta(dir, id) || rebuildSessionMeta(id);
|
|
2399
|
+
if (!meta) continue;
|
|
2400
|
+
const indexedMeta = indexScan?.latestBySessionId?.get(id) || null;
|
|
2401
|
+
if (
|
|
2402
|
+
indexedMeta?.deleted !== true &&
|
|
2403
|
+
indexedMeta?.last_hash === meta.last_hash &&
|
|
2404
|
+
Number(indexedMeta?.updated_at_ms || 0) ===
|
|
2405
|
+
Number(meta.updated_at_ms || 0) &&
|
|
2406
|
+
Number(indexedMeta?.event_count || 0) === Number(meta.event_count || 0)
|
|
2407
|
+
) {
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
rebuilt.push(meta);
|
|
2131
2411
|
}
|
|
2132
2412
|
// Directory enumeration order is not activity order. Re-append the rebuilt
|
|
2133
2413
|
// snapshots oldest-first so reverse journal reads preserve the transcript
|
|
@@ -2160,18 +2440,25 @@ export function renameSession(sessionId, title) {
|
|
|
2160
2440
|
return { id: sessionId, title: normalized.slice(0, 200) };
|
|
2161
2441
|
}
|
|
2162
2442
|
|
|
2163
|
-
/** Delete the canonical transcript under its writer lock and publish
|
|
2164
|
-
*
|
|
2165
|
-
*
|
|
2443
|
+
/** Delete the canonical transcript under its writer lock and publish durable
|
|
2444
|
+
* tombstone witnesses so an already-waiting stale writer cannot resurrect the
|
|
2445
|
+
* session after the deletion commits. */
|
|
2166
2446
|
export function deleteJsonlSession(sessionId) {
|
|
2167
2447
|
if (isUnsafeSessionId(sessionId)) return false;
|
|
2168
2448
|
const filePath = sessionPath(sessionId);
|
|
2169
|
-
if (!existsSync(filePath)) return false;
|
|
2449
|
+
if (!existsSync(filePath) && !hasLiveSessionWitness(sessionId)) return false;
|
|
2170
2450
|
return withFileLock(
|
|
2171
2451
|
filePath,
|
|
2172
2452
|
() => {
|
|
2173
|
-
|
|
2174
|
-
|
|
2453
|
+
const transcriptExists = existsSync(filePath);
|
|
2454
|
+
const existingMeta = readSessionPersistenceWitness(sessionId);
|
|
2455
|
+
if (
|
|
2456
|
+
!transcriptExists &&
|
|
2457
|
+
(existingMeta === null || existingMeta?.deleted === true)
|
|
2458
|
+
) {
|
|
2459
|
+
return false;
|
|
2460
|
+
}
|
|
2461
|
+
if (transcriptExists) rmSync(filePath, { force: true });
|
|
2175
2462
|
try {
|
|
2176
2463
|
recordSessionDeleted(getSessionsDir(), sessionId);
|
|
2177
2464
|
} catch (error) {
|
|
@@ -2873,14 +3160,120 @@ export function createBranchSession({
|
|
|
2873
3160
|
);
|
|
2874
3161
|
}
|
|
2875
3162
|
|
|
3163
|
+
export const SESSION_PRESENCE = Object.freeze({
|
|
3164
|
+
ABSENT: "absent",
|
|
3165
|
+
PRESENT: "present",
|
|
3166
|
+
MISSING_TRANSCRIPT: "missing-transcript",
|
|
3167
|
+
TOMBSTONED: "tombstoned",
|
|
3168
|
+
CONFLICT: "conflict",
|
|
3169
|
+
});
|
|
3170
|
+
|
|
3171
|
+
export function getSessionPresence(sessionId) {
|
|
3172
|
+
if (isUnsafeSessionId(sessionId)) return SESSION_PRESENCE.ABSENT;
|
|
3173
|
+
const transcriptExists = existsSync(sessionPath(sessionId));
|
|
3174
|
+
const witness = readSessionPersistenceWitness(sessionId);
|
|
3175
|
+
if (transcriptExists && witness?.deleted === true) {
|
|
3176
|
+
return SESSION_PRESENCE.CONFLICT;
|
|
3177
|
+
}
|
|
3178
|
+
if (transcriptExists) return SESSION_PRESENCE.PRESENT;
|
|
3179
|
+
if (witness?.deleted === true) return SESSION_PRESENCE.TOMBSTONED;
|
|
3180
|
+
if (witness !== null) return SESSION_PRESENCE.MISSING_TRANSCRIPT;
|
|
3181
|
+
return SESSION_PRESENCE.ABSENT;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/** True when a host must treat this id as canonical instead of falling back. */
|
|
3185
|
+
export function sessionHasPersistedEvidence(sessionId) {
|
|
3186
|
+
return [
|
|
3187
|
+
SESSION_PRESENCE.PRESENT,
|
|
3188
|
+
SESSION_PRESENCE.MISSING_TRANSCRIPT,
|
|
3189
|
+
SESSION_PRESENCE.TOMBSTONED,
|
|
3190
|
+
SESSION_PRESENCE.CONFLICT,
|
|
3191
|
+
].includes(getSessionPresence(sessionId));
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
/** True only for a physical, non-tombstoned transcript path. */
|
|
2876
3195
|
export function sessionExists(sessionId) {
|
|
2877
|
-
|
|
2878
|
-
return existsSync(sessionPath(sessionId));
|
|
3196
|
+
return getSessionPresence(sessionId) === SESSION_PRESENCE.PRESENT;
|
|
2879
3197
|
}
|
|
2880
3198
|
|
|
2881
3199
|
export function getLastSessionId() {
|
|
2882
|
-
|
|
2883
|
-
|
|
3200
|
+
// Seed/rebuild missing indexes for physical transcripts first, then include
|
|
3201
|
+
// a live missing-transcript witness so --continue fails closed on the newest
|
|
3202
|
+
// canonical identity instead of silently selecting an older healthy chat.
|
|
3203
|
+
const dir = getSessionsDir();
|
|
3204
|
+
listJsonlSessions({ limit: 1 });
|
|
3205
|
+
const sessions = listIndexedSessions(dir, {
|
|
3206
|
+
limit: 1,
|
|
3207
|
+
hasSession: (id) =>
|
|
3208
|
+
[
|
|
3209
|
+
SESSION_PRESENCE.PRESENT,
|
|
3210
|
+
SESSION_PRESENCE.MISSING_TRANSCRIPT,
|
|
3211
|
+
SESSION_PRESENCE.CONFLICT,
|
|
3212
|
+
].includes(getSessionPresence(id)),
|
|
3213
|
+
includeDeleted: (id) =>
|
|
3214
|
+
getSessionPresence(id) === SESSION_PRESENCE.CONFLICT,
|
|
3215
|
+
});
|
|
3216
|
+
const candidateRisk = (presence) =>
|
|
3217
|
+
[SESSION_PRESENCE.MISSING_TRANSCRIPT, SESSION_PRESENCE.CONFLICT].includes(
|
|
3218
|
+
presence,
|
|
3219
|
+
)
|
|
3220
|
+
? 1
|
|
3221
|
+
: 0;
|
|
3222
|
+
let latest =
|
|
3223
|
+
sessions.length > 0
|
|
3224
|
+
? {
|
|
3225
|
+
id: sessions[0].id,
|
|
3226
|
+
updatedAt: Date.parse(sessions[0].updated_at) || 0,
|
|
3227
|
+
risk: candidateRisk(getSessionPresence(sessions[0].id)),
|
|
3228
|
+
}
|
|
3229
|
+
: null;
|
|
3230
|
+
|
|
3231
|
+
// A stale/corrupt disposable activity journal cannot rediscover a sidecar-
|
|
3232
|
+
// only missing witness. Directory enumeration already occurs in the list
|
|
3233
|
+
// path; normally read only metadata files that have no transcript
|
|
3234
|
+
// counterpart. Durable tombstone namespace witnesses are enumerable, so
|
|
3235
|
+
// restored transcript conflicts are checked without opening every live
|
|
3236
|
+
// sidecar. This remains correct when the disposable activity journal is
|
|
3237
|
+
// absent, corrupt, or a parseable stale prefix.
|
|
3238
|
+
if (existsSync(dir)) {
|
|
3239
|
+
const files = readdirSync(dir);
|
|
3240
|
+
const transcriptIds = new Set(
|
|
3241
|
+
files
|
|
3242
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
3243
|
+
.map((file) => basename(file, ".jsonl")),
|
|
3244
|
+
);
|
|
3245
|
+
const tombstoneIds = new Set(
|
|
3246
|
+
files
|
|
3247
|
+
.filter((file) => file.endsWith(SESSION_TOMBSTONE_MARKER_SUFFIX))
|
|
3248
|
+
.map((file) => file.slice(0, -SESSION_TOMBSTONE_MARKER_SUFFIX.length)),
|
|
3249
|
+
);
|
|
3250
|
+
for (const file of files) {
|
|
3251
|
+
if (!file.endsWith(".meta.json")) continue;
|
|
3252
|
+
const id = file.slice(0, -".meta.json".length);
|
|
3253
|
+
const hasTranscript = transcriptIds.has(id);
|
|
3254
|
+
if (isUnsafeSessionId(id) || (hasTranscript && !tombstoneIds.has(id))) {
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
const meta = readSessionMeta(dir, id);
|
|
3258
|
+
const presence = getSessionPresence(id);
|
|
3259
|
+
const isMissing = presence === SESSION_PRESENCE.MISSING_TRANSCRIPT;
|
|
3260
|
+
const isRecoveredConflict = presence === SESSION_PRESENCE.CONFLICT;
|
|
3261
|
+
if (!meta || (!isMissing && !isRecoveredConflict)) continue;
|
|
3262
|
+
const updatedAt = Math.max(0, Number(meta?.updated_at_ms) || 0);
|
|
3263
|
+
const risk = candidateRisk(presence);
|
|
3264
|
+
if (
|
|
3265
|
+
latest === null ||
|
|
3266
|
+
updatedAt > latest.updatedAt ||
|
|
3267
|
+
(updatedAt === latest.updatedAt && risk > latest.risk) ||
|
|
3268
|
+
(updatedAt === latest.updatedAt &&
|
|
3269
|
+
risk === latest.risk &&
|
|
3270
|
+
id > latest.id)
|
|
3271
|
+
) {
|
|
3272
|
+
latest = { id, updatedAt, risk };
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return latest?.id || null;
|
|
2884
3277
|
}
|
|
2885
3278
|
|
|
2886
3279
|
export function migrateLegacySessions(sourceDir, options = {}) {
|
|
@@ -3068,7 +3461,8 @@ function performLegacySessionMigration(sourcePath, options) {
|
|
|
3068
3461
|
};
|
|
3069
3462
|
}
|
|
3070
3463
|
|
|
3071
|
-
|
|
3464
|
+
const existingPresence = getSessionPresence(sessionId);
|
|
3465
|
+
if (!options.force && existingPresence !== SESSION_PRESENCE.ABSENT) {
|
|
3072
3466
|
return {
|
|
3073
3467
|
file: sourcePath,
|
|
3074
3468
|
sessionId,
|
|
@@ -3078,8 +3472,17 @@ function performLegacySessionMigration(sourcePath, options) {
|
|
|
3078
3472
|
}
|
|
3079
3473
|
|
|
3080
3474
|
if (!options.dryRun) {
|
|
3081
|
-
if (
|
|
3082
|
-
|
|
3475
|
+
if (
|
|
3476
|
+
options.force &&
|
|
3477
|
+
![SESSION_PRESENCE.ABSENT, SESSION_PRESENCE.TOMBSTONED].includes(
|
|
3478
|
+
existingPresence,
|
|
3479
|
+
)
|
|
3480
|
+
) {
|
|
3481
|
+
if (!deleteJsonlSession(sessionId)) {
|
|
3482
|
+
throw new Error(
|
|
3483
|
+
`could not tombstone existing JSONL session before forced migration: ${sessionId}`,
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3083
3486
|
}
|
|
3084
3487
|
startSession(sessionId, legacy.meta);
|
|
3085
3488
|
for (const message of legacy.messages) {
|