chainlesschain 0.162.158 → 0.162.159

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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
  6. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  7. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
  52. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  103. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
  128. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  129. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-manifest.json +17 -3
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +143 -3
  153. package/src/commands/browse.js +86 -0
  154. package/src/commands/cloud.js +232 -0
  155. package/src/commands/doctor.js +61 -6
  156. package/src/commands/mcp.js +61 -0
  157. package/src/commands/plugin.js +63 -1
  158. package/src/commands/routine.js +367 -0
  159. package/src/commands/session.js +326 -0
  160. package/src/data/changelog.json +7 -2
  161. package/src/harness/jsonl-session-store.js +82 -2
  162. package/src/harness/session-index.js +295 -0
  163. package/src/harness/session-mirror.js +142 -0
  164. package/src/harness/transcript-integrity.js +182 -0
  165. package/src/index.js +8 -0
  166. package/src/lib/background-agent-supervisor.js +126 -2
  167. package/src/lib/channels/channel-manager.js +126 -0
  168. package/src/lib/channels/telegram-channel.js +112 -0
  169. package/src/lib/channels/webhook-channel.js +140 -0
  170. package/src/lib/chrome-connector.js +386 -0
  171. package/src/lib/cloud/bundle.js +110 -0
  172. package/src/lib/cloud/cloud-client.js +152 -0
  173. package/src/lib/computer-use/authorization.js +173 -0
  174. package/src/lib/computer-use/computer-use-server.js +276 -0
  175. package/src/lib/computer-use/control-backend.js +150 -0
  176. package/src/lib/doctor-checkup.js +652 -0
  177. package/src/lib/jsonl-session-store.js +4 -0
  178. package/src/lib/lsp/lsp-server-registry.js +48 -21
  179. package/src/lib/plugin-runtime/governance.js +174 -0
  180. package/src/lib/pr-link-ledger.js +221 -0
  181. package/src/lib/routine-store.js +352 -0
  182. package/src/lib/session-store-guard.js +40 -0
  183. package/src/lib/session-usage.js +318 -2
  184. package/src/lib/sub-agent-context.js +23 -0
  185. package/src/repl/agent-repl.js +361 -3
  186. package/src/repl/bg-dashboard.js +355 -0
  187. package/src/repl/fast-mode.js +103 -0
  188. package/src/repl/fullscreen-tui.js +197 -0
  189. package/src/repl/slash-command-registry.js +11 -3
  190. package/src/repl/voice-dictation.js +132 -0
  191. package/src/runtime/agent-core.js +242 -1
  192. package/src/runtime/coding-agent-contract-shared.cjs +88 -0
  193. package/src/runtime/coding-agent-policy.cjs +17 -0
  194. package/src/runtime/headless-runner.js +71 -7
  195. package/src/runtime/headless-stream.js +14 -8
  196. package/src/runtime/policies/agent-policy.js +3 -0
  197. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
  199. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  200. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  201. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -197,6 +197,15 @@ export function registerSessionCommand(program) {
197
197
  process.exit(1);
198
198
  }
199
199
 
200
+ // PR/session linking: surface PRs this session created/touched.
201
+ try {
202
+ const { getPrLinks } = await import("../lib/pr-link-ledger.js");
203
+ const prLinks = getPrLinks(sess.id);
204
+ if (prLinks.length > 0) sess.prLinks = prLinks;
205
+ } catch (_err) {
206
+ // PR decoration is cosmetic
207
+ }
208
+
200
209
  if (options.json) {
201
210
  console.log(JSON.stringify(sess, null, 2));
202
211
  } else {
@@ -206,6 +215,15 @@ export function registerSessionCommand(program) {
206
215
  `ID: ${sess.id} Provider: ${sess.provider} Model: ${sess.model} Messages: ${sess.message_count}`,
207
216
  ),
208
217
  );
218
+ if (sess.prLinks) {
219
+ for (const pr of sess.prLinks) {
220
+ logger.log(
221
+ chalk.magenta(
222
+ `PR: #${pr.number}${pr.state ? ` ${pr.state}` : ""}${pr.url ? ` ${pr.url}` : ""}`,
223
+ ),
224
+ );
225
+ }
226
+ }
209
227
  logger.log("");
210
228
 
211
229
  let messages = sess.messages;
