chainlesschain 0.162.183 → 0.162.184

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 (196) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BapS7lz4.js → AIOps-DzAl5fGx.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CXYdXEUI.js → ActionButton-DAHtMGeC.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-CKR5Lexn.js → Analytics-BHQaexv5.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DATVUOXB.js → AppLayout-Phz_pD0n.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-vb_0ZlV1.js → Artifacts-DHt47kCA.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-BI1l9GWt.js → Audit--Y_8FULl.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BFXbaUEf.js → BackgroundAgents-N1zchH7D.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-DEYBJJ6H.js → Backup-B_BoyN30.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-COdzeO0M.js → BaseInput-Gl4uQHcT.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-oSN6sfM5.js → Chat-Cg-eIyAZ.js} +6 -6
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-BzeUi3JH.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-BezEa4tm.js → Checkbox-D0MGR2wA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CNEL0Ife.js → Codegen-BdObJEFn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Bf2Y2Bn1.js → Col--ghxXT3z.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-BfUlu4Hc.js → Community-BUFeXOqz.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-BN4xWP9e.js → Compact-DhQO2_Mr.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-Bt6BKmEP.js → Compliance-3HoTqKYd.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DKLp8YxO.js → Cowork-B3zmO4VW.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-o7Hhw6ky.js → Cron-CJDrZOcc.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DaNXWJIw.js → Crosschain-SMl15obd.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-B-Qa2mha.js → DID-CgTrTWrl.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-CMX0Qnxw.js → Dashboard-dXFKaKol.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BLuLyRu_.js → Dropdown-qQHEhCvI.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CFBAeMij.js → EmailListRenderer-C_ej0MWH.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DWzXQ67V.js → FamilyGuardDashboard-D6ajoE7I.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-DB1QSLgr.js → Federation-Cio2y49h.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-C-OKCToP.js → FormItemContext-ClJz1cYp.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DMJFtFZf.js → GenericCardRenderer-Dr_8Maca.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DsGSujOV.js → Git-CevLwp5u.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-B0iS9NEW.js → Governance-CW9YyQUY.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CH004H7c.js → Inference-CQqeAyER.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-Q9MTQ83c.js → KnowledgeGraph-cuGVxHrw.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-B_fyjZK9.js → Logs-DIuHri5d.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-C0o4FtpD.js → MarkdownRenderer-R91Qw5JA.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-BIStF9d1.js → Marketplace-CY1VhFGx.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-pGfal768.js → McpTools-BBAbi1vt.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-xbNqpIqB.js → Memory-BWYGHxJA.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-B5EQGrIK.js → MobileBridge-CPIFkW7A.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-DQyWxCho.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-a_rmfxJH.js → Mtc-Bo3GnQbx.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-BmKuofUr.js → MtcAudit-C8-c4rSW.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-BQbJ-CAN.js → Multisig-ChqMTtoq.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-JtPJKX25.js → NLProgramming-C48TyhvI.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-ZFKznTSG.js → Notes-zogUFWxp.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-B6aHRBBR.js → NotificationSettings-v-K9XpSB.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-P94JGGNz.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-BFDTAUgS.js → Organization-Boi1dQfR.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-0FoTfbWH.js → Overflow-DM5YWwgA.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CWd90etw.js → P2P-C57LKpKK.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-DtCGC-W-.js → PdhVaultBrowser-BFkPqu6Q.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-CvQ7nr7T.js → Permissions-D6fB9YSp.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-rdNPcR-Z.js → PersonalDataHub-Bp7EefEo.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-CmrfiNOs.js → Pipeline-BVln3hZf.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-D7ep1h_t.js → Privacy-Bf8P3nF6.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-C0xKINHS.js → ProjectInit-DM6wA2E2.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-AOmZFp-F.js → ProjectSettings-CGbc5Iho.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-BWHSkXZP.js → Projects-D0p9EOxm.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-Cb7JCTPL.js → Providers-B2kJcxS6.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-BDMehcXC.js → QrScannerModal-Cxu7dXYy.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-BF6_1nvx.js → QuickAsk-DTlrTPms.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-BHfT-A28.js → Recommend-Cm3X-cCj.js} +1 -1
  64. package/src/assets/web-panel/assets/RemoteSession-DNb2kjvt.js +5 -0
  65. package/src/assets/web-panel/assets/{Reputation-CsWHLEcj.js → Reputation-CZUFFjc1.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-D6srnWhm.js → Row-DAPgloxA.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-Xhr-KyVd.js → RssFeed-dsx9S793.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-vIbIJvwL.js → Search-DnnC8kz-.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-Bzi5Aiko.js → Security-VuZknRMK.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BHtVX52P.js → Services-B3XNX8Vx.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-C0R8B2jf.js → Skeleton-Dl1nVtCj.js} +1 -1
  72. package/src/assets/web-panel/assets/Skills-CY09CZ_W.js +1 -0
  73. package/src/assets/web-panel/assets/{Sla-0B37mxVU.js → Sla-HzU56n1z.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-0V2jE1Bs.js → SpeechSettings-CRle7Jl2.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CYedRzaj.js → SyncSettings-DWadz7Sl.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-D-R1x8vm.js → Tasks-CNVTDGg8.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-BKoHztGT.js → Templates-BBAgvrlM.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant--17nwVNZ.js → Tenant-BsbbvGN8.js} +1 -1
  79. package/src/assets/web-panel/assets/Terminal-uscpZI4q.js +3 -0
  80. package/src/assets/web-panel/assets/{TimelineRenderer-C5jx4AE3.js → TimelineRenderer-mTyjNE6r.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-DYJKYa9t.js → Tokens-Bz97oBgs.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DChpWiQ9.js → Trigger-BTRKC140.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DfWG52EA.js → Trust-B4A_KuyX.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-CMugYcyA.js → UkeySign-DES9TmLU.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-BOSmrHAd.js → VideoEditing-ClXs7Qxp.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-D5g3k3Sa.js → Wallet-IzZqFz-7.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-D-7u4Mkc.js → WebAuthn-BtOP04g_.js} +3 -3
  88. package/src/assets/web-panel/assets/{WorkflowEditor-CFzZMSUJ.js → WorkflowEditor-_0cqnopY.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-VbR6l6mc.js → chat-ax7193uU.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BSZn8EDS.js → colors-Bu4WGJah.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-y74obMEq.js → compact-item-BX1dlTW0.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-C326bjex.js → createContext-CktA_Dj7.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-x877iKKS.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-D_INJQT_.js → hasIn-D5wd817l.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CsovTpx9.js → index-5PYYkr8S.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-9PIVuXqo.js → index-A4uBPQCz.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BjcWKg1S.js → index-BAtBNfkr.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CMHqs3Jb.js → index-BLLsUNDc.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B7SrC-On.js → index-BQtoRyTO.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DUU2606o.js → index-BRfXoRuh.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Iw0FfqUz.js → index-Bad2CYeH.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Dlxh9D8D.js → index-BcGdL3sG.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DuIkfnTF.js → index-C3HmzCLt.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-E_v1ZCTP.js → index-C8DkWyNr.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-4eyH-eC2.js → index-C8tLR2TG.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BnSfnSMP.js → index-CP8ELJBc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DwHSZLXg.js → index-CUForzsG.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C2NyllnW.js → index-CYRE0NJA.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bnua8IX3.js → index-CeVZaJSC.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-KF9Zmqln.js → index-CfzxR61U.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Cr3uuz0C.js → index-Ci3y1ZAv.js} +3 -3
  112. package/src/assets/web-panel/assets/{index-DBkzrpiL.js → index-CjEJjl5C.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-abmn_8Yz.js → index-Cv7bVWJ5.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-qMA4GYdQ.js → index-D-o4Rwjk.js} +1 -1
  115. package/src/assets/web-panel/assets/index-D7pfPouB.js +1 -0
  116. package/src/assets/web-panel/assets/{index-jYaogFE8.js → index-D9op8poE.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-GA80ir_u.js → index-DF1TysIF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CzE53HiB.js → index-DYo9S5OT.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-gINyI1rh.js → index-D_cycOET.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-FQBsuExX.js → index-Db3CpWTr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-IZAWt0H8.js → index-DkD1K-fF.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C327pLbw.js → index-DmPJ6T6F.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DdyjrCAu.js → index-DmVUeWUk.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CfLJ42kv.js → index-Dzdr0zDf.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DkH1JvEt.js → index-PBLzwYnu.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DNMxIsDg.js → index-bFCaM7Eq.js} +1 -1
  127. package/src/assets/web-panel/assets/index-e4i-VLBd.js +1 -0
  128. package/src/assets/web-panel/assets/{index-C-ZCaUJc.js → index-h3KJZr95.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Bfw2hf2s.js → index-iHcPtQ2e.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BJi3UqBt.js → index-jzlzm4VK.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BY8-nCgs.js → index-o14NNnxw.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-nSn2-C4N.js → index-tYPSfGbd.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-4PIeTM--.js → index-ytp2H5Zx.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-DI0Zaf8-.js → initDefaultProps-D2Iut83B.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BZDKSy3Z.js → motion-BuaJVdK9.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DsIW01Vp.js → move-CaEuM92y.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CohkDhsf.js → mtc-parser-wuKNBwys.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-DpiN5rzg.js → omit-7a3eklaq.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-qbuqni3y.js → pickAttrs-Dn4z7KvJ.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-mBky6714.js → placementArrow-DVn_qT4-.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-Bas3eQ2K.js → responsiveObserve-Dodz1OMQ.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-CBx_SKoK.js → slide-CxoGskbp.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BlYguJSV.js → statusUtils-DBh_epQH.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BCYQmRGv.js → styleChecker-RXQ4gG5P.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-Dp5dybLM.js → useFlexGapSupport-1Wxm0HEG.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-DHAMBgA2.js → useFs-CI_ySNxJ.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-W6jsSw76.js → usePersonalDataHub-C6973rZP.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-BkI6aayc.js → vnode-CalynwKr.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-C_IMGueq.js → zoom-CM1cd-Q0.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agenda.js +74 -1
  152. package/src/commands/background-session.js +11 -1
  153. package/src/commands/batch.js +160 -5
  154. package/src/commands/plugin.js +90 -27
  155. package/src/commands/session.js +31 -1
  156. package/src/commands/team.js +212 -6
  157. package/src/data/changelog.json +13 -0
  158. package/src/harness/jsonl-session-store.js +57 -6
  159. package/src/harness/mcp-client.js +65 -43
  160. package/src/lib/agent-batch.js +4 -1
  161. package/src/lib/agent-team/team-worktree.js +3 -0
  162. package/src/lib/async-hook-supervisor.cjs +82 -10
  163. package/src/lib/async-hook-supervisor.js +50 -4
  164. package/src/lib/collaboration-run-store.js +362 -0
  165. package/src/lib/hook-runner.cjs +98 -17
  166. package/src/lib/hook-runner.js +77 -6
  167. package/src/lib/hook-shell-command.cjs +148 -0
  168. package/src/lib/hooks-v2-runtime.js +188 -77
  169. package/src/lib/jsonl-session-store.js +2 -0
  170. package/src/lib/lsp/__tests__/lsp-client.test.js +18 -0
  171. package/src/lib/lsp/lsp-client.js +12 -1
  172. package/src/lib/plugin-monitor-supervisor.js +53 -16
  173. package/src/lib/plugin-runtime/bin.js +1 -1
  174. package/src/lib/plugin-runtime/install.js +249 -57
  175. package/src/lib/plugin-runtime/mcp.js +21 -2
  176. package/src/lib/plugin-runtime/monitors.js +19 -3
  177. package/src/lib/plugin-runtime/sandbox-policy.js +99 -0
  178. package/src/lib/process-execution-broker/index.js +453 -12
  179. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +1347 -0
  180. package/src/lib/process-execution-broker/linux-generic-bwrap.js +1260 -0
  181. package/src/lib/process-execution-broker/platform-sandbox.js +580 -56
  182. package/src/lib/session-usage.js +202 -2
  183. package/src/lib/stream-retry.js +29 -0
  184. package/src/repl/agent-repl.js +32 -3
  185. package/src/runtime/agent-core.js +32 -4
  186. package/src/runtime/headless-runner.js +37 -0
  187. package/src/runtime/headless-stream.js +34 -2
  188. package/src/assets/web-panel/assets/ChatBubbleRenderer-CI_jT0ru.js +0 -1
  189. package/src/assets/web-panel/assets/MobileProjects-VTc3gT1y.js +0 -1
  190. package/src/assets/web-panel/assets/OrderTableRenderer-DdcsOqEz.js +0 -1
  191. package/src/assets/web-panel/assets/RemoteSession-B6EEf8hh.js +0 -5
  192. package/src/assets/web-panel/assets/Skills-DtT6rWOX.js +0 -1
  193. package/src/assets/web-panel/assets/Terminal-Dnkos7vW.js +0 -3
  194. package/src/assets/web-panel/assets/devWarning-D3Bho_6u.js +0 -1
  195. package/src/assets/web-panel/assets/index-5iAFwYx1.js +0 -1
  196. package/src/assets/web-panel/assets/index-BfnB7LPx.js +0 -1
@@ -32,6 +32,33 @@ function toNumber(v) {
32
32
  return Number.isFinite(n) ? n : 0;
33
33
  }
34
34
 
35
+ const MAX_OBSERVED_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
36
+
37
+ function observedDuration(value) {
38
+ if (value === null || value === undefined || value === "") return null;
39
+ const number = Number(value);
40
+ if (!Number.isFinite(number) || number < 0) return null;
41
+ return Math.min(MAX_OBSERVED_DURATION_MS, Math.round(number));
42
+ }
43
+
44
+ function eventDuration(event) {
45
+ return observedDuration(
46
+ event?.data?.duration_ms ??
47
+ event?.data?.durationMs ??
48
+ event?.data?.result?.toolTelemetryRecord?.durationMs,
49
+ );
50
+ }
51
+
52
+ function addObservedDuration(row, durationMs) {
53
+ if (durationMs === null) return;
54
+ row.durationMs = (row.durationMs || 0) + durationMs;
55
+ row.timedCalls = (row.timedCalls || 0) + 1;
56
+ }
57
+
58
+ function addObservedRetry(row) {
59
+ row.retries = (row.retries || 0) + 1;
60
+ }
61
+
35
62
  /**
36
63
  * Extract the optional usage-attribution frame (用量归因) persisted alongside
37
64
  * a token_usage event's data: `{ origin, skill?, subagentId?, role?,
@@ -209,6 +236,56 @@ function _isToolResultError(event) {
209
236
  );
210
237
  }
211
238
 
239
+ /**
240
+ * Aggregate compact `llm_retry` events. Each event represents one failed
241
+ * streaming attempt that the runtime actually retried and includes only a
242
+ * controlled reason plus its elapsed time — never the raw provider error.
243
+ */
244
+ export function aggregateLlmRetries(events) {
245
+ const byReason = new Map();
246
+ const byModel = new Map();
247
+ let totalRetries = 0;
248
+ let durationMs = 0;
249
+
250
+ const add = (map, key, seed, elapsed) => {
251
+ const row = map.get(key) || { ...seed, retries: 0, durationMs: 0 };
252
+ row.retries++;
253
+ row.durationMs += elapsed;
254
+ map.set(key, row);
255
+ };
256
+
257
+ for (const event of events || []) {
258
+ if (event?.type !== "llm_retry") continue;
259
+ const data = event.data || {};
260
+ const elapsed = eventDuration(event) ?? 0;
261
+ const reason =
262
+ typeof data.reason === "string" && data.reason ? data.reason : "unknown";
263
+ const provider =
264
+ typeof data.provider === "string" && data.provider ? data.provider : null;
265
+ const model =
266
+ typeof data.model === "string" && data.model ? data.model : null;
267
+ totalRetries++;
268
+ durationMs += elapsed;
269
+ add(byReason, reason, { reason }, elapsed);
270
+ add(
271
+ byModel,
272
+ `${provider || "?"}/${model || "?"}`,
273
+ { provider, model },
274
+ elapsed,
275
+ );
276
+ }
277
+
278
+ if (totalRetries === 0) return null;
279
+ const byRetriesDesc = (a, b) =>
280
+ b.retries - a.retries || b.durationMs - a.durationMs;
281
+ return {
282
+ totalRetries,
283
+ durationMs,
284
+ byReason: Array.from(byReason.values()).sort(byRetriesDesc),
285
+ byModel: Array.from(byModel.values()).sort(byRetriesDesc),
286
+ };
287
+ }
288
+
212
289
  /**
213
290
  * Aggregate the transcript's tool events: per-tool call/error counts, an MCP
214
291
  * server bucket (`mcp__<server>__*`), and a turn-associated token figure.
@@ -232,6 +309,9 @@ export function aggregateToolCalls(events) {
232
309
  const byPlugin = new Map();
233
310
  let totalCalls = 0;
234
311
  let totalErrors = 0;
312
+ let totalDurationMs = 0;
313
+ let timedCalls = 0;
314
+ let retryCalls = 0;
235
315
 
236
316
  const bumpTool = (name) => {
237
317
  let t = byTool.get(name);
@@ -276,6 +356,8 @@ export function aggregateToolCalls(events) {
276
356
  let turnPlugins = new Set();
277
357
  let turnTokens = 0;
278
358
  let pendingPlugin = null;
359
+ let pendingObservation = null;
360
+ let failedToolsInTurn = new Set();
279
361
  const flushTurn = () => {
280
362
  if (turnTokens > 0) {
281
363
  for (const name of turnTools) bumpTool(name).turnTokens += turnTokens;
@@ -291,6 +373,8 @@ export function aggregateToolCalls(events) {
291
373
  turnPlugins = new Set();
292
374
  turnTokens = 0;
293
375
  pendingPlugin = null;
376
+ pendingObservation = null;
377
+ failedToolsInTurn = new Set();
294
378
  };
295
379
 
296
380
  for (const evt of events || []) {
@@ -301,20 +385,35 @@ export function aggregateToolCalls(events) {
301
385
  }
302
386
  if (evt.type === "tool_call") {
303
387
  const name = evt.data?.tool || "?";
388
+ const durationMs = eventDuration(evt);
389
+ const isRetry = failedToolsInTurn.has(name);
304
390
  totalCalls++;
305
391
  const t = bumpTool(name);
306
392
  t.calls++;
307
393
  const isErr = evt.data?.is_error === true;
394
+ if (durationMs !== null) {
395
+ totalDurationMs += durationMs;
396
+ timedCalls++;
397
+ addObservedDuration(t, durationMs);
398
+ }
399
+ if (isRetry) {
400
+ retryCalls++;
401
+ addObservedRetry(t);
402
+ }
308
403
  if (isErr) {
309
404
  totalErrors++;
310
405
  t.errors++;
311
406
  }
407
+ if (isErr) failedToolsInTurn.add(name);
408
+ else failedToolsInTurn.delete(name);
312
409
  turnTools.add(name);
313
410
  const server = mcpServerOf(name);
314
411
  if (server) {
315
412
  const s = bumpServer(server);
316
413
  s.calls++;
317
414
  if (isErr) s.errors++;
415
+ addObservedDuration(s, durationMs);
416
+ if (isRetry) addObservedRetry(s);
318
417
  turnServers.add(server);
319
418
  }
320
419
  const plugin =
@@ -326,24 +425,43 @@ export function aggregateToolCalls(events) {
326
425
  ? evt.data.plugin_version
327
426
  : null;
328
427
  pendingPlugin = { tool: name, plugin, version: pluginVersion };
428
+ pendingObservation = {
429
+ tool: name,
430
+ durationCounted: durationMs !== null,
431
+ errorCounted: isErr,
432
+ };
329
433
  if (plugin) {
330
434
  const p = bumpPlugin(plugin, pluginVersion);
331
435
  p.calls++;
332
436
  if (isErr) p.errors++;
437
+ addObservedDuration(p, durationMs);
438
+ if (isRetry) addObservedRetry(p);
333
439
  turnPlugins.add(`${plugin}@${pluginVersion || "?"}`);
334
440
  }
335
441
  continue;
336
442
  }
337
443
  if (evt.type === "tool_result") {
338
444
  const name = evt.data?.tool || "?";
445
+ const durationMs = eventDuration(evt);
339
446
  turnTools.add(name);
340
447
  const server = mcpServerOf(name);
341
448
  if (server) turnServers.add(server);
342
- if (_isToolResultError(evt)) {
449
+ const isErr = _isToolResultError(evt);
450
+ const pairedObservation =
451
+ pendingObservation?.tool === name ? pendingObservation : null;
452
+ if (isErr && !pairedObservation?.errorCounted) {
343
453
  totalErrors++;
344
454
  bumpTool(name).errors++;
345
455
  if (server) bumpServer(server).errors++;
346
456
  }
457
+ if (isErr) failedToolsInTurn.add(name);
458
+ else failedToolsInTurn.delete(name);
459
+ if (durationMs !== null && !pairedObservation?.durationCounted) {
460
+ totalDurationMs += durationMs;
461
+ timedCalls++;
462
+ addObservedDuration(bumpTool(name), durationMs);
463
+ if (server) addObservedDuration(bumpServer(server), durationMs);
464
+ }
347
465
  const extracted = extractPluginUsageAttribution(evt.data?.result);
348
466
  const plugin =
349
467
  typeof evt.data?.plugin === "string" && evt.data.plugin
@@ -360,10 +478,14 @@ export function aggregateToolCalls(events) {
360
478
  pendingPlugin?.plugin === plugin &&
361
479
  (pendingPlugin?.version || null) === (pluginVersion || null);
362
480
  if (!countedByCall) p.calls++;
363
- if (_isToolResultError(evt) && !countedByCall) p.errors++;
481
+ if (isErr && !countedByCall) p.errors++;
482
+ if (!countedByCall || !pairedObservation?.durationCounted) {
483
+ addObservedDuration(p, durationMs);
484
+ }
364
485
  turnPlugins.add(`${plugin}@${pluginVersion || "?"}`);
365
486
  }
366
487
  pendingPlugin = null;
488
+ pendingObservation = null;
367
489
  continue;
368
490
  }
369
491
  const u = extractUsage(evt);
@@ -375,6 +497,8 @@ export function aggregateToolCalls(events) {
375
497
  return {
376
498
  totalCalls,
377
499
  totalErrors,
500
+ ...(timedCalls > 0 ? { totalDurationMs, timedCalls } : {}),
501
+ ...(retryCalls > 0 ? { retryCalls } : {}),
378
502
  byTool: Array.from(byTool.values()).sort(byCallsDesc),
379
503
  byMcpServer: Array.from(byServer.values()).sort(byCallsDesc),
380
504
  byPlugin: Array.from(byPlugin.values()).sort(byCallsDesc),
@@ -386,9 +510,11 @@ export function aggregateToolCalls(events) {
386
510
  * roll-ups + the tool/MCP call aggregation. Pure.
387
511
  */
388
512
  export function sessionAttribution(events) {
513
+ const retries = aggregateLlmRetries(events);
389
514
  return {
390
515
  ...aggregateUsageAttribution(events),
391
516
  tools: aggregateToolCalls(events),
517
+ ...(retries ? { retries } : {}),
392
518
  };
393
519
  }
394
520
 
@@ -519,8 +645,15 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
519
645
  const gTool = new Map();
520
646
  const gServer = new Map();
521
647
  const gPlugin = new Map();
648
+ const gRetryReason = new Map();
649
+ const gRetryModel = new Map();
522
650
  let gToolCalls = 0;
523
651
  let gToolErrors = 0;
652
+ let gToolDurationMs = 0;
653
+ let gTimedToolCalls = 0;
654
+ let gToolRetryCalls = 0;
655
+ let gLlmRetries = 0;
656
+ let gLlmRetryDurationMs = 0;
524
657
  const mergeRow = (map, key, row, seed) => {
525
658
  const entry = map.get(key) || { ...seed, ..._newSums() };
526
659
  _addSums(entry, row);
@@ -545,6 +678,9 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
545
678
  const tools = a.tools || {};
546
679
  gToolCalls += tools.totalCalls || 0;
547
680
  gToolErrors += tools.totalErrors || 0;
681
+ gToolDurationMs += tools.totalDurationMs || 0;
682
+ gTimedToolCalls += tools.timedCalls || 0;
683
+ gToolRetryCalls += tools.retryCalls || 0;
548
684
  for (const row of tools.byTool || []) {
549
685
  const entry = gTool.get(row.tool) || {
550
686
  tool: row.tool,
@@ -556,6 +692,12 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
556
692
  entry.calls += row.calls || 0;
557
693
  entry.errors += row.errors || 0;
558
694
  entry.turnTokens += row.turnTokens || 0;
695
+ if (row.durationMs !== undefined) {
696
+ entry.durationMs = (entry.durationMs || 0) + (row.durationMs || 0);
697
+ entry.timedCalls = (entry.timedCalls || 0) + (row.timedCalls || 0);
698
+ }
699
+ if (row.retries !== undefined)
700
+ entry.retries = (entry.retries || 0) + (row.retries || 0);
559
701
  gTool.set(row.tool, entry);
560
702
  }
561
703
  for (const row of tools.byMcpServer || []) {
@@ -568,6 +710,12 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
568
710
  entry.calls += row.calls || 0;
569
711
  entry.errors += row.errors || 0;
570
712
  entry.turnTokens += row.turnTokens || 0;
713
+ if (row.durationMs !== undefined) {
714
+ entry.durationMs = (entry.durationMs || 0) + (row.durationMs || 0);
715
+ entry.timedCalls = (entry.timedCalls || 0) + (row.timedCalls || 0);
716
+ }
717
+ if (row.retries !== undefined)
718
+ entry.retries = (entry.retries || 0) + (row.retries || 0);
571
719
  gServer.set(row.server, entry);
572
720
  }
573
721
  for (const row of tools.byPlugin || []) {
@@ -582,10 +730,45 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
582
730
  entry.calls += row.calls || 0;
583
731
  entry.errors += row.errors || 0;
584
732
  entry.turnTokens += row.turnTokens || 0;
733
+ if (row.durationMs !== undefined) {
734
+ entry.durationMs = (entry.durationMs || 0) + (row.durationMs || 0);
735
+ entry.timedCalls = (entry.timedCalls || 0) + (row.timedCalls || 0);
736
+ }
737
+ if (row.retries !== undefined)
738
+ entry.retries = (entry.retries || 0) + (row.retries || 0);
585
739
  gPlugin.set(key, entry);
586
740
  }
741
+ const retries = a.retries || null;
742
+ if (retries) {
743
+ gLlmRetries += retries.totalRetries || 0;
744
+ gLlmRetryDurationMs += retries.durationMs || 0;
745
+ for (const row of retries.byReason || []) {
746
+ const entry = gRetryReason.get(row.reason) || {
747
+ reason: row.reason,
748
+ retries: 0,
749
+ durationMs: 0,
750
+ };
751
+ entry.retries += row.retries || 0;
752
+ entry.durationMs += row.durationMs || 0;
753
+ gRetryReason.set(row.reason, entry);
754
+ }
755
+ for (const row of retries.byModel || []) {
756
+ const key = `${row.provider || "?"}/${row.model || "?"}`;
757
+ const entry = gRetryModel.get(key) || {
758
+ provider: row.provider || null,
759
+ model: row.model || null,
760
+ retries: 0,
761
+ durationMs: 0,
762
+ };
763
+ entry.retries += row.retries || 0;
764
+ entry.durationMs += row.durationMs || 0;
765
+ gRetryModel.set(key, entry);
766
+ }
767
+ }
587
768
  }
588
769
  const byCallsDesc = (a, b) => b.calls - a.calls;
770
+ const byRetriesDesc = (a, b) =>
771
+ b.retries - a.retries || b.durationMs - a.durationMs;
589
772
 
590
773
  return {
591
774
  sessions: perSession,
@@ -601,10 +784,27 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
601
784
  tools: {
602
785
  totalCalls: gToolCalls,
603
786
  totalErrors: gToolErrors,
787
+ ...(gTimedToolCalls > 0
788
+ ? {
789
+ totalDurationMs: gToolDurationMs,
790
+ timedCalls: gTimedToolCalls,
791
+ }
792
+ : {}),
793
+ ...(gToolRetryCalls > 0 ? { retryCalls: gToolRetryCalls } : {}),
604
794
  byTool: Array.from(gTool.values()).sort(byCallsDesc),
605
795
  byMcpServer: Array.from(gServer.values()).sort(byCallsDesc),
606
796
  byPlugin: Array.from(gPlugin.values()).sort(byCallsDesc),
607
797
  },
798
+ ...(gLlmRetries > 0
799
+ ? {
800
+ retries: {
801
+ totalRetries: gLlmRetries,
802
+ durationMs: gLlmRetryDurationMs,
803
+ byReason: Array.from(gRetryReason.values()).sort(byRetriesDesc),
804
+ byModel: Array.from(gRetryModel.values()).sort(byRetriesDesc),
805
+ },
806
+ }
807
+ : {}),
608
808
  },
609
809
  };
610
810
  }
