chainlesschain 0.162.157 → 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 (212) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-ByIiF5bT.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-NlgSFZq5.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-D17cOUcb.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-B4mEZcbl.js} +5 -5
  8. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  9. package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
  10. package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DYAia2e2.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-B6agK6bA.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DM43R2vy.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-Dtdp3eXp.js} +6 -6
  15. package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-D2xf7uZt.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-D3j45jJL.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-voPJ9fUZ.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-DEsyTESF.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-OHV_QXLD.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-eDTJWC-J.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-BBvy-6d1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-Dw2eoJxu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-DQHms_Bh.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-Cyu7Id1c.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-sIDzkG92.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-DUW3T2ey.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-CECELXW8.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-BEZIQX84.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-BcdUMznL.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-63LhjBx2.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYtQtIcH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-BwR6UUox.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-C4q5h7Ut.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
  40. package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +1 -0
  41. package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-Plksliuw.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DZUFfeuG.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-NoDYt29V.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CEXGTkdh.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-DnbJkbZo.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-DTAa0cwy.js} +5 -5
  47. package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CVV7LnNS.js} +4 -4
  48. package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-BqqGrjt_.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-C4WP8DRp.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-Zo4DJUVI.js} +3 -3
  51. package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DBcjRaz2.js} +1 -1
  52. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  53. package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BT9WPBOX.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-D6b4bZ3q.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-Cw3OsYx-.js} +2 -2
  56. package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C0nyiK2b.js} +4 -4
  57. package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-zVBZEB_C.js} +3 -3
  58. package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-BOspsFew.js} +2 -2
  59. package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-CQWfZ4XH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-C2oGJ_WL.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-BZMR4qY1.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DAujoP63.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-DvH_P2Uq.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-D_RGHM-v.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-5D8URoI_.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-Df4kC8Gy.js} +1 -1
  68. package/src/assets/web-panel/assets/RemoteSession-DGi3f3vO.js +4 -0
  69. package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-jmkGFNWb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-DP6Pcu3m.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-D60hSuPO.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-0mf1BQUK.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-CfZtLFYV.js} +3 -3
  74. package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-D9zpZLWz.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-D1_44eMD.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-cVFa8zkz.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-Cinpzc3L.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-bqdiJE_P.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Dgl_SLgj.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-DawWAEhy.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-Ca0nY651.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-CgaGEhLm.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-D-9EnpRx.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-BpwLZpL3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DgowwBK5.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-BZutapjE.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-QZPt8_Sp.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DlQx5UDJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-ZfdTgtWA.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-CAp0y_Cr.js} +5 -5
  92. package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-BVITZEmr.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-CC2tPaNg.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-31brc9CA.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DLMkC9C-.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-BMtveI6F.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-DbD6WHW2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-3oBsXZmF.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-ALvoNTRF.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-AaIKG60c.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BFRLmCR3.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-BKAB6-6V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BOM-VFlv.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-BPpS-lGb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-Bbc2JeQG.js} +1 -1
  107. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  108. package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-BiRQUoyE.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-BnFLzejT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-BoO4WEJB.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-C-cGlCDW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-C1PfeI88.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-C21pdBqs.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-C7LHNw_N.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-CEXa-4zd.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-CEwjBA9q.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-CFXKbOek.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-CJhREOlG.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-COHHQ9cU.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-CUmLMFsy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-CWgfipcC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-C_eUKGYP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CcVszn2b.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-Cod97Urs.js} +3 -3
  125. package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-Cwbqh0KZ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-D67kIagl.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-D7cV0tu0.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-DJvxFyIz.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-DZl-UXB6.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-Dm0byjm1.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DndNnYc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-WQUO2vvK.js} +1 -1
  133. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  134. package/src/assets/web-panel/assets/{index-CExWtm98.js → index-bdaJwTXJ.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-ep6JchNG.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-jG6h22Qg.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-ntVQqSML.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-D5UoBRhN.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-DltOw0CR.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-_oPkk5zj.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-EES8ROCS.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-B-_39x_M.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-l6D0yJHz.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-DIeUK31L.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-dG4EzUQo.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-DWFzYGdH.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-CECdTA5H.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-D6kP-mMK.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-Bb-j6rzo.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-DNpwzFTM.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-vNLpFQBW.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-manifest.json +17 -3
  156. package/src/commands/agent.js +11 -1
  157. package/src/commands/background-session.js +143 -3
  158. package/src/commands/browse.js +86 -0
  159. package/src/commands/cloud.js +232 -0
  160. package/src/commands/doctor.js +61 -6
  161. package/src/commands/mcp.js +61 -0
  162. package/src/commands/plugin.js +63 -1
  163. package/src/commands/routine.js +367 -0
  164. package/src/commands/session.js +326 -0
  165. package/src/data/changelog.json +12 -2
  166. package/src/harness/jsonl-session-store.js +82 -2
  167. package/src/harness/session-index.js +295 -0
  168. package/src/harness/session-mirror.js +142 -0
  169. package/src/harness/transcript-integrity.js +182 -0
  170. package/src/index.js +8 -0
  171. package/src/lib/background-agent-supervisor.js +126 -2
  172. package/src/lib/channels/channel-manager.js +126 -0
  173. package/src/lib/channels/telegram-channel.js +112 -0
  174. package/src/lib/channels/webhook-channel.js +140 -0
  175. package/src/lib/chrome-connector.js +425 -4
  176. package/src/lib/cloud/bundle.js +110 -0
  177. package/src/lib/cloud/cloud-client.js +152 -0
  178. package/src/lib/computer-use/authorization.js +173 -0
  179. package/src/lib/computer-use/computer-use-server.js +276 -0
  180. package/src/lib/computer-use/control-backend.js +150 -0
  181. package/src/lib/doctor-checkup.js +652 -0
  182. package/src/lib/jsonl-session-store.js +4 -0
  183. package/src/lib/lsp/lsp-server-registry.js +48 -21
  184. package/src/lib/plugin-runtime/governance.js +174 -0
  185. package/src/lib/pr-link-ledger.js +221 -0
  186. package/src/lib/remote-control.js +35 -0
  187. package/src/lib/routine-store.js +352 -0
  188. package/src/lib/session-store-guard.js +40 -0
  189. package/src/lib/session-usage.js +318 -2
  190. package/src/lib/sub-agent-context.js +23 -0
  191. package/src/lib/web-ui-server.js +120 -7
  192. package/src/repl/agent-repl.js +592 -61
  193. package/src/repl/bg-dashboard.js +355 -0
  194. package/src/repl/fast-mode.js +103 -0
  195. package/src/repl/fullscreen-tui.js +197 -0
  196. package/src/repl/remote-approval.js +111 -0
  197. package/src/repl/slash-command-registry.js +11 -3
  198. package/src/repl/voice-dictation.js +132 -0
  199. package/src/runtime/agent-core.js +278 -1
  200. package/src/runtime/coding-agent-contract-shared.cjs +141 -0
  201. package/src/runtime/coding-agent-policy.cjs +33 -0
  202. package/src/runtime/headless-runner.js +71 -7
  203. package/src/runtime/headless-stream.js +14 -8
  204. package/src/runtime/policies/agent-policy.js +6 -0
  205. package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
  206. package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
  207. package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
  208. package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
  209. package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
  210. package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
  211. package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
  212. package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * REPL-side remote-approval glue (gap-analysis 批26 — 批17/18 REPL 遗留收口).
3
+ *
4
+ * Interactive sessions race the LOCAL terminal prompt against a paired
5
+ * device's decision (headless gates on the remote decision alone):
6
+ *
7
+ * - local answer first → settle the device card too (resolveLocally
8
+ * publishes permission.resolved so the phone/web UI clears it)
9
+ * - remote answer first → cancel the local readline prompt and print
10
+ * the outcome
11
+ * - remote leg times out or the bridge closes mid-ask → NOT a decision:
12
+ * keep waiting on the terminal. A silent phone must never auto-deny a
13
+ * user sitting at the keyboard — the fail-closed timeout is headless
14
+ * semantics only.
15
+ *
16
+ * Pure over an injected bridge + local prompt handle ({promise, cancel}),
17
+ * so the race is unit-testable without a WS server or readline.
18
+ */
19
+
20
+ /**
21
+ * Normalize the shapes the REPL's two confirmers receive (ApprovalGate
22
+ * `{tool, args, riskLevel}` / settings-hook `{tool, args, rule, reason}`)
23
+ * into the bridge's `{tool, action, detail}` ask payload.
24
+ */
25
+ export function describeAskContext({
26
+ tool,
27
+ args,
28
+ rule = null,
29
+ reason = null,
30
+ riskLevel = null,
31
+ } = {}) {
32
+ const detail =
33
+ typeof args?.command === "string"
34
+ ? args.command
35
+ : typeof args?.path === "string"
36
+ ? args.path
37
+ : args
38
+ ? JSON.stringify(args).slice(0, 2000)
39
+ : null;
40
+ return {
41
+ tool: tool || null,
42
+ action: rule
43
+ ? `ask-rule:${rule}`
44
+ : reason || (riskLevel ? `${riskLevel}-risk` : null),
45
+ detail,
46
+ };
47
+ }
48
+
49
+ // Remote outcomes that are NOT a decision for an interactive session: the
50
+ // devices went silent (timeout) or the bridge was torn down mid-ask (closed).
51
+ const NON_DECISIVE_REMOTE = new Set(["timeout", "closed"]);
52
+
53
+ /**
54
+ * Race the local interactive prompt against the paired-device decision.
55
+ *
56
+ * @param {object} opts
57
+ * @param {import("../lib/remote-approval-bridge.js").RemoteApprovalBridge} opts.bridge
58
+ * @param {{tool?, action?, detail?}} opts.ask - describeAskContext() payload
59
+ * @param {{promise: Promise<boolean>, cancel?: () => void}} opts.local -
60
+ * the already-started local prompt; `cancel` closes its readline when the
61
+ * device answers first (implementations must mute their idle-timeout
62
+ * message after cancel).
63
+ * @param {(text: string) => void} [opts.writeOut]
64
+ * @returns {Promise<boolean>} approved
65
+ */
66
+ export async function raceLocalAndRemote({
67
+ bridge,
68
+ ask = {},
69
+ local,
70
+ writeOut = () => {},
71
+ }) {
72
+ let requestId = null;
73
+ const remoteLeg = bridge
74
+ .requestDecision({
75
+ ...ask,
76
+ onRequestId: (id) => {
77
+ requestId = id;
78
+ },
79
+ })
80
+ .then((decision) => ({ src: "remote", decision }));
81
+ const localLeg = local.promise.then(
82
+ (approved) => ({ src: "local", approved: approved === true }),
83
+ () => ({ src: "local", approved: false }),
84
+ );
85
+ let winner = await Promise.race([remoteLeg, localLeg]);
86
+ if (
87
+ winner.src === "remote" &&
88
+ NON_DECISIVE_REMOTE.has(winner.decision?.via)
89
+ ) {
90
+ winner = await localLeg;
91
+ }
92
+ if (winner.src === "local") {
93
+ if (requestId) {
94
+ try {
95
+ bridge.resolveLocally(requestId, winner.approved);
96
+ } catch {
97
+ // clearing the device card is best-effort
98
+ }
99
+ }
100
+ return winner.approved;
101
+ }
102
+ try {
103
+ local.cancel?.();
104
+ } catch {
105
+ // readline already closed
106
+ }
107
+ writeOut(
108
+ `\n ✓ ${winner.decision.approved ? "allowed" : "denied"} from paired device\n`,
109
+ );
110
+ return winner.decision.approved === true;
111
+ }
@@ -35,7 +35,13 @@ export class SlashCommandRegistry {
35
35
  }
36
36
 
37
37
  getCommand(name) {
38
- return this._commands.get(name);
38
+ const direct = this._commands.get(name);
39
+ if (direct) return direct;
40
+ // Alias resolution: a command registered with aliases answers to them too.
41
+ for (const cmd of this._commands.values()) {
42
+ if (cmd.aliases && cmd.aliases.includes(name)) return cmd;
43
+ }
44
+ return undefined;
39
45
  }
40
46
 
41
47
  getAllCommands() {
@@ -43,7 +49,7 @@ export class SlashCommandRegistry {
43
49
  }
44
50
 
45
51
  hasCommand(name) {
46
- return this._commands.has(name);
52
+ return Boolean(this.getCommand(name));
47
53
  }
48
54
 
49
55
  _registerBuiltinCommands() {
@@ -188,8 +194,10 @@ export class SlashCommandRegistry {
188
194
  ],
189
195
  ];
190
196
 
197
+ // Claude-Code 2.1.205 parity: /checkup is an alias of /doctor.
198
+ const ALIASES = { "/doctor": ["/checkup"] };
191
199
  for (const [name, description, handler] of commands) {
192
- this.register(name, { description, handler });
200
+ this.register(name, { description, handler, aliases: ALIASES[name] });
193
201
  }
194
202
  }
195
203
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Voice dictation for the agent REPL (gap-2026-07-11 P2#11).
3
+ *
4
+ * Claude Code's `/voice` dictates a prompt by speech. cc generalizes it to a
5
+ * pluggable STT backend with a strict local-first priority so nothing leaves
6
+ * the box unless the user opts into cloud transcription:
7
+ *
8
+ * local Whisper > system STT (OS dictation) > cloud (opt-in)
9
+ *
10
+ * Two capture modes mirror the upstream UX:
11
+ * - hold : push-to-talk — record while a key is held, transcribe on release.
12
+ * - tap : toggle — tap to start, tap again to stop.
13
+ *
14
+ * Real audio capture is a host binding (a Whisper binary, an OS dictation API);
15
+ * it is injected, so on a host with no backend — or a headless/SSH session where
16
+ * there is no microphone — `/voice` degrades to a clear message instead of
17
+ * pretending to listen. This module is the pure decision core (mode/command
18
+ * parse, environment degradation, backend priority, status render).
19
+ */
20
+
21
+ export const VOICE_MODES = Object.freeze(["off", "hold", "tap"]);
22
+ export const DEFAULT_VOICE_MODE = "off";
23
+
24
+ /**
25
+ * STT backends in priority order. `local` never sends audio off the box;
26
+ * `cloud` requires explicit opt-in (config voice.allowCloud). Each entry names
27
+ * the capability probe the host binding fills in.
28
+ */
29
+ export const STT_BACKENDS = Object.freeze([
30
+ { id: "whisper-local", kind: "local", label: "local Whisper" },
31
+ { id: "system-stt", kind: "system", label: "system STT" },
32
+ { id: "cloud", kind: "cloud", label: "cloud transcription" },
33
+ ]);
34
+
35
+ /**
36
+ * Parse `/voice [hold|tap|off|status]`. Returns null for non-/voice input,
37
+ * `{ error }` for a bad arg, else `{ action }`. Bare `/voice` shows status.
38
+ * Pure.
39
+ */
40
+ export function parseVoiceCommand(input) {
41
+ const t = String(input || "").trim();
42
+ if (t !== "/voice" && !t.startsWith("/voice ")) return null;
43
+ const arg = t.slice("/voice".length).trim().toLowerCase();
44
+ if (!arg || arg === "status") return { action: "status" };
45
+ if (arg === "hold" || arg === "ptt" || arg === "push")
46
+ return { action: "hold" };
47
+ if (arg === "tap" || arg === "toggle") return { action: "tap" };
48
+ if (arg === "off" || arg === "stop" || arg === "0") return { action: "off" };
49
+ return {
50
+ error: `Unknown /voice option "${arg}". Use: hold | tap | off | status`,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Decide whether voice capture is usable in this environment. Headless / SSH /
56
+ * CI sessions have no local microphone, so dictation degrades. Pure over the
57
+ * passed env + tty flags.
58
+ * @returns {{ supported, reason }}
59
+ */
60
+ export function detectVoiceEnvironment({ env = {}, isTTY = true } = {}) {
61
+ if (!isTTY) {
62
+ return {
63
+ supported: false,
64
+ reason: "not a TTY — no interactive microphone",
65
+ };
66
+ }
67
+ if (env.SSH_TTY || env.SSH_CONNECTION) {
68
+ return {
69
+ supported: false,
70
+ reason:
71
+ "remote SSH session — capture audio locally and paste, or use a cloud STT relay",
72
+ };
73
+ }
74
+ if (isTruthy(env.CI)) {
75
+ return { supported: false, reason: "CI environment — no microphone" };
76
+ }
77
+ if (isTruthy(env.CC_NO_VOICE)) {
78
+ return { supported: false, reason: "disabled via CC_NO_VOICE" };
79
+ }
80
+ return { supported: true, reason: "" };
81
+ }
82
+
83
+ /**
84
+ * Pick the highest-priority available STT backend. `available` is a map of
85
+ * backend id → boolean from the host capability probe. Cloud is only eligible
86
+ * when `allowCloud` is true (config voice.allowCloud). Returns
87
+ * `{ backend, reason }` or `{ backend: null, reason }`. Pure.
88
+ */
89
+ export function resolveSttBackend({
90
+ available = {},
91
+ allowCloud = false,
92
+ prefer = null,
93
+ } = {}) {
94
+ const eligible = STT_BACKENDS.filter((b) => {
95
+ if (!available[b.id]) return false;
96
+ if (b.kind === "cloud" && !allowCloud) return false;
97
+ return true;
98
+ });
99
+ if (eligible.length === 0) {
100
+ return {
101
+ backend: null,
102
+ reason:
103
+ "no STT backend available — install a local Whisper build or enable system dictation (voice.allowCloud for cloud)",
104
+ };
105
+ }
106
+ // Honor an explicit preference if it's eligible; else take the top priority.
107
+ const chosen =
108
+ (prefer && eligible.find((b) => b.id === prefer)) || eligible[0];
109
+ return { backend: chosen, reason: `using ${chosen.label}` };
110
+ }
111
+
112
+ /** One-line `/voice status`. Pure. */
113
+ export function renderVoiceStatus({ mode = "off", backend = null, env } = {}) {
114
+ const m = VOICE_MODES.includes(mode) ? mode : "off";
115
+ if (env && !env.supported) {
116
+ return `Voice: unavailable — ${env.reason}.`;
117
+ }
118
+ if (m === "off") {
119
+ return "Voice: off. /voice hold (push-to-talk) or /voice tap (toggle).";
120
+ }
121
+ const be = backend ? backend.label : "no backend";
122
+ const how =
123
+ m === "hold" ? "hold the dictation key to talk" : "tap to start/stop";
124
+ return `Voice: ${m} — ${how}; ${be}.`;
125
+ }
126
+
127
+ function isTruthy(v) {
128
+ const n = String(v == null ? "" : v)
129
+ .toLowerCase()
130
+ .trim();
131
+ return n === "1" || n === "true" || n === "yes" || n === "on";
132
+ }
@@ -1291,6 +1291,38 @@ export async function executeTool(name, args, context = {}) {
1291
1291
  }
1292
1292
  }
1293
1293
 
1294
+ // Session-store write guard (transcript tamper protection): JSONL
1295
+ // transcripts under ~/.chainlesschain/sessions are hash-chained and treated
1296
+ // as an audit surface — an agent editing a transcript rewrites the history
1297
+ // that `cc session verify` and resume trust. Same posture as the
1298
+ // sensitive-file guard: confirm-first, an explicit settings `allow` rule is
1299
+ // the only bypass, headless without a confirmer fails closed.
1300
+ if (
1301
+ (name === "write_file" ||
1302
+ name === "edit_file" ||
1303
+ name === "notebook_edit") &&
1304
+ settingsVerdict.decision !== "allow" &&
1305
+ args?.path
1306
+ ) {
1307
+ const { sessionStorePathReason } =
1308
+ await import("../lib/session-store-guard.js");
1309
+ const storeReason = sessionStorePathReason(args.path, { cwd });
1310
+ if (storeReason) {
1311
+ const ok = await requestInteractivePermission(name, args, context, cwd, {
1312
+ tool: name,
1313
+ args,
1314
+ rule: null,
1315
+ reason: `session store: ${storeReason}`,
1316
+ });
1317
+ if (!ok) {
1318
+ return {
1319
+ error: `[Session Store] Writing "${args.path}" would modify a session transcript (${storeReason}) — denied. Add a settings allow rule to pre-authorize.`,
1320
+ policy: { decision: "ask", via: "session-store-guard" },
1321
+ };
1322
+ }
1323
+ }
1324
+ }
1325
+
1294
1326
  // Destructive-git guard (Claude-Code 2.1.183 parity: "destructive git
1295
1327
  // commands blocked when unintended"). The `git` tool otherwise runs any
1296
1328
  // command unguarded in auto mode — including `reset --hard`, `clean -fd`,
@@ -1438,6 +1470,14 @@ export async function executeTool(name, args, context = {}) {
1438
1470
  parentMessages: context.parentMessages,
1439
1471
  interaction: context.interaction,
1440
1472
  sessionId: context.sessionId || null,
1473
+ // Parent LLM config — documented at toolContext as forwarded to
1474
+ // spawn_sub_agent for provider/key inheritance; without this line it
1475
+ // never actually reached executeToolInner (sub-agents silently fell
1476
+ // back to config defaults and their usage was mis-attributed).
1477
+ llmOptions: context.llmOptions || null,
1478
+ // 用量归因: shared per-run sink for child-loop (sub-agent / isolated
1479
+ // skill) token usage, drained by agentLoop as attributed events.
1480
+ subAgentUsageSink: context.subAgentUsageSink || null,
1441
1481
  hostManagedToolPolicy: context.hostManagedToolPolicy || null,
1442
1482
  externalToolDescriptors: context.externalToolDescriptors || null,
1443
1483
  externalToolExecutors: context.externalToolExecutors || null,
@@ -2074,6 +2114,7 @@ async function executeToolInner(
2074
2114
  settingsHooks = null,
2075
2115
  signal = null,
2076
2116
  backgroundSubAgents = null,
2117
+ subAgentUsageSink = null,
2077
2118
  },
2078
2119
  ) {
2079
2120
  const localToolDescriptor =
@@ -2706,6 +2747,21 @@ async function executeToolInner(
2706
2747
  }
2707
2748
  output = res.stdout ?? "";
2708
2749
  }
2750
+ // PR/session linking (gap-2026-07-11 P1#9): a successful `gh pr …`
2751
+ // (or `git push`) ties this session to the PR it touched. Async and
2752
+ // best-effort — never delays or fails the tool result.
2753
+ if (sessionId) {
2754
+ import("../lib/pr-link-ledger.js")
2755
+ .then((m) =>
2756
+ m.recordFromShellCommand({
2757
+ sessionId,
2758
+ command: args.command,
2759
+ output: String(output || ""),
2760
+ cwd: args.cwd || cwd,
2761
+ }),
2762
+ )
2763
+ .catch(() => {});
2764
+ }
2709
2765
  return attachDescriptor(
2710
2766
  {
2711
2767
  stdout: output.substring(0, 30000),
@@ -2883,6 +2939,7 @@ async function executeToolInner(
2883
2939
  settingsHooks,
2884
2940
  signal,
2885
2941
  backgroundSubAgents,
2942
+ subAgentUsageSink,
2886
2943
  }),
2887
2944
  );
2888
2945
  }
@@ -3187,6 +3244,111 @@ async function executeToolInner(
3187
3244
  }
3188
3245
  }
3189
3246
 
3247
+ case "browser_state": {
3248
+ // P1 #8: first-class Chrome CDP observation — the agent side of the
3249
+ // "locate error → fix → verify" web loop against the user's real,
3250
+ // logged-in browser (`cc browse chrome launch`). DOM is capped harder
3251
+ // than the CLI command's 150k default so a page dump cannot flood the
3252
+ // conversation; screenshots go to a generated temp file (never an
3253
+ // agent-chosen path) to keep the tool strictly read-only.
3254
+ try {
3255
+ const { captureState } = await import("../lib/chrome-connector.js");
3256
+ const screenshotPath = args.screenshot
3257
+ ? path.join(os.tmpdir(), `cc-browser-state-${Date.now()}.png`)
3258
+ : null;
3259
+ const state = await captureState({
3260
+ port: args.port != null ? Number(args.port) : undefined,
3261
+ tab: args.tab != null ? Number(args.tab) : undefined,
3262
+ reload: args.reload === true,
3263
+ watchMs: args.watch_ms != null ? Number(args.watch_ms) : undefined,
3264
+ includeDom: args.include_dom !== false,
3265
+ domCap: args.dom_cap != null ? Number(args.dom_cap) : 40000,
3266
+ screenshotPath,
3267
+ });
3268
+ if (!state.ok) {
3269
+ return attachDescriptor({
3270
+ error: `browser_state failed: ${state.error}`,
3271
+ });
3272
+ }
3273
+ return attachDescriptor(state);
3274
+ } catch (err) {
3275
+ return attachDescriptor({
3276
+ error: `browser_state failed: ${err.message}`,
3277
+ });
3278
+ }
3279
+ }
3280
+
3281
+ case "browser_act": {
3282
+ // Gap-analysis #6: the ACTION side of the Chrome connector.
3283
+ // browser_state stays the read-only default; browser_act explicitly
3284
+ // drives the user's logged-in browser (click/type/press/navigate/
3285
+ // waitForSelector/screenshot/assertText) and is gated like run_code:
3286
+ // HIGH risk through the ApprovalGate (CONFIRM even on the trusted/auto
3287
+ // tier), pre-authorizable only by an explicit settings allow rule
3288
+ // (ruleAllowed). Unlike run_code there is no legacy ungated behavior to
3289
+ // preserve, so the gate applies whenever an ApprovalGate is wired —
3290
+ // headless without a confirmer fails closed. Screenshot paths are
3291
+ // generated inside performActions (never agent-chosen), and each
3292
+ // executed step is audit-logged to ~/.chainlesschain/browser-actions/.
3293
+ try {
3294
+ if (
3295
+ approvalGate &&
3296
+ !ruleAllowed &&
3297
+ typeof approvalGate.decide === "function"
3298
+ ) {
3299
+ const { APPROVAL_RISK, APPROVAL_DECISION } =
3300
+ await import("@chainlesschain/session-core");
3301
+ const summary = Array.isArray(args.actions)
3302
+ ? args.actions
3303
+ .map((a) => a?.type)
3304
+ .filter(Boolean)
3305
+ .slice(0, 10)
3306
+ .join(",")
3307
+ : "";
3308
+ const gate = await approvalGate.decide({
3309
+ sessionId,
3310
+ riskLevel: APPROVAL_RISK.HIGH,
3311
+ tool: "browser_act",
3312
+ args: { actions: summary },
3313
+ });
3314
+ if (gate.decision !== APPROVAL_DECISION.ALLOW) {
3315
+ const tierLabel =
3316
+ typeof gate.policy === "string" ? `"${gate.policy}" ` : "";
3317
+ return attachDescriptor({
3318
+ error: `[ApprovalGate] browser_act denied by the ${tierLabel}approval policy (via ${gate.via}). Retrying won't help — driving the user's browser needs their approval. Tell the user (they can approve it or relax the policy) and continue with other work.`,
3319
+ approval: {
3320
+ decision: gate.decision,
3321
+ via: gate.via,
3322
+ riskLevel: "high",
3323
+ policy: gate.policy,
3324
+ },
3325
+ });
3326
+ }
3327
+ }
3328
+ const { performActions } = await import("../lib/chrome-connector.js");
3329
+ const result = await performActions(args.actions, {
3330
+ port: args.port != null ? Number(args.port) : undefined,
3331
+ cdpUrl: args.cdp_url != null ? String(args.cdp_url) : null,
3332
+ tab: args.tab != null ? Number(args.tab) : undefined,
3333
+ continueOnError: args.continue_on_error === true,
3334
+ sessionId: sessionId ? String(sessionId) : null,
3335
+ });
3336
+ if (!result.ok && result.error) {
3337
+ // Nothing ran (validation / attach failure) — surface as an error.
3338
+ // A step-level failure returns the per-step outcomes instead so the
3339
+ // model can see exactly which step broke.
3340
+ return attachDescriptor({
3341
+ error: `browser_act failed: ${result.error}`,
3342
+ });
3343
+ }
3344
+ return attachDescriptor(result);
3345
+ } catch (err) {
3346
+ return attachDescriptor({
3347
+ error: `browser_act failed: ${err.message}`,
3348
+ });
3349
+ }
3350
+ }
3351
+
3190
3352
  case "search_files": {
3191
3353
  // An explicit directory scopes the search to one root; otherwise span
3192
3354
  // cwd plus any --add-dir roots so cross-package searches find matches.
@@ -3395,13 +3557,46 @@ async function executeToolInner(
3395
3557
  // Check if skill requests isolation (via SKILL.md frontmatter)
3396
3558
  const skillIsolation = match.isolation === true;
3397
3559
  if (skillIsolation) {
3560
+ // 用量归因: an isolated skill runs as a child loop whose real token
3561
+ // usage would otherwise be invisible — forward it into the parent
3562
+ // run's sink tagged origin:"skill" so `cc session usage --by skill`
3563
+ // can break it out. A nested frame passes through unchanged.
3564
+ const skillUsageSink = Array.isArray(subAgentUsageSink)
3565
+ ? subAgentUsageSink
3566
+ : null;
3567
+ let skillSubRef = null;
3398
3568
  // Run skill through isolated sub-agent context
3399
3569
  const subCtx = SubAgentContext.create({
3400
3570
  role: `skill-${args.skill_name}`,
3401
3571
  task: `Execute the "${args.skill_name}" skill with input: ${(args.input || "").substring(0, 200)}`,
3402
3572
  allowedTools: ["read_file", "search_files", "list_dir"],
3403
3573
  cwd,
3574
+ onUsage: skillUsageSink
3575
+ ? (u) => {
3576
+ try {
3577
+ skillUsageSink.push(
3578
+ u && u.attribution
3579
+ ? u
3580
+ : {
3581
+ provider: u?.provider ?? null,
3582
+ model: u?.model ?? null,
3583
+ usage: u?.usage || null,
3584
+ attribution: {
3585
+ origin: "skill",
3586
+ skill: args.skill_name,
3587
+ subagentId: skillSubRef?.id || null,
3588
+ parentSessionId: sessionId || null,
3589
+ depth: (subAgentDepth || 0) + 1,
3590
+ },
3591
+ },
3592
+ );
3593
+ } catch (_e) {
3594
+ // usage forwarding is best-effort
3595
+ }
3596
+ }
3597
+ : null,
3404
3598
  });
3599
+ skillSubRef = subCtx;
3405
3600
  try {
3406
3601
  const result = await subCtx.run(args.input);
3407
3602
  return attachDescriptor({
@@ -3986,6 +4181,27 @@ function _backgroundSubAgentResultText(entry) {
3986
4181
  );
3987
4182
  }
3988
4183
 
4184
+ /**
4185
+ * Drain the run's attributed child-loop usage sink (spawn_sub_agent /
4186
+ * isolated run_skill — see toolContext.subAgentUsageSink), re-yielding each
4187
+ * record as a regular `token-usage` event that carries its `attribution`
4188
+ * frame. Consumers that ignore `attribution` see ordinary usage events;
4189
+ * attribution-aware consumers (REPL persistence, headless runner) can split
4190
+ * child spend from the main conversation's.
4191
+ */
4192
+ function* _drainSubAgentUsage(sink) {
4193
+ while (Array.isArray(sink) && sink.length > 0) {
4194
+ const u = sink.shift();
4195
+ yield {
4196
+ type: "token-usage",
4197
+ provider: u?.provider ?? null,
4198
+ model: u?.model ?? null,
4199
+ usage: u?.usage || {},
4200
+ attribution: u?.attribution || null,
4201
+ };
4202
+ }
4203
+ }
4204
+
3989
4205
  async function _executeSpawnSubAgent(args, ctx) {
3990
4206
  // Nesting cap: refuse before any context/registry work.
3991
4207
  const currentDepth = ctx.subAgentDepth || 0;
@@ -4135,6 +4351,40 @@ async function _executeSpawnSubAgent(args, ctx) {
4135
4351
  model: mdModel || parentLlm.model || undefined,
4136
4352
  };
4137
4353
 
4354
+ // 用量归因: forward the child's real token usage into the parent run's sink
4355
+ // (threaded through toolContext) so agentLoop re-yields it as attributed
4356
+ // `token-usage` events. A nested child's already-attributed record passes
4357
+ // through unchanged (deepest frame wins). subCtxRef closes over the created
4358
+ // context so the frame can carry the sub-agent's id.
4359
+ const usageSink = Array.isArray(ctx.subAgentUsageSink)
4360
+ ? ctx.subAgentUsageSink
4361
+ : null;
4362
+ let subCtxRef = null;
4363
+ const onUsage = usageSink
4364
+ ? (u) => {
4365
+ try {
4366
+ usageSink.push(
4367
+ u && u.attribution
4368
+ ? u
4369
+ : {
4370
+ provider: u?.provider ?? null,
4371
+ model: u?.model ?? null,
4372
+ usage: u?.usage || null,
4373
+ attribution: {
4374
+ origin: "subagent",
4375
+ subagentId: subCtxRef?.id || null,
4376
+ role: subCtxRef?.role || role || null,
4377
+ parentSessionId,
4378
+ depth: currentDepth + 1,
4379
+ },
4380
+ },
4381
+ );
4382
+ } catch (_e) {
4383
+ // usage forwarding is best-effort
4384
+ }
4385
+ }
4386
+ : null;
4387
+
4138
4388
  const subCtx = SubAgentContext.create({
4139
4389
  role,
4140
4390
  task,
@@ -4148,7 +4398,9 @@ async function _executeSpawnSubAgent(args, ctx) {
4148
4398
  // Same shared counter object so the child's own spawns draw from the run's
4149
4399
  // single total-sub-agent pool (breadth cap spans the whole tree).
4150
4400
  subAgentBudget: ctx.subAgentBudget || null,
4401
+ onUsage,
4151
4402
  });
4403
+ subCtxRef = subCtx;
4152
4404
 
4153
4405
  const emit = (type, payload) => {
4154
4406
  if (!interaction || typeof interaction.emit !== "function") return;
@@ -5759,8 +6011,18 @@ export async function* agentLoop(messages, options) {
5759
6011
  // any are still running (it waits, injects, and gives the model one more
5760
6012
  // turn) so a background result can never be silently lost.
5761
6013
  backgroundSubAgents: new Map(),
6014
+ // 用量归因: per-run sink for child-loop (spawn_sub_agent / isolated
6015
+ // run_skill) token usage. Child loops consume their own generator events,
6016
+ // so their real usage never reaches this loop's consumers — the spawn
6017
+ // wiring pushes it here and the loop drains it at iteration boundaries as
6018
+ // `token-usage` events carrying an `attribution` frame. Callers may pass
6019
+ // their own array to observe it directly.
6020
+ subAgentUsageSink: Array.isArray(options.subAgentUsageSink)
6021
+ ? options.subAgentUsageSink
6022
+ : [],
5762
6023
  };
5763
6024
  const backgroundSubAgents = toolContext.backgroundSubAgents;
6025
+ const subAgentUsageSink = toolContext.subAgentUsageSink;
5764
6026
 
5765
6027
  throwIfAborted(signal);
5766
6028
 
@@ -5896,6 +6158,10 @@ export async function* agentLoop(messages, options) {
5896
6158
  budget.consume();
5897
6159
  throwIfAborted(signal);
5898
6160
 
6161
+ // Surface attributed child-loop usage collected since the last boundary
6162
+ // (blocking spawns push during executeTool; background spawns push live).
6163
+ yield* _drainSubAgentUsage(subAgentUsageSink);
6164
+
5899
6165
  // Emit progressive warnings (once per level)
5900
6166
  const level = budget.warningLevel();
5901
6167
  if (
@@ -6470,7 +6736,9 @@ export async function* agentLoop(messages, options) {
6470
6736
  }
6471
6737
  }
6472
6738
 
6473
- // Budget exhausted — yield exhaustion event + final message
6739
+ // Budget exhausted — flush any child usage the final iteration produced,
6740
+ // then yield exhaustion event + final message
6741
+ yield* _drainSubAgentUsage(subAgentUsageSink);
6474
6742
  yield { type: "iteration-budget-exhausted", budget: budget.toSummary() };
6475
6743
  yield {
6476
6744
  type: "response-complete",
@@ -6523,6 +6791,15 @@ export function formatToolArgs(name, args) {
6523
6791
  return `${args.level || "info"}: ${(args.title || "").substring(0, 50)}`;
6524
6792
  case "publish_artifact":
6525
6793
  return `${args.kind || "other"}: ${(args.title || args.path || "").substring(0, 60)}`;
6794
+ case "browser_state":
6795
+ return `tab=${args.tab ?? 0} port=${args.port ?? 9222}${args.reload ? " reload" : ""}`;
6796
+ case "browser_act": {
6797
+ const kinds = Array.isArray(args.actions)
6798
+ ? args.actions.map((a) => a?.type).filter(Boolean)
6799
+ : [];
6800
+ const head = kinds.slice(0, 4).join(",");
6801
+ return `${kinds.length} action(s)${head ? `: ${head}` : ""}${kinds.length > 4 ? ",…" : ""} tab=${args.tab ?? 0} port=${args.port ?? 9222}`;
6802
+ }
6526
6803
  case "schedule":
6527
6804
  return args.action === "cron"
6528
6805
  ? `cron ${args.cron || ""}`.trim()