chainlesschain 0.162.198 → 0.162.200
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/bin/chainlesschain.js +14 -6
- package/package.json +4 -1
- package/scripts/command-lifecycle-report.mjs +94 -0
- package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-DcQ0bWKy.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-GecwfFPt.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-CD6iSt9v.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CDEtuEYf.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DS3jAjeW.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CcSJA048.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-DRo9Morb.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-Vw5MrWft.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DNHIu823.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-DvAsO8-b.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-9aDuObay.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-CKCa7tvn.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-CIFvZdaT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-C79yz7bK.js} +1 -1
- package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-DVvZskLJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-oaew61hV.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-DqA7wmRx.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-B-qGadxa.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-CUlCl4sS.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-CNOlr9DU.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DJqFCT_3.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CznGpphf.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-_cJw218E.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Dr-1BZ9C.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Dxf4xtSk.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-DOiG3i_1.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BzbMcFB9.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-VbBrYttP.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-CLG8E6ED.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-LGJQfk_q.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DlQKx9lF.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-DU8vEouV.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-FZIQOdln.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BJNWIrbw.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-C_xd-zZI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-q-YktphS.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-Cq9OhDin.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-B03_R98T.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-C8qfktxn.js → MobileProjects-DZrk-qmF.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-BidCKW28.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-r-iaMVM7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-CgTUFvff.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-B7NvzfAV.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-80CWNqJI.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-0H6xdiY3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-CKP4AFWy.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-B8sxCV_h.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-CW9JBBhQ.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-bdSQV7oZ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-DQad42nW.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-CueexxTR.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-fqxflhR8.js} +6 -6
- package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-KFwwnIMC.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-ClnD9buY.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-CZEz6sm3.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-B6NzpmOI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-DYyuwPCs.js} +1 -1
- package/src/assets/web-panel/assets/Providers-C3tz_jdh.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-BzuOBbqB.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-CmNO9qSj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-VJNj3_NF.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-CrbdbSBF.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-DTyMd45j.js} +1 -1
- package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-DHrQ-Icz.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-DqWxY9c5.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-ctF1Ks3J.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-BrQz8uo5.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-D4scVFar.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-oUScaOjv.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-CL-y0eLt.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-DQExhwXj.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-iNp_I7kN.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-CTDlMo0A.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-4kb8l3sc.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-BXcL9oXW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-DLE8eS0D.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DGpUZPPK.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-C1pWXGPC.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BYfbfqhZ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-K9YfLOCw.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-Ctkq77Hy.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-C6_CSe9J.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-Be3rpOWs.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-D0UIplxk.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-CF9bM9I3.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-D0H1XVPY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-Cgzzbe0o.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-BwNxEVS0.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-D60Z4Hhs.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-MFk58XB0.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BJiw369i.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-CRj2SvGO.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index--s45_1cb.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4l82Me1.js → index--tquQ2ZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-1T1rhokn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B-M82_rx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-BE_el1Zs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-BhRkR7IO.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-Bo0y96-K.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoFak7le.js → index-CJgu5JUK.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-CLWwdebO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-CNAK5QG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-CQjJjPRD.js} +1 -1
- package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTDbAfae.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CVI_9sRK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-CY14oMQK.js} +1 -1
- package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CbiTJkZf.js} +1 -1
- package/src/assets/web-panel/assets/index-CiMouosn.js +1 -0
- package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-CqWpxMqx.js} +1 -1
- package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-CvB8lYUR.js} +1 -1
- package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-D51XwtCY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-D6HvK_1O.js} +1 -1
- package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-D9jVIwLJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-DA4tE98u.js} +1 -1
- package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-DBwOtXw0.js} +1 -1
- package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DKdNzTIb.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-DRySdmvN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DWzyTev7.js} +1 -1
- package/src/assets/web-panel/assets/index-DXcKlwy7.js +1 -0
- package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-DYjs7jAP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DbROzMBp.js} +1 -1
- package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-DhFtk5PR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-Di2AbMLg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvmocQsM.js} +1 -1
- package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-Dw-o0zEF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DysPoP38.js} +1 -1
- package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-LX9pJxm_.js} +1 -1
- package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-OI_fXWx0.js} +3 -3
- package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-eDTOX8Xj.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-gWKDsTzF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-q_GDUVAA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-C61kZqEd.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-DZnT5U1L.js} +1 -1
- package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-L8wzwbel.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-DLdYSejc.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-D53cbS59.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-Bbc5HPtu.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-BkvmO860.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-u2y1ryS1.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-BBqj7svU.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-GN0mqu2d.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BvwPKoBT.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-CCt8tnfY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-DXAxl2SV.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-DihlR2dj.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-D7BOX2gi.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-CVbO-nL7.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/mcp.js +119 -0
- package/src/commands/session-show.js +8 -10
- package/src/commands/session.js +60 -52
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/session-protocol.js +14 -4
- package/src/gateways/ws/ws-agent-handler.js +31 -0
- package/src/harness/jsonl-session-store.js +1147 -130
- package/src/harness/mcp-client.js +44 -11
- package/src/harness/session-index.js +80 -80
- package/src/harness/session-list-index.js +147 -2
- package/src/lib/background-interaction-journal.js +2 -2
- package/src/lib/command-lifecycle-report.js +516 -0
- package/src/lib/mcp-stdio-environment.js +48 -0
- package/src/lib/mcp-stdio-executable-identity.js +552 -17
- package/src/lib/mcp-stdio-package-materialization.js +1424 -0
- package/src/lib/packer/native-update-state.js +979 -1
- package/src/lib/packer/pack-update-applier.js +1050 -125
- package/src/lib/packer/pkg-config-generator.js +1 -0
- package/src/lib/paths.js +53 -0
- package/src/lib/process-execution-broker/index.js +180 -37
- package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
- package/src/lib/recent-session.js +17 -17
- package/src/lib/secure-file-identity.js +47 -0
- package/src/lib/secure-fs.js +21 -2
- package/src/lib/session-anti-rollback-anchor.js +766 -0
- package/src/lib/session-host-lease.js +492 -0
- package/src/lib/session-persistence-failure.js +109 -0
- package/src/lib/session-picker.js +26 -3
- package/src/lib/session-search.js +25 -11
- package/src/lib/side-effect-ledger-store.js +2 -2
- package/src/lib/skill-execution-authority.js +13 -9
- package/src/lib/turn-binding-store.js +2 -2
- package/src/repl/agent-repl.js +309 -39
- package/src/runtime/agent-core.js +88 -5
- package/src/runtime/cleanup-deadline.js +143 -0
- package/src/runtime/headless-runner.js +357 -139
- package/src/runtime/headless-stream.js +207 -28
- package/src/runtime/output-backpressure.js +53 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-CU-JB2Ns.js +0 -1
- package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
- package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
- package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* JSONL Session Store — append-only session persistence.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import fs, {
|
|
6
6
|
constants as fsConstants,
|
|
7
7
|
existsSync,
|
|
8
8
|
appendFileSync,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fstatSync,
|
|
17
17
|
lstatSync,
|
|
18
18
|
readSync,
|
|
19
|
+
writeSync,
|
|
19
20
|
ftruncateSync,
|
|
20
21
|
} from "node:fs";
|
|
21
22
|
import { join, basename, resolve } from "node:path";
|
|
@@ -23,9 +24,16 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
23
24
|
import { TextDecoder } from "node:util";
|
|
24
25
|
import { getHomeDir } from "../lib/paths.js";
|
|
25
26
|
import { ensurePrivateDirectory, ensurePrivateFile } from "../lib/secure-fs.js";
|
|
27
|
+
import {
|
|
28
|
+
isAffectedWindowsZeroDeviceStatRuntime,
|
|
29
|
+
samePathHandleStableFileIdentity,
|
|
30
|
+
withTrustedFileParentSync,
|
|
31
|
+
} from "../lib/secure-file-identity.js";
|
|
26
32
|
import {
|
|
27
33
|
computeEventHash,
|
|
34
|
+
latestChainHash,
|
|
28
35
|
TRANSCRIPT_CHAIN_STATUS,
|
|
36
|
+
verifyTranscriptText,
|
|
29
37
|
} from "./transcript-integrity.js";
|
|
30
38
|
import { withFileLock } from "../lib/with-file-lock.js";
|
|
31
39
|
import {
|
|
@@ -48,12 +56,24 @@ import {
|
|
|
48
56
|
publicSessionMeta,
|
|
49
57
|
readLatestSessionActivity,
|
|
50
58
|
readSessionMeta,
|
|
59
|
+
readSessionTombstoneMarker,
|
|
51
60
|
recordSessionDeleted,
|
|
52
61
|
recordSessionEvent,
|
|
53
62
|
recordSessionActivity,
|
|
54
63
|
replaceSessionMeta,
|
|
64
|
+
normalizeSessionGenerationAuthority,
|
|
65
|
+
SESSION_GENERATION_AUTHORITY_FIELD,
|
|
66
|
+
SESSION_GENERATION_AUTHORITY_SCHEMA,
|
|
55
67
|
SESSION_TOMBSTONE_MARKER_SUFFIX,
|
|
56
68
|
} from "./session-list-index.js";
|
|
69
|
+
import { createSessionPersistenceFailure } from "../lib/session-persistence-failure.js";
|
|
70
|
+
import { withSessionHostWriteAuthority } from "../lib/session-host-lease.js";
|
|
71
|
+
import {
|
|
72
|
+
listSessionAntiRollbackIds,
|
|
73
|
+
publishSessionAntiRollbackAnchor,
|
|
74
|
+
readSessionAntiRollbackAnchor,
|
|
75
|
+
sessionAntiRollbackPredecessorWitness,
|
|
76
|
+
} from "../lib/session-anti-rollback-anchor.js";
|
|
57
77
|
|
|
58
78
|
let securedSessionsDir = null;
|
|
59
79
|
let securedSessionsDirIdentity = null;
|
|
@@ -62,6 +82,7 @@ let securedSessionsDirIdentity = null;
|
|
|
62
82
|
// gate. The hooks are inert unless the dedicated child process opts in; this
|
|
63
83
|
// keeps production behavior and ordinary tests on the exact same append path.
|
|
64
84
|
export const _sessionScaleFaultHooks = Object.seal({
|
|
85
|
+
beforeTranscriptAppend: null,
|
|
65
86
|
afterTranscriptAppend: null,
|
|
66
87
|
afterForkCopy: null,
|
|
67
88
|
afterForkLineage: null,
|
|
@@ -79,6 +100,11 @@ const WS_TURN_INPUT_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
|
79
100
|
const WS_TURN_CLAIM_ID_PATTERN = /^claim-[0-9a-f-]{36}$/;
|
|
80
101
|
const WS_TURN_FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
81
102
|
|
|
103
|
+
export {
|
|
104
|
+
SESSION_GENERATION_AUTHORITY_FIELD,
|
|
105
|
+
SESSION_GENERATION_AUTHORITY_SCHEMA,
|
|
106
|
+
};
|
|
107
|
+
|
|
82
108
|
function wsTurnError(message, code) {
|
|
83
109
|
const error = new Error(message);
|
|
84
110
|
error.code = code;
|
|
@@ -511,6 +537,100 @@ export function sessionPath(sessionId) {
|
|
|
511
537
|
return join(getSessionsDir(), `${sessionId}.jsonl`);
|
|
512
538
|
}
|
|
513
539
|
|
|
540
|
+
function physicalTranscriptStateFromStats(stats) {
|
|
541
|
+
const exact = (value) =>
|
|
542
|
+
typeof value === "bigint" ? String(value) : undefined;
|
|
543
|
+
const legacyNumber = (value) => Number(value);
|
|
544
|
+
const timeMs = (name) => {
|
|
545
|
+
const nanoseconds = stats[`${name}Ns`];
|
|
546
|
+
return typeof nanoseconds === "bigint"
|
|
547
|
+
? Number(nanoseconds / 1_000_000n) +
|
|
548
|
+
Number(nanoseconds % 1_000_000n) / 1_000_000
|
|
549
|
+
: Number(stats[`${name}Ms`]);
|
|
550
|
+
};
|
|
551
|
+
return Object.freeze({
|
|
552
|
+
// Keep the rounded legacy fields so metadata produced by older releases
|
|
553
|
+
// remains readable. BigIntStats supplies the exact witness used by new
|
|
554
|
+
// writers; this avoids Node 22.12 Windows fstat/lstat precision drift.
|
|
555
|
+
dev: String(legacyNumber(stats.dev)),
|
|
556
|
+
ino: String(legacyNumber(stats.ino)),
|
|
557
|
+
size: legacyNumber(stats.size),
|
|
558
|
+
mtimeMs: timeMs("mtime"),
|
|
559
|
+
ctimeMs: timeMs("ctime"),
|
|
560
|
+
devExact: exact(stats.dev),
|
|
561
|
+
inoExact: exact(stats.ino),
|
|
562
|
+
sizeExact: exact(stats.size),
|
|
563
|
+
mtimeNs: exact(stats.mtimeNs),
|
|
564
|
+
ctimeNs: exact(stats.ctimeNs),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function readPhysicalTranscriptStats(filePath) {
|
|
569
|
+
const stats = lstatSync(filePath, { bigint: true });
|
|
570
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
571
|
+
const error = new Error("Session transcript path is not a regular file");
|
|
572
|
+
error.code = "SESSION_TRANSCRIPT_IDENTITY_CHANGED";
|
|
573
|
+
error.commitState = "not-committed";
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
return stats;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function readPhysicalTranscriptState(filePath) {
|
|
580
|
+
return physicalTranscriptStateFromStats(
|
|
581
|
+
readPhysicalTranscriptStats(filePath),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function samePhysicalTranscriptState(left, right, { content = true } = {}) {
|
|
586
|
+
if (!left || !right) return left === right;
|
|
587
|
+
const sameField = (exactField, legacyField, { numeric = false } = {}) => {
|
|
588
|
+
if (left[exactField] != null && right[exactField] != null) {
|
|
589
|
+
return String(left[exactField]) === String(right[exactField]);
|
|
590
|
+
}
|
|
591
|
+
return numeric
|
|
592
|
+
? Number(left[legacyField]) === Number(right[legacyField])
|
|
593
|
+
: String(left[legacyField]) === String(right[legacyField]);
|
|
594
|
+
};
|
|
595
|
+
return (
|
|
596
|
+
sameField("devExact", "dev") &&
|
|
597
|
+
sameField("inoExact", "ino") &&
|
|
598
|
+
(!content ||
|
|
599
|
+
(sameField("sizeExact", "size", { numeric: true }) &&
|
|
600
|
+
sameField("mtimeNs", "mtimeMs", { numeric: true }) &&
|
|
601
|
+
sameField("ctimeNs", "ctimeMs", { numeric: true })))
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function samePhysicalTranscriptContentState(left, right) {
|
|
606
|
+
if (!left || !right) return left === right;
|
|
607
|
+
const sameField = (exactField, legacyField) => {
|
|
608
|
+
if (left[exactField] != null && right[exactField] != null) {
|
|
609
|
+
return String(left[exactField]) === String(right[exactField]);
|
|
610
|
+
}
|
|
611
|
+
return Number(left[legacyField]) === Number(right[legacyField]);
|
|
612
|
+
};
|
|
613
|
+
return (
|
|
614
|
+
sameField("sizeExact", "size") &&
|
|
615
|
+
sameField("mtimeNs", "mtimeMs") &&
|
|
616
|
+
sameField("ctimeNs", "ctimeMs")
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function withTranscriptFileParent(filePath, task) {
|
|
621
|
+
if (!isAffectedWindowsZeroDeviceStatRuntime()) {
|
|
622
|
+
return task({ canonicalPath: filePath, parentDevice: null });
|
|
623
|
+
}
|
|
624
|
+
return withTrustedFileParentSync(fs, filePath, task);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function transcriptIdentityError(commitState, reason) {
|
|
628
|
+
const error = new Error(`Session transcript identity changed: ${reason}`);
|
|
629
|
+
error.code = "SESSION_TRANSCRIPT_IDENTITY_CHANGED";
|
|
630
|
+
error.commitState = commitState;
|
|
631
|
+
return error;
|
|
632
|
+
}
|
|
633
|
+
|
|
514
634
|
export function appendTokenUsage(sessionId, usage) {
|
|
515
635
|
appendEvent(sessionId, "token_usage", usage || {});
|
|
516
636
|
}
|
|
@@ -661,19 +781,26 @@ function appendVerifiedWsAuthorityEventLocked(
|
|
|
661
781
|
const core = { type, timestamp: Date.now(), data };
|
|
662
782
|
const hash = computeEventHash(previousHeadHash, core);
|
|
663
783
|
const event = { ...core, prevHash: previousHeadHash, hash };
|
|
664
|
-
appendFileSync(filePath, `${JSON.stringify(event)}\n`, {
|
|
665
|
-
encoding: "utf8",
|
|
666
|
-
mode: 0o600,
|
|
667
|
-
});
|
|
668
784
|
ensurePrivateFile(filePath);
|
|
669
|
-
|
|
785
|
+
const expectedState = readPhysicalTranscriptState(filePath);
|
|
786
|
+
const transcriptState = appendTranscriptEvent(
|
|
670
787
|
sessionId,
|
|
671
788
|
type,
|
|
672
789
|
event,
|
|
673
790
|
filePath,
|
|
674
|
-
|
|
791
|
+
expectedState,
|
|
792
|
+
);
|
|
675
793
|
try {
|
|
676
|
-
|
|
794
|
+
const nextMeta = recordSessionEvent(
|
|
795
|
+
getSessionsDir(),
|
|
796
|
+
sessionId,
|
|
797
|
+
event,
|
|
798
|
+
hash,
|
|
799
|
+
{
|
|
800
|
+
transcriptState,
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
publishSessionAntiRollbackWitness(sessionId, nextMeta, filePath, "live");
|
|
677
804
|
} catch (cause) {
|
|
678
805
|
const error = new Error(
|
|
679
806
|
`Session authority anchor could not be persisted: ${sessionId}`,
|
|
@@ -711,9 +838,17 @@ function appendVerifiedWsAuthorityEventLocked(
|
|
|
711
838
|
return Object.freeze({ hash, event });
|
|
712
839
|
}
|
|
713
840
|
|
|
841
|
+
function withSessionHostWriterLock(sessionId, filePath, task, options) {
|
|
842
|
+
return withFileLock(
|
|
843
|
+
filePath,
|
|
844
|
+
() => withSessionHostWriteAuthority(sessionId, task),
|
|
845
|
+
options,
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
714
849
|
function withVerifiedWsTurnLock(sessionId, task) {
|
|
715
850
|
const filePath = sessionPath(sessionId);
|
|
716
|
-
return
|
|
851
|
+
return withSessionHostWriterLock(sessionId, filePath, () => task(filePath), {
|
|
717
852
|
failIfUnavailable: true,
|
|
718
853
|
timeoutMs: 30_000,
|
|
719
854
|
retryMs: 1,
|
|
@@ -740,6 +875,153 @@ function encodeEventMessageProvenance(type, data) {
|
|
|
740
875
|
return data;
|
|
741
876
|
}
|
|
742
877
|
|
|
878
|
+
function appendTranscriptEvent(
|
|
879
|
+
sessionId,
|
|
880
|
+
type,
|
|
881
|
+
event,
|
|
882
|
+
filePath,
|
|
883
|
+
expectedState,
|
|
884
|
+
) {
|
|
885
|
+
const payload = { sessionId, type, event, filePath };
|
|
886
|
+
try {
|
|
887
|
+
runSessionScaleFaultHook("beforeTranscriptAppend", payload);
|
|
888
|
+
} catch (cause) {
|
|
889
|
+
throw createSessionPersistenceFailure(cause, {
|
|
890
|
+
sessionId,
|
|
891
|
+
operation: "transcript-append",
|
|
892
|
+
commitState: "not-committed",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
return withTranscriptFileParent(
|
|
897
|
+
filePath,
|
|
898
|
+
({ canonicalPath, parentDevice }) =>
|
|
899
|
+
appendTranscriptEventAtPath({
|
|
900
|
+
sessionId,
|
|
901
|
+
event,
|
|
902
|
+
payload,
|
|
903
|
+
filePath: canonicalPath,
|
|
904
|
+
expectedState,
|
|
905
|
+
parentDevice,
|
|
906
|
+
}),
|
|
907
|
+
);
|
|
908
|
+
} catch (cause) {
|
|
909
|
+
if (
|
|
910
|
+
cause?.code === "SESSION_TRANSCRIPT_IDENTITY_CHANGED" ||
|
|
911
|
+
cause?.operation === "transcript-append" ||
|
|
912
|
+
cause?.operation === "transcript-settlement"
|
|
913
|
+
) {
|
|
914
|
+
throw cause;
|
|
915
|
+
}
|
|
916
|
+
throw createSessionPersistenceFailure(cause, {
|
|
917
|
+
sessionId,
|
|
918
|
+
operation: "transcript-settlement",
|
|
919
|
+
commitState: "unknown",
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function appendTranscriptEventAtPath({
|
|
925
|
+
sessionId,
|
|
926
|
+
event,
|
|
927
|
+
payload,
|
|
928
|
+
filePath,
|
|
929
|
+
expectedState,
|
|
930
|
+
parentDevice,
|
|
931
|
+
}) {
|
|
932
|
+
let fd = null;
|
|
933
|
+
let wroteBytes = false;
|
|
934
|
+
let appendCompleted = false;
|
|
935
|
+
try {
|
|
936
|
+
let flags = fsConstants.O_WRONLY | fsConstants.O_APPEND;
|
|
937
|
+
if (expectedState === null) {
|
|
938
|
+
flags |= fsConstants.O_CREAT | fsConstants.O_EXCL;
|
|
939
|
+
}
|
|
940
|
+
if (typeof fsConstants.O_NOFOLLOW === "number") {
|
|
941
|
+
flags |= fsConstants.O_NOFOLLOW;
|
|
942
|
+
}
|
|
943
|
+
fd = openSync(filePath, flags, 0o600);
|
|
944
|
+
const openedStats = fstatSync(fd, { bigint: true });
|
|
945
|
+
const openedState = physicalTranscriptStateFromStats(openedStats);
|
|
946
|
+
if (expectedState !== null) {
|
|
947
|
+
if (parentDevice === null) {
|
|
948
|
+
if (!samePhysicalTranscriptState(expectedState, openedState)) {
|
|
949
|
+
throw transcriptIdentityError(
|
|
950
|
+
"not-committed",
|
|
951
|
+
"the append descriptor does not match the verified path",
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
const publishedBeforeStats = readPhysicalTranscriptStats(filePath);
|
|
956
|
+
const publishedBeforeState =
|
|
957
|
+
physicalTranscriptStateFromStats(publishedBeforeStats);
|
|
958
|
+
if (
|
|
959
|
+
!samePhysicalTranscriptState(expectedState, publishedBeforeState) ||
|
|
960
|
+
!samePathHandleStableFileIdentity(
|
|
961
|
+
publishedBeforeStats,
|
|
962
|
+
openedStats,
|
|
963
|
+
parentDevice,
|
|
964
|
+
)
|
|
965
|
+
) {
|
|
966
|
+
throw transcriptIdentityError(
|
|
967
|
+
"not-committed",
|
|
968
|
+
"the append descriptor does not match the verified path",
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const bytes = Buffer.from(`${JSON.stringify(event)}\n`, "utf8");
|
|
974
|
+
let offset = 0;
|
|
975
|
+
while (offset < bytes.length) {
|
|
976
|
+
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
|
977
|
+
if (!Number.isSafeInteger(written) || written <= 0) {
|
|
978
|
+
throw new Error("Session transcript append made no forward progress");
|
|
979
|
+
}
|
|
980
|
+
offset += written;
|
|
981
|
+
wroteBytes = true;
|
|
982
|
+
}
|
|
983
|
+
appendCompleted = true;
|
|
984
|
+
runSessionScaleFaultHook("afterTranscriptAppend", payload);
|
|
985
|
+
const descriptorStats = fstatSync(fd, { bigint: true });
|
|
986
|
+
const descriptorState = physicalTranscriptStateFromStats(descriptorStats);
|
|
987
|
+
const pathStats = readPhysicalTranscriptStats(filePath);
|
|
988
|
+
const pathState = physicalTranscriptStateFromStats(pathStats);
|
|
989
|
+
const publishedIdentityMatches =
|
|
990
|
+
parentDevice === null
|
|
991
|
+
? samePhysicalTranscriptState(descriptorState, pathState, {
|
|
992
|
+
content: false,
|
|
993
|
+
})
|
|
994
|
+
: samePathHandleStableFileIdentity(
|
|
995
|
+
pathStats,
|
|
996
|
+
descriptorStats,
|
|
997
|
+
parentDevice,
|
|
998
|
+
);
|
|
999
|
+
if (!publishedIdentityMatches) {
|
|
1000
|
+
throw transcriptIdentityError(
|
|
1001
|
+
"unknown",
|
|
1002
|
+
"the published path no longer names the appended descriptor",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
return pathState;
|
|
1006
|
+
} catch (cause) {
|
|
1007
|
+
if (cause?.code === "SESSION_TRANSCRIPT_IDENTITY_CHANGED") throw cause;
|
|
1008
|
+
throw createSessionPersistenceFailure(cause, {
|
|
1009
|
+
sessionId,
|
|
1010
|
+
operation: appendCompleted
|
|
1011
|
+
? "transcript-settlement"
|
|
1012
|
+
: "transcript-append",
|
|
1013
|
+
// ENOSPC can follow a short write. EROFS fails before the append can
|
|
1014
|
+
// mutate the target.
|
|
1015
|
+
commitState:
|
|
1016
|
+
!appendCompleted && (cause?.code === "EROFS" || !wroteBytes)
|
|
1017
|
+
? "not-committed"
|
|
1018
|
+
: "unknown",
|
|
1019
|
+
});
|
|
1020
|
+
} finally {
|
|
1021
|
+
if (fd !== null) closeSync(fd);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
743
1025
|
const SESSION_AUTHORITY_RECOVERY_TEXT_LIMITS = Object.freeze({
|
|
744
1026
|
safetyId: 512,
|
|
745
1027
|
safetyIdentity: 512,
|
|
@@ -774,6 +1056,174 @@ function sanitizeSessionAuthorityRecoveryEvidence(value) {
|
|
|
774
1056
|
return Object.keys(evidence).length > 0 ? Object.freeze(evidence) : null;
|
|
775
1057
|
}
|
|
776
1058
|
|
|
1059
|
+
function sessionDeletedError(sessionId) {
|
|
1060
|
+
const error = new Error(`Session was deleted: ${sessionId}`);
|
|
1061
|
+
error.code = "SESSION_DELETED";
|
|
1062
|
+
return error;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function createSessionGenerationAuthority(
|
|
1066
|
+
sessionId,
|
|
1067
|
+
predecessorWitness = null,
|
|
1068
|
+
generationId = `generation-${randomUUID()}`,
|
|
1069
|
+
) {
|
|
1070
|
+
const previous = normalizeSessionGenerationAuthority(
|
|
1071
|
+
predecessorWitness?.generation,
|
|
1072
|
+
);
|
|
1073
|
+
const isReplacement = predecessorWitness !== null;
|
|
1074
|
+
return Object.freeze({
|
|
1075
|
+
schema: SESSION_GENERATION_AUTHORITY_SCHEMA,
|
|
1076
|
+
sessionId,
|
|
1077
|
+
generationId,
|
|
1078
|
+
ordinal:
|
|
1079
|
+
previous?.sessionId === sessionId
|
|
1080
|
+
? previous.ordinal + 1
|
|
1081
|
+
: isReplacement
|
|
1082
|
+
? 2
|
|
1083
|
+
: 1,
|
|
1084
|
+
predecessor: isReplacement
|
|
1085
|
+
? Object.freeze({
|
|
1086
|
+
kind:
|
|
1087
|
+
previous?.sessionId === sessionId
|
|
1088
|
+
? "tombstone"
|
|
1089
|
+
: "legacy-tombstone",
|
|
1090
|
+
generationId:
|
|
1091
|
+
previous?.sessionId === sessionId ? previous.generationId : null,
|
|
1092
|
+
headHash:
|
|
1093
|
+
typeof predecessorWitness?.last_hash === "string" &&
|
|
1094
|
+
/^[0-9a-f]{64}$/.test(predecessorWitness.last_hash)
|
|
1095
|
+
? predecessorWitness.last_hash
|
|
1096
|
+
: null,
|
|
1097
|
+
eventCount: Math.max(0, Number(predecessorWitness?.event_count) || 0),
|
|
1098
|
+
tombstonedAtMs: Number.isSafeInteger(
|
|
1099
|
+
Number(predecessorWitness?.deleted_at_ms),
|
|
1100
|
+
)
|
|
1101
|
+
? Math.max(0, Number(predecessorWitness.deleted_at_ms))
|
|
1102
|
+
: null,
|
|
1103
|
+
})
|
|
1104
|
+
: null,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function encodeSessionGenerationData(data, authority = null) {
|
|
1109
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
1110
|
+
return authority
|
|
1111
|
+
? { [SESSION_GENERATION_AUTHORITY_FIELD]: authority }
|
|
1112
|
+
: data;
|
|
1113
|
+
}
|
|
1114
|
+
const clean = { ...data };
|
|
1115
|
+
// The namespace generation is store-owned authority. Callers may not forge
|
|
1116
|
+
// it on arbitrary events or choose a replacement predecessor.
|
|
1117
|
+
delete clean[SESSION_GENERATION_AUTHORITY_FIELD];
|
|
1118
|
+
if (authority) clean[SESSION_GENERATION_AUTHORITY_FIELD] = authority;
|
|
1119
|
+
return clean;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function sessionAntiRollbackCandidateFromWitness(witness, status = "live") {
|
|
1123
|
+
if (!witness || typeof witness !== "object") return null;
|
|
1124
|
+
const eventCount = Math.max(0, Number(witness.event_count) || 0);
|
|
1125
|
+
const headHash =
|
|
1126
|
+
typeof witness.last_hash === "string" &&
|
|
1127
|
+
/^[0-9a-f]{64}$/.test(witness.last_hash)
|
|
1128
|
+
? witness.last_hash
|
|
1129
|
+
: null;
|
|
1130
|
+
const generation = normalizeSessionGenerationAuthority(witness.generation);
|
|
1131
|
+
const legacyDeletion =
|
|
1132
|
+
status === "deleted" && generation === null && headHash === null;
|
|
1133
|
+
if (!legacyDeletion && (eventCount === 0) !== (headHash === null))
|
|
1134
|
+
return null;
|
|
1135
|
+
return Object.freeze({
|
|
1136
|
+
status,
|
|
1137
|
+
generation,
|
|
1138
|
+
headHash,
|
|
1139
|
+
eventCount,
|
|
1140
|
+
deletedAtMs:
|
|
1141
|
+
status === "deleted"
|
|
1142
|
+
? Math.max(
|
|
1143
|
+
0,
|
|
1144
|
+
Number(witness.deleted_at_ms || witness.updated_at_ms) || 0,
|
|
1145
|
+
)
|
|
1146
|
+
: null,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function antiRollbackAnchorMatchesCandidate(anchor, candidate) {
|
|
1151
|
+
return (
|
|
1152
|
+
anchor !== null &&
|
|
1153
|
+
candidate !== null &&
|
|
1154
|
+
anchor.status === candidate.status &&
|
|
1155
|
+
JSON.stringify(anchor.generation) ===
|
|
1156
|
+
JSON.stringify(candidate.generation) &&
|
|
1157
|
+
anchor.headHash === candidate.headHash &&
|
|
1158
|
+
anchor.eventCount === candidate.eventCount &&
|
|
1159
|
+
anchor.deletedAtMs === candidate.deletedAtMs
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function transcriptHashAtEventCount(filePath, eventCount) {
|
|
1164
|
+
if (eventCount === 0) return null;
|
|
1165
|
+
let count = 0;
|
|
1166
|
+
for (const { line } of iterateFileLinesSync(filePath)) {
|
|
1167
|
+
let event;
|
|
1168
|
+
try {
|
|
1169
|
+
event = JSON.parse(line);
|
|
1170
|
+
} catch {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof event?.hash !== "string") continue;
|
|
1174
|
+
count += 1;
|
|
1175
|
+
if (count === eventCount) return event.hash;
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function publishSessionAntiRollbackWitness(
|
|
1181
|
+
sessionId,
|
|
1182
|
+
witness,
|
|
1183
|
+
filePath,
|
|
1184
|
+
status = "live",
|
|
1185
|
+
) {
|
|
1186
|
+
const candidate = sessionAntiRollbackCandidateFromWitness(witness, status);
|
|
1187
|
+
if (candidate === null) {
|
|
1188
|
+
const error = new Error(
|
|
1189
|
+
`Session anti-rollback candidate is incomplete: ${sessionId}`,
|
|
1190
|
+
);
|
|
1191
|
+
error.code = "CC_SESSION_ANTI_ROLLBACK_UNAVAILABLE";
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
return publishSessionAntiRollbackAnchor(sessionId, candidate, {
|
|
1195
|
+
provePrefix: (current) =>
|
|
1196
|
+
current.status === "live" &&
|
|
1197
|
+
existsSync(filePath) &&
|
|
1198
|
+
transcriptHashAtEventCount(filePath, current.eventCount) ===
|
|
1199
|
+
current.headHash,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function assertSessionAntiRollbackBeforeWrite(
|
|
1204
|
+
sessionId,
|
|
1205
|
+
witness,
|
|
1206
|
+
filePath,
|
|
1207
|
+
status = "live",
|
|
1208
|
+
) {
|
|
1209
|
+
const current = readSessionAntiRollbackAnchor(sessionId);
|
|
1210
|
+
if (current === null && witness === null) return null;
|
|
1211
|
+
const candidate = sessionAntiRollbackCandidateFromWitness(witness, status);
|
|
1212
|
+
if (candidate === null) return current;
|
|
1213
|
+
if (antiRollbackAnchorMatchesCandidate(current, candidate)) return current;
|
|
1214
|
+
|
|
1215
|
+
if (status === "live" && existsSync(filePath)) {
|
|
1216
|
+
const verification = verifyTranscriptFile(filePath);
|
|
1217
|
+
assertLocalVerifiedTranscriptAnchor(sessionId, verification);
|
|
1218
|
+
}
|
|
1219
|
+
return publishSessionAntiRollbackWitness(
|
|
1220
|
+
sessionId,
|
|
1221
|
+
witness,
|
|
1222
|
+
filePath,
|
|
1223
|
+
status,
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
777
1227
|
function appendEventLocked(
|
|
778
1228
|
sessionId,
|
|
779
1229
|
type,
|
|
@@ -781,35 +1231,69 @@ function appendEventLocked(
|
|
|
781
1231
|
{ expectedHeadHash, compareHead = false, requireIndexAnchor = false } = {},
|
|
782
1232
|
) {
|
|
783
1233
|
const filePath = sessionPath(sessionId);
|
|
784
|
-
return
|
|
1234
|
+
return withSessionHostWriterLock(
|
|
1235
|
+
sessionId,
|
|
785
1236
|
filePath,
|
|
786
1237
|
() => {
|
|
787
|
-
|
|
788
|
-
const
|
|
1238
|
+
let existingMeta = readSessionMeta(getSessionsDir(), sessionId);
|
|
1239
|
+
const marker = readSessionTombstoneMarker(getSessionsDir(), sessionId);
|
|
1240
|
+
const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
|
|
1241
|
+
const presence = getSessionPresence(sessionId);
|
|
789
1242
|
const transcriptExists = existsSync(filePath);
|
|
790
1243
|
const startsNewGeneration =
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1244
|
+
presence === SESSION_PRESENCE.TOMBSTONED && type === "session_start";
|
|
1245
|
+
if (presence === SESSION_PRESENCE.CONFLICT) {
|
|
1246
|
+
if (marker !== null || existingMeta?.deleted === true) {
|
|
1247
|
+
throw sessionDeletedError(sessionId);
|
|
1248
|
+
}
|
|
1249
|
+
throw unverifiedTranscriptError(
|
|
1250
|
+
sessionId,
|
|
1251
|
+
transcriptAnchorMismatchVerification(),
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
794
1254
|
if (
|
|
795
|
-
|
|
796
|
-
|
|
1255
|
+
presence === SESSION_PRESENCE.TOMBSTONED &&
|
|
1256
|
+
type !== "session_start"
|
|
797
1257
|
) {
|
|
798
|
-
|
|
799
|
-
error.code = "SESSION_DELETED";
|
|
800
|
-
throw error;
|
|
1258
|
+
throw sessionDeletedError(sessionId);
|
|
801
1259
|
}
|
|
802
1260
|
if (
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1261
|
+
presence === SESSION_PRESENCE.MISSING_TRANSCRIPT ||
|
|
1262
|
+
(!transcriptExists &&
|
|
1263
|
+
existingMeta !== null &&
|
|
1264
|
+
existingMeta?.deleted !== true)
|
|
806
1265
|
) {
|
|
807
1266
|
throw unverifiedTranscriptError(
|
|
808
1267
|
sessionId,
|
|
809
1268
|
missingTranscriptVerification(),
|
|
810
1269
|
);
|
|
811
1270
|
}
|
|
1271
|
+
const duplicateLiveStart =
|
|
1272
|
+
presence === SESSION_PRESENCE.PRESENT && type === "session_start";
|
|
812
1273
|
const { prevHash, recovery } = _resolveChainTail(filePath);
|
|
1274
|
+
let expectedTranscriptState = transcriptExists
|
|
1275
|
+
? readPhysicalTranscriptState(filePath)
|
|
1276
|
+
: null;
|
|
1277
|
+
if (
|
|
1278
|
+
transcriptExists &&
|
|
1279
|
+
existingMeta?.transcript &&
|
|
1280
|
+
(!samePhysicalTranscriptState(
|
|
1281
|
+
existingMeta.transcript,
|
|
1282
|
+
expectedTranscriptState,
|
|
1283
|
+
{ content: recovery === null },
|
|
1284
|
+
) ||
|
|
1285
|
+
Number(existingMeta.transcript.size) !==
|
|
1286
|
+
Number(expectedTranscriptState.size))
|
|
1287
|
+
) {
|
|
1288
|
+
throw transcriptIdentityError(
|
|
1289
|
+
"not-committed",
|
|
1290
|
+
"the transcript no longer matches its persisted physical witness",
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
if (transcriptExists) {
|
|
1294
|
+
ensurePrivateFile(filePath);
|
|
1295
|
+
expectedTranscriptState = readPhysicalTranscriptState(filePath);
|
|
1296
|
+
}
|
|
813
1297
|
if (
|
|
814
1298
|
transcriptExists &&
|
|
815
1299
|
existingMeta !== null &&
|
|
@@ -834,6 +1318,29 @@ function appendEventLocked(
|
|
|
834
1318
|
// publish a count of one for a multi-event transcript. Restore the
|
|
835
1319
|
// projection under the same writer lock before advancing the chain.
|
|
836
1320
|
rebuildSessionMetaUnlocked(getSessionsDir(), sessionId, filePath);
|
|
1321
|
+
existingMeta = readSessionMeta(getSessionsDir(), sessionId);
|
|
1322
|
+
}
|
|
1323
|
+
if (transcriptExists) {
|
|
1324
|
+
assertSessionAntiRollbackBeforeWrite(
|
|
1325
|
+
sessionId,
|
|
1326
|
+
existingMeta,
|
|
1327
|
+
filePath,
|
|
1328
|
+
"live",
|
|
1329
|
+
);
|
|
1330
|
+
} else if (presence === SESSION_PRESENCE.TOMBSTONED) {
|
|
1331
|
+
assertSessionAntiRollbackBeforeWrite(
|
|
1332
|
+
sessionId,
|
|
1333
|
+
existingMeta ||
|
|
1334
|
+
marker ||
|
|
1335
|
+
sessionAntiRollbackPredecessorWitness(externalAnchor),
|
|
1336
|
+
filePath,
|
|
1337
|
+
"deleted",
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
if (duplicateLiveStart) {
|
|
1341
|
+
const error = new Error(`Session already exists: ${sessionId}`);
|
|
1342
|
+
error.code = "SESSION_ALREADY_EXISTS";
|
|
1343
|
+
throw error;
|
|
837
1344
|
}
|
|
838
1345
|
if (compareHead && prevHash !== (expectedHeadHash || null)) {
|
|
839
1346
|
const error = new Error(
|
|
@@ -844,35 +1351,60 @@ function appendEventLocked(
|
|
|
844
1351
|
error.actualHeadHash = prevHash;
|
|
845
1352
|
throw error;
|
|
846
1353
|
}
|
|
847
|
-
const
|
|
1354
|
+
const generationAuthority =
|
|
1355
|
+
type === "session_start" &&
|
|
1356
|
+
[SESSION_PRESENCE.ABSENT, SESSION_PRESENCE.TOMBSTONED].includes(
|
|
1357
|
+
presence,
|
|
1358
|
+
)
|
|
1359
|
+
? createSessionGenerationAuthority(
|
|
1360
|
+
sessionId,
|
|
1361
|
+
startsNewGeneration
|
|
1362
|
+
? existingMeta ||
|
|
1363
|
+
marker ||
|
|
1364
|
+
sessionAntiRollbackPredecessorWitness(externalAnchor)
|
|
1365
|
+
: null,
|
|
1366
|
+
)
|
|
1367
|
+
: null;
|
|
1368
|
+
const persistedData = encodeSessionGenerationData(
|
|
1369
|
+
encodeEventMessageProvenance(type, data),
|
|
1370
|
+
generationAuthority,
|
|
1371
|
+
);
|
|
848
1372
|
const core = { type, timestamp: Date.now(), data: persistedData };
|
|
849
1373
|
const hash = computeEventHash(prevHash, core);
|
|
850
1374
|
const event = { ...core, prevHash, hash };
|
|
851
|
-
const
|
|
852
|
-
appendFileSync(filePath, line, { encoding: "utf8", mode: 0o600 });
|
|
853
|
-
ensurePrivateFile(filePath);
|
|
854
|
-
runSessionScaleFaultHook("afterTranscriptAppend", {
|
|
1375
|
+
const transcriptState = appendTranscriptEvent(
|
|
855
1376
|
sessionId,
|
|
856
1377
|
type,
|
|
857
1378
|
event,
|
|
858
1379
|
filePath,
|
|
859
|
-
|
|
1380
|
+
expectedTranscriptState,
|
|
1381
|
+
);
|
|
860
1382
|
try {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1383
|
+
const nextMeta = recordSessionEvent(
|
|
1384
|
+
getSessionsDir(),
|
|
1385
|
+
sessionId,
|
|
1386
|
+
event,
|
|
1387
|
+
hash,
|
|
1388
|
+
{
|
|
1389
|
+
resetGeneration: startsNewGeneration,
|
|
1390
|
+
transcriptState,
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
publishSessionAntiRollbackWitness(
|
|
1394
|
+
sessionId,
|
|
1395
|
+
nextMeta,
|
|
1396
|
+
filePath,
|
|
1397
|
+
"live",
|
|
1398
|
+
);
|
|
864
1399
|
} catch (cause) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
// The metadata index is explicitly rebuildable. Never turn a committed
|
|
875
|
-
// transcript event into an apparent failure that a caller may retry.
|
|
1400
|
+
const error = new Error(
|
|
1401
|
+
`Session authority anchor could not be persisted: ${sessionId}`,
|
|
1402
|
+
{ cause },
|
|
1403
|
+
);
|
|
1404
|
+
error.code = "SESSION_INDEX_ANCHOR_FAILED";
|
|
1405
|
+
error.sessionId = sessionId;
|
|
1406
|
+
error.commitState = "unknown";
|
|
1407
|
+
throw error;
|
|
876
1408
|
}
|
|
877
1409
|
if (requireIndexAnchor) {
|
|
878
1410
|
const verification = verifyTranscriptFile(filePath);
|
|
@@ -891,10 +1423,11 @@ function appendEventLocked(
|
|
|
891
1423
|
error.code = "SESSION_INDEX_ANCHOR_FAILED";
|
|
892
1424
|
error.sessionId = sessionId;
|
|
893
1425
|
error.verification = verification;
|
|
1426
|
+
error.commitState = "unknown";
|
|
894
1427
|
throw error;
|
|
895
1428
|
}
|
|
896
1429
|
}
|
|
897
|
-
return { hash, recovery };
|
|
1430
|
+
return { hash, recovery, commitState: "committed" };
|
|
898
1431
|
},
|
|
899
1432
|
{
|
|
900
1433
|
failIfUnavailable: true,
|
|
@@ -907,8 +1440,24 @@ function appendEventLocked(
|
|
|
907
1440
|
);
|
|
908
1441
|
}
|
|
909
1442
|
|
|
1443
|
+
function runClassifiedAppend(sessionId, operation, append) {
|
|
1444
|
+
try {
|
|
1445
|
+
return append();
|
|
1446
|
+
} catch (cause) {
|
|
1447
|
+
throw createSessionPersistenceFailure(cause, {
|
|
1448
|
+
sessionId,
|
|
1449
|
+
operation,
|
|
1450
|
+
// Preserve an inner append/settlement classification. Without one,
|
|
1451
|
+
// EROFS defaults to not-committed and ENOSPC remains unknown.
|
|
1452
|
+
commitState: cause?.commitState,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
910
1457
|
export function appendEvent(sessionId, type, data) {
|
|
911
|
-
return
|
|
1458
|
+
return runClassifiedAppend(sessionId, "append-event", () =>
|
|
1459
|
+
appendEventLocked(sessionId, type, data),
|
|
1460
|
+
);
|
|
912
1461
|
}
|
|
913
1462
|
|
|
914
1463
|
/**
|
|
@@ -916,9 +1465,11 @@ export function appendEvent(sessionId, type, data) {
|
|
|
916
1465
|
* crash/tail-truncation anchor are durably updated and still agree.
|
|
917
1466
|
*/
|
|
918
1467
|
export function appendAuthorityEvent(sessionId, type, data) {
|
|
919
|
-
return
|
|
920
|
-
|
|
921
|
-
|
|
1468
|
+
return runClassifiedAppend(sessionId, "append-authority-event", () =>
|
|
1469
|
+
appendEventLocked(sessionId, type, data, {
|
|
1470
|
+
requireIndexAnchor: true,
|
|
1471
|
+
}),
|
|
1472
|
+
);
|
|
922
1473
|
}
|
|
923
1474
|
|
|
924
1475
|
/**
|
|
@@ -932,10 +1483,12 @@ export function appendEventIfHead(
|
|
|
932
1483
|
data,
|
|
933
1484
|
expectedHeadHash = null,
|
|
934
1485
|
) {
|
|
935
|
-
return
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1486
|
+
return runClassifiedAppend(sessionId, "compare-and-append-event", () =>
|
|
1487
|
+
appendEventLocked(sessionId, type, data, {
|
|
1488
|
+
expectedHeadHash,
|
|
1489
|
+
compareHead: true,
|
|
1490
|
+
}),
|
|
1491
|
+
);
|
|
939
1492
|
}
|
|
940
1493
|
|
|
941
1494
|
/** Authority-bearing compare-and-append with a mandatory index anchor. */
|
|
@@ -945,11 +1498,16 @@ export function appendAuthorityEventIfHead(
|
|
|
945
1498
|
data,
|
|
946
1499
|
expectedHeadHash = null,
|
|
947
1500
|
) {
|
|
948
|
-
return
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1501
|
+
return runClassifiedAppend(
|
|
1502
|
+
sessionId,
|
|
1503
|
+
"compare-and-append-authority-event",
|
|
1504
|
+
() =>
|
|
1505
|
+
appendEventLocked(sessionId, type, data, {
|
|
1506
|
+
expectedHeadHash,
|
|
1507
|
+
compareHead: true,
|
|
1508
|
+
requireIndexAnchor: true,
|
|
1509
|
+
}),
|
|
1510
|
+
);
|
|
953
1511
|
}
|
|
954
1512
|
|
|
955
1513
|
/**
|
|
@@ -984,7 +1542,8 @@ export function appendAuthorityEventWithVerifiedProjection(
|
|
|
984
1542
|
);
|
|
985
1543
|
}
|
|
986
1544
|
const filePath = sessionPath(sessionId);
|
|
987
|
-
return
|
|
1545
|
+
return withSessionHostWriterLock(
|
|
1546
|
+
sessionId,
|
|
988
1547
|
filePath,
|
|
989
1548
|
() => {
|
|
990
1549
|
const projection = createProjection();
|
|
@@ -1093,7 +1652,8 @@ export function withSessionAuthorityTransaction(
|
|
|
1093
1652
|
}
|
|
1094
1653
|
|
|
1095
1654
|
const filePath = sessionPath(sessionId);
|
|
1096
|
-
return
|
|
1655
|
+
return withSessionHostWriterLock(
|
|
1656
|
+
sessionId,
|
|
1097
1657
|
filePath,
|
|
1098
1658
|
() => {
|
|
1099
1659
|
const existingMeta = readSessionMeta(getSessionsDir(), sessionId);
|
|
@@ -1560,20 +2120,77 @@ export function listSessionIds() {
|
|
|
1560
2120
|
.map((file) => basename(file, ".jsonl"));
|
|
1561
2121
|
}
|
|
1562
2122
|
|
|
1563
|
-
/**
|
|
1564
|
-
|
|
2123
|
+
/**
|
|
2124
|
+
* Enumerate every durable id occupying the canonical JSONL namespace.
|
|
2125
|
+
*
|
|
2126
|
+
* A transcript is not the only authority witness: a live/missing sidecar and
|
|
2127
|
+
* a tombstone marker must continue to fence legacy stores even when the JSONL
|
|
2128
|
+
* path itself is absent. Keeping this enumeration content-free also lets
|
|
2129
|
+
* prefix resolution detect ambiguity without opening transcript bodies.
|
|
2130
|
+
*/
|
|
2131
|
+
export function listSessionEvidenceIds() {
|
|
2132
|
+
const dir = getSessionsDir();
|
|
2133
|
+
const ids = new Set(listSessionAntiRollbackIds());
|
|
2134
|
+
if (!existsSync(dir)) return [...ids].sort();
|
|
2135
|
+
for (const file of readdirSync(dir)) {
|
|
2136
|
+
let id = null;
|
|
2137
|
+
if (file.endsWith(".jsonl")) {
|
|
2138
|
+
id = basename(file, ".jsonl");
|
|
2139
|
+
} else if (file.endsWith(".meta.json")) {
|
|
2140
|
+
id = file.slice(0, -".meta.json".length);
|
|
2141
|
+
} else if (file.endsWith(SESSION_TOMBSTONE_MARKER_SUFFIX)) {
|
|
2142
|
+
id = file.slice(0, -SESSION_TOMBSTONE_MARKER_SUFFIX.length);
|
|
2143
|
+
}
|
|
2144
|
+
if (id && !isUnsafeSessionId(id)) ids.add(id);
|
|
2145
|
+
}
|
|
2146
|
+
return [...ids].sort();
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Resolve an exact id or unambiguous prefix across the complete persisted
|
|
2151
|
+
* namespace, including damaged and tombstoned generations.
|
|
2152
|
+
*
|
|
2153
|
+
* Returning the presence separately from the id prevents callers from using
|
|
2154
|
+
* "not readable" as "not canonical" and then silently consulting a legacy
|
|
2155
|
+
* store. Ambiguity is fail-closed even when only one candidate is currently
|
|
2156
|
+
* readable.
|
|
2157
|
+
*/
|
|
2158
|
+
export function resolveSessionAuthority(input) {
|
|
1565
2159
|
if (isUnsafeSessionId(input)) return null;
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2160
|
+
const exactPresence = getSessionPresence(input);
|
|
2161
|
+
if (exactPresence !== SESSION_PRESENCE.ABSENT) {
|
|
2162
|
+
return Object.freeze({
|
|
2163
|
+
id: input,
|
|
2164
|
+
match: "exact",
|
|
2165
|
+
presence: exactPresence,
|
|
2166
|
+
readable: exactPresence === SESSION_PRESENCE.PRESENT,
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
const matches = listSessionEvidenceIds()
|
|
2170
|
+
.filter((id) => id.startsWith(input))
|
|
2171
|
+
.map((id) => ({ id, presence: getSessionPresence(id) }))
|
|
2172
|
+
.filter(({ presence }) => presence !== SESSION_PRESENCE.ABSENT);
|
|
1570
2173
|
if (matches.length > 1) {
|
|
1571
2174
|
const error = new Error(`Ambiguous session id prefix: ${input}`);
|
|
1572
2175
|
error.code = "AMBIGUOUS_SESSION_ID";
|
|
1573
|
-
error.matches = matches.slice(0, 20);
|
|
2176
|
+
error.matches = matches.slice(0, 20).map(({ id }) => id);
|
|
2177
|
+
error.authorities = matches.slice(0, 20).map((item) => ({ ...item }));
|
|
1574
2178
|
throw error;
|
|
1575
2179
|
}
|
|
1576
|
-
|
|
2180
|
+
if (matches.length === 0) return null;
|
|
2181
|
+
const [{ id, presence }] = matches;
|
|
2182
|
+
return Object.freeze({
|
|
2183
|
+
id,
|
|
2184
|
+
match: "prefix",
|
|
2185
|
+
presence,
|
|
2186
|
+
readable: presence === SESSION_PRESENCE.PRESENT,
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/** Resolve an exact id or one unambiguous prefix without reading transcripts. */
|
|
2191
|
+
export function resolveSessionId(input) {
|
|
2192
|
+
const authority = resolveSessionAuthority(input);
|
|
2193
|
+
return authority?.readable ? authority.id : null;
|
|
1577
2194
|
}
|
|
1578
2195
|
|
|
1579
2196
|
export { TRANSCRIPT_CHAIN_STATUS };
|
|
@@ -1593,11 +2210,17 @@ export function startSession(sessionId, meta = {}) {
|
|
|
1593
2210
|
}
|
|
1594
2211
|
|
|
1595
2212
|
export function appendUserMessage(sessionId, content) {
|
|
1596
|
-
appendEvent(sessionId, "user_message", {
|
|
2213
|
+
return appendEvent(sessionId, "user_message", {
|
|
2214
|
+
role: "user",
|
|
2215
|
+
content,
|
|
2216
|
+
});
|
|
1597
2217
|
}
|
|
1598
2218
|
|
|
1599
2219
|
export function appendAssistantMessage(sessionId, content) {
|
|
1600
|
-
appendEvent(sessionId, "assistant_message", {
|
|
2220
|
+
return appendEvent(sessionId, "assistant_message", {
|
|
2221
|
+
role: "assistant",
|
|
2222
|
+
content,
|
|
2223
|
+
});
|
|
1601
2224
|
}
|
|
1602
2225
|
|
|
1603
2226
|
function wsTurnLifecycleResult(snapshot, overrides = {}) {
|
|
@@ -2019,25 +2642,82 @@ function readSessionPersistenceWitness(sessionId) {
|
|
|
2019
2642
|
|
|
2020
2643
|
function hasLiveSessionWitness(sessionId) {
|
|
2021
2644
|
const witness = readSessionPersistenceWitness(sessionId);
|
|
2022
|
-
|
|
2645
|
+
if (witness !== null && witness?.deleted !== true) return true;
|
|
2646
|
+
return readSessionAntiRollbackAnchor(sessionId)?.status === "live";
|
|
2023
2647
|
}
|
|
2024
2648
|
|
|
2025
|
-
function
|
|
2649
|
+
function assertLocalVerifiedTranscriptAnchor(
|
|
2650
|
+
sessionId,
|
|
2651
|
+
verification,
|
|
2652
|
+
physicalState = null,
|
|
2653
|
+
) {
|
|
2026
2654
|
const meta = readSessionMeta(getSessionsDir(), sessionId);
|
|
2655
|
+
let physicalAnchorMatches = true;
|
|
2656
|
+
let currentPhysicalState = physicalState;
|
|
2657
|
+
if (meta?.transcript) {
|
|
2658
|
+
try {
|
|
2659
|
+
currentPhysicalState ||= readPhysicalTranscriptState(
|
|
2660
|
+
sessionPath(sessionId),
|
|
2661
|
+
);
|
|
2662
|
+
physicalAnchorMatches = samePhysicalTranscriptState(
|
|
2663
|
+
meta.transcript,
|
|
2664
|
+
currentPhysicalState,
|
|
2665
|
+
);
|
|
2666
|
+
} catch {
|
|
2667
|
+
physicalAnchorMatches = false;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2027
2670
|
const trustedStatus =
|
|
2028
2671
|
verification.status === TRANSCRIPT_CHAIN_STATUS.VERIFIED;
|
|
2672
|
+
const headMatches = meta?.last_hash === verification.lastHash;
|
|
2673
|
+
const eventCountMatches =
|
|
2674
|
+
Number(meta?.event_count) === verification.chainedEvents;
|
|
2029
2675
|
const anchoredHead =
|
|
2030
2676
|
meta?.deleted !== true &&
|
|
2031
|
-
|
|
2032
|
-
|
|
2677
|
+
headMatches &&
|
|
2678
|
+
eventCountMatches &&
|
|
2679
|
+
physicalAnchorMatches;
|
|
2033
2680
|
if (
|
|
2034
2681
|
!trustedStatus ||
|
|
2035
2682
|
!anchoredHead ||
|
|
2036
2683
|
verification.malformedLines > 0 ||
|
|
2037
2684
|
verification.truncatedTail
|
|
2038
2685
|
) {
|
|
2039
|
-
|
|
2686
|
+
const error = unverifiedTranscriptError(sessionId, verification);
|
|
2687
|
+
error.anchor = Object.freeze({
|
|
2688
|
+
metadataPresent: meta !== null,
|
|
2689
|
+
metadataDeleted: meta?.deleted === true,
|
|
2690
|
+
headMatches,
|
|
2691
|
+
eventCountMatches,
|
|
2692
|
+
physicalAnchorMatches,
|
|
2693
|
+
metadataHeadHash: meta?.last_hash ?? null,
|
|
2694
|
+
metadataEventCount: Number.isSafeInteger(Number(meta?.event_count))
|
|
2695
|
+
? Number(meta.event_count)
|
|
2696
|
+
: null,
|
|
2697
|
+
metadataTranscript: meta?.transcript ?? null,
|
|
2698
|
+
currentTranscript: currentPhysicalState ?? null,
|
|
2699
|
+
});
|
|
2700
|
+
throw error;
|
|
2040
2701
|
}
|
|
2702
|
+
return meta;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function assertVerifiedTranscriptAnchor(
|
|
2706
|
+
sessionId,
|
|
2707
|
+
verification,
|
|
2708
|
+
physicalState = null,
|
|
2709
|
+
) {
|
|
2710
|
+
const meta = assertLocalVerifiedTranscriptAnchor(
|
|
2711
|
+
sessionId,
|
|
2712
|
+
verification,
|
|
2713
|
+
physicalState,
|
|
2714
|
+
);
|
|
2715
|
+
publishSessionAntiRollbackWitness(
|
|
2716
|
+
sessionId,
|
|
2717
|
+
meta,
|
|
2718
|
+
sessionPath(sessionId),
|
|
2719
|
+
"live",
|
|
2720
|
+
);
|
|
2041
2721
|
}
|
|
2042
2722
|
|
|
2043
2723
|
/**
|
|
@@ -2052,8 +2732,8 @@ function assertVerifiedTranscriptAnchor(sessionId, verification) {
|
|
|
2052
2732
|
*
|
|
2053
2733
|
* This removes the mandatory all-event array from resume projections. It does
|
|
2054
2734
|
* not make verification IO sublinear: authenticating a plain chained JSONL
|
|
2055
|
-
* transcript remains O(N)
|
|
2056
|
-
*
|
|
2735
|
+
* transcript remains O(N). The local sidecar is checked against an independent
|
|
2736
|
+
* machine-local anti-rollback witness; it is not itself that witness.
|
|
2057
2737
|
*/
|
|
2058
2738
|
export function readVerifiedProjection(
|
|
2059
2739
|
sessionId,
|
|
@@ -2087,13 +2767,11 @@ export function readVerifiedProjection(
|
|
|
2087
2767
|
);
|
|
2088
2768
|
}
|
|
2089
2769
|
if (!existsSync(filePath)) {
|
|
2090
|
-
const
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
error.code = "SESSION_DELETED";
|
|
2094
|
-
throw error;
|
|
2770
|
+
const presence = getSessionPresence(sessionId);
|
|
2771
|
+
if (presence === SESSION_PRESENCE.TOMBSTONED) {
|
|
2772
|
+
throw sessionDeletedError(sessionId);
|
|
2095
2773
|
}
|
|
2096
|
-
if (
|
|
2774
|
+
if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
|
|
2097
2775
|
throw unverifiedTranscriptError(
|
|
2098
2776
|
sessionId,
|
|
2099
2777
|
missingTranscriptVerification(),
|
|
@@ -2153,6 +2831,112 @@ export function readVerifiedEvents(sessionId) {
|
|
|
2153
2831
|
});
|
|
2154
2832
|
}
|
|
2155
2833
|
|
|
2834
|
+
/**
|
|
2835
|
+
* Return the exact JSONL bytes only after the transcript and its persisted
|
|
2836
|
+
* anchor verify under the canonical writer lock. This is the export/mirror
|
|
2837
|
+
* boundary; derived consumers must never copy a merely parseable raw path.
|
|
2838
|
+
*/
|
|
2839
|
+
export function readVerifiedTranscriptBytes(sessionId) {
|
|
2840
|
+
if (isUnsafeSessionId(sessionId)) {
|
|
2841
|
+
throw unverifiedTranscriptError(
|
|
2842
|
+
sessionId,
|
|
2843
|
+
transcriptAnchorMismatchVerification(),
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
const filePath = sessionPath(sessionId);
|
|
2847
|
+
return withFileLock(
|
|
2848
|
+
filePath,
|
|
2849
|
+
() => {
|
|
2850
|
+
const presence = getSessionPresence(sessionId);
|
|
2851
|
+
if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
|
|
2852
|
+
throw unverifiedTranscriptError(
|
|
2853
|
+
sessionId,
|
|
2854
|
+
missingTranscriptVerification(),
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
if (presence !== SESSION_PRESENCE.PRESENT) {
|
|
2858
|
+
throw unverifiedTranscriptError(
|
|
2859
|
+
sessionId,
|
|
2860
|
+
transcriptAnchorMismatchVerification(),
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
return withTranscriptFileParent(
|
|
2864
|
+
filePath,
|
|
2865
|
+
({ canonicalPath, parentDevice }) =>
|
|
2866
|
+
readVerifiedTranscriptBytesAtPath(
|
|
2867
|
+
sessionId,
|
|
2868
|
+
canonicalPath,
|
|
2869
|
+
parentDevice,
|
|
2870
|
+
),
|
|
2871
|
+
);
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
failIfUnavailable: true,
|
|
2875
|
+
timeoutMs: 30_000,
|
|
2876
|
+
retryMs: 1,
|
|
2877
|
+
maxRetryMs: 8,
|
|
2878
|
+
retryJitterMs: 4,
|
|
2879
|
+
yieldAfterReleaseMs: 2,
|
|
2880
|
+
},
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
function readVerifiedTranscriptBytesAtPath(sessionId, filePath, parentDevice) {
|
|
2885
|
+
let flags = fsConstants.O_RDONLY;
|
|
2886
|
+
if (typeof fsConstants.O_NOFOLLOW === "number") {
|
|
2887
|
+
flags |= fsConstants.O_NOFOLLOW;
|
|
2888
|
+
}
|
|
2889
|
+
const fd = openSync(filePath, flags);
|
|
2890
|
+
try {
|
|
2891
|
+
const beforeStats = fstatSync(fd, { bigint: true });
|
|
2892
|
+
const before = physicalTranscriptStateFromStats(beforeStats);
|
|
2893
|
+
const publishedBeforeStats = readPhysicalTranscriptStats(filePath);
|
|
2894
|
+
const publishedBefore =
|
|
2895
|
+
physicalTranscriptStateFromStats(publishedBeforeStats);
|
|
2896
|
+
const descriptorMatchesPublished =
|
|
2897
|
+
parentDevice === null
|
|
2898
|
+
? samePhysicalTranscriptState(before, publishedBefore)
|
|
2899
|
+
: samePathHandleStableFileIdentity(
|
|
2900
|
+
publishedBeforeStats,
|
|
2901
|
+
beforeStats,
|
|
2902
|
+
parentDevice,
|
|
2903
|
+
) && samePhysicalTranscriptContentState(before, publishedBefore);
|
|
2904
|
+
if (!descriptorMatchesPublished) {
|
|
2905
|
+
throw transcriptIdentityError(
|
|
2906
|
+
"not-committed",
|
|
2907
|
+
"the read descriptor does not match the canonical path",
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
const text = readFileSync(fd, "utf8");
|
|
2911
|
+
const afterStats = fstatSync(fd, { bigint: true });
|
|
2912
|
+
const after = physicalTranscriptStateFromStats(afterStats);
|
|
2913
|
+
const publishedStats = readPhysicalTranscriptStats(filePath);
|
|
2914
|
+
const published = physicalTranscriptStateFromStats(publishedStats);
|
|
2915
|
+
const publishedStillMatches =
|
|
2916
|
+
parentDevice === null
|
|
2917
|
+
? samePhysicalTranscriptState(after, published)
|
|
2918
|
+
: samePathHandleStableFileIdentity(
|
|
2919
|
+
publishedStats,
|
|
2920
|
+
afterStats,
|
|
2921
|
+
parentDevice,
|
|
2922
|
+
) && samePhysicalTranscriptContentState(after, published);
|
|
2923
|
+
if (!samePhysicalTranscriptState(before, after) || !publishedStillMatches) {
|
|
2924
|
+
throw transcriptIdentityError(
|
|
2925
|
+
"unknown",
|
|
2926
|
+
"the transcript changed while verified bytes were read",
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
const verification = {
|
|
2930
|
+
...verifyTranscriptText(text),
|
|
2931
|
+
lastHash: latestChainHash(text),
|
|
2932
|
+
};
|
|
2933
|
+
assertVerifiedTranscriptAnchor(sessionId, verification, published);
|
|
2934
|
+
return text;
|
|
2935
|
+
} finally {
|
|
2936
|
+
closeSync(fd);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2156
2940
|
/**
|
|
2157
2941
|
* Read the active context only after transcript and sidecar-anchor validation.
|
|
2158
2942
|
* Unlike the compatibility `rebuildMessages()` reader, this is allowed to
|
|
@@ -2208,30 +2992,29 @@ export function readVerifiedWsTurnState(sessionId, requestId, options = {}) {
|
|
|
2208
2992
|
}
|
|
2209
2993
|
|
|
2210
2994
|
/**
|
|
2211
|
-
* Return the newest event matching a type/predicate
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2995
|
+
* Return the newest event matching a type/predicate after authenticating the
|
|
2996
|
+
* complete transcript and sidecar anchor. Heap remains bounded to one event;
|
|
2997
|
+
* the forward integrity pass is intentionally O(N).
|
|
2214
2998
|
*/
|
|
2215
2999
|
export function findLatestEvent(sessionId, type, predicate = null) {
|
|
2216
3000
|
if (isUnsafeSessionId(sessionId)) return null;
|
|
2217
|
-
const filePath = sessionPath(sessionId);
|
|
2218
|
-
if (!existsSync(filePath)) return null;
|
|
2219
3001
|
const wanted = Array.isArray(type) ? new Set(type) : null;
|
|
2220
|
-
|
|
2221
|
-
let
|
|
2222
|
-
|
|
2223
|
-
event
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
3002
|
+
return readVerifiedProjection(sessionId, () => {
|
|
3003
|
+
let latest = null;
|
|
3004
|
+
return {
|
|
3005
|
+
accept(event) {
|
|
3006
|
+
const typeMatches = wanted
|
|
3007
|
+
? wanted.has(event?.type)
|
|
3008
|
+
: type == null || event?.type === type;
|
|
3009
|
+
if (!typeMatches) return;
|
|
3010
|
+
if (typeof predicate === "function" && !predicate(event)) return;
|
|
3011
|
+
latest = event;
|
|
3012
|
+
},
|
|
3013
|
+
finish() {
|
|
3014
|
+
return latest;
|
|
3015
|
+
},
|
|
3016
|
+
};
|
|
3017
|
+
});
|
|
2235
3018
|
}
|
|
2236
3019
|
|
|
2237
3020
|
/**
|
|
@@ -2343,6 +3126,7 @@ function rebuildSessionMetaUnlocked(dir, sessionId, filePath) {
|
|
|
2343
3126
|
// a best-effort projection over all intact events.
|
|
2344
3127
|
}
|
|
2345
3128
|
}
|
|
3129
|
+
meta.transcript = readPhysicalTranscriptState(filePath);
|
|
2346
3130
|
return replaceSessionMeta(dir, meta);
|
|
2347
3131
|
}
|
|
2348
3132
|
|
|
@@ -2352,7 +3136,30 @@ function rebuildSessionMeta(sessionId) {
|
|
|
2352
3136
|
if (!existsSync(filePath)) return null;
|
|
2353
3137
|
return withFileLock(
|
|
2354
3138
|
filePath,
|
|
2355
|
-
() =>
|
|
3139
|
+
() => {
|
|
3140
|
+
const verification = verifyTranscriptFile(filePath);
|
|
3141
|
+
const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
|
|
3142
|
+
if (
|
|
3143
|
+
externalAnchor === null &&
|
|
3144
|
+
[
|
|
3145
|
+
TRANSCRIPT_CHAIN_STATUS.LEGACY,
|
|
3146
|
+
TRANSCRIPT_CHAIN_STATUS.PARTIAL,
|
|
3147
|
+
].includes(verification.status) &&
|
|
3148
|
+
!verification.truncatedTail
|
|
3149
|
+
) {
|
|
3150
|
+
return rebuildSessionMetaUnlocked(dir, sessionId, filePath);
|
|
3151
|
+
}
|
|
3152
|
+
if (
|
|
3153
|
+
verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
|
|
3154
|
+
verification.malformedLines > 0 ||
|
|
3155
|
+
verification.truncatedTail
|
|
3156
|
+
) {
|
|
3157
|
+
throw unverifiedTranscriptError(sessionId, verification);
|
|
3158
|
+
}
|
|
3159
|
+
const meta = rebuildSessionMetaUnlocked(dir, sessionId, filePath);
|
|
3160
|
+
assertVerifiedTranscriptAnchor(sessionId, verification);
|
|
3161
|
+
return meta;
|
|
3162
|
+
},
|
|
2356
3163
|
{
|
|
2357
3164
|
failIfUnavailable: true,
|
|
2358
3165
|
timeoutMs: 30_000,
|
|
@@ -2425,6 +3232,58 @@ export function listJsonlSessions(options = {}) {
|
|
|
2425
3232
|
return indexed;
|
|
2426
3233
|
}
|
|
2427
3234
|
|
|
3235
|
+
/**
|
|
3236
|
+
* List canonical namespace occupants for recent-session and picker consumers.
|
|
3237
|
+
* Damaged generations are deliberately represented as blocked rows so a
|
|
3238
|
+
* newer missing/conflicting canonical session cannot disappear and cause an
|
|
3239
|
+
* older legacy conversation to be resumed instead. Tombstones fence duplicate
|
|
3240
|
+
* legacy ids but are not resumable choices unless explicitly requested.
|
|
3241
|
+
*/
|
|
3242
|
+
export function listSessionAuthoritySummaries(options = {}) {
|
|
3243
|
+
const limit = Math.max(1, Number(options.limit) || 20);
|
|
3244
|
+
const evidenceIds = listSessionEvidenceIds();
|
|
3245
|
+
const readable = new Map(
|
|
3246
|
+
listJsonlSessions({ limit: Math.max(limit, evidenceIds.length) }).map(
|
|
3247
|
+
(item) => [item.id, item],
|
|
3248
|
+
),
|
|
3249
|
+
);
|
|
3250
|
+
const rows = [];
|
|
3251
|
+
for (const id of evidenceIds) {
|
|
3252
|
+
const presence = getSessionPresence(id);
|
|
3253
|
+
if (
|
|
3254
|
+
presence === SESSION_PRESENCE.ABSENT ||
|
|
3255
|
+
(presence === SESSION_PRESENCE.TOMBSTONED &&
|
|
3256
|
+
options.includeTombstoned !== true)
|
|
3257
|
+
) {
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
const item = readable.get(id);
|
|
3261
|
+
const meta = item ? null : readSessionMeta(getSessionsDir(), id);
|
|
3262
|
+
rows.push({
|
|
3263
|
+
...(item ||
|
|
3264
|
+
publicSessionMeta({
|
|
3265
|
+
...emptySessionMeta(id),
|
|
3266
|
+
...(meta || {}),
|
|
3267
|
+
id,
|
|
3268
|
+
})),
|
|
3269
|
+
id,
|
|
3270
|
+
_store: "jsonl",
|
|
3271
|
+
_presence: presence,
|
|
3272
|
+
_blocked: presence !== SESSION_PRESENCE.PRESENT,
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
return rows
|
|
3276
|
+
.sort((a, b) => {
|
|
3277
|
+
const delta =
|
|
3278
|
+
(Date.parse(b.updated_at || "") || 0) -
|
|
3279
|
+
(Date.parse(a.updated_at || "") || 0);
|
|
3280
|
+
if (delta !== 0) return delta;
|
|
3281
|
+
if (a._blocked !== b._blocked) return a._blocked ? -1 : 1;
|
|
3282
|
+
return String(b.id).localeCompare(String(a.id));
|
|
3283
|
+
})
|
|
3284
|
+
.slice(0, limit);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2428
3287
|
/**
|
|
2429
3288
|
* Rename a session (gap-analysis 2026-07-11 P1 "命名会话"): appends a
|
|
2430
3289
|
* `session_rename` event so the hash chain stays intact (no rewrite). The
|
|
@@ -2447,25 +3306,83 @@ export function deleteJsonlSession(sessionId) {
|
|
|
2447
3306
|
if (isUnsafeSessionId(sessionId)) return false;
|
|
2448
3307
|
const filePath = sessionPath(sessionId);
|
|
2449
3308
|
if (!existsSync(filePath) && !hasLiveSessionWitness(sessionId)) return false;
|
|
2450
|
-
return
|
|
3309
|
+
return withSessionHostWriterLock(
|
|
3310
|
+
sessionId,
|
|
2451
3311
|
filePath,
|
|
2452
3312
|
() => {
|
|
2453
3313
|
const transcriptExists = existsSync(filePath);
|
|
2454
|
-
|
|
3314
|
+
let existingMeta = readSessionPersistenceWitness(sessionId);
|
|
3315
|
+
const marker = readSessionTombstoneMarker(getSessionsDir(), sessionId);
|
|
3316
|
+
const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
|
|
2455
3317
|
if (
|
|
2456
3318
|
!transcriptExists &&
|
|
2457
3319
|
(existingMeta === null || existingMeta?.deleted === true)
|
|
2458
3320
|
) {
|
|
2459
3321
|
return false;
|
|
2460
3322
|
}
|
|
3323
|
+
const deletionWitness =
|
|
3324
|
+
existingMeta?.deleted === true
|
|
3325
|
+
? existingMeta
|
|
3326
|
+
: sessionAntiRollbackPredecessorWitness(externalAnchor) || marker;
|
|
3327
|
+
if (
|
|
3328
|
+
transcriptExists &&
|
|
3329
|
+
existingMeta === null &&
|
|
3330
|
+
deletionWitness === null
|
|
3331
|
+
) {
|
|
3332
|
+
const verification = verifyTranscriptFile(filePath);
|
|
3333
|
+
if (
|
|
3334
|
+
verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
|
|
3335
|
+
verification.malformedLines > 0 ||
|
|
3336
|
+
verification.truncatedTail
|
|
3337
|
+
) {
|
|
3338
|
+
throw unverifiedTranscriptError(sessionId, verification);
|
|
3339
|
+
}
|
|
3340
|
+
rebuildSessionMetaUnlocked(getSessionsDir(), sessionId, filePath);
|
|
3341
|
+
existingMeta = readSessionPersistenceWitness(sessionId);
|
|
3342
|
+
}
|
|
3343
|
+
if (deletionWitness === null) {
|
|
3344
|
+
assertSessionAntiRollbackBeforeWrite(
|
|
3345
|
+
sessionId,
|
|
3346
|
+
existingMeta,
|
|
3347
|
+
filePath,
|
|
3348
|
+
"live",
|
|
3349
|
+
);
|
|
3350
|
+
} else if (existingMeta?.deleted !== true) {
|
|
3351
|
+
existingMeta = replaceSessionMeta(getSessionsDir(), {
|
|
3352
|
+
...emptySessionMeta(sessionId),
|
|
3353
|
+
id: sessionId,
|
|
3354
|
+
generation: deletionWitness.generation || null,
|
|
3355
|
+
last_hash: deletionWitness.last_hash || null,
|
|
3356
|
+
event_count: Math.max(0, Number(deletionWitness.event_count) || 0),
|
|
3357
|
+
updated_at_ms: Math.max(
|
|
3358
|
+
0,
|
|
3359
|
+
Number(deletionWitness.deleted_at_ms) || Date.now(),
|
|
3360
|
+
),
|
|
3361
|
+
deleted_at_ms: Math.max(
|
|
3362
|
+
0,
|
|
3363
|
+
Number(deletionWitness.deleted_at_ms) || Date.now(),
|
|
3364
|
+
),
|
|
3365
|
+
deleted: true,
|
|
3366
|
+
transcript: null,
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
2461
3369
|
if (transcriptExists) rmSync(filePath, { force: true });
|
|
2462
3370
|
try {
|
|
2463
|
-
recordSessionDeleted(getSessionsDir(), sessionId);
|
|
3371
|
+
const tombstone = recordSessionDeleted(getSessionsDir(), sessionId);
|
|
3372
|
+
publishSessionAntiRollbackWitness(
|
|
3373
|
+
sessionId,
|
|
3374
|
+
tombstone,
|
|
3375
|
+
filePath,
|
|
3376
|
+
"deleted",
|
|
3377
|
+
);
|
|
2464
3378
|
} catch (error) {
|
|
2465
3379
|
// writeMetaAtomic precedes the activity-journal append. If only the
|
|
2466
3380
|
// rebuildable journal lock/release failed, the durable sidecar still
|
|
2467
3381
|
// prevents a stale writer from resurrecting this deleted session.
|
|
2468
|
-
if (
|
|
3382
|
+
if (
|
|
3383
|
+
readSessionMeta(getSessionsDir(), sessionId)?.deleted !== true ||
|
|
3384
|
+
readSessionAntiRollbackAnchor(sessionId)?.status !== "deleted"
|
|
3385
|
+
) {
|
|
2469
3386
|
throw error;
|
|
2470
3387
|
}
|
|
2471
3388
|
}
|
|
@@ -2768,12 +3685,22 @@ export function forkSession(sourceId, options = {}) {
|
|
|
2768
3685
|
sourceHeadHash: sourceVerification.lastHash,
|
|
2769
3686
|
sourceEventCount: sourceVerification.chainedEvents,
|
|
2770
3687
|
};
|
|
3688
|
+
const forkGenerationId = `generation-${createHash("sha256")
|
|
3689
|
+
.update(`fork-generation\0${newId}`, "utf8")
|
|
3690
|
+
.digest("hex")
|
|
3691
|
+
.slice(0, 32)}`;
|
|
2771
3692
|
const core = {
|
|
2772
3693
|
type: "system",
|
|
2773
3694
|
timestamp: Date.now(),
|
|
2774
3695
|
data: {
|
|
2775
3696
|
...persistedData,
|
|
2776
3697
|
[SESSION_FORK_AUTHORITY_FIELD]: forkAuthority,
|
|
3698
|
+
[SESSION_GENERATION_AUTHORITY_FIELD]:
|
|
3699
|
+
createSessionGenerationAuthority(
|
|
3700
|
+
newId,
|
|
3701
|
+
null,
|
|
3702
|
+
forkGenerationId,
|
|
3703
|
+
),
|
|
2777
3704
|
},
|
|
2778
3705
|
};
|
|
2779
3706
|
const hash = computeEventHash(prevHash, core);
|
|
@@ -2803,6 +3730,17 @@ export function forkSession(sourceId, options = {}) {
|
|
|
2803
3730
|
sourceHeadHash: authority?.sourceHeadHash,
|
|
2804
3731
|
sourceEventCount: authorityEventCount,
|
|
2805
3732
|
};
|
|
3733
|
+
const generationAuthority = normalizeSessionGenerationAuthority(
|
|
3734
|
+
target.lineageEvent?.data?.[SESSION_GENERATION_AUTHORITY_FIELD],
|
|
3735
|
+
);
|
|
3736
|
+
const hasGenerationAuthority =
|
|
3737
|
+
target.lineageEvent?.data?.[SESSION_GENERATION_AUTHORITY_FIELD] !==
|
|
3738
|
+
undefined;
|
|
3739
|
+
const generationMatches =
|
|
3740
|
+
!hasGenerationAuthority ||
|
|
3741
|
+
(generationAuthority?.sessionId === newId &&
|
|
3742
|
+
generationAuthority.ordinal === 1 &&
|
|
3743
|
+
generationAuthority.predecessor === null);
|
|
2806
3744
|
const lineageMatches =
|
|
2807
3745
|
target.matchingLineageEvents === 1 &&
|
|
2808
3746
|
target.lineageEvent?.type === "system" &&
|
|
@@ -2811,10 +3749,16 @@ export function forkSession(sourceId, options = {}) {
|
|
|
2811
3749
|
typeof authority?.sourceHeadHash === "string" &&
|
|
2812
3750
|
target.lineageEventIndex === creationEventCount &&
|
|
2813
3751
|
target.lineageEvent?.prevHash === authority.sourceHeadHash &&
|
|
3752
|
+
generationMatches &&
|
|
2814
3753
|
JSON.stringify(target.lineageEvent?.data) ===
|
|
2815
3754
|
JSON.stringify({
|
|
2816
3755
|
...persistedData,
|
|
2817
3756
|
[SESSION_FORK_AUTHORITY_FIELD]: expectedAuthority,
|
|
3757
|
+
...(hasGenerationAuthority
|
|
3758
|
+
? {
|
|
3759
|
+
[SESSION_GENERATION_AUTHORITY_FIELD]: generationAuthority,
|
|
3760
|
+
}
|
|
3761
|
+
: {}),
|
|
2818
3762
|
});
|
|
2819
3763
|
if (
|
|
2820
3764
|
target.verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
|
|
@@ -2976,7 +3920,7 @@ export function createBranchSession({
|
|
|
2976
3920
|
provider: typeof cleanMeta.provider === "string" ? cleanMeta.provider : "",
|
|
2977
3921
|
model: typeof cleanMeta.model === "string" ? cleanMeta.model : "",
|
|
2978
3922
|
};
|
|
2979
|
-
const
|
|
3923
|
+
const digestEvents = [
|
|
2980
3924
|
{ type: "session_start", data: startData },
|
|
2981
3925
|
{
|
|
2982
3926
|
type: "session_branch",
|
|
@@ -2989,19 +3933,19 @@ export function createBranchSession({
|
|
|
2989
3933
|
let count = 0;
|
|
2990
3934
|
for (const m of canonicalMessages) {
|
|
2991
3935
|
if (m.role === "user") {
|
|
2992
|
-
|
|
3936
|
+
digestEvents.push({
|
|
2993
3937
|
type: "user_message",
|
|
2994
3938
|
data: { role: "user", content: m.content },
|
|
2995
3939
|
});
|
|
2996
3940
|
count += 1;
|
|
2997
3941
|
} else if (m.role === "assistant") {
|
|
2998
|
-
|
|
3942
|
+
digestEvents.push({
|
|
2999
3943
|
type: "assistant_message",
|
|
3000
3944
|
data: { role: "assistant", content: m.content },
|
|
3001
3945
|
});
|
|
3002
3946
|
count += 1;
|
|
3003
3947
|
} else if (m.role === "system") {
|
|
3004
|
-
|
|
3948
|
+
digestEvents.push({
|
|
3005
3949
|
type: "system",
|
|
3006
3950
|
data: encodePersistedMessage(m),
|
|
3007
3951
|
});
|
|
@@ -3011,8 +3955,27 @@ export function createBranchSession({
|
|
|
3011
3955
|
// canonical projection above retains only explicitly durable systems.
|
|
3012
3956
|
}
|
|
3013
3957
|
const inputDigest = `sha256:${createHash("sha256")
|
|
3014
|
-
.update(JSON.stringify(
|
|
3958
|
+
.update(JSON.stringify(digestEvents), "utf8")
|
|
3015
3959
|
.digest("hex")}`;
|
|
3960
|
+
const branchGenerationId = `generation-${createHash("sha256")
|
|
3961
|
+
.update(`branch-generation\0${branchSessionId}`, "utf8")
|
|
3962
|
+
.digest("hex")
|
|
3963
|
+
.slice(0, 32)}`;
|
|
3964
|
+
const plannedEvents = digestEvents.map((event, index) =>
|
|
3965
|
+
index === 0
|
|
3966
|
+
? {
|
|
3967
|
+
...event,
|
|
3968
|
+
data: encodeSessionGenerationData(
|
|
3969
|
+
event.data,
|
|
3970
|
+
createSessionGenerationAuthority(
|
|
3971
|
+
branchSessionId,
|
|
3972
|
+
null,
|
|
3973
|
+
branchGenerationId,
|
|
3974
|
+
),
|
|
3975
|
+
),
|
|
3976
|
+
}
|
|
3977
|
+
: event,
|
|
3978
|
+
);
|
|
3016
3979
|
plannedEvents.push({
|
|
3017
3980
|
type: "session_branch_complete",
|
|
3018
3981
|
data: {
|
|
@@ -3027,6 +3990,23 @@ export function createBranchSession({
|
|
|
3027
3990
|
return withFileLock(
|
|
3028
3991
|
filePath,
|
|
3029
3992
|
() => {
|
|
3993
|
+
const presence = getSessionPresence(branchSessionId);
|
|
3994
|
+
if (
|
|
3995
|
+
presence === SESSION_PRESENCE.TOMBSTONED ||
|
|
3996
|
+
(presence === SESSION_PRESENCE.CONFLICT &&
|
|
3997
|
+
readSessionTombstoneMarker(sessionsDir, branchSessionId) !== null)
|
|
3998
|
+
) {
|
|
3999
|
+
throw sessionDeletedError(branchSessionId);
|
|
4000
|
+
}
|
|
4001
|
+
if (
|
|
4002
|
+
presence === SESSION_PRESENCE.MISSING_TRANSCRIPT ||
|
|
4003
|
+
presence === SESSION_PRESENCE.CONFLICT
|
|
4004
|
+
) {
|
|
4005
|
+
throw unverifiedTranscriptError(
|
|
4006
|
+
branchSessionId,
|
|
4007
|
+
transcriptAnchorMismatchVerification(),
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
3030
4010
|
if (existsSync(filePath)) inspectPhysicalTail(filePath);
|
|
3031
4011
|
let verification = existsSync(filePath)
|
|
3032
4012
|
? verifyTranscriptFile(filePath)
|
|
@@ -3048,12 +4028,22 @@ export function createBranchSession({
|
|
|
3048
4028
|
: [];
|
|
3049
4029
|
const prefixMatches = existingEvents
|
|
3050
4030
|
.slice(0, Math.min(existingEvents.length, plannedEvents.length))
|
|
3051
|
-
.every(
|
|
3052
|
-
|
|
4031
|
+
.every((event, index) => {
|
|
4032
|
+
let plannedData = plannedEvents[index].data;
|
|
4033
|
+
// Existing v1 branches predate generation authority. They remain
|
|
4034
|
+
// idempotently readable; only newly created branches publish it.
|
|
4035
|
+
if (
|
|
4036
|
+
index === 0 &&
|
|
4037
|
+
event?.data?.[SESSION_GENERATION_AUTHORITY_FIELD] === undefined
|
|
4038
|
+
) {
|
|
4039
|
+
plannedData = { ...plannedData };
|
|
4040
|
+
delete plannedData[SESSION_GENERATION_AUTHORITY_FIELD];
|
|
4041
|
+
}
|
|
4042
|
+
return (
|
|
3053
4043
|
event.type === plannedEvents[index].type &&
|
|
3054
|
-
JSON.stringify(event.data) ===
|
|
3055
|
-
|
|
3056
|
-
);
|
|
4044
|
+
JSON.stringify(event.data) === JSON.stringify(plannedData)
|
|
4045
|
+
);
|
|
4046
|
+
});
|
|
3057
4047
|
if (!prefixMatches) {
|
|
3058
4048
|
const error = new Error(
|
|
3059
4049
|
`Branch session conflicts with its deterministic input: ${branchSessionId}`,
|
|
@@ -3108,6 +4098,9 @@ export function createBranchSession({
|
|
|
3108
4098
|
}
|
|
3109
4099
|
|
|
3110
4100
|
let prevHash = verification.lastHash;
|
|
4101
|
+
let expectedTranscriptState = existsSync(filePath)
|
|
4102
|
+
? readPhysicalTranscriptState(filePath)
|
|
4103
|
+
: null;
|
|
3111
4104
|
for (
|
|
3112
4105
|
let index = existingEvents.length;
|
|
3113
4106
|
index < plannedEvents.length;
|
|
@@ -3121,17 +4114,13 @@ export function createBranchSession({
|
|
|
3121
4114
|
};
|
|
3122
4115
|
const hash = computeEventHash(prevHash, core);
|
|
3123
4116
|
const event = { ...core, prevHash, hash };
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
});
|
|
3128
|
-
ensurePrivateFile(filePath);
|
|
3129
|
-
runSessionScaleFaultHook("afterTranscriptAppend", {
|
|
3130
|
-
sessionId: branchSessionId,
|
|
3131
|
-
type: planned.type,
|
|
4117
|
+
expectedTranscriptState = appendTranscriptEvent(
|
|
4118
|
+
branchSessionId,
|
|
4119
|
+
planned.type,
|
|
3132
4120
|
event,
|
|
3133
4121
|
filePath,
|
|
3134
|
-
|
|
4122
|
+
expectedTranscriptState,
|
|
4123
|
+
);
|
|
3135
4124
|
prevHash = hash;
|
|
3136
4125
|
}
|
|
3137
4126
|
|
|
@@ -3170,14 +4159,42 @@ export const SESSION_PRESENCE = Object.freeze({
|
|
|
3170
4159
|
|
|
3171
4160
|
export function getSessionPresence(sessionId) {
|
|
3172
4161
|
if (isUnsafeSessionId(sessionId)) return SESSION_PRESENCE.ABSENT;
|
|
4162
|
+
const dir = getSessionsDir();
|
|
3173
4163
|
const transcriptExists = existsSync(sessionPath(sessionId));
|
|
4164
|
+
const tombstoneMarkerExists = existsSync(
|
|
4165
|
+
join(dir, `${sessionId}${SESSION_TOMBSTONE_MARKER_SUFFIX}`),
|
|
4166
|
+
);
|
|
3174
4167
|
const witness = readSessionPersistenceWitness(sessionId);
|
|
3175
|
-
|
|
4168
|
+
const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
|
|
4169
|
+
if (
|
|
4170
|
+
transcriptExists &&
|
|
4171
|
+
(witness?.deleted === true ||
|
|
4172
|
+
tombstoneMarkerExists ||
|
|
4173
|
+
externalAnchor?.status === "deleted")
|
|
4174
|
+
) {
|
|
3176
4175
|
return SESSION_PRESENCE.CONFLICT;
|
|
3177
4176
|
}
|
|
3178
|
-
if (transcriptExists)
|
|
3179
|
-
|
|
3180
|
-
|
|
4177
|
+
if (transcriptExists) {
|
|
4178
|
+
if (witness !== null && witness?.deleted !== true) {
|
|
4179
|
+
assertSessionAntiRollbackBeforeWrite(
|
|
4180
|
+
sessionId,
|
|
4181
|
+
witness,
|
|
4182
|
+
sessionPath(sessionId),
|
|
4183
|
+
"live",
|
|
4184
|
+
);
|
|
4185
|
+
}
|
|
4186
|
+
return SESSION_PRESENCE.PRESENT;
|
|
4187
|
+
}
|
|
4188
|
+
if (
|
|
4189
|
+
witness?.deleted === true ||
|
|
4190
|
+
tombstoneMarkerExists ||
|
|
4191
|
+
externalAnchor?.status === "deleted"
|
|
4192
|
+
) {
|
|
4193
|
+
return SESSION_PRESENCE.TOMBSTONED;
|
|
4194
|
+
}
|
|
4195
|
+
if (witness !== null || externalAnchor?.status === "live") {
|
|
4196
|
+
return SESSION_PRESENCE.MISSING_TRANSCRIPT;
|
|
4197
|
+
}
|
|
3181
4198
|
return SESSION_PRESENCE.ABSENT;
|
|
3182
4199
|
}
|
|
3183
4200
|
|