@@ -81,3 +81,32 @@ export function isRetryableStreamError(err, signal) {
81
81
  msg,
82
82
  );
83
83
  }
84
+
85
+ /**
86
+ * Reduce a retryable stream failure to a small, secret-free reason vocabulary
87
+ * suitable for persisted usage telemetry. Never persist the provider's raw
88
+ * error message: it can contain a request URL, tenant id, or proxy details.
89
+ */
90
+ export function classifyStreamRetryReason(err) {
91
+ const code = String(err?.code || err?.cause?.code || "").toUpperCase();
92
+ if (code === "ETIMEDOUT") return "timeout";
93
+ if (["EAI_AGAIN", "ENOTFOUND"].includes(code)) return "dns";
94
+ if (code === "ECONNREFUSED") return "connection_refused";
95
+ if (code === "ENETUNREACH") return "network_unreachable";
96
+ if (["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"].includes(code)) {
97
+ return "connection_reset";
98
+ }
99
+ const msg = String(err?.message || err || "").toLowerCase();
100
+ if (/timed?\s*out|etimedout/.test(msg)) return "timeout";
101
+ if (/eai_again|enotfound|dns/.test(msg)) return "dns";
102
+ if (/econnrefused/.test(msg)) return "connection_refused";
103
+ if (/network unreachable|enetunreach/.test(msg)) return "network_unreachable";
104
+ if (
105
+ /econnreset|socket hang ?up|terminated|fetch failed|network error|premature close/.test(
106
+ msg,
107
+ )
108
+ ) {
109
+ return "connection_reset";
110
+ }
111
+ return "unknown";
112
+ }
@@ -48,10 +48,12 @@ import {
48
48
  appendCompactEvent,
49
49
  appendTokenUsage,
50
50
  appendToolCallCompact,
51
+ appendLlmRetryCompact,
51
52
  appendEvent,
52
53
  rebuildMessages,
53
54
  sessionExists,
54
55
  } from "../harness/jsonl-session-store.js";
