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.
Files changed (215) hide show
  1. package/README.md +29 -6
  2. package/completions/_cc +2 -2
  3. package/completions/cc.bash +2 -2
  4. package/completions/cc.fish +2 -2
  5. package/completions/cc.ps1 +2 -2
  6. package/package.json +1 -1
  7. package/src/assets/web-panel/assets/{AIOps-DBLkaTje.js → AIOps-CSN-D2xb.js} +1 -1
  8. package/src/assets/web-panel/assets/{ActionButton-BAy-Qebo.js → ActionButton-CBSMlGzv.js} +1 -1
  9. package/src/assets/web-panel/assets/{Analytics-Dfb1Mlau.js → Analytics-3yr3Nt2t.js} +3 -3
  10. package/src/assets/web-panel/assets/{AppLayout-BbFkN8m0.js → AppLayout-1b-o3lMH.js} +4 -4
  11. package/src/assets/web-panel/assets/{Artifacts-kM4-cEEH.js → Artifacts-Dr8rJi17.js} +1 -1
  12. package/src/assets/web-panel/assets/{Audit-BSQfYWf3.js → Audit-2V689HC2.js} +1 -1
  13. package/src/assets/web-panel/assets/{BackgroundAgents-CWDbI_fG.js → BackgroundAgents-DIxkNrD_.js} +1 -1
  14. package/src/assets/web-panel/assets/{Backup-BxLPbq_Q.js → Backup-Pg6BcwrE.js} +1 -1
  15. package/src/assets/web-panel/assets/{BaseInput-DuIbdrp6.js → BaseInput-CLUpqyfH.js} +1 -1
  16. package/src/assets/web-panel/assets/{Chat-NEmjGahU.js → Chat-qVS8rD4G.js} +5 -5
  17. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DfJEo-5_.js → ChatBubbleRenderer-ti49X12Q.js} +1 -1
  18. package/src/assets/web-panel/assets/{Checkbox-CaoeG3Ns.js → Checkbox-Bs9AaEay.js} +1 -1
  19. package/src/assets/web-panel/assets/{Codegen-BFf_g3xY.js → Codegen-Dvc1mq8q.js} +1 -1
  20. package/src/assets/web-panel/assets/{Col-CNOd8mdQ.js → Col-C4_biO7A.js} +1 -1
  21. package/src/assets/web-panel/assets/{Community-BhpDqz-i.js → Community-al25XQUV.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compact-CQ42T7Z6.js → Compact-D-ZDB8M1.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compliance-DI5PcLMN.js → Compliance-6lIfDz1W.js} +1 -1
  24. package/src/assets/web-panel/assets/{Cowork-wYYZe0wR.js → Cowork-DdI48XMJ.js} +3 -3
  25. package/src/assets/web-panel/assets/{Cron-Dlq03DD_.js → Cron-DF5HZd2g.js} +2 -2
  26. package/src/assets/web-panel/assets/{Crosschain-BzqS8PP3.js → Crosschain-DuxeSvt3.js} +1 -1
  27. package/src/assets/web-panel/assets/{DID-It55Yem7.js → DID-B9yHkUTd.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dashboard-9HkH6oxn.js → Dashboard-Z5EuI3IM.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dropdown-00e2YWRB.js → Dropdown-vuRfO3dY.js} +1 -1
  30. package/src/assets/web-panel/assets/{EmailListRenderer-By-ugRDL.js → EmailListRenderer-QFLCc1uL.js} +1 -1
  31. package/src/assets/web-panel/assets/{FamilyGuardDashboard-C396yu5m.js → FamilyGuardDashboard-BZ7EXkFJ.js} +1 -1
  32. package/src/assets/web-panel/assets/{Federation-BME5sbZY.js → Federation-DycIEg2s.js} +1 -1
  33. package/src/assets/web-panel/assets/{FormItemContext-B4TEZCr-.js → FormItemContext-Ba0H_7yG.js} +1 -1
  34. package/src/assets/web-panel/assets/{GenericCardRenderer-_9CX9ieY.js → GenericCardRenderer--JgSTK-C.js} +1 -1
  35. package/src/assets/web-panel/assets/{Git-BrS6WVSr.js → Git-DEvTbldE.js} +2 -2
  36. package/src/assets/web-panel/assets/{Governance-BMdCWnKF.js → Governance-BJIljy18.js} +1 -1
  37. package/src/assets/web-panel/assets/{Inference-Bcp3vzcA.js → Inference-CVyW3G3q.js} +1 -1
  38. package/src/assets/web-panel/assets/{KnowledgeGraph-DlLuqGY5.js → KnowledgeGraph-v8-borge.js} +1 -1
  39. package/src/assets/web-panel/assets/{Logs-D-W_6x09.js → Logs-BCdS7gGo.js} +2 -2
  40. package/src/assets/web-panel/assets/{MarkdownRenderer-DElGPxCZ.js → MarkdownRenderer-CU-JB2Ns.js} +1 -1
  41. package/src/assets/web-panel/assets/{Marketplace-Dzn2guvI.js → Marketplace-AN6tGA2B.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-uPict-KL.js → McpTools-DGViXASN.js} +6 -6
  43. package/src/assets/web-panel/assets/{Memory-BCw43kOb.js → Memory-Bpvmx9X4.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-DmV8Wv7F.js → MobileBridge-CTP-DOTC.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-C-HaCmSM.js → MobileProjects-C8qfktxn.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-C6jxJ34Y.js → Mtc-ndYr0M1s.js} +5 -5
  47. package/src/assets/web-panel/assets/{MtcAudit-Za8ExwhQ.js → MtcAudit-Ci0-YK7W.js} +5 -5
  48. package/src/assets/web-panel/assets/{Multisig-KKDbuEgb.js → Multisig-BehSTFW7.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-Dj97zbCk.js → NLProgramming-B91_YBLv.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-DBXvMraU.js → Notes-BQlJ0-gq.js} +4 -4
  51. package/src/assets/web-panel/assets/{NotificationSettings-DOxITLYl.js → NotificationSettings-CDgYMjYK.js} +1 -1
  52. package/src/assets/web-panel/assets/{OrderTableRenderer-7DVjZwMM.js → OrderTableRenderer-BJkSlKeH.js} +1 -1
  53. package/src/assets/web-panel/assets/{Organization-D1i73mjb.js → Organization-B-b3cWCp.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-kW6Q3rvg.js → Overflow-CILUL4nL.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-C-v_8p2v.js → P2P-BTCFBkF0.js} +2 -2
  56. package/src/assets/web-panel/assets/{PdhVaultBrowser-tUbbY64W.js → PdhVaultBrowser-BAR6AUx5.js} +5 -5
  57. package/src/assets/web-panel/assets/{Permissions-DkHxEZ58.js → Permissions-RRf6HbWJ.js} +4 -4
  58. package/src/assets/web-panel/assets/{PersonalDataHub-DWuMPGrK.js → PersonalDataHub-BGTMiICa.js} +4 -4
  59. package/src/assets/web-panel/assets/{Pipeline-30IYXWWm.js → Pipeline-945qY9YF.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-DZNQ4RrH.js → Privacy-DPZwI6lJ.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-De2MCElf.js → ProjectInit-BPH2NRKc.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-5uHk9wsC.js → ProjectSettings-BZCRNCvb.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-XjDfrFLb.js → Projects-BTI__tBt.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-Bgwj6gcE.js → Providers-DDCf_nIw.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-gKql9umv.js → QrScannerModal-N6rbqt1r.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-BWvmwkh7.js → QuickAsk-DrwjPS7M.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-Uok4hZPd.js → Recommend-Cog9Wd99.js} +1 -1
  68. package/src/assets/web-panel/assets/{RemoteSession-DLJzTk9a.js → RemoteSession-DC9pynjM.js} +3 -3
  69. package/src/assets/web-panel/assets/{Reputation-ZVwIPMSO.js → Reputation-D4th4gKS.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-CN04HxWR.js → Row-ogiWR6v9.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-BdSrEG4g.js → RssFeed-uYlk-2Oj.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-5PwVd0IR.js → Search-DYzzwvtM.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-DdBo4zjN.js → Security-Da7YOyX6.js} +4 -4
  74. package/src/assets/web-panel/assets/{Services-D3letU7M.js → Services-CLI1VM1X.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton--Jb83f1v.js → Skeleton-DK77dzDC.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-Bg-CulHX.js → Skills-C_z7Lmhl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-7QOa7S-X.js → Sla-D6413Q8L.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-DQwppjRw.js → SpeechSettings-C07ydbj2.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-dNPQGL-s.js → SyncSettings-DGOm2W22.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-Ggs-Z48q.js → Tasks-Cr_PVyLM.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-78TYHgRO.js → Templates-BhL2mZ0t.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-xHwwv9vO.js → Tenant-CPk5oyUr.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-BJXSJfm4.js → Terminal-ClhqUsqV.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-fLbjRhJw.js → TimelineRenderer-BCC7MV5a.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-oMSBSBmB.js → Tokens-DD1Jt4oO.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-CfnY_wX_.js → Trigger-BCydFvN2.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-AdyTgYKO.js → Trust-DDHT1noa.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-C5K_81dS.js → UkeySign-CWPZC7oI.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-vcGVDqki.js → VideoEditing-9Ea0nJR0.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-hhOc3hlY.js → Wallet-BjxhLxTf.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-CVloC11s.js → WebAuthn-C6uCye67.js} +4 -4
  92. package/src/assets/web-panel/assets/{WorkflowEditor-CYeE32R5.js → WorkflowEditor-d2g4reYo.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-CNROR5bN.js → chat-frTc6ScX.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-Cjx-cnwU.js → colors-B5K6lG3v.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-DOIfelvD.js → compact-item-Cdn46r8D.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-DfA7UZ8u.js → createContext-DU1jqVQi.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-BMSCzcfM.js → hasIn-DdNH-aW_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CAnvhwg6.js → index-32ajXmTb.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BEMwBBHk.js → index-7ehytj1K.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CVL27kXC.js → index-BC4eGkar.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-FBc_CJBd.js → index-BVLL-mq_.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-dCd8cSUq.js → index-BXKhhx9V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BemJ2u_E.js → index-BcGwGIaR.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CVm1_i3t.js → index-Be4OtL-N.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Cs6l7uRu.js → index-BoFak7le.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DPAWBrMf.js → index-BwlIxAmX.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DLmn0BoK.js → index-CGiwfwB9.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-B7gf77CJ.js → index-CHmkkdfT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-m2vG9gG_.js → index-CQorP1Ng.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-ClNMrqEY.js → index-CUoNAmE8.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DHtpHZAQ.js → index-Cj6P9Nst.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C2K9SNVX.js → index-CtRPi4YC.js} +1 -1
  114. package/src/assets/web-panel/assets/index-CzrCHrFy.js +1 -0
  115. package/src/assets/web-panel/assets/{index-BolaOQxx.js → index-D1qIC4GR.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BM1y_9hM.js → index-D23xGYjM.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Cyweh0Hy.js → index-D4l82Me1.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-By61ojUx.js → index-D4miNkWv.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BbQ2eQ8H.js → index-DIVyTmZ3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Icdoqmzt.js → index-DM5FmPZU.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DjEsxO5h.js → index-DRzBBdQ0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B4iYzpkL.js → index-Dca_i7H8.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C3L5gdiD.js → index-DftZuyA7.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CdYBye8B.js → index-DtBCCkC4.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-AWg-0M1S.js → index-HjCvXHf5.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CEKMcOyU.js → index-LaivuXyF.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BxOk9vkG.js → index-MM4Zz2OE.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CNC5onm4.js → index-NmEXVjzt.js} +1 -1
  129. package/src/assets/web-panel/assets/index-OJsGskZw.js +1 -0
  130. package/src/assets/web-panel/assets/{index-BUonmKSE.js → index-PjAS0sIZ.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-B5Udeb_6.js → index-VDM2KKo9.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-5-HXgOX5.js → index-cYaWV4Mq.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DBpWXFed.js → index-gg3QPC3i.js} +3 -3
  134. package/src/assets/web-panel/assets/{index-C7xn0Nub.js → index-kVO_MDm-.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-ti_lnTWl.js → index-mgU3Q-lN.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-CshQbilz.js → index-t_OBzIP4.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-mTpHoEzr.js → index-yZRuZpwK.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-Bu8k7H1Y.js → initDefaultProps-DwLTRNdo.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-s3iVK41k.js → motion-BchZDXnh.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-B6B-E5Jz.js → move-e8M-zZFl.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-BiYgTidA.js → mtc-parser-Ci40fQb3.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-pU1RdUDA.js → omit-C7110_f2.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-Cdoq_SeM.js → pickAttrs-skteMgnM.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-DIuvAn2K.js → placementArrow-MGZ-lh2B.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-8w16YR2D.js → responsiveObserve-N4Sd7uy-.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-VZ01cZe3.js → slide-C7HDGRaa.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-lh8YMN8Z.js → statusUtils-BhRexlgy.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-CmDyreec.js → styleChecker-ChEoQkN2.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-hiJWMU5y.js → useFlexGapSupport-CDbG1s2Y.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-Thnu-Iaj.js → useFs-CUIx0tcJ.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-LBdOxjwY.js → usePersonalDataHub-bkO7L7ec.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-Cg6lFY_X.js → vnode-B_ZvABIG.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-DVfsR1YO.js → zoom-D5shjpS8.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-help-index.json +4 -4
  156. package/src/command-manifest.json +214 -50
  157. package/src/command-surface-policy.js +115 -0
  158. package/src/commands/agent.js +3 -3
  159. package/src/commands/background-session.js +145 -0
  160. package/src/commands/mcp.js +125 -7
  161. package/src/commands/serve.js +1 -1
  162. package/src/commands/session-show.js +131 -0
  163. package/src/commands/session.js +50 -106
  164. package/src/commands/skill.js +87 -50
  165. package/src/data/changelog.json +18 -5
  166. package/src/gateways/ws/ws-agent-handler.js +8 -1
  167. package/src/harness/jsonl-session-store.js +428 -25
  168. package/src/harness/mcp-client.js +2652 -404
  169. package/src/harness/session-list-index.js +65 -7
  170. package/src/lazy-dispatch.js +76 -8
  171. package/src/lib/abort-utils.js +68 -0
  172. package/src/lib/agent-core.js +1 -59
  173. package/src/lib/chat-core.js +24 -6
  174. package/src/lib/cowork-mcp-tools.js +47 -2
  175. package/src/lib/cowork-task-runner.js +11 -3
  176. package/src/lib/doctor-checkup.js +9 -7
  177. package/src/lib/learning/skill-improver.js +21 -2
  178. package/src/lib/mcp-call-ledger-store.js +116 -3
  179. package/src/lib/mcp-call-ledger.js +59 -14
  180. package/src/lib/mcp-client.js +1 -0
  181. package/src/lib/mcp-stdio-executable-identity.js +598 -0
  182. package/src/lib/mcp-stdio-execution-authority.js +363 -0
  183. package/src/lib/mcp-tool-metadata.js +436 -0
  184. package/src/lib/mcp-tool-result.js +367 -0
  185. package/src/lib/observability/index.js +71 -0
  186. package/src/lib/pr-link-ledger.js +8 -27
  187. package/src/lib/pr-link-store.js +26 -0
  188. package/src/lib/process-execution-broker/index.js +14 -0
  189. package/src/lib/process-tree-termination.js +253 -0
  190. package/src/lib/public-ide-capability-manifest.js +6 -0
  191. package/src/lib/session-host-snapshot.js +9 -5
  192. package/src/lib/session-projection.js +5 -1
  193. package/src/lib/session-tail.js +31 -1
  194. package/src/lib/skill-budget.js +222 -0
  195. package/src/lib/skill-execution-authority.js +381 -0
  196. package/src/lib/skill-execution-identity.js +39 -2
  197. package/src/lib/skill-loader.js +322 -86
  198. package/src/lib/skill-mcp.js +43 -1
  199. package/src/repl/agent-repl.js +349 -46
  200. package/src/repl/bg-dashboard.js +81 -28
  201. package/src/repl/chat-repl.js +77 -12
  202. package/src/repl/prompt-interactions.js +67 -7
  203. package/src/repl/slash-command-registry.js +3 -4
  204. package/src/runtime/agent-core.js +340 -108
  205. package/src/runtime/agent-runtime.js +26 -3
  206. package/src/runtime/headless-runner.js +150 -23
  207. package/src/runtime/headless-stream.js +309 -59
  208. package/src/runtime/mcp-config.js +157 -10
  209. package/src/runtime/mcp-tool-search.js +0 -0
  210. package/src/runtime/output-backpressure.js +366 -0
  211. package/src/runtime/pipe-safety.js +56 -21
  212. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +0 -1
  213. package/src/assets/web-panel/assets/index-Q2IajL54.js +0 -1
  214. package/src/assets/web-panel/assets/index-bRkHgOMc.js +0 -1
  215. 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
- !existsSync(filePath) &&
789
- existingMeta?.deleted === true &&
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
- if (!existsSync(filePath) && existingMeta?.deleted === true) {
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) && existingMeta !== null) {
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
- return readdirSync(dir)
1221
- .filter((file) => file.endsWith(".jsonl"))
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((file) => verifySession(basename(file, ".jsonl")));
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((id) => id.startsWith(input));
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
- const meta = readSessionMeta(dir, id);
2130
- rebuilt.push(meta || rebuildSessionMeta(id));
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 a
2164
- * rebuildable tombstone so an already-waiting stale writer cannot resurrect
2165
- * the session after the deletion commits. */
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
- if (!existsSync(filePath)) return false;
2174
- rmSync(filePath, { force: true });
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
- if (isUnsafeSessionId(sessionId)) return false; // never resolve a traversal id
2878
- return existsSync(sessionPath(sessionId));
3196
+ return getSessionPresence(sessionId) === SESSION_PRESENCE.PRESENT;
2879
3197
  }
2880
3198
 
2881
3199
  export function getLastSessionId() {
2882
- const sessions = listJsonlSessions({ limit: 1 });
2883
- return sessions.length > 0 ? sessions[0].id : null;
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
- if (!options.force && sessionExists(sessionId)) {
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 (options.force && sessionExists(sessionId)) {
3082
- rmSync(sessionPath(sessionId), { force: true });
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) {