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
@@ -31,6 +31,30 @@ function toNumber(v) {
31
31
  return Number.isFinite(n) ? n : 0;
32
32
  }
33
33
 
34
+ /**
35
+ * Extract the optional usage-attribution frame (用量归因) persisted alongside
36
+ * a token_usage event's data: `{ origin, skill?, subagentId?, role?,
37
+ * parentSessionId?, depth? }`. Written by the agent drivers for sub-agent /
38
+ * isolated-skill usage; absent on legacy events and on plain main-loop usage
39
+ * (absence ⇒ origin "main"). Returns a normalized copy or null.
40
+ */
41
+ export function extractAttribution(event) {
42
+ const a = event?.data?.attribution;
43
+ if (!a || typeof a !== "object") return null;
44
+ const origin = typeof a.origin === "string" && a.origin ? a.origin : null;
45
+ if (!origin) return null;
46
+ const out = { origin };
47
+ if (typeof a.skill === "string" && a.skill) out.skill = a.skill;
48
+ if (typeof a.subagentId === "string" && a.subagentId)
49
+ out.subagentId = a.subagentId;
50
+ if (typeof a.role === "string" && a.role) out.role = a.role;
51
+ if (typeof a.parentSessionId === "string" && a.parentSessionId)
52
+ out.parentSessionId = a.parentSessionId;
53
+ const depth = Number(a.depth);
54
+ if (Number.isFinite(depth)) out.depth = depth;
55
+ return out;
56
+ }
57
+
34
58
  /**
35
59
  * Extract a normalized usage record from a single event, or null if none.
36
60
  * Accepts both snake_case (OpenAI/Anthropic) and camelCase variants.
@@ -75,6 +99,9 @@ export function extractUsage(event) {
75
99
  return null;
76
100
  }
77
101
 
102
+ // Attribution rides along ONLY when present, so records built from legacy
103
+ // events stay byte-identical (callers using exact-equality keep passing).
104
+ const attribution = extractAttribution(event);
78
105
  return {
79
106
  provider: d.provider || raw.provider || null,
80
107
  model: d.model || raw.model || null,
@@ -84,6 +111,223 @@ export function extractUsage(event) {
84
111
  cacheReadTokens,
85
112
  cacheCreationTokens,
86
113
  timestamp: event.timestamp || null,
114
+ ...(attribution ? { attribution } : {}),
115
+ };
116
+ }
117
+
118
+ // ── Usage attribution roll-ups (用量归因) ───────────────────────────────────
119
+
120
+ function _newSums() {
121
+ return {
122
+ inputTokens: 0,
123
+ outputTokens: 0,
124
+ totalTokens: 0,
125
+ cacheReadTokens: 0,
126
+ cacheCreationTokens: 0,
127
+ calls: 0,
128
+ };
129
+ }
130
+
131
+ function _addSums(sums, u) {
132
+ sums.inputTokens += u.inputTokens || 0;
133
+ sums.outputTokens += u.outputTokens || 0;
134
+ sums.totalTokens += u.totalTokens || 0;
135
+ sums.cacheReadTokens += u.cacheReadTokens || 0;
136
+ sums.cacheCreationTokens += u.cacheCreationTokens || 0;
137
+ sums.calls += u.calls != null ? u.calls : 1;
138
+ }
139
+
140
+ const _byTokensDesc = (a, b) => b.totalTokens - a.totalTokens;
141
+
142
+ /**
143
+ * Roll usage events up by attribution: origin (main | subagent | skill —
144
+ * events without an attribution frame count as "main", which keeps every
145
+ * pre-attribution transcript aggregating exactly as before), by skill name,
146
+ * and by individual sub-agent.
147
+ */
148
+ export function aggregateUsageAttribution(events) {
149
+ const byOrigin = new Map();
150
+ const bySkill = new Map();
151
+ const bySubagent = new Map();
152
+
153
+ for (const evt of events || []) {
154
+ const u = extractUsage(evt);
155
+ if (!u) continue;
156
+ const attr = u.attribution || null;
157
+ const origin = attr?.origin || "main";
158
+
159
+ const o = byOrigin.get(origin) || { origin, ..._newSums() };
160
+ _addSums(o, u);
161
+ byOrigin.set(origin, o);
162
+
163
+ if (origin === "skill") {
164
+ const key = attr?.skill || "?";
165
+ const s = bySkill.get(key) || { skill: key, ..._newSums() };
166
+ _addSums(s, u);
167
+ bySkill.set(key, s);
168
+ }
169
+
170
+ if (attr?.subagentId) {
171
+ const key = attr.subagentId;
172
+ const r = bySubagent.get(key) || {
173
+ subagentId: key,
174
+ role: attr.role || null,
175
+ origin,
176
+ ..._newSums(),
177
+ };
178
+ _addSums(r, u);
179
+ bySubagent.set(key, r);
180
+ }
181
+ }
182
+
183
+ return {
184
+ byOrigin: Array.from(byOrigin.values()).sort(_byTokensDesc),
185
+ bySkill: Array.from(bySkill.values()).sort(_byTokensDesc),
186
+ bySubagent: Array.from(bySubagent.values()).sort(_byTokensDesc),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * MCP tools reach the loop as `mcp__<server>__<tool>`; returns the server
192
+ * segment or null for a non-MCP tool name.
193
+ */
194
+ export function mcpServerOf(toolName) {
195
+ if (typeof toolName !== "string" || !toolName.startsWith("mcp__"))
196
+ return null;
197
+ const rest = toolName.slice(5);
198
+ const sep = rest.indexOf("__");
199
+ return sep > 0 ? rest.slice(0, sep) : null;
200
+ }
201
+
202
+ /** Lenient tool_result error detection (mirrors session-insights). */
203
+ function _isToolResultError(event) {
204
+ const r = event?.data?.result;
205
+ return Boolean(
206
+ event?.data?.error ||
207
+ (r && typeof r === "object" && (r.error || r.is_error || r.isError)),
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Aggregate the transcript's tool events: per-tool call/error counts, an MCP
213
+ * server bucket (`mcp__<server>__*`), and a turn-associated token figure.
214
+ *
215
+ * `turnTokens` is an APPROXIMATION: turns are delimited by `user_message`
216
+ * events, and every token_usage event inside a turn is attributed to EVERY
217
+ * tool that ran in that turn ("tokens of turns that used tool X"). A turn's
218
+ * tokens therefore count once per distinct tool — do NOT sum turnTokens
219
+ * across tools/servers, it is not a partition. Headless sessions persist one
220
+ * aggregated usage event per run, so there the whole run's tokens associate
221
+ * with each tool the run used.
222
+ *
223
+ * Accepts both the compact `tool_call` shape ({tool, is_error, skill?}) the
224
+ * drivers write now and the legacy `tool_call`({tool, args}) + `tool_result`
225
+ * pair (errors read off the result, calls only off tool_call — no double
226
+ * count). Sessions without tool events aggregate to zeros.
227
+ */
228
+ export function aggregateToolCalls(events) {
229
+ const byTool = new Map();
230
+ const byServer = new Map();
231
+ let totalCalls = 0;
232
+ let totalErrors = 0;
233
+
234
+ const bumpTool = (name) => {
235
+ let t = byTool.get(name);
236
+ if (!t) {
237
+ t = {
238
+ tool: name,
239
+ mcpServer: mcpServerOf(name),
240
+ calls: 0,
241
+ errors: 0,
242
+ turnTokens: 0,
243
+ };
244
+ byTool.set(name, t);
245
+ }
246
+ return t;
247
+ };
248
+ const bumpServer = (server) => {
249
+ let s = byServer.get(server);
250
+ if (!s) {
251
+ s = { server, calls: 0, errors: 0, turnTokens: 0 };
252
+ byServer.set(server, s);
253
+ }
254
+ return s;
255
+ };
256
+
257
+ let turnTools = new Set();
258
+ let turnServers = new Set();
259
+ let turnTokens = 0;
260
+ const flushTurn = () => {
261
+ if (turnTokens > 0) {
262
+ for (const name of turnTools) bumpTool(name).turnTokens += turnTokens;
263
+ for (const server of turnServers)
264
+ bumpServer(server).turnTokens += turnTokens;
265
+ }
266
+ turnTools = new Set();
267
+ turnServers = new Set();
268
+ turnTokens = 0;
269
+ };
270
+
271
+ for (const evt of events || []) {
272
+ if (!evt || typeof evt !== "object") continue;
273
+ if (evt.type === "user_message") {
274
+ flushTurn();
275
+ continue;
276
+ }
277
+ if (evt.type === "tool_call") {
278
+ const name = evt.data?.tool || "?";
279
+ totalCalls++;
280
+ const t = bumpTool(name);
281
+ t.calls++;
282
+ const isErr = evt.data?.is_error === true;
283
+ if (isErr) {
284
+ totalErrors++;
285
+ t.errors++;
286
+ }
287
+ turnTools.add(name);
288
+ const server = mcpServerOf(name);
289
+ if (server) {
290
+ const s = bumpServer(server);
291
+ s.calls++;
292
+ if (isErr) s.errors++;
293
+ turnServers.add(server);
294
+ }
295
+ continue;
296
+ }
297
+ if (evt.type === "tool_result") {
298
+ const name = evt.data?.tool || "?";
299
+ turnTools.add(name);
300
+ const server = mcpServerOf(name);
301
+ if (server) turnServers.add(server);
302
+ if (_isToolResultError(evt)) {
303
+ totalErrors++;
304
+ bumpTool(name).errors++;
305
+ if (server) bumpServer(server).errors++;
306
+ }
307
+ continue;
308
+ }
309
+ const u = extractUsage(evt);
310
+ if (u) turnTokens += u.totalTokens;
311
+ }
312
+ flushTurn();
313
+
314
+ const byCallsDesc = (a, b) => b.calls - a.calls;
315
+ return {
316
+ totalCalls,
317
+ totalErrors,
318
+ byTool: Array.from(byTool.values()).sort(byCallsDesc),
319
+ byMcpServer: Array.from(byServer.values()).sort(byCallsDesc),
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Full attribution section for a transcript: origin/skill/subagent usage
325
+ * roll-ups + the tool/MCP call aggregation. Pure.
326
+ */
327
+ export function sessionAttribution(events) {
328
+ return {
329
+ ...aggregateUsageAttribution(events),
330
+ tools: aggregateToolCalls(events),
87
331
  };
88
332
  }
89
333
 
@@ -141,12 +385,13 @@ export function aggregateUsage(events) {
141
385
  }
142
386
 
143
387
  /**
144
- * Roll up usage for a single JSONL session.
388
+ * Roll up usage for a single JSONL session. The pre-attribution keys
389
+ * (total/byModel) are unchanged; `attribution` is additive.
145
390
  */
146
391
  export function sessionUsage(sessionId) {
147
392
  const events = _deps.readEvents(sessionId);
148
393
  const agg = aggregateUsage(events);
149
- return { sessionId, ...agg };
394
+ return { sessionId, ...agg, attribution: sessionAttribution(events) };
150
395
  }
151
396
 
152
397
  /**
@@ -206,6 +451,66 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
206
451
  }
207
452
  }
208
453
 
454
+ // Global attribution roll-up: merge each session's attribution rows by key.
455
+ const gOrigin = new Map();
456
+ const gSkill = new Map();
457
+ const gSubagent = new Map();
458
+ const gTool = new Map();
459
+ const gServer = new Map();
460
+ let gToolCalls = 0;
461
+ let gToolErrors = 0;
462
+ const mergeRow = (map, key, row, seed) => {
463
+ const entry = map.get(key) || { ...seed, ..._newSums() };
464
+ _addSums(entry, row);
465
+ map.set(key, entry);
466
+ };
467
+ for (const s of perSession) {
468
+ const a = s.attribution;
469
+ if (!a) continue;
470
+ for (const row of a.byOrigin || []) {
471
+ mergeRow(gOrigin, row.origin, row, { origin: row.origin });
472
+ }
473
+ for (const row of a.bySkill || []) {
474
+ mergeRow(gSkill, row.skill, row, { skill: row.skill });
475
+ }
476
+ for (const row of a.bySubagent || []) {
477
+ mergeRow(gSubagent, row.subagentId, row, {
478
+ subagentId: row.subagentId,
479
+ role: row.role || null,
480
+ origin: row.origin,
481
+ });
482
+ }
483
+ const tools = a.tools || {};
484
+ gToolCalls += tools.totalCalls || 0;
485
+ gToolErrors += tools.totalErrors || 0;
486
+ for (const row of tools.byTool || []) {
487
+ const entry = gTool.get(row.tool) || {
488
+ tool: row.tool,
489
+ mcpServer: row.mcpServer || null,
490
+ calls: 0,
491
+ errors: 0,
492
+ turnTokens: 0,
493
+ };
494
+ entry.calls += row.calls || 0;
495
+ entry.errors += row.errors || 0;
496
+ entry.turnTokens += row.turnTokens || 0;
497
+ gTool.set(row.tool, entry);
498
+ }
499
+ for (const row of tools.byMcpServer || []) {
500
+ const entry = gServer.get(row.server) || {
501
+ server: row.server,
502
+ calls: 0,
503
+ errors: 0,
504
+ turnTokens: 0,
505
+ };
506
+ entry.calls += row.calls || 0;
507
+ entry.errors += row.errors || 0;
508
+ entry.turnTokens += row.turnTokens || 0;
509
+ gServer.set(row.server, entry);
510
+ }
511
+ }
512
+ const byCallsDesc = (a, b) => b.calls - a.calls;
513
+
209
514
  return {
210
515
  sessions: perSession,
211
516
  skipped,
@@ -213,6 +518,17 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
213
518
  byModel: Array.from(byKey.values()).sort(
214
519
  (a, b) => b.totalTokens - a.totalTokens,
215
520
  ),
521
+ attribution: {
522
+ byOrigin: Array.from(gOrigin.values()).sort(_byTokensDesc),
523
+ bySkill: Array.from(gSkill.values()).sort(_byTokensDesc),
524
+ bySubagent: Array.from(gSubagent.values()).sort(_byTokensDesc),
525
+ tools: {
526
+ totalCalls: gToolCalls,
527
+ totalErrors: gToolErrors,
528
+ byTool: Array.from(gTool.values()).sort(byCallsDesc),
529
+ byMcpServer: Array.from(gServer.values()).sort(byCallsDesc),
530
+ },
531
+ },
216
532
  };
217
533
  }
218
534
 
@@ -115,6 +115,14 @@ export class SubAgentContext {
115
115
  // Optional progress callback for streaming events to consumers
116
116
  this._onProgress = options.onProgress || null;
117
117
 
118
+ // Usage-attribution seam (用量归因): forwards the child loop's REAL
119
+ // provider-reported token usage to the spawner (the ~4-chars/token
120
+ // estimate kept in _tokenCount below is only a budget heuristic and never
121
+ // leaves this context). Wired by spawn_sub_agent / isolated run_skill so
122
+ // the parent loop can re-yield attributed `token-usage` events.
123
+ this._onUsage =
124
+ typeof options.onUsage === "function" ? options.onUsage : null;
125
+
118
126
  // Optional abort signal for cancellation
119
127
  this._signal = options.signal || null;
120
128
 
@@ -295,6 +303,21 @@ export class SubAgentContext {
295
303
  for await (const event of gen) {
296
304
  this._iterationCount++;
297
305
 
306
+ if (event.type === "token-usage" && this._onUsage) {
307
+ // Forward real usage to the spawner. A nested child's event already
308
+ // carries its own attribution frame — preserve it (deepest wins).
309
+ try {
310
+ this._onUsage({
311
+ provider: event.provider || null,
312
+ model: event.model || null,
313
+ usage: event.usage || null,
314
+ attribution: event.attribution || null,
315
+ });
316
+ } catch (_e) {
317
+ // Usage forwarding is best-effort — never break the child loop
318
+ }
319
+ }
320
+
298
321
  if (event.type === "tool-executing") {
299
322
  this._toolsUsed.push(event.tool);
300
323
  }
@@ -19,6 +19,7 @@
19
19
  import http from "http";
20
20
  import fs from "fs";
21
21
  import path from "path";
22
+ import crypto from "crypto";
22
23
  import { fileURLToPath } from "url";
23
24
  import { getInlineSource as getEnvelopeInlineSource } from "./web-ui-envelope.js";
24
25
  import { CLISkillLoader } from "./skill-loader.js";
@@ -1224,28 +1225,140 @@ function findWebPanelDist(staticDir) {
1224
1225
  return null;
1225
1226
  }
1226
1227
 
1228
+ /** Constant-time token check (same construction as ws-server's): hash both
1229
+ * sides to a fixed length so length differences don't leak, then compare in
1230
+ * constant time. `null`/absent configured token → no auth required. */
1231
+ function downloadTokenOk(configuredToken, req) {
1232
+ if (!configuredToken) return true;
1233
+ let supplied = "";
1234
+ const auth = String(req.headers?.authorization || "");
1235
+ if (auth.toLowerCase().startsWith("bearer ")) {
1236
+ supplied = auth.slice(7).trim();
1237
+ } else {
1238
+ try {
1239
+ supplied =
1240
+ new URL(req.url, "http://localhost").searchParams.get("token") || "";
1241
+ } catch {
1242
+ supplied = "";
1243
+ }
1244
+ }
1245
+ const a = crypto.createHash("sha256").update(String(supplied)).digest();
1246
+ const b = crypto
1247
+ .createHash("sha256")
1248
+ .update(String(configuredToken))
1249
+ .digest();
1250
+ return crypto.timingSafeEqual(a, b);
1251
+ }
1252
+
1253
+ /** RFC 5987 filename* value for a Content-Disposition header. */
1254
+ function encodeDispositionFilename(name) {
1255
+ const base = String(name || "artifact").replace(/[\r\n"]/g, "_");
1256
+ return `attachment; filename*=UTF-8''${encodeURIComponent(base)}`;
1257
+ }
1258
+
1227
1259
  /**
1228
1260
  * Handle `/api/*` routes. Returns `true` if the request was handled (caller
1229
1261
  * must stop further processing) and `false` for everything else (caller
1230
1262
  * continues with the static/SPA handler).
1231
1263
  *
1232
- * Currently exposes a single read-only endpoint:
1264
+ * Endpoints:
1233
1265
  *
1234
1266
  * GET /api/skills → 200 { schema: 1, skills: [{name, source, category,
1235
1267
  * description, version}] }
1268
+ * GET /api/artifacts/<id>/download → the artifact's stored copy as an
1269
+ * attachment (full file — the WS artifact-content preview
1270
+ * route is capped; this is the uncapped download path the
1271
+ * web-panel's 下载 button uses). When the server was
1272
+ * created with a wsToken, the same token must accompany
1273
+ * the request (`Authorization: Bearer` or `?token=`).
1236
1274
  *
1237
- * The smoke-runner's project-mode probe (packer/smoke-runner.js) hits this
1238
- * to verify bundled skills were materialized and registered. The shape is
1239
- * deliberately minimal — the Web UI's richer skill views use WS.
1275
+ * The smoke-runner's project-mode probe (packer/smoke-runner.js) hits
1276
+ * /api/skills to verify bundled skills were materialized and registered. The
1277
+ * shapes are deliberately minimal — the Web UI's richer views use WS.
1240
1278
  *
1241
1279
  * @param {import("http").IncomingMessage} req
1242
1280
  * @param {import("http").ServerResponse} res
1281
+ * @param {{ wsToken?: string|null }} [ctx] server context (auth for downloads)
1243
1282
  * @returns {boolean} true if handled, false if the caller should continue
1244
1283
  */
1245
- export function handleApiRequest(req, res) {
1284
+ export function handleApiRequest(req, res, ctx = {}) {
1246
1285
  const urlPath = req.url.split("?")[0];
1247
1286
  if (!urlPath.startsWith("/api/")) return false;
1248
1287
 
1288
+ const artifactMatch = urlPath.match(
1289
+ /^\/api\/artifacts\/([A-Za-z0-9_]+)\/download$/,
1290
+ );
1291
+ if (artifactMatch) {
1292
+ if (req.method !== "GET") {
1293
+ res.writeHead(405, { "Content-Type": "application/json" });
1294
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
1295
+ return true;
1296
+ }
1297
+ // The panel authenticates its WS with this token; the download endpoint
1298
+ // serves artifact FILE CONTENT, so it must not be weaker than the WS.
1299
+ if (!downloadTokenOk(ctx.wsToken || null, req)) {
1300
+ res.writeHead(401, { "Content-Type": "application/json" });
1301
+ res.end(JSON.stringify({ error: "Unauthorized" }));
1302
+ return true;
1303
+ }
1304
+ // Async body in a sync router: the route is claimed (return true) and the
1305
+ // response completes when the store import resolves.
1306
+ (async () => {
1307
+ try {
1308
+ const { ArtifactStore } = await import("./artifact-store.js");
1309
+ const store = new ArtifactStore();
1310
+ const entry = store.get(artifactMatch[1]);
1311
+ if (!entry) {
1312
+ res.writeHead(404, { "Content-Type": "application/json" });
1313
+ res.end(JSON.stringify({ error: "Not Found" }));
1314
+ return;
1315
+ }
1316
+ // Same tamper guard as the WS content route: the stored filename must
1317
+ // be a plain basename or the index row is refused.
1318
+ if (
1319
+ !entry.file ||
1320
+ entry.file !== path.basename(entry.file) ||
1321
+ entry.file.includes("..")
1322
+ ) {
1323
+ res.writeHead(400, { "Content-Type": "application/json" });
1324
+ res.end(JSON.stringify({ error: "Bad artifact index row" }));
1325
+ return;
1326
+ }
1327
+ const storedPath = store.storedPath(entry);
1328
+ let size;
1329
+ try {
1330
+ size = fs.statSync(storedPath).size;
1331
+ } catch {
1332
+ res.writeHead(404, { "Content-Type": "application/json" });
1333
+ res.end(JSON.stringify({ error: "Stored copy missing" }));
1334
+ return;
1335
+ }
1336
+ const downloadName =
1337
+ entry.title && path.extname(entry.title)
1338
+ ? entry.title
1339
+ : path.basename(entry.sourcePath || entry.file);
1340
+ res.writeHead(200, {
1341
+ "Content-Type": entry.mime || "application/octet-stream",
1342
+ "Content-Length": size,
1343
+ "Content-Disposition": encodeDispositionFilename(downloadName),
1344
+ "Cache-Control": "no-store",
1345
+ "X-Content-Type-Options": "nosniff",
1346
+ });
1347
+ const stream = fs.createReadStream(storedPath);
1348
+ stream.on("error", () => res.destroy());
1349
+ stream.pipe(res);
1350
+ } catch (err) {
1351
+ try {
1352
+ res.writeHead(500, { "Content-Type": "application/json" });
1353
+ res.end(JSON.stringify({ error: err.message }));
1354
+ } catch {
1355
+ /* headers already sent — connection is torn down by the stream */
1356
+ }
1357
+ }
1358
+ })();
1359
+ return true;
1360
+ }
1361
+
1249
1362
  if (urlPath === "/api/skills") {
1250
1363
  if (req.method !== "GET") {
1251
1364
  res.writeHead(405, { "Content-Type": "application/json" });
@@ -1328,7 +1441,7 @@ export function createWebUIServer(opts) {
1328
1441
  return http.createServer((req, res) => {
1329
1442
  // /api/* routes are handled before the static file logic so GET /api/skills
1330
1443
  // doesn't accidentally resolve to index.html via the SPA fallback.
1331
- if (handleApiRequest(req, res)) return;
1444
+ if (handleApiRequest(req, res, { wsToken: opts.wsToken || null })) return;
1332
1445
 
1333
1446
  if (req.method !== "GET") {
1334
1447
  res.writeHead(405, { "Content-Type": "text/plain" });
@@ -1415,7 +1528,7 @@ export function createWebUIServer(opts) {
1415
1528
  // ── Fallback: embedded classic single-page HTML ─────────────────────────
1416
1529
  const html = buildHtml(opts);
1417
1530
  return http.createServer((req, res) => {
1418
- if (handleApiRequest(req, res)) return;
1531
+ if (handleApiRequest(req, res, { wsToken: opts.wsToken || null })) return;
1419
1532
 
1420
1533
  const urlPath = req.url.split("?")[0];
1421
1534
  if (