56
+ import { classifyStreamRetryReason } from "../lib/stream-retry.js";
55
57
  import { CLISkillLoader } from "../lib/skill-loader.js";
56
58
  import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
57
59
  import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
@@ -353,13 +355,25 @@ export async function agentLoop(messages, options) {
353
355
  chalk.gray(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`),
354
356
  );
355
357
  } else if (event.type === "tool-executing") {
356
- _lastExec = { tool: event.tool, args: event.args };
358
+ _lastExec = {
359
+ tool: event.tool,
360
+ args: event.args,
361
+ startedAt: options.now ? options.now() : Date.now(),
362
+ };
357
363
  process.stdout.write(
358
364
  chalk.gray(
359
365
  ` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
360
366
  ),
361
367
  );
362
368
  } else if (event.type === "tool-result") {
369
+ const durationMs =
370
+ event.result?.toolTelemetryRecord?.durationMs ??
371
+ (_lastExec?.tool === event.tool
372
+ ? Math.max(
373
+ 0,
374
+ (options.now ? options.now() : Date.now()) - _lastExec.startedAt,
375
+ )
376
+ : undefined);
363
377
  // 用量归因: persist a compact tool_call record (name + error flag +
364
378
  // skill/plugin hints — never args, which can carry whole file bodies)
365
379
  // so `cc session usage --by tool|mcp|plugin` and `cc insights` can
@@ -375,6 +389,7 @@ export async function agentLoop(messages, options) {
375
389
  ? _lastExec.args?.skill_name
376
390
  : undefined,
377
391
  ...extractPluginUsageAttribution(event.result),
392
+ durationMs,
378
393
  });
