chainlesschain 0.162.198 → 0.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +5 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/scripts/postinstall.mjs +19 -3
  5. package/scripts/repair-node-pty.mjs +74 -0
  6. package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-CSy9BB6Z.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-CFyrEVfq.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-8419vb47.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CHu3Hkdx.js} +5 -5
  10. package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DJnt57Fs.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CHMq9ajE.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-Cyfa4ub2.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-ACXUrUv7.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DjC-3qRE.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-7_VDQ851.js} +6 -6
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-B4zct0dY.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-DxbDl_rk.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-BvZa5854.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-CX5Ow2Zl.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-wFun-jSg.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-C6vJDuSn.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-Cm4sBytH.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-DiEcMwE8.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-RbeexsSQ.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-BBsyCKOp.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DE36g7Hz.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CdfLSTz1.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-CyaT6SyW.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Nv_66bkM.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Di-Sb-Ad.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-B3mdht-U.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BeP93u4f.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-CETLayUr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-Bsat6hgI.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-YVmA7SwN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DCa94nGl.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-CIKEU7T0.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-DUxP0MnS.js} +2 -2
  39. package/src/assets/web-panel/assets/{MarkdownRenderer-CU-JB2Ns.js → MarkdownRenderer-Mw5o3e7M.js} +1 -1
  40. package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-BU0X2lqu.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-BSsg58nZ.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-CjqNIXlO.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-DSsa4doG.js} +1 -1
  44. package/src/assets/web-panel/assets/MobileProjects-DLl1qzid.js +1 -0
  45. package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-Du080G7t.js} +5 -5
  46. package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-JzQk-arw.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-BMVT5Uln.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-D_38QYjs.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-DvbdjNm5.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-DVHCW6ii.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-DmMBFD3g.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-DC-9pAmP.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-BRxWq0Qq.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-CUdzhyLH.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-CwG21s9P.js} +3 -3
  56. package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-6bHd4zgK.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-BXgTDCba.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-BFhKCBNj.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-Byz9IqUF.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-B9LNoyoN.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-DEj4eVdl.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-BAAvus06.js} +1 -1
  63. package/src/assets/web-panel/assets/Providers-Bu_pQsk6.js +1 -0
  64. package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-CeDey0SJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-exUvRIgX.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-4hJWmtSI.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-BmYUpl0m.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-B_txYAyY.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-BPtqOqGh.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-CyWTxmq0.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-cItU1tmC.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-B0Z9_BUt.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-Dzpz2uFp.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-CBtoylP8.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-Cw9RJiVn.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-D2vLfvgl.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-I9dKbcOU.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-BOju53RT.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-C3LBy_7x.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-C2ttpPql.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-XQ6Qg8-Q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DYB9dUnS.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-gShtO4I8.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BvoJTCJg.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-BwYRTMY_.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-C5rt6fCF.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-DPBrHUgL.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-BcxywO_K.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-CZvTpsPz.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-LI_jAuOx.js} +4 -4
  91. package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-B2a6bANB.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-B6DPMfRB.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-IpLhw9T8.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-BzodimsI.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-DL2jaWH5.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-CZB0EDsa.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-C4Geudre.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B5Sn7CtW.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-B6w4fpSI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-BRj6YKuI.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-BX4pKudW.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-BaTvPxY0.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-BbWRxWM2.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-BeZhISVl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-Bi6Zkv5M.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-BvKoSvko.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-C3LzRvkw.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-CAjyUjZl.js} +3 -3
  109. package/src/assets/web-panel/assets/index-CI7k8Sk9.js +1 -0
  110. package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-COxGCo8v.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-CQBPhJlk.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-CQPOCB4h.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTlmw_cr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CVFXYmUe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-C_SxaIPI.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CaKKxqCI.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-CcgoKNXN.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-CczBR6Du.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index-CeUnSSEP.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-D4l82Me1.js → index-CnwHp5Oi.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-DGBRBg3D.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-DHdD0Y7h.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-DMY1waPC.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DPTHBSR8.js +1 -0
  125. package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-DREx4gaS.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DXQ2OtXt.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DZU7Y67X.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-DZtKnnTW.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DlABpgCv.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BoFak7le.js → index-Dl_QAtzq.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-DnBybYhx.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DoyIjk6r.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-DvEnrn8M.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvOzjf2F.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-EjW1R_a7.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-F-SCUTee.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-CW5mTl3w.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-BtjIT9np.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-DZv4fZAV.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-BwCix1O-.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-BW72HDUD.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-DBW2jcMc.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-D3YmWlIS.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-CL4-a-9Q.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-Cw9zIbXQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-mMLVioo-.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BK8857AH.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-C1iSaJij.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-D3riGOaP.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-Bwqvmz4X.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-Byk4zzkf.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-D7INFTp2.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +1 -1
  155. package/src/commands/mcp.js +119 -0
  156. package/src/commands/session-show.js +8 -10
  157. package/src/commands/session.js +60 -52
  158. package/src/data/changelog.json +40 -1
  159. package/src/gateways/ws/session-protocol.js +14 -4
  160. package/src/gateways/ws/ws-agent-handler.js +31 -0
  161. package/src/harness/jsonl-session-store.js +1147 -130
  162. package/src/harness/mcp-client.js +44 -11
  163. package/src/harness/session-index.js +80 -80
  164. package/src/harness/session-list-index.js +147 -2
  165. package/src/lib/background-interaction-journal.js +2 -2
  166. package/src/lib/command-lifecycle-report.js +516 -0
  167. package/src/lib/mcp-stdio-environment.js +48 -0
  168. package/src/lib/mcp-stdio-executable-identity.js +552 -17
  169. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  170. package/src/lib/packer/native-update-state.js +979 -1
  171. package/src/lib/packer/pack-update-applier.js +1050 -125
  172. package/src/lib/packer/pkg-config-generator.js +1 -0
  173. package/src/lib/paths.js +53 -0
  174. package/src/lib/process-execution-broker/index.js +180 -37
  175. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  176. package/src/lib/recent-session.js +17 -17
  177. package/src/lib/secure-file-identity.js +47 -0
  178. package/src/lib/secure-fs.js +21 -2
  179. package/src/lib/session-anti-rollback-anchor.js +766 -0
  180. package/src/lib/session-host-lease.js +492 -0
  181. package/src/lib/session-persistence-failure.js +109 -0
  182. package/src/lib/session-picker.js +26 -3
  183. package/src/lib/session-search.js +25 -11
  184. package/src/lib/side-effect-ledger-store.js +2 -2
  185. package/src/lib/skill-execution-authority.js +13 -9
  186. package/src/lib/turn-binding-store.js +2 -2
  187. package/src/repl/agent-repl.js +309 -39
  188. package/src/runtime/agent-core.js +88 -5
  189. package/src/runtime/cleanup-deadline.js +143 -0
  190. package/src/runtime/headless-runner.js +357 -139
  191. package/src/runtime/headless-stream.js +207 -28
  192. package/src/runtime/output-backpressure.js +53 -0
  193. package/src/assets/web-panel/assets/MobileProjects-C8qfktxn.js +0 -1
  194. package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
  195. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
  196. package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
  197. package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