@@ -248,6 +266,10 @@ export function registerSessionCommand(program) {
248
266
  )
249
267
  .option("--model <model>", "Model name")
250
268
  .option("--provider <provider>", "LLM provider")
269
+ .option(
270
+ "--allow-tampered",
271
+ "Resume even when the transcript hash chain is broken (context is untrusted)",
272
+ )
251
273
  .action(async (id, options) => {
252
274
  try {
253
275
  const ctx = await bootstrap({ verbose: program.opts().verbose });
@@ -270,6 +292,29 @@ export function registerSessionCommand(program) {
270
292
 
271
293
  // Try JSONL first
272
294
  if (feature("JSONL_SESSION") && sessionExists(id)) {
295
+ // Tamper gate: a broken hash chain means the transcript was edited
296
+ // outside the store — never silently rebuild it as trusted context.
297
+ const { verifySession } =
298
+ await import("../harness/jsonl-session-store.js");
299
+ const trust = verifySession(id);
300
+ if (trust.status === "tampered") {
301
+ if (!options.allowTampered) {
302
+ logger.error(
303
+ `Session ${id} transcript failed integrity verification: ${trust.reason}` +
304
+ (trust.firstInvalidLine
305
+ ? ` (line ${trust.firstInvalidLine})`
306
+ : ""),
307
+ );
308
+ logger.error(
309
+ "Refusing to resume tampered context. Inspect it read-only with " +
310
+ `'cc session show ${id}' / 'cc session verify ${id}', or pass --allow-tampered to override.`,
311
+ );
312
+ process.exit(1);
313
+ }
314
+ logger.warn(
315
+ `⚠ Resuming a TAMPERED transcript (${trust.reason}) — treat restored context as untrusted.`,
316
+ );
317
+ }
273
318
  const events = readEvents(id);
274
319
  const startEvent = events.find((e) => e.type === "session_start");
275
320
  sess = {
@@ -594,6 +639,143 @@ export function registerSessionCommand(program) {
594
639
  }
595
640
  });
596
641
 
642
+ // session verify — transcript hash-chain integrity (tamper-evidence).
643
+ // Distinct from `validate` (structural: parseable lines + session_start):
644
+ // verify recomputes the per-event hash chain and flags edits / deletions /
645
+ // insertions / reordering of chained records.
646
+ session
647
+ .command("verify")
648
+ .description(
649
+ "Verify transcript hash-chain integrity (tamper-evidence); omit id to verify all",
650
+ )
651
+ .argument("[id]", "Session ID to verify (omit to verify every session)")
652
+ .option("--json", "Output as JSON")
653
+ .action(async (id, options) => {
654
+ try {
655
+ const store = await import("../harness/jsonl-session-store.js");
656
+ const result = id ? store.verifySession(id) : store.verifyAllSessions();
657
+
658
+ if (options.json) {
659
+ console.log(JSON.stringify(result, null, 2));
660
+ } else {
661
+ const results = Array.isArray(result) ? result : [result];
662
+ if (results.length === 0) {
663
+ logger.log("No sessions to verify.");
664
+ }
665
+ for (const item of results) {
666
+ const color =
667
+ item.status === "tampered"
668
+ ? chalk.red
669
+ : item.status === "verified"
670
+ ? chalk.green
671
+ : chalk.yellow;
672
+ const counts =
673
+ item.chainedEvents != null
674
+ ? ` (${item.chainedEvents} chained, ${item.legacyEvents} legacy${item.truncatedTail ? ", truncated tail" : ""})`
675
+ : "";
676
+ logger.log(`${color(item.status)} ${item.sessionId}${counts}`);
677
+ if (item.reason) {
678
+ const where = item.firstInvalidLine
679
+ ? ` at line ${item.firstInvalidLine}`
680
+ : "";
681
+ logger.log(` ${chalk.gray(item.reason + where)}`);
682
+ }
683
+ }
684
+ }
685
+
686
+ const anyTampered = (Array.isArray(result) ? result : [result]).some(
687
+ (item) => item.status === "tampered",
688
+ );
689
+ if (anyTampered) process.exit(1);
690
+ } catch (err) {
691
+ logger.error(`Failed: ${err.message}`);
692
+ process.exit(1);
693
+ }
694
+ });
695
+
696
+ // session index — SQLite index over the JSONL store (gap P2#14). Gives fast
697
+ // list/search without scanning every transcript, and can mirror sessions to a
698
+ // configured off-box target (fs/http). The index is a derived cache: safe to
699
+ // delete and rebuild from the JSONL at any time.
700
+ session
701
+ .command("index")
702
+ .description(
703
+ "Build/refresh the SQLite session index; search it, or mirror sessions off-box",
704
+ )
705
+ .argument("[query]", "Search the index for a term (omit to just sync)")
706
+ .option("--rebuild", "Force a full re-index (ignore mtime cache)")
707
+ .option("--stats", "Show index-wide counters")
708
+ .option("--mirror <ids...>", "Push session id(s) to the configured mirror")
709
+ .option(
710
+ "--limit <n>",
711
+ "Max rows for search/list",
712
+ (v) => parseInt(v, 10),
713
+ 20,
714
+ )
715
+ .option("--json", "Output as JSON")
716
+ .action(async (query, options) => {
717
+ let db;
718
+ try {
719
+ const idx = await import("../harness/session-index.js");
720
+ db = idx.openIndex();
721
+ const sync = idx.syncIndex(db, { force: Boolean(options.rebuild) });
722
+
723
+ if (options.mirror && options.mirror.length) {
724
+ const result = await mirrorSessions(options.mirror);
725
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
726
+ for (const r of result.pushed)
727
+ logger.log(chalk.green(` mirrored ${r.id} (${r.bytes} bytes)`));
728
+ for (const r of result.errors) logger.error(` ${r.id}: ${r.error}`);
729
+ if (!result.target)
730
+ logger.log(
731
+ chalk.yellow(
732
+ "No mirror configured — set session.mirror.kind (fs|http) + target in config.",
733
+ ),
734
+ );
735
+ return;
736
+ }
737
+
738
+ if (options.stats) {
739
+ const st = idx.indexStats(db);
740
+ if (options.json) return console.log(JSON.stringify(st, null, 2));
741
+ logger.log(
742
+ `${st.sessions} sessions, ${st.messages} messages, ${st.events} events indexed`,
743
+ );
744
+ return;
745
+ }
746
+
747
+ if (query) {
748
+ const hits = idx.searchSessions(db, query, { limit: options.limit });
749
+ if (options.json) return console.log(JSON.stringify(hits, null, 2));
750
+ if (hits.length === 0) {
751
+ logger.log(chalk.gray(`No sessions match "${query}".`));
752
+ return;
753
+ }
754
+ for (const h of hits) {
755
+ logger.log(
756
+ `${chalk.cyan(h.id)} ${chalk.gray(h.title)} ${chalk.gray(h.updated_at)}`,
757
+ );
758
+ if (h.snippet) logger.log(` …${h.snippet}…`);
759
+ }
760
+ return;
761
+ }
762
+
763
+ if (options.json) return console.log(JSON.stringify(sync, null, 2));
764
+ logger.success(
765
+ `Index synced: ${sync.updated} updated, ${sync.removed} removed, ${sync.total} total.`,
766
+ );
767
+ } catch (err) {
768
+ logger.error(`Failed: ${err.message}`);
769
+ process.exit(1);
770
+ } finally {
771
+ try {
772
+ db?.close();
773
+ } catch {
774
+ /* best-effort */
775
+ }
776
+ }
777
+ });
778
+
597
779
  // session policy — Managed Agents parity Phase E1 approval policy
598
780
  session
599
781
  .command("policy")
@@ -822,14 +1004,106 @@ export function registerSessionCommand(program) {
822
1004
  // ---------------------------------------------------------------
823
1005
  // session usage [id] — Phase I: aggregate token usage
824
1006
  // ---------------------------------------------------------------
1007
+ const USAGE_BY_DIMENSIONS = [
1008
+ "origin",
1009
+ "skill",
1010
+ "subagent",
1011
+ "tool",
1012
+ "mcp",
1013
+ "model",
1014
+ ];
1015
+
1016
+ // Shared `--by` renderer for the per-session and global modes (both result
1017
+ // shapes carry the same `attribution` section). turnTokens is a turn-level
1018
+ // approximation — a turn's tokens count once per distinct tool, so the
1019
+ // column must not be summed across rows.
1020
+ const printUsageBy = (result, by) => {
1021
+ const a = result.attribution || {};
1022
+ const tokRow = (label, r) =>
1023
+ logger.log(
1024
+ ` ${chalk.gray(String(label).padEnd(28))} in=${r.inputTokens} out=${r.outputTokens} total=${chalk.cyan(r.totalTokens.toLocaleString())} calls=${r.calls}`,
1025
+ );
1026
+ const toolRow = (label, r) =>
1027
+ logger.log(
1028
+ ` ${chalk.gray(String(label).padEnd(28))} calls=${r.calls} errors=${r.errors} turnTokens≈${chalk.cyan((r.turnTokens || 0).toLocaleString())}`,
1029
+ );
1030
+ const empty = (what) => logger.log(chalk.gray(` (no ${what} recorded)`));
1031
+
1032
+ switch (by) {
1033
+ case "model": {
1034
+ const rows = result.byModel || [];
1035
+ if (rows.length === 0) return empty("token_usage events");
1036
+ for (const r of rows)
1037
+ tokRow(`${r.provider || "?"}/${r.model || "?"}`, r);
1038
+ return;
1039
+ }
1040
+ case "origin": {
1041
+ const rows = a.byOrigin || [];
1042
+ if (rows.length === 0) return empty("token_usage events");
1043
+ for (const r of rows) tokRow(r.origin, r);
1044
+ return;
1045
+ }
1046
+ case "skill": {
1047
+ const rows = a.bySkill || [];
1048
+ if (rows.length === 0)
1049
+ return empty("skill-attributed usage (isolated skill runs)");
1050
+ for (const r of rows) tokRow(r.skill, r);
1051
+ return;
1052
+ }
1053
+ case "subagent": {
1054
+ const rows = a.bySubagent || [];
1055
+ if (rows.length === 0) return empty("sub-agent-attributed usage");
1056
+ for (const r of rows)
1057
+ tokRow(`${r.subagentId}${r.role ? ` [${r.role}]` : ""}`, r);
1058
+ return;
1059
+ }
1060
+ case "tool": {
1061
+ const rows = a.tools?.byTool || [];
1062
+ if (rows.length === 0) return empty("tool_call events");
1063
+ for (const r of rows) toolRow(r.tool, r);
1064
+ logger.log(
1065
+ chalk.gray(
1066
+ " turnTokens = tokens of turns that used the tool (approximation; do not sum across rows)",
1067
+ ),
1068
+ );
1069
+ return;
1070
+ }
1071
+ case "mcp": {
1072
+ const rows = a.tools?.byMcpServer || [];
1073
+ if (rows.length === 0)
1074
+ return empty("MCP tool calls (mcp__<server>__*)");
1075
+ for (const r of rows) toolRow(r.server, r);
1076
+ logger.log(
1077
+ chalk.gray(
1078
+ " turnTokens = tokens of turns that used the server (approximation; do not sum across rows)",
1079
+ ),
1080
+ );
1081
+ return;
1082
+ }
1083
+ default:
1084
+ break;
1085
+ }
1086
+ };
1087
+
825
1088
  session
826
1089
  .command("usage")
827
1090
  .description("Aggregate token usage (per-session or global)")
828
1091
  .argument("[id]", "Session ID (omit for global rollup)")
829
1092
  .option("--json", "Output as JSON")
1093
+ .option(
1094
+ "--by <dimension>",
1095
+ `Breakdown dimension: ${USAGE_BY_DIMENSIONS.join("|")}`,
1096
+ )
830
1097
  .option("--limit <n>", "Max sessions for global rollup", "1000")
831
1098
  .action(async (id, options) => {
832
1099
  try {
1100
+ const by = options.by ? String(options.by).toLowerCase() : null;
1101
+ if (by && !USAGE_BY_DIMENSIONS.includes(by)) {
1102
+ logger.error(
1103
+ `Invalid --by "${options.by}" — expected one of: ${USAGE_BY_DIMENSIONS.join(", ")}`,
1104
+ );
1105
+ process.exit(1);
1106
+ }
833
1107
  const { sessionUsage, allSessionsUsage } =
834
1108
  await import("../lib/session-usage.js");
835
1109
  const result = id
@@ -841,6 +1115,22 @@ export function registerSessionCommand(program) {
841
1115
  return;
842
1116
  }
843
1117
 
1118
+ if (by) {
1119
+ const t = result.total;
1120
+ logger.log(
1121
+ chalk.bold(
1122
+ id
1123
+ ? `Session ${chalk.gray(id.slice(0, 16))} — by ${by}`
1124
+ : `Global usage — by ${by}`,
1125
+ ),
1126
+ );
1127
+ logger.log(
1128
+ ` total: ${chalk.cyan(t.totalTokens.toLocaleString())} tokens in=${t.inputTokens} out=${t.outputTokens} calls=${t.calls}`,
1129
+ );
1130
+ printUsageBy(result, by);
1131
+ return;
1132
+ }
1133
+
844
1134
  if (id) {
845
1135
  logger.log(chalk.bold(`Session ${chalk.gray(id.slice(0, 16))}`));
846
1136
  const t = result.total;
@@ -1194,6 +1484,42 @@ export function registerSessionCommand(program) {
1194
1484
  });
1195
1485
  }
1196
1486
 
1487
+ /**
1488
+ * Push the given session ids to the configured off-box mirror (gap P2#14).
1489
+ * Mirror config: session.mirror = { kind: "fs"|"http", dir|baseUrl, token? }.
1490
+ * Returns { target, pushed, errors }; target null when no mirror is configured.
1491
+ */
1492
+ async function mirrorSessions(ids) {
1493
+ const { createMirror } = await import("../harness/session-mirror.js");
1494
+ const store = await import("../harness/jsonl-session-store.js");
1495
+ let cfg = {};
1496
+ try {
1497
+ cfg =
1498
+ (await import("../lib/config-manager.js")).loadConfig()?.session
1499
+ ?.mirror || {};
1500
+ } catch {
1501
+ cfg = {};
1502
+ }
1503
+ const mirror = createMirror(cfg);
1504
+ if (!mirror) return { target: null, pushed: [], errors: [] };
1505
+
1506
+ const pushed = [];
1507
+ const errors = [];
1508
+ const { readFileSync, existsSync } = await import("node:fs");
1509
+ for (const id of ids) {
1510
+ try {
1511
+ if (store.isUnsafeSessionId(id)) throw new Error("unsafe session id");
1512
+ const file = store.sessionPath(id);
1513
+ if (!existsSync(file)) throw new Error("session not found");
1514
+ const bytes = readFileSync(file, "utf-8");
1515
+ pushed.push(await mirror.push(id, bytes));
1516
+ } catch (err) {
1517
+ errors.push({ id, error: err.message });
1518
+ }
1519
+ }
1520
+ return { target: mirror.target, pushed, errors };
1521
+ }
1522
+
1197
1523
  // === Iter21 V2 governance overlay ===
1198
1524
  export function registerSesgovV2Commands(program) {
1199
1525
  const parent = program.commands.find((c) => c.name() === "session");
@@ -3,7 +3,7 @@
3
3
  "releases": [
4
4
  {
5
5
  "productVersion": "Unreleased",
6
- "cliVersion": "0.162.158",
6
+ "cliVersion": "0.162.159",
7
7
  "cliVersions": [
8
8
  "0.4.46",
9
9
  "0.4.51",
@@ -16,11 +16,16 @@
16
16
  "0.162.155",
17
17
  "0.162.156",
18
18
  "0.162.157",
19
- "0.162.158"
19
+ "0.162.158",
20
+ "0.162.159"
20
21
  ],
21
22
  "date": null,
22
23
  "title": "",
23
24
  "sections": [
25
+ {
26
+ "heading": "Added — cc CLI 0.162.159:Claude-Code 平价 gap-analysis 14 项收口(cloud handoff + /tui + /voice + /fast + plugin 治理 + session SQLite 索引)(CLI-only npm 发版)",
27
+ "body": "> `chainlesschain` 0.162.158 → **0.162.159** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量;未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。顶层命令数 **173 → 175**(新增 `cc cloud` + `cc routine`;`cc session index` 是 `session` 子命令、`/tui` `/voice` `/fast` 为 REPL-only 均不计数)。对照 `docs/internal/cli-claude-code-gap-analysis-2026-07-11.md` 14 项全落地。\n\n- **`cc cloud`(P1#7)自托管 cloud handoff**:`cc cloud run/status/attach/list`——`git bundle`(base64)当前分支上传到配置的私有 runner(`cloud.baseUrl`/`cloud.token` 或 `CC_CLOUD_URL`/`CC_CLOUD_TOKEN`),runner 完成后 `cc cloud attach` 轮询到终态并 `git apply --3way` 回流 patch、落 plan/artifacts、显示 PR——无 Anthropic 云依赖。本地 job 台账 `~/.chainlesschain/cloud-jobs/`(穿越守卫),不落地的 patch 存旁不失败。\n- **`/tui`(P1#10)全屏无闪烁视图**:`/tui fullscreen` 进备用屏缓冲(`?1049h`,保滚动历史)+ 尾锚定视口窗口化 + diff 重绘(仅改动行光标寻址重写,无整屏清屏→无闪烁);`/tui default` 退回流式滚动;`CC_NO_FLICKER=1` 启动即全屏;屏幕阅读器模式强制 default。持久化 `cli.tuiMode`。\n- **`/voice`(P2#11)语音听写**:`/voice hold|tap|off`(hold=按住说话,tap=切换);严格 local-first STT 优先级(本地 Whisper > 系统 STT > 云,云需 `voice.allowCloud` 显式开);SSH/无 TTY/CI/`CC_NO_VOICE` 环境清晰降级不假装监听;真实采集是主机绑定 DI seam。\n- **`/fast`(P2#12)延迟档**:`/fast on|off|status`——下一轮最小化推理(thinking off)+ 除非用户 `/model` 钉过模型否则换到 provider 的低延迟模型(anthropic→haiku-4-5、openai→4o-mini、deepseek/dashscope/mistral/gemini),始终显示成本/质量权衡。持久化 `cli.fastMode`。\n- **插件治理(P2#13)三项**:manifest `dependencies`(插件名→semver 区间 + 保留 `host`/`cc` 约束 CLI 版本)经 `cc plugin validate` 校验缺失/越界依赖;LSP 注册表改**每 languageId 有序候选表**——一个插件 LSP server 二进制缺失不再遮蔽 builtin(回退到下一候选/内建,扩展名不再\"变黑\");`isPluginInUse` 把 LSP/MCP/hooks/monitors 被动组件视为 in-use,不再被\"未调用即弃用\"误判。\n- **`cc session index`(P2#14)外部 session 存储**:JSONL 仍是权威源,新增 SQLite 索引(`~/.chainlesschain/session-index.db`)——增量按 mtime+size 同步 + 内容/标题搜索(LIKE 转义通配符)+ stats + `--rebuild`,取代逐文件解析(1072 真 session 验过);可插拔 off-box mirror(`fs`/`http` 驱动,S3/WebDAV/Redis 同接口待配),`session.mirror.{kind,dir|baseUrl,token}` 配置,加密/访问控制由驱动独立负责。\n- **P0/P1 早前提交同随本版发出**:`cc bg view` 交互式 agent 面板(P0#1)+ background supervisor 稳定性加固(P0#2)+ `/doctor` 可修复 checkup + `/checkup` 别名(P0#3)+ transcript hash-chain 防篡改 `cc session verify`(P0#4)+ Inbound Channels(webhook/telegram allowlist,P0#5)+ computer-use MCP 显式开启(P1#6)+ Routines(cron/once/webhook/github,P1#8)+ PR/session linking(P1#9)。"
28
+ },
24
29
  {
25
30
  "heading": "Added — cc CLI 0.162.158:browser_state agent 工具 + REPL /remote-control + web-panel 直连配对/审批卡片 + 交付物预览下载(CLI-only npm 发版)",
26
31
  "body": "> `chainlesschain` 0.162.157 → **0.162.158** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` + web-panel dist 随包;未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。命令数不变(**173**);AGENT_TOOLS **24 → 25**(新增 `browser_state`)。\n\n- **`browser_state` 一等 agent 工具(IDE gap P1 #8 收口)**:Chrome CDP 连接器(`cc browse chrome`)暴露为第 25 个 agent 工具——URL/标题/标签页 + 观察窗口 console/失败网络请求 + DOM 快照(agent 面 40k cap,CLI 命令面 150k)+ 可选截图(**生成的**临时路径,防 agent 自选路径经读工具覆盖任意文件);LOW/READ/plan 模式可用,纯观察不点击不输入不导航,仅 loopback CDP。\n- **REPL `/remote-control`(别名 `/rc`)on|off|status(批 26)**:交互式会话内起自托管 WS + RemoteApprovalBridge 配对(URI + 可选二维码),TAB 补全 + /help;`cc agent --remote-control` 对交互式会话同样生效(启动失败降级 local-only 不中断会话)。**交互式远程审批赛跑语义**(新 `src/repl/remote-approval.js`):两个交互 confirmer(ApprovalGate + settings/hook ask)把终端 prompt 与已配对设备赛跑,先答先赢;与 headless 刻意不同——远端超时/断开**不是决定**(静默的手机永不代键盘用户 auto-deny),设备先答则取消本地 readline 并压掉其搁浅的 idle-timeout。\n- **web-panel 直连(direct-LAN)配对 + `permission.request` 审批卡片(批 28)**:remoteSession store 自动识别双 URI 方案——relay E2EE 路径不变,新增 `chainlesschain://remote-control/pair#` 直连 transport(browser 版 parseDirectPairingUri + 客户端过期检查);直连控制走 remote-session-publish **顶层 commandId+seq**(applyControlIdempotent direct-client 契约);一次性 pairing token → 直连断开**刻意不自动重连**(重 join 必失败,诚实 disconnected 态 + 重配对提示);两种 transport 喂同一 pendingApprovals 卡片(requestId 去重 + permission.resolved 任意裁决方清除 + 首等公民审批卡列表批准/拒绝)。\n- **交付物 Markdown 预览 + 浏览器下载(批 25)**:面板 text/markdown 预览走既有 MarkdownRenderer(marked+DOMPurify+hljs,零新依赖);新 `GET /api/artifacts/<id>/download` 流式下载完整存档副本(WS 预览仍 cap)——Content-Disposition(RFC 5987)+ no-store/nosniff + 配置 wsToken 时同 WS 强度 token 门(sha256+timingSafeEqual,Bearer 头不进 URL)+ basename 防篡改守卫。\n- **安全修复 ×2**:① **直连配对信任边界**——直连(LAN)配对无密码学主机身份(区别 relay E2EE),钓鱼配对 URI 可把面板指向任意公网 ws:// 端点导走 serverToken/prompt/审批决定;两端 parser(web-panel + CLI 孪生)现拒绝指向非私网主机的明文 ws://(仅 loopback/RFC-1918/link-local/ULA,即 pickLanAddress 可能产出的全集;wss:// 不限),IPv6 括号 + IPv4-mapped 点分/hex 双形态识别,RED 反证过。② **chrome-connector launch-arg 加固**——launch URL 曾逐字进 Chrome argv(`--` 开头值被 Chrome 当开关解析=潜在命令执行面),现限 http(s)://、about:(URL parse 校验字节原样透传)+ CDP 端口处处整型边界校验。\n- **健壮性**:web-panel 审批卡发送失败恢复——approve() 乐观清除后若答复未离开本端(直连 publish 拒绝/socket 断/relay 未配对)恢复卡片并浮错,可重新裁决;失败窗口内别设备已 resolved 则抑制恢复(无僵尸卡)。RED 反证 + web-panel 全套 2512 绿。\n- **测试稳健性(仓库面,不进 npm 包)**:checkpoint-store `vi.setConfig 30s` + web-panel doctor 90s 两处负载敏感预算加固(trap #31 形态);ai-doc-creator 集成测试改 chdir tmpDir,不再向仓库根泄漏 12 个产物文件。"
@@ -14,6 +14,12 @@ import {
14
14
  import { join, basename, resolve } from "node:path";
15
15
  import { createHash } from "node:crypto";
16
16
  import { getHomeDir } from "../lib/paths.js";
17
+ import {
18
+ computeEventHash,
19
+ latestChainHash,
20
+ verifyTranscriptText,
21
+ TRANSCRIPT_CHAIN_STATUS,
22
+ } from "./transcript-integrity.js";
17
23
 
18
24
  function getSessionsDir() {
19
25
  const dir = join(getHomeDir(), "sessions");
@@ -54,11 +60,66 @@ export function appendTokenUsage(sessionId, usage) {
54
60
  appendEvent(sessionId, "token_usage", usage || {});
55
61
  }
56
62
 
63
+ // Chain-tail cache: hash of the last chained record per session, so appends
64
+ // stay O(1) instead of re-reading the file. Keyed per process — two processes
65
+ // appending the SAME session concurrently could fork the chain (verify would
66
+ // then flag it), but the store has always assumed a single writer per session.
67
+ const _chainTailCache = new Map();
68
+
69
+ function _resolveChainTail(sessionId, filePath) {
70
+ if (!existsSync(filePath)) {
71
+ // Missing file self-heals a stale cache entry (file deleted externally,
72
+ // or a test tearing down the sessions dir between cases).
73
+ _chainTailCache.set(sessionId, null);
74
+ return null;
75
+ }
76
+ const cached = _chainTailCache.get(sessionId);
77
+ if (cached !== undefined) return cached;
78
+ const tail = latestChainHash(readFileSync(filePath, "utf-8"));
79
+ _chainTailCache.set(sessionId, tail);
80
+ return tail;
81
+ }
82
+
57
83
  export function appendEvent(sessionId, type, data) {
58
- const line = JSON.stringify({ type, timestamp: Date.now(), data }) + "\n";
59
- appendFileSync(sessionPath(sessionId), line, "utf-8");
84
+ const filePath = sessionPath(sessionId);
85
+ const prevHash = _resolveChainTail(sessionId, filePath);
86
+ const core = { type, timestamp: Date.now(), data };
87
+ const hash = computeEventHash(prevHash, core);
88
+ const line = JSON.stringify({ ...core, prevHash, hash }) + "\n";
89
+ appendFileSync(filePath, line, "utf-8");
90
+ _chainTailCache.set(sessionId, hash);
91
+ }
92
+
93
+ /**
94
+ * Verify a session transcript's hash chain (tamper-evidence).
95
+ * Statuses: verified | partial (legacy prefix + valid chain) | legacy
96
+ * (pre-chaining transcript) | tampered | empty — plus not-found / invalid-id.
97
+ */
98
+ export function verifySession(sessionId) {
99
+ if (isUnsafeSessionId(sessionId)) {
100
+ return { sessionId, status: "invalid-id", reason: "invalid session id" };
101
+ }
102
+ const filePath = sessionPath(sessionId);
103
+ if (!existsSync(filePath)) {
104
+ return { sessionId, status: "not-found", reason: "session file not found" };
105
+ }
106
+ return {
107
+ sessionId,
108
+ ...verifyTranscriptText(readFileSync(filePath, "utf-8")),
109
+ };
60
110
  }
61
111
 
112
+ export function verifyAllSessions(options = {}) {
113
+ const dir = getSessionsDir();
114
+ if (!existsSync(dir)) return [];
115
+ return readdirSync(dir)
116
+ .filter((file) => file.endsWith(".jsonl"))
117
+ .slice(0, options.limit || 1000)
118
+ .map((file) => verifySession(basename(file, ".jsonl")));
119
+ }
120
+
121
+ export { TRANSCRIPT_CHAIN_STATUS };
122
+
62
123
  export function startSession(sessionId, meta = {}) {
63
124
  const id =
64
125
  sessionId ||
@@ -85,6 +146,25 @@ export function appendToolCall(sessionId, toolName, args) {
85
146
  appendEvent(sessionId, "tool_call", { tool: toolName, args });
86
147
  }
87
148
 
149
+ /**
150
+ * Compact tool-call record for usage attribution (用量归因): tool name +
151
+ * error flag (+ an optional skill hint for `run_skill`) — deliberately NOT
152
+ * the args, which can carry whole file bodies (write_file content) and would
153
+ * bloat the transcript. Written at tool-result time by the agent drivers so
154
+ * `cc session usage --by tool|mcp` and `cc insights` can aggregate tool use
155
+ * for any persisted session.
156
+ */
157
+ export function appendToolCallCompact(
158
+ sessionId,
159
+ { tool, isError, skill } = {},
160
+ ) {
161
+ appendEvent(sessionId, "tool_call", {
162
+ tool: tool || "?",
163
+ is_error: Boolean(isError),
164
+ ...(skill ? { skill: String(skill) } : {}),
165
+ });
166
+ }
167
+
88
168
  export function appendToolResult(sessionId, toolName, result) {
89
169
  appendEvent(sessionId, "tool_result", { tool: toolName, result });
90
170
  }