379
394
  } catch (_e) {
380
395
  // persistence is best-effort — never break the turn
@@ -5570,12 +5585,26 @@ export async function startAgentRepl(options = {}) {
5570
5585
  // streaming call hits a transient connection drop and retries, tell the
5571
5586
  // user instead of leaving them staring at a silent pause. To stderr so
5572
5587
  // it never corrupts the streamed answer on stdout.
5573
- onStreamRetry: (attempt) =>
5588
+ onStreamRetry: (attempt, error, telemetry = {}) => {
5574
5589
  process.stderr.write(
5575
5590
  chalk.dim(
5576
5591
  ` ⟳ connection dropped — retrying (attempt ${attempt})…\n`,
5577
5592
  ),
5578
- ),
5593
+ );
5594
+ if (useJsonl && sessionId) {
5595
+ try {
5596
+ appendLlmRetryCompact(sessionId, {
5597
+ attempt,
5598
+ durationMs: telemetry.durationMs,
5599
+ provider: telemetry.provider || provider,
5600
+ model: telemetry.model || activeModel,
5601
+ reason: classifyStreamRetryReason(error),
5602
+ });
5603
+ } catch {
5604
+ /* usage telemetry is best-effort */
5605
+ }
5606
+ }
5607
+ },
5579
5608
  // Stream-stall hint (Claude-Code 2.1.185): the connection is alive but
5580
5609
  // the API has gone silent mid-response — tell the user we're still
5581
5610
  // waiting instead of leaving a frozen spinner. stderr so it never
@@ -6742,7 +6742,15 @@ export async function chatWithTools(rawMessages, options) {
6742
6742
  options.streamStallMs,
6743
6743
  options.streamStallTimeoutMs,
6744
6744
  ),
6745
- { signal, onRetry: options.onStreamRetry },
6745
+ {
6746
+ signal,
6747
+ onRetry: (attempt, error, telemetry) =>
6748
+ options.onStreamRetry?.(attempt, error, {
6749
+ ...telemetry,
6750
+ provider: "ollama",
6751
+ model: model || null,
6752
+ }),
6753
+ },
6746
6754
  );