@@ -54,6 +54,7 @@ import {
54
54
  rebuildMessages as jsonlRebuildMessages,
55
55
  sessionExists as jsonlSessionExists,
56
56
  sessionHasPersistedEvidence as jsonlSessionHasPersistedEvidence,
57
+ resolveSessionAuthority as jsonlResolveSessionAuthority,
57
58
  getLastSessionId as jsonlGetLastSessionId,
58
59
  verifySession as jsonlVerifySession,
59
60
  } from "../harness/jsonl-session-store.js";
@@ -72,6 +73,7 @@ import {
72
73
  } from "../lib/mcp-call-ledger-store.js";
73
74
  import { createMcpHostRecoveryRuntime } from "../lib/mcp-host-recovery-runtime.js";
74
75
  import { readSessionHostResumeState } from "../lib/session-host-snapshot.js";
76
+ import { acquireSessionHostLease } from "../lib/session-host-lease.js";
75
77
  import { TurnBindingLog, createTurnBindingFeed } from "../lib/turn-binding.js";
76
78
  import { TURN_BINDING_EVENT } from "../lib/turn-binding-store.js";
77
79
  import { operationIdempotencyKey } from "../lib/idempotency.js";
@@ -155,6 +157,14 @@ const VALID_OUTPUT_FORMATS = Object.freeze(["text", "json", "stream-json"]);
155
157
  export { installPipeSafety } from "./pipe-safety.js";
156
158
  import { installPipeSafety } from "./pipe-safety.js";
157
159
  import { createHeadlessOutputBackpressure } from "./output-backpressure.js";
160
+ import {
161
+ cleanupDeadlineError,
162
+ createCleanupDeadline,
163
+ } from "./cleanup-deadline.js";
164
+ import {
165
+ createSessionPersistenceFailure,
166
+ projectSessionPersistenceFailure,
167
+ } from "../lib/session-persistence-failure.js";
158
168
 
159
169
  /**
160
170
  * Normalize a public --permission-mode spelling to the canonical internal mode.
@@ -290,6 +300,19 @@ export function resolveHeadlessSession(options = {}, store = {}, fallbackId) {
290
300
  } else if (typeof resume === "string" && resume.trim()) {
291
301
  resumeId = resume.trim();
292
302
  }
303
+ if (resumeId && typeof store.resolveSessionAuthority === "function") {
304
+ const authority = store.resolveSessionAuthority(resumeId);
305
+ if (authority && !authority.readable) {
306
+ const error = new Error(
307
+ `Canonical session ${authority.id} is not resumable (${authority.presence})`,
308
+ );
309
+ error.code = "SESSION_CANONICAL_UNAVAILABLE";
310
+ error.sessionId = authority.id;
311
+ error.presence = authority.presence;
312
+ throw error;
313
+ }
314
+ if (authority?.id) resumeId = authority.id;
315
+ }
293
316
  const persist =
294
317
  ephemeral === true
295
318
  ? false
@@ -363,6 +386,67 @@ export function applyForkSession(opts = {}, store = {}) {
363
386
  * @param {object} [deps] Injection seam for tests.
364
387
  * @returns {Promise<{ exitCode:number, result:string, isError:boolean, sessionSnapshot?:object }>}
365
388
  */