6747
6755
  }
6748
6756
  const response = await fetch(apiUrl, {
@@ -6868,7 +6876,15 @@ export async function chatWithTools(rawMessages, options) {
6868
6876
  options.streamStallMs,
6869
6877
  options.streamStallTimeoutMs,
6870
6878
  ),
6871
- { signal, onRetry: options.onStreamRetry },
6879
+ {
6880
+ signal,
6881
+ onRetry: (attempt, error, telemetry) =>
6882
+ options.onStreamRetry?.(attempt, error, {
6883
+ ...telemetry,
6884
+ provider: "anthropic",
6885
+ model: effModel,
6886
+ }),
6887
+ },
6872
6888
  );
6873
6889
  }
6874
6890
 
@@ -6972,7 +6988,15 @@ export async function chatWithTools(rawMessages, options) {
6972
6988
  options.streamStallMs,
6973
6989
  options.streamStallTimeoutMs,
6974
6990
  ),
6975
- { signal, onRetry: options.onStreamRetry },
6991
+ {
6992
+ signal,
6993
+ onRetry: (attempt, error, telemetry) =>
6994
+ options.onStreamRetry?.(attempt, error, {
6995
+ ...telemetry,
6996
+ provider,
6997
+ model: model || defaultModels[provider] || "gpt-4o-mini",
6998
+ }),
6999
+ },
6976
7000
  );