389
+ const HEADLESS_SESSION_RESOLUTION = Symbol("headlessSessionResolution");
390
+ const HEADLESS_SESSION_HOST_LEASE = Symbol("headlessSessionHostLease");
391
+
392
+ async function withHeadlessSessionHostLease(options, deps, task) {
393
+ const prompt = (options.prompt || "").trim();
394
+ if (!prompt) {
395
+ throw new Error(
396
+ "runAgentHeadless requires a non-empty prompt (use -p, a positional arg, or pipe stdin).",
397
+ );
398
+ }
399
+ const outputFormat = options.outputFormat || "text";
400
+ if (!VALID_OUTPUT_FORMATS.includes(outputFormat)) {
401
+ throw new Error(
402
+ `Invalid --output-format "${outputFormat}". Expected one of: ${VALID_OUTPUT_FORMATS.join(", ")}`,
403
+ );
404
+ }
405
+ const hasInjectedSessionStore =
406
+ typeof deps.sessionHasPersistedEvidence === "function" ||
407
+ typeof deps.sessionExists === "function" ||
408
+ typeof deps.rebuildMessages === "function" ||
409
+ typeof deps.appendEvent === "function" ||
410
+ typeof deps.appendAuthorityEvent === "function" ||
411
+ typeof deps.readVerifiedEvents === "function";
412
+ const resolution = resolveHeadlessSession(
413
+ options,
414
+ {
415
+ getLastSessionId: deps.getLastSessionId || jsonlGetLastSessionId,
416
+ resolveSessionAuthority:
417
+ deps.resolveSessionAuthority ||
418
+ (!hasInjectedSessionStore ? jsonlResolveSessionAuthority : undefined),
419
+ },
420
+ `headless-${Date.now()}-${process.pid}`,
421
+ );
422
+ const authoritySessionId =
423
+ resolution.resumeId || (resolution.persist ? resolution.sessionId : null);
424
+ const acquireHostLease =
425
+ deps.acquireSessionHostLease ||
426
+ (!hasInjectedSessionStore ? acquireSessionHostLease : null);
427
+ let lease = null;
428
+ try {
429
+ if (authoritySessionId && typeof acquireHostLease === "function") {
430
+ lease = acquireHostLease(authoritySessionId, { hostKind: "headless" });
431
+ }
432
+ const scopedOptions = lease?.signal
433
+ ? {
434
+ ...options,
435
+ signal: options.signal
436
+ ? AbortSignal.any([options.signal, lease.signal])
437
+ : lease.signal,
438
+ }
439
+ : options;
440
+ return await task(scopedOptions, {
441
+ ...deps,
442
+ [HEADLESS_SESSION_RESOLUTION]: resolution,
443
+ [HEADLESS_SESSION_HOST_LEASE]: lease,
444
+ });
445
+ } finally {
446
+ lease?.release?.();
447
+ }
448
+ }
449
+
366
450
  export async function runAgentHeadless(options = {}, deps = {}) {
367
451
  const trustedWorkspaceRoot = options.cwd || process.cwd();
368
452
  const pipeState = { closed: false };
@@ -388,6 +472,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
388
472
  writeOut: deps.writeOut,
389
473
  writeErr: deps.writeErr,
390
474
  maxQueuedBytes: deps.outputBackpressureMaxBytes,
475
+ drainTimeoutMs: deps.outputBackpressureTimeoutMs,
391
476
  onFailure: (error) => {
392
477
  outputAbort?.abort(error);
393
478
  if (error?.code === "EPIPE") handlePipeClosed();
@@ -423,7 +508,12 @@ export async function runAgentHeadless(options = {}, deps = {}) {
423
508
  let failure = null;
424
509
  try {
425
510
  outcome = await runWithHostHooksV2Workspace(trustedWorkspaceRoot, () =>
426
- runAgentHeadlessInWorkspace(runtimeOptions, runtimeDeps, pipeState),
511
+ withHeadlessSessionHostLease(
512
+ runtimeOptions,
513
+ runtimeDeps,
514
+ (scopedOptions, scopedDeps) =>
515
+ runAgentHeadlessInWorkspace(scopedOptions, scopedDeps, pipeState),
516
+ ),
427
517
  );
428
518
  } catch (error) {
429
519
  failure = error;
@@ -470,6 +560,10 @@ async function runAgentHeadlessInWorkspace(
470
560
  const isText = outputFormat === "text";
471
561
  const writeOut = deps.writeOut;
472
562
  const writeErr = deps.writeErr;
563
+ const cleanupDeadline = createCleanupDeadline({
564
+ timeoutMs: deps.cleanupDeadlineMs,
565
+ label: "headless-runner-cleanup",
566
+ });
473
567
 
474
568
  const hasInjectedSessionStore =
475
569
  typeof deps.sessionHasPersistedEvidence === "function" ||
@@ -478,11 +572,19 @@ async function runAgentHeadlessInWorkspace(
478
572
  typeof deps.appendEvent === "function" ||
479
573
  typeof deps.appendAuthorityEvent === "function" ||
480
574
  typeof deps.readVerifiedEvents === "function";
481
- const { sessionId, resumeId, persist } = resolveHeadlessSession(
482
- options,
483
- { getLastSessionId: deps.getLastSessionId || jsonlGetLastSessionId },
484
- `headless-${Date.now()}-${process.pid}`,
485
- );
575
+ const { sessionId, resumeId, persist } =
576
+ deps[HEADLESS_SESSION_RESOLUTION] ||
577
+ resolveHeadlessSession(
578
+ options,
579
+ {
580
+ getLastSessionId: deps.getLastSessionId || jsonlGetLastSessionId,
581
+ resolveSessionAuthority:
582
+ deps.resolveSessionAuthority ||
583
+ (!hasInjectedSessionStore ? jsonlResolveSessionAuthority : undefined),
584
+ },
585
+ `headless-${Date.now()}-${process.pid}`,
586
+ );
587
+ const sessionHostLease = deps[HEADLESS_SESSION_HOST_LEASE] || null;
486
588
 
487
589
  const emitHeadlessError = (resultMsg) => {
488
590
  if (isStream) {
@@ -544,8 +646,8 @@ async function runAgentHeadlessInWorkspace(
544
646
  sessionSnapshot: canonicalAdmission.snapshot,
545
647
  };
546
648
  }
547
- // A persist-only target must pass the same canonical admission boundary, but
548
- // it does not opt into replaying the target's prior conversation.
649
+ // A persist-only target must pass the same canonical admission boundary,
650
+ // but it does not opt into replaying the target's prior conversation.
549
651
  const canonicalResume = resumeId ? canonicalAdmission : null;
550
652
 
551
653
  // `let` (not const): a custom-command macro's `model:` frontmatter may
@@ -1026,7 +1128,7 @@ async function runAgentHeadlessInWorkspace(
1026
1128
  if (!persist) return;
1027
1129
  if (sideEffectLedgerLoadError) throw sideEffectLedgerLoadError;
1028
1130
  try {
1029
- const persisted = store.appendEvent(
1131
+ const persisted = store.appendAuthorityEvent(
1030
1132
  sessionId,
1031
1133
  SIDE_EFFECT_LEDGER_EVENT,
1032
1134
  sideEffectLedger.toJSON(),
@@ -1081,13 +1183,16 @@ async function runAgentHeadlessInWorkspace(
1081
1183
  // (createTurnBindingFeed) so the interactive REPL producer can never drift
1082
1184
  // from this runner's mapping. No feed at all for non-persisted runs.
1083
1185
  const turnBindingFeed = persist
1084
- ? createTurnBindingFeed({ log: turnBindingLog, nonce: sideEffectRunNonce })
1186
+ ? createTurnBindingFeed({
1187
+ log: turnBindingLog,
1188
+ nonce: sideEffectRunNonce,
1189
+ })
1085
1190
  : null;
1086
1191
  const persistTurnBindingLog = () => {
1087
1192
  if (!turnBindingFeed || !turnBindingFeed.isDirty()) return;
1088
1193
  if (turnBindingLoadError) throw turnBindingLoadError;
1089
1194
  try {
1090
- const persisted = store.appendEvent(
1195
+ const persisted = store.appendAuthorityEvent(
1091
1196
  sessionId,
1092
1197
  TURN_BINDING_EVENT,
1093
1198
  turnBindingLog.toJSON(),
@@ -1271,7 +1376,9 @@ async function runAgentHeadlessInWorkspace(
1271
1376
  }
1272
1377
 
1273
1378
  const budget = Number.isFinite(options.maxTurns)
1274
- ? new IterationBudget({ limit: Math.max(1, Math.floor(options.maxTurns)) })
1379
+ ? new IterationBudget({
1380
+ limit: Math.max(1, Math.floor(options.maxTurns)),
1381
+ })
1275
1382
  : new IterationBudget();
1276
1383
 
1277
1384
  // Effective system prompt: built-in base, optionally replaced by
@@ -1463,9 +1570,10 @@ async function runAgentHeadlessInWorkspace(
1463
1570
  { role: "user", content: userMessageContent },
1464
1571
  ]);
1465
1572
 
1466
- // Persist the user turn up front (best-effort) so a session is recoverable
1467
- // even if the run crashes mid-loop. startSession is append-safe: only seed
1468
- // the header when the file does not yet exist.
1573
+ // Persist the user turn before model/tool side effects. A full or read-only
1574
+ // session disk is a recoverability contract failure, so it fails closed
1575
+ // while the requested turn is still side-effect free. startSession is
1576
+ // append-safe: only seed the header when the file does not yet exist.
1469
1577
  if (persist) {
1470
1578
  try {
1471
1579
  if (!store.sessionExists(sessionId)) {
@@ -1478,8 +1586,62 @@ async function runAgentHeadlessInWorkspace(
1478
1586
  // Persist the expanded content so a resumed session faithfully replays
1479
1587
  // what the model actually saw (the file snapshot, not just the @token).
1480
1588
  store.appendUserMessage(sessionId, userContent);
1481
- } catch {
1482
- // Persistence is best-effort — never fail the run over it.
1589
+ } catch (error) {
1590
+ const persistenceError = createSessionPersistenceFailure(error, {
1591
+ sessionId,
1592
+ operation: "user-turn-append",
1593
+ });
1594
+ const persistence = projectSessionPersistenceFailure(persistenceError, {
1595
+ phase: "before-model",
1596
+ });
1597
+ if (persistence) {
1598
+ try {
1599
+ deps.onPersistenceFailure?.(persistence);
1600
+ } catch {
1601
+ // Observability cannot change persistence semantics.
1602
+ }
1603
+ const resultMsg = `${persistence.code}: session turn was not durably persisted`;
1604
+ if (isStream) {
1605
+ writeOut(
1606
+ JSON.stringify({
1607
+ type: "result",
1608
+ subtype: "error_persistence",
1609
+ is_error: true,
1610
+ error: resultMsg,
1611
+ session_id: sessionId,
1612
+ persistence,
1613
+ }) + "\n",
1614
+ );
1615
+ } else if (isJson) {
1616
+ writeOut(
1617
+ JSON.stringify(
1618
+ buildResultEnvelope({
1619
+ subtype: "error_persistence",
1620
+ isError: true,
1621
+ result: resultMsg,
1622
+ sessionId,
1623
+ toolCalls: [],
1624
+ usage: {},
1625
+ numTurns: 0,
1626
+ durationMs: 0,
1627
+ persistence,
1628
+ }),
1629
+ ) + "\n",
1630
+ );
1631
+ } else {
1632
+ writeErr(
1633
+ `${resultMsg} (${persistence.fs_code}; ${persistence.commit_state})\n`,
1634
+ );
1635
+ }
1636
+ return {
1637
+ exitCode: 1,
1638
+ result: resultMsg,
1639
+ isError: true,
1640
+ persistence,
1641
+ };
1642
+ }
1643
+ // Preserve legacy best-effort handling for errors outside the explicit
1644
+ // ENOSPC/EROFS durability contract.
1483
1645
  }
1484
1646
  }
1485
1647
 
@@ -1630,7 +1792,13 @@ async function runAgentHeadlessInWorkspace(
1630
1792
  ppt = resolvePermissionPromptTool(mcp, options.permissionPromptTool);
1631
1793
  } catch (err) {
1632
1794
  writeErr(`Error: ${err.message}\n`);
1633
- if (mcp?.mcpClient) await mcp.mcpClient.disconnectAll().catch(() => {});
1795
+ await cleanupDeadline.run("mcp", () => mcp?.mcpClient?.disconnectAll?.());
1796
+ const cleanupReport = cleanupDeadline.report();
1797
+ try {
1798
+ deps.onCleanupReport?.(cleanupReport);
1799
+ } catch {
1800
+ // Metrics/reporting cannot change cleanup semantics.
1801
+ }
1634
1802
  emitHeadlessError(err.message);
1635
1803
  // A bad --permission-prompt-tool reference is a CONFIG error.
1636
1804
  return {
@@ -2261,8 +2429,11 @@ async function runAgentHeadlessInWorkspace(
2261
2429
  process.once("SIGINT", _onHardSignal);
2262
2430
  process.once("SIGTERM", _onHardSignal);
2263
2431
 
2432
+ let loopFailureOutcome = null;
2433
+ let cleanupFailure = null;
2264
2434
  try {
2265
2435
  while (true) {
2436
+ sessionHostLease?.assert?.();
2266
2437
  // P1 turn→checkpoint binding: one drain of the loop = one turn. Anchor
2267
2438
  // the conversation offset before the model sees the new user turn. A
2268
2439
  // `--worktree` run stamps its isolation worktree (branch name, threaded
@@ -2286,7 +2457,11 @@ async function runAgentHeadlessInWorkspace(
2286
2457
  case "checkpoint": {
2287
2458
  if (isText)
2288
2459
  writeErr(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`);
2289
- emitStream({ type: "checkpoint", id: event.id, tool: event.tool });
2460
+ emitStream({
2461
+ type: "checkpoint",
2462
+ id: event.id,
2463
+ tool: event.tool,
2464
+ });
2290
2465
  break;
2291
2466
  }
2292
2467
  case "tool-executing": {
@@ -2330,6 +2505,11 @@ async function runAgentHeadlessInWorkspace(
2330
2505
  })
2331
2506
  .start(opId);
2332
2507
  persistSideEffectLedger();
2508
+ // The generator executes the tool only after this yielded
2509
+ // prewrite is handled. Recheck the same durable lease at that
2510
+ // final host boundary so a fenced host never resumes into the
2511
+ // external operation.
2512
+ sessionHostLease?.assert?.();
2333
2513
  }
2334
2514
  }
2335
2515
  break;
@@ -2724,16 +2904,18 @@ async function runAgentHeadlessInWorkspace(
2724
2904
  }
2725
2905
  // Provider/transport failures get their own exit code (5) so CI can tell
2726
2906
  // "the model call failed" from "the run itself errored" (1).
2727
- return { exitCode: classifyLoopError(err), result: message, isError: true };
2907
+ loopFailureOutcome = {
2908
+ exitCode: classifyLoopError(err),
2909
+ result: message,
2910
+ isError: true,
2911
+ };
2728
2912
  } finally {
2729
2913
  // Capture body-cache hits/misses caused by list_skills/run_skill during the
2730
2914
  // turn. `cc context --sources` reads the newest snapshot.
2731
2915
  persistContextSources();
2732
- try {
2733
- _backgroundInteraction?.close?.();
2734
- } catch {
2735
- // Best-effort listener cleanup; never mask the run result.
2736
- }
2916
+ await cleanupDeadline.run("background-interaction", () =>
2917
+ _backgroundInteraction?.close?.(),
2918
+ );
2737
2919
  // Drop the signal handlers first — a normal return must not leave a
2738
2920
  // process-wide SIGINT/SIGTERM listener behind (a later Ctrl-C would wrongly
2739
2921
  // exit with 130, and repeated in-process runs would leak listeners).
@@ -2742,136 +2924,142 @@ async function runAgentHeadlessInWorkspace(
2742
2924
  // Tear down ad-hoc MCP servers (--mcp-config) before returning, whether the
2743
2925
  // loop completed or threw. Best-effort: a failed disconnect never masks the
2744
2926
  // run's own outcome.
2745
- if (mcp?.mcpClient) {
2746
- try {
2747
- await mcp.mcpClient.disconnectAll();
2748
- } catch {
2749
- // ignore — disconnect is best-effort
2750
- }
2751
- }
2927
+ await cleanupDeadline.run("mcp", () => mcp?.mcpClient?.disconnectAll?.());
2752
2928
  // Kill any background run_shell tasks this run spawned so a backgrounded
2753
2929
  // command (e.g. a dev server) doesn't outlive the headless invocation.
2754
- try {
2755
- killAllBackgroundShellTasks();
2756
- } catch {
2757
- // best-effort — never mask the run's own outcome
2758
- }
2930
+ await cleanupDeadline.run("background-shells", () =>
2931
+ killAllBackgroundShellTasks(),
2932
+ );
2759
2933
  // Tear down the --remote-control approval bridge + its self-hosted WS
2760
2934
  // server so the run's port/socket never outlives the invocation.
2761
- if (_remoteApproval) {
2762
- try {
2763
- await _remoteApproval.close();
2764
- } catch {
2765
- // best-effort — never mask the run's own outcome
2766
- }
2767
- }
2935
+ await cleanupDeadline.run("remote-approval", () =>
2936
+ _remoteApproval?.close?.(),
2937
+ );
2768
2938
  // settings.json Stop + SessionEnd hooks when the run finishes. Stop is the
2769
2939
  // canonical async-hook trigger ("run the test suite at turn end"); fire its
2770
2940
  // async hooks fire-and-forget, then settle so a fast background check can
2771
2941
  // report back within this one-shot run.
2772
- if (settingsHooks) {
2773
- try {
2774
- const { runObserveHooks, dispatchAsyncHooks } =
2775
- await import("../lib/settings-hook-events.js");
2776
- const stopPayload = {
2777
- reason: pipeState.closed ? "pipe_closed" : endReason,
2778
- cwd,
2779
- session_id: sessionId,
2780
- };
2781
- const stopOutcome = runObserveHooks(
2782
- settingsHooks,
2783
- "Stop",
2784
- stopPayload,
2785
- { cwd },
2786
- );
2787
- const stopFailures = (stopOutcome?.results || []).filter(
2788
- (result) =>
2789
- result?.nonBlockingError === true ||
2790
- result?.malformedDecision === true ||
2791
- result?.breakerOpen === true,
2792
- );
2793
- if (stopFailures.length > 0) {
2794
- emitHooksV2Event("StopFailure", {
2795
- schema_version: 1,
2796
- session_id: sessionId,
2797
- phase: "stop-hook",
2798
- failures: stopFailures.map((failure) => ({
2799
- command: failure.command || null,
2800
- exit_code: failure.exitCode ?? null,
2801
- reason: failure.reason || failure.error || null,
2802
- })),
2803
- });
2804
- }
2805
- // Skip the async Stop dispatch/settle here if the auto-rewake re-drive
2806
- // loop already fired + drained it this run (avoids double-execution).
2807
- if (_hookSupervisor && !_asyncStopHandled) {
2808
- dispatchAsyncHooks(settingsHooks, "Stop", stopPayload, {
2942
+ await cleanupDeadline.run("settings-hooks", async () => {
2943
+ if (settingsHooks) {
2944
+ try {
2945
+ const { runObserveHooks, dispatchAsyncHooks } =
2946
+ await import("../lib/settings-hook-events.js");
2947
+ const stopPayload = {
2948
+ reason: pipeState.closed ? "pipe_closed" : endReason,
2809
2949
  cwd,
2810
- supervisor: _hookSupervisor,
2811
- });
2812
- // Bounded settle: wait for in-flight async hooks (Stop + any
2813
- // PostToolUse dispatched during the loop) to finish, capped so a
2814
- // slow/hung check can never stall the run. Default 5s; tunable.
2815
- const waitMs = Number.isFinite(options.asyncHookWaitMs)
2816
- ? options.asyncHookWaitMs
2817
- : 5000;
2818
- const started = Date.now();
2819
- while (
2820
- _hookSupervisor.runningCount() > 0 &&
2821
- Date.now() - started < waitMs
2822
- ) {
2823
- await new Promise((r) => setTimeout(r, 50));
2824
- }
2825
- // Surface any results/rewakes out-of-band on stderr (never touches the
2826
- // stdout envelope). A rewake means a background check FAILED and would
2827
- // re-engage the agent in an interactive session — flag it clearly so a
2828
- // headless caller/CI can react.
2829
- for (const rw of _hookSupervisor.drainRewakes()) {
2950
+ session_id: sessionId,
2951
+ };
2952
+ const stopOutcome = runObserveHooks(
2953
+ settingsHooks,
2954
+ "Stop",
2955
+ stopPayload,
2956
+ { cwd },
2957
+ );
2958
+ const stopFailures = (stopOutcome?.results || []).filter(
2959
+ (result) =>
2960
+ result?.nonBlockingError === true ||
2961
+ result?.malformedDecision === true ||
2962
+ result?.breakerOpen === true,
2963
+ );
2964
+ if (stopFailures.length > 0) {
2830
2965
  emitHooksV2Event("StopFailure", {
2831
2966
  schema_version: 1,
2832
2967
  session_id: sessionId,
2833
- phase: "async-stop-hook",
2834
- command: rw.command || null,
2835
- exit_code: rw.exitCode ?? null,
2836
- reason: rw.error || "async stop hook requested rewake",
2968
+ phase: "stop-hook",
2969
+ failures: stopFailures.map((failure) => ({
2970
+ command: failure.command || null,
2971
+ exit_code: failure.exitCode ?? null,
2972
+ reason: failure.reason || failure.error || null,
2973
+ })),
2837
2974
  });
2838
- writeErr(
2839
- `[async-hook REWAKE] ${rw.command} failed` +
2840
- `${rw.exitCode != null ? ` (exit ${rw.exitCode})` : ""}` +
2841
- `${rw.error ? `: ${rw.error}` : ""}\n`,
2842
- );
2843
2975
  }
2844
- for (const rs of _hookSupervisor.drainResults()) {
2845
- if (rs.additionalContext) {
2846
- writeErr(`[async-hook] ${rs.command}: ${rs.additionalContext}\n`);
2976
+ // Skip the async Stop dispatch/settle here if the auto-rewake re-drive
2977
+ // loop already fired + drained it this run (avoids double-execution).
2978
+ if (_hookSupervisor && !_asyncStopHandled) {
2979
+ dispatchAsyncHooks(settingsHooks, "Stop", stopPayload, {
2980
+ cwd,
2981
+ supervisor: _hookSupervisor,
2982
+ });
2983
+ // Bounded settle: wait for in-flight async hooks (Stop + any
2984
+ // PostToolUse dispatched during the loop) to finish, capped so a
2985
+ // slow/hung check can never stall the run. Default 5s; tunable.
2986
+ const waitMs = Number.isFinite(options.asyncHookWaitMs)
2987
+ ? options.asyncHookWaitMs
2988
+ : 5000;
2989
+ const started = Date.now();
2990
+ while (
2991
+ _hookSupervisor.runningCount() > 0 &&
2992
+ Date.now() - started < waitMs
2993
+ ) {
2994
+ await new Promise((r) => setTimeout(r, 50));
2995
+ }
2996
+ // Surface any results/rewakes out-of-band on stderr (never touches the
2997
+ // stdout envelope). A rewake means a background check FAILED and would
2998
+ // re-engage the agent in an interactive session — flag it clearly so a
2999
+ // headless caller/CI can react.
3000
+ for (const rw of _hookSupervisor.drainRewakes()) {
3001
+ emitHooksV2Event("StopFailure", {
3002
+ schema_version: 1,
3003
+ session_id: sessionId,
3004
+ phase: "async-stop-hook",
3005
+ command: rw.command || null,
3006
+ exit_code: rw.exitCode ?? null,
3007
+ reason: rw.error || "async stop hook requested rewake",
3008
+ });
3009
+ writeErr(
3010
+ `[async-hook REWAKE] ${rw.command} failed` +
3011
+ `${rw.exitCode != null ? ` (exit ${rw.exitCode})` : ""}` +
3012
+ `${rw.error ? `: ${rw.error}` : ""}\n`,
3013
+ );
3014
+ }
3015
+ for (const rs of _hookSupervisor.drainResults()) {
3016
+ if (rs.additionalContext) {
3017
+ writeErr(
3018
+ `[async-hook] ${rs.command}: ${rs.additionalContext}\n`,
3019
+ );
3020
+ }
2847
3021
  }
2848
3022
  }
2849
- }
2850
- // Always reap the supervisor (kills any straggler child + detaches the
2851
- // exit reaper), whether the async Stop was handled here or in re-drive.
2852
- if (_hookSupervisor) _hookSupervisor.stopAll();
2853
- runObserveHooks(
2854
- settingsHooks,
2855
- "SessionEnd",
2856
- {
2857
- reason: pipeState.closed ? "pipe_closed" : "completed",
2858
- cwd,
3023
+ // Always reap the supervisor (kills any straggler child + detaches the
3024
+ // exit reaper), whether the async Stop was handled here or in re-drive.
3025
+ if (_hookSupervisor) _hookSupervisor.stopAll();
3026
+ runObserveHooks(
3027
+ settingsHooks,
3028
+ "SessionEnd",
3029
+ {
3030
+ reason: pipeState.closed ? "pipe_closed" : "completed",
3031
+ cwd,
3032
+ session_id: sessionId,
3033
+ },
3034
+ { cwd },
3035
+ );
3036
+ } catch (error) {
3037
+ emitHooksV2Event("StopFailure", {
3038
+ schema_version: 1,
2859
3039
  session_id: sessionId,
2860
- },
2861
- { cwd },
2862
- );
2863
- } catch (error) {
2864
- emitHooksV2Event("StopFailure", {
2865
- schema_version: 1,
2866
- session_id: sessionId,
2867
- phase: "stop-hook-dispatch",
2868
- reason: error?.message || String(error),
2869
- });
2870
- // observe-only + best-effort async surfacing
3040
+ phase: "stop-hook-dispatch",
3041
+ reason: error?.message || String(error),
3042
+ });
3043
+ // observe-only + best-effort async surfacing
3044
+ }
2871
3045
  }
3046
+ });
3047
+ const cleanupReport = cleanupDeadline.report();
3048
+ try {
3049
+ deps.onCleanupReport?.(cleanupReport);
3050
+ } catch {
3051
+ // Metrics/reporting cannot change cleanup semantics.
3052
+ }
3053
+ if (cleanupReport.timedOut) {
3054
+ cleanupFailure = cleanupDeadlineError(cleanupReport);
2872
3055
  }
2873
3056
  }
2874
3057
 
3058
+ // Preserve the primary model/runtime failure. A simultaneous cleanup timeout
3059
+ // remains observable through onCleanupReport but must not replace it.
3060
+ if (loopFailureOutcome) return loopFailureOutcome;
3061
+ if (cleanupFailure) throw cleanupFailure;
3062
+
2875
3063
  // A downstream consumer closed stdout/stderr. The abort above unwound the
2876
3064
  // model loop through the same `finally` as every other termination, so MCP,
2877
3065
  // background tasks, approval bridges, and hooks are settled. Do not attempt
@@ -2886,8 +3074,8 @@ async function runAgentHeadlessInWorkspace(
2886
3074
  const exhausted =
2887
3075
  endReason === "budget-exhausted" || endReason === "max_turns";
2888
3076
  const costStopped = endReason === "cost-budget-exhausted";
2889
- const isError = exhausted || costStopped || endReason === "no-response";
2890
- const subtype = exhausted
3077
+ let isError = exhausted || costStopped || endReason === "no-response";
3078
+ let subtype = exhausted
2891
3079
  ? "error_max_turns"
2892
3080
  : costStopped
2893
3081
  ? "error_max_budget"
@@ -2895,6 +3083,7 @@ async function runAgentHeadlessInWorkspace(
2895
3083
  ? "error"
2896
3084
  : "success";
2897
3085
  const durationMs = (deps.now ? deps.now() : Date.now()) - startedAt;
3086
+ let persistenceFailure = null;
2898
3087
 
2899
3088
  // Persist the assistant turn so a later --resume / --continue replays it.
2900
3089
  // The user turn was already recorded up front; only append on a clean run.
@@ -2921,8 +3110,30 @@ async function runAgentHeadlessInWorkspace(
2921
3110
  store.appendTokenUsage(sessionId, au);
2922
3111
  }
2923
3112
  store.appendTokenUsage(sessionId, usage);
2924
- } catch {
2925
- // Persistence is best-effort — never fail the run over it.
3113
+ } catch (error) {
3114
+ const persistenceError = createSessionPersistenceFailure(error, {
3115
+ sessionId,
3116
+ operation: "assistant-turn-append",
3117
+ });
3118
+ persistenceFailure = projectSessionPersistenceFailure(persistenceError, {
3119
+ phase: "after-model",
3120
+ });
3121
+ if (persistenceFailure) {
3122
+ isError = true;
3123
+ subtype = "error_persistence";
3124
+ try {
3125
+ deps.onPersistenceFailure?.(persistenceFailure);
3126
+ } catch {
3127
+ // Observability cannot change persistence semantics.
3128
+ }
3129
+ if (isText) {
3130
+ writeErr(
3131
+ `CC_SESSION_PERSISTENCE_FAILED: assistant turn durability is ${persistenceFailure.commit_state} (${persistenceFailure.fs_code})\n`,
3132
+ );
3133
+ }
3134
+ }
3135
+ // Preserve legacy best-effort handling for errors outside the explicit
3136
+ // ENOSPC/EROFS durability contract.
2926
3137
  }
2927
3138
  }
2928
3139
 
@@ -2983,7 +3194,9 @@ async function runAgentHeadlessInWorkspace(
2983
3194
  if (isText) {
2984
3195
  writeErr(
2985
3196
  `\n ${denials.length} tool call(s) were denied by policy this run:\n` +
2986
- formatDenials(denials, { now: deps.now ? deps.now() : Date.now() }) +
3197
+ formatDenials(denials, {
3198
+ now: deps.now ? deps.now() : Date.now(),
3199
+ }) +
2987
3200
  "\n",
2988
3201
  );
2989
3202
  } else if (isStream) {
@@ -3016,6 +3229,7 @@ async function runAgentHeadlessInWorkspace(
3016
3229
  ...(compactionDegradations.length
3017
3230
  ? { compaction_degradations: compactionDegradations }
3018
3231
  : {}),
3232
+ ...(persistenceFailure ? { persistence: persistenceFailure } : {}),
3019
3233
  });
3020
3234
  } else if (isJson) {
3021
3235
  writeOut(
@@ -3031,6 +3245,7 @@ async function runAgentHeadlessInWorkspace(
3031
3245
  durationMs,
3032
3246
  denials,
3033
3247
  compactionDegradations,
3248
+ persistence: persistenceFailure,
3034
3249
  }),
3035
3250
  ) + "\n",
3036
3251
  );
@@ -3075,6 +3290,7 @@ async function runAgentHeadlessInWorkspace(
3075
3290
  exitCode: exitCodeForEndReason(endReason, isError),
3076
3291
  result: finalText,
3077
3292
  isError,
3293
+ ...(persistenceFailure ? { persistence: persistenceFailure } : {}),
3078
3294
  };
3079
3295
  }
3080
3296
 
@@ -3089,6 +3305,7 @@ function buildResultEnvelope({
3089
3305
  durationMs,
3090
3306
  denials,
3091
3307
  compactionDegradations,
3308
+ persistence,
3092
3309
  }) {
3093
3310
  const env = {
3094
3311
  type: "result",
@@ -3107,5 +3324,6 @@ function buildResultEnvelope({
3107
3324
  if (Array.isArray(compactionDegradations) && compactionDegradations.length) {
3108
3325
  env.compaction_degradations = compactionDegradations;
3109
3326
  }
3327
+ if (persistence) env.persistence = persistence;
3110
3328
  return env;
3111
3329
  }