6977
7001
  }
6978
7002
 
@@ -7171,8 +7195,10 @@ export async function _retryStreamingChat(streamFn, opts = {}) {
7171
7195
  const base = opts.baseDelayMs ?? STREAM_RETRY_BASE_MS;
7172
7196
  const signal = opts.signal;
7173
7197
  const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
7198
+ const now = typeof opts.now === "function" ? opts.now : Date.now;
7174
7199
  let attempt = 0;
7175
7200
  for (;;) {
7201
+ const attemptStartedAt = now();
7176
7202
  try {
7177
7203
  return await streamFn();
7178
7204
  } catch (err) {
@@ -7181,7 +7207,9 @@ export async function _retryStreamingChat(streamFn, opts = {}) {
7181
7207
  attempt++;
7182
7208
  if (typeof opts.onRetry === "function") {
7183
7209
  try {
7184
- opts.onRetry(attempt, err);
7210
+ opts.onRetry(attempt, err, {
7211
+ durationMs: Math.max(0, now() - attemptStartedAt),
7212
+ });
7185
7213
  } catch {
7186
7214
  /* the retry notice is best-effort; never let it mask the retry */
7187
7215
  }
@@ -43,6 +43,7 @@ import {
43
43
  appendAssistantMessage as jsonlAppendAssistantMessage,
44
44
  appendTokenUsage as jsonlAppendTokenUsage,
45
45
  appendToolCallCompact as jsonlAppendToolCallCompact,
46
+ appendLlmRetryCompact as jsonlAppendLlmRetryCompact,
46
47
  appendCompactEvent as jsonlAppendCompactEvent,
47
48
  appendEvent as jsonlAppendEvent,
48
49
  readEvents as jsonlReadEvents,
@@ -51,6 +52,7 @@ import {
51
52
  getLastSessionId as jsonlGetLastSessionId,
52
53
  verifySession as jsonlVerifySession,
53
54
  } from "../harness/jsonl-session-store.js";
55
+ import { classifyStreamRetryReason } from "../lib/stream-retry.js";
54
56
  import {
55
57
  SideEffectLedger,
56
58
  reconcileSideEffects,
@@ -503,6 +505,8 @@ export async function runAgentHeadless(options = {}, deps = {}) {
503
505
  appendTokenUsage: deps.appendTokenUsage || jsonlAppendTokenUsage,
504
506
  appendToolCallCompact:
505
507
  deps.appendToolCallCompact || jsonlAppendToolCallCompact,
508
+ appendLlmRetryCompact:
509
+ deps.appendLlmRetryCompact || jsonlAppendLlmRetryCompact,
506
510
  appendCompactEvent: deps.appendCompactEvent || jsonlAppendCompactEvent,
507
511
  appendEvent: deps.appendEvent || jsonlAppendEvent,
508
512
  readEvents: deps.readEvents || jsonlReadEvents,
@@ -1626,6 +1630,22 @@ export async function runAgentHeadless(options = {}, deps = {}) {
1626
1630
  // while a human watching a long `cc agent -p` run learns we're still waiting
1627
1631
  // and, when a hard inactivity timeout is set, when it will auto-retry. Plain
1628
1632
  // text (no chalk) since headless stderr is frequently piped/non-TTY.
1633
+ onStreamRetry: (attempt, error, telemetry = {}) => {
1634
+ writeErr(` ⟳ connection dropped — retrying (attempt ${attempt})…\n`);
1635
+ if (persist) {
1636
+ try {
1637
+ store.appendLlmRetryCompact(sessionId, {
1638
+ attempt,
1639
+ durationMs: telemetry.durationMs,
1640
+ provider: telemetry.provider || provider,
1641
+ model: telemetry.model || model,
1642
+ reason: classifyStreamRetryReason(error),
1643
+ });
1644
+ } catch {
1645
+ /* usage telemetry is best-effort */
1646
+ }
1647
+ }
1648
+ },
1629
1649
  onStall: (ms, timeoutMs) => {
1630
1650
  const silent = Math.round(ms / 1000);
1631
1651
  const retryIn = timeoutMs > ms ? Math.round((timeoutMs - ms) / 1000) : 0;
@@ -1678,6 +1698,11 @@ export async function runAgentHeadless(options = {}, deps = {}) {
1678
1698
 
1679
1699
  const startedAt = deps.now ? deps.now() : Date.now();
1680
1700
  const toolCalls = [];
1701
+ // The core normally attaches a precise toolTelemetryRecord on success.
1702
+ // Keep an event-boundary fallback for denials/early throws that settle
1703
+ // before that record is produced. Tools are serial in this loop, so one
1704
+ // timestamp is sufficient and never leaks into the result envelope.
1705
+ let currentToolStartedAt = null;
1681
1706
  // Policy denials (blocked tool calls) collected for an end-of-run summary,
1682
1707
  // so a non-interactive run surfaces what got blocked the way the REPL's
1683
1708
  // `/permissions denials` does (Claude-Code 2.1.193 denial reasons).
@@ -1970,6 +1995,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
1970
1995
  break;
1971
1996
  }
1972
1997
  case "tool-executing": {
1998
+ currentToolStartedAt = deps.now ? deps.now() : Date.now();
1973
1999
  const line = ` [${event.tool}] ${formatToolArgs(event.tool, event.args)}`;
1974
2000
  if (isText) writeErr(line + "\n");
1975
2001
  emitStream({
@@ -2057,11 +2083,21 @@ export async function runAgentHeadless(options = {}, deps = {}) {
2057
2083
  if (toolCalls.length > 0) {
2058
2084
  const settledCall = toolCalls[toolCalls.length - 1];
2059
2085
  settledCall.is_error = Boolean(err);
2086
+ settledCall.durationMs =
2087
+ event.result?.toolTelemetryRecord?.durationMs ??
2088
+ (currentToolStartedAt === null
2089
+ ? undefined
2090
+ : Math.max(
2091
+ 0,
2092
+ (deps.now ? deps.now() : Date.now()) -
2093
+ currentToolStartedAt,
2094
+ ));
2060
2095
  Object.assign(
2061
2096
  settledCall,
2062
2097
  extractPluginUsageAttribution(event.result),
2063
2098
  );
2064
2099
  }
2100
+ currentToolStartedAt = null;
2065
2101
  // Track policy denials (not plain tool failures) for the end-of-run
2066
2102
  // summary. The preceding tool-executing pushed the args.
2067
2103
  if (err) {
@@ -2540,6 +2576,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
2540
2576
  tc.tool === "run_skill" ? tc.args?.skill_name || null : undefined,
2541
2577
  plugin: tc.plugin || undefined,
2542
2578
  pluginVersion: tc.pluginVersion || undefined,
2579
+ durationMs: tc.durationMs,
2543
2580
  });
2544
2581
  }
2545
2582
  // Attributed child-loop usage first (chronology: it happened during