chainlesschain 0.163.6 → 0.163.7

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 (218) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DDIUofA8.js → AIOps-axqQfwv8.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Bm-5xYRn.js → ActionButton-DV-Ro0By.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-D5UH4ljI.js → Analytics-Bt72WEVu.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D_ifjOOP.js → AppLayout-BtepG55E.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BY1mXdC7.js → Artifacts-B7KaJ7S_.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-Bujr4Ml3.js → Audit-FNFaUg7A.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-sioKSTcC.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DDOAK8RK.js → Backup-CC7YBhyj.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CtCsVVke.js → BaseInput-RkgTNVtj.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DpNj0vnd.js → Chat-hHEmGZy-.js} +4 -4
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZta3Ftr.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DoHVNSXq.js → Checkbox-z5wXOQP1.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-DlVl7Qmv.js → Codegen-DVaDJnxy.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-GIqoPYty.js → Col-CJmaRDR5.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CIAPEVji.js → Community-DF29_2bI.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BN7JiI_h.js → Compact-DtkQkBzn.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CqUWCM9B.js → Compliance-xoHDxp7I.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-A4iPp7A4.js → Cowork-BUm-R7ht.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-B2zeXXWb.js → Cron-Cgl14ZkB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BR2xmWr8.js → Crosschain-BNcbmoqh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BzFDjrIv.js → DID-Bwja0Zo9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-DKtDo3yA.js → Dashboard-Bk5XjEdJ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-RNx43_YG.js → Dropdown-Ci36Oitq.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-SwMt4Rcy.js → EmailListRenderer--U04ZN4q.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CsYebZ4X.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BmRKvBTy.js → Federation-C4KghYn7.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CVxGTA-0.js → FormItemContext-QL7x_Q_0.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-x2iXpyB7.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DfqbkiOP.js → Git-2HassK5k.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-RuHyyj5O.js → Governance-DoBIp7oe.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-Bn7uZ7rI.js → Inference--W01QhZ-.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DlP7XefZ.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BYNdOiCd.js → Logs-BBJ4yAel.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-aMtgN-R6.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-JQYmDqM8.js → Marketplace-D5lNngSI.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-Cyx4yZ-9.js → McpTools-CkEVOMKH.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-VDuGZTN2.js → Memory-B1Ow1xVE.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-_yKo97gi.js → MobileBridge-SgYbUfb6.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CXRXW3q7.js → MobileProjects-BWdOvQAj.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-acTGtfLH.js → Mtc-DcTXSSma.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-ClpjXfkY.js → MtcAudit-Dt3WG4oj.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-_6Tn3o96.js → Multisig-B9B38UZx.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CK7pbZzP.js → NLProgramming-B0S4GGvG.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Czy6V_P8.js → Notes-DZ5U58Ng.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Cqx-UVv3.js → NotificationSettings-PjL5ZW34.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-C69WDxGy.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DFnA9vLQ.js → Organization-Cv1GYs-c.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-ReZkk2yT.js → Overflow-Bbkp_PL3.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-DzjSI0aI.js → P2P-BD8QUgoZ.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DKTf5xIP.js → PdhVaultBrowser-BO-2PeaB.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DhQ5klhR.js → Permissions-D_OPVa02.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-CUL1iHrW.js → PersonalDataHub-mh-pEmaB.js} +4 -4
  54. package/src/assets/web-panel/assets/{Pipeline-DU88iu4N.js → Pipeline-6JC2EDdb.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-Bw-sYfyK.js → Privacy-CUshzUAG.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-Bq3Q5y0z.js → ProjectInit-Cs2Aalri.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-C8spzpho.js → ProjectSettings-1ak49tcj.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BV4oApsm.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-Bf4eTP9W.js → Providers-CXXxvafH.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Dme9BDU1.js → QrScannerModal-PTcDsF58.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CIXfv1rE.js → QuickAsk-CDwucrJw.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CIxvdeZ-.js → Recommend-CJlLVRXR.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C6k7MExI.js → RemoteSession-DlN-ETsO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-CISDz2bl.js → Reputation-DI23LR6m.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-CdnDxXyf.js → Row-Cl7UQMrM.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C-QpRHsl.js → RssFeed-CexHNfh8.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DBjaGzSn.js → Search-BneBWvRN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CQKukOeE.js → Security-DX7Phgrp.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-DZz86jWb.js → Services-BWXO8Mte.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-jb8RMK0d.js → Skeleton-DCyOjKPJ.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CdY76fZG.js → Skills-CXqERsTB.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-C_CSiZX9.js → Sla-bhbrbz8E.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-3IPU0HBZ.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-qvimGO0O.js → SyncSettings-DBNTQLbM.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Dng5LPJC.js → Tasks-quP0bkN-.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Bwqq5Wll.js → Templates-BouWmgho.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DNWuwP7Z.js → Tenant-qv0D9s7z.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-B-6Othpp.js → Terminal-BxvbE8BB.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-DKpKZfzx.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-DzfGMd2J.js → Tokens-CFMKKyVG.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-BXwUi74J.js → Trigger-L8as3UuJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BNzUq4ui.js → Trust-MxmA4VZG.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BYugxww4.js → UkeySign-C0VvJYRi.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DdauhtSJ.js → VideoEditing-Bf5QZj8N.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-CSC9bCe_.js → Wallet-BIJ9riRz.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Q8AN3bTO.js → WebAuthn-B8p9Mhkw.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-d-LBGCuA.js → WorkflowEditor-BVsuaPen.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-BLVDCgsZ.js → chat-BfkjoKpV.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BZlPSEHl.js → colors-DeAlsWK8.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-IWseP6R6.js → compact-item-Cs97bz5j.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-DnVRV3Ao.js → createContext-BqbG60LH.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DLQZA_Wn.js → hasIn-i_RY0b1X.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B_N4-Gq9.js → index-0-agYlWL.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DB0MJgF_.js → index-79V_zv1s.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BOZJBTWZ.js → index-B-3tEpTc.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DUq0c7gq.js → index-B-JVTrpr.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Dy0R5ZED.js → index-B1CbOH1i.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Bxp7uDD8.js → index-B5UkyA3c.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DpVrK6fr.js → index-BESLFt4q.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CT9vVN-X.js → index-BOVkf1K3.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DOoQpuQY.js → index-BYUVuwKt.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CINJQEj6.js → index-BcUXAB2s.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CarMTIyQ.js → index-BkBi5TmC.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C5dgnilC.js → index-Bm7lLR4c.js} +1 -1
  106. package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
  107. package/src/assets/web-panel/assets/{index-CbQNL0ue.js → index-BuLQobzW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CpBaJ3TD.js → index-CE2VtTlp.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-jWb70ZgZ.js → index-CED2PL_y.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BEu2lMBy.js → index-CG3PRyAL.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DocEhinm.js → index-CIG8fQ2_.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D_LQodeJ.js → index-COdGA-8n.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DZpYeqrh.js → index-CeWoNHau.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-gt8bNefW.js → index-Cf3WCLmy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-N_GoWZX4.js → index-CgsqdnRV.js} +1 -1
  116. package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
  117. package/src/assets/web-panel/assets/{index-CZDekj7T.js → index-D2B4VPyP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-YSw_gxlZ.js → index-D9evtvg2.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DTbxp4UH.js → index-DUUrv141.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Bbj2NS_I.js → index-DiSkx4UB.js} +3 -3
  121. package/src/assets/web-panel/assets/{index-BZs5bBnk.js → index-Dk2qouAz.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DalA0TKC.js → index-Dk7-v4hP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-1jeAudHd.js → index-DmbfhJUP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C1ifZtXo.js → index-DqBgBpgp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BhA3QEni.js → index-PT_5sBMA.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BSjilZ9L.js → index-U4f5izHz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C2FqfMvU.js → index-YXgLng0K.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-COev0W2g.js → index-ZxO_-E8a.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-M7wxfAbm.js → index-dIHr4B_u.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DjWnONdy.js → index-gyFpvL7l.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Dkwsi1Kn.js → index-s1opAL-M.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-hwVDeoC0.js → index-yObj1O4n.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Co67Kqmb.js → initDefaultProps-C9h9yKL3.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BMOxQSF_.js → motion-BvmLsPdu.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-C5fWDw-N.js → move-CoL4ekU1.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-BJevpL0e.js → mtc-parser-C4pMy6xV.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-BCzNU8s6.js → omit-DPuJfNxx.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-CybWvDPk.js → pickAttrs-D0KEeaK2.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BMWS9HnW.js → placementArrow-BpkyINxt.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BoL2UnGY.js → responsiveObserve-DQI_wOq8.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DWNfXraU.js → slide-DYfdDiRg.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BquTM3tX.js → statusUtils-B5dtvb0D.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BEkCleim.js → styleChecker-CF310mSj.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-Oecnq7EA.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CkDffJ4F.js → useFs-C7_iyvJK.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-qbvKXesB.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-C6v8n6mA.js → vnode-D3dsi0lZ.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-4CTeFILV.js → zoom-WdbOlvdQ.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-help-index.json +2 -2
  151. package/src/commands/agent.js +90 -1
  152. package/src/commands/chat.js +11 -2
  153. package/src/commands/compact.js +106 -23
  154. package/src/commands/loop.js +28 -10
  155. package/src/commands/routine.js +1 -1
  156. package/src/commands/scheduler-daemon.js +359 -2
  157. package/src/commands/session-observability.js +305 -0
  158. package/src/commands/session.js +38 -0
  159. package/src/data/changelog.json +14 -1
  160. package/src/gateways/ws/ws-agent-handler.js +439 -5
  161. package/src/harness/jsonl-session-store.js +463 -125
  162. package/src/harness/prompt-compressor.js +86 -13
  163. package/src/harness/provider-backed-compaction.js +100 -27
  164. package/src/lib/advisor-runtime.js +3 -1
  165. package/src/lib/agent-schedule-store.js +414 -4
  166. package/src/lib/agent-team/team-adjudication.js +40 -1
  167. package/src/lib/automation-engine.js +494 -0
  168. package/src/lib/background-agent-supervisor.js +793 -215
  169. package/src/lib/background-command-argv.js +24 -0
  170. package/src/lib/background-interaction-journal.js +361 -52
  171. package/src/lib/background-interaction-resolver.js +32 -0
  172. package/src/lib/background-launch-profile.js +2 -0
  173. package/src/lib/causal-observability.js +2661 -0
  174. package/src/lib/chat-session-admission.js +76 -0
  175. package/src/lib/cost-budget.js +5 -7
  176. package/src/lib/cowork/ab-comparator-cli.js +11 -0
  177. package/src/lib/cowork/debate-review-cli.js +12 -0
  178. package/src/lib/cowork-adapter.js +159 -38
  179. package/src/lib/cowork-cron.js +357 -1
  180. package/src/lib/cowork-task-runner.js +112 -17
  181. package/src/lib/delivery-coordinator.js +142 -0
  182. package/src/lib/direct-model-usage.js +121 -0
  183. package/src/lib/file-lines.js +183 -47
  184. package/src/lib/llm-pricing.js +12 -17
  185. package/src/lib/loop.js +26 -2
  186. package/src/lib/routine-store.js +380 -21
  187. package/src/lib/runtime-usage-ledger.js +182 -0
  188. package/src/lib/scheduler-kernel/agenda-adapter.js +308 -3
  189. package/src/lib/scheduler-kernel/automation-adapter.js +410 -8
  190. package/src/lib/scheduler-kernel/automation-event-adapter.js +36 -0
  191. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +285 -3
  192. package/src/lib/scheduler-kernel/loop-adapter.js +541 -6
  193. package/src/lib/scheduler-kernel/migration-admin.js +346 -0
  194. package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
  195. package/src/lib/scheduler-kernel/routine-adapter.js +394 -20
  196. package/src/lib/scheduler-kernel/runtime.js +63 -1
  197. package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
  198. package/src/lib/scheduler-kernel/store.js +2586 -85
  199. package/src/lib/session-host-lease.js +171 -2
  200. package/src/lib/session-host-snapshot.js +14 -3
  201. package/src/lib/session-persistence-failure.js +1 -0
  202. package/src/lib/session-tail.js +179 -34
  203. package/src/lib/session-transcript-structure.js +96 -0
  204. package/src/lib/sub-agent-context.js +328 -22
  205. package/src/lib/ws-agent-handler.js +4 -1
  206. package/src/repl/agent-repl.js +732 -91
  207. package/src/repl/chat-repl.js +5 -1
  208. package/src/runtime/agent-core.js +1272 -207
  209. package/src/runtime/fallback-model.js +5 -0
  210. package/src/runtime/headless-runner.js +324 -82
  211. package/src/runtime/headless-stream.js +232 -26
  212. package/src/runtime/policies/agent-policy.js +4 -0
  213. package/src/workers/background-agent-worker.js +403 -96
  214. package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +0 -1
  215. package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +0 -1
  216. package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +0 -1
  217. package/src/assets/web-panel/assets/index-Zb2MeDSy.js +0 -1
  218. package/src/assets/web-panel/assets/index-fikqml4k.js +0 -1
@@ -2857,6 +2857,12 @@ export async function executeTool(name, args, context = {}) {
2857
2857
  // 用量归因: shared per-run sink for child-loop (sub-agent / isolated
2858
2858
  // skill) token usage, drained by agentLoop as attributed events.
2859
2859
  subAgentUsageSink: context.subAgentUsageSink || null,
2860
+ strictUsageTelemetry: context.strictUsageTelemetry === true,
2861
+ onUsageBoundary: context.onUsageBoundary || null,
2862
+ onUsageSettlement: context.onUsageSettlement || null,
2863
+ onToolCallBoundary: context.onToolCallBoundary || null,
2864
+ onToolCallSettlement: context.onToolCallSettlement || null,
2865
+ backgroundUsageFailureState: context.backgroundUsageFailureState || null,
2860
2866
  planManager,
2861
2867
  permissionRules: context.permissionRules || null,
2862
2868
  effectiveAllowedToolNames: context.effectiveAllowedToolNames ?? null,
@@ -4259,6 +4265,12 @@ async function executeToolInner(
4259
4265
  signal = null,
4260
4266
  backgroundSubAgents = null,
4261
4267
  subAgentUsageSink = null,
4268
+ strictUsageTelemetry = false,
4269
+ onUsageBoundary = null,
4270
+ onUsageSettlement = null,
4271
+ onToolCallBoundary = null,
4272
+ onToolCallSettlement = null,
4273
+ backgroundUsageFailureState = null,
4262
4274
  toolAdmission = null,
4263
4275
  unattendedActionPolicy = null,
4264
4276
  managedCheckpoint = false,
@@ -5715,6 +5727,12 @@ async function executeToolInner(
5715
5727
  signal,
5716
5728
  backgroundSubAgents,
5717
5729
  subAgentUsageSink,
5730
+ strictUsageTelemetry,
5731
+ onUsageBoundary,
5732
+ onUsageSettlement,
5733
+ onToolCallBoundary,
5734
+ onToolCallSettlement,
5735
+ backgroundUsageFailureState,
5718
5736
  toolAdmission,
5719
5737
  }),
5720
5738
  );
@@ -6597,7 +6615,75 @@ async function executeToolInner(
6597
6615
  const skillUsageSink = Array.isArray(subAgentUsageSink)
6598
6616
  ? subAgentUsageSink
6599
6617
  : null;
6618
+ if (strictUsageTelemetry === true) {
6619
+ _assertStrictUsageObserver(onUsageBoundary, "boundary");
6620
+ _assertStrictUsageObserver(onUsageSettlement, "settlement");
6621
+ }
6600
6622
  let skillSubRef = null;
6623
+ const projectSkillUsage = (event) =>
6624
+ event && event.attribution
6625
+ ? event
6626
+ : {
6627
+ ...event,
6628
+ source:
6629
+ event?.source === "semantic-compaction"
6630
+ ? "semantic-compaction"
6631
+ : "subagent",
6632
+ attribution: {
6633
+ origin: "skill",
6634
+ skill: args.skill_name,
6635
+ subagentId: skillSubRef?.id || null,
6636
+ parentSessionId: sessionId || null,
6637
+ depth: (subAgentDepth || 0) + 1,
6638
+ },
6639
+ };
6640
+ const observeSkillBoundary =
6641
+ strictUsageTelemetry === true
6642
+ ? (event) =>
6643
+ _notifyStrictUsageBoundary(
6644
+ onUsageBoundary,
6645
+ projectSkillUsage(event),
6646
+ )
6647
+ : null;
6648
+ const observeSkillSettlement =
6649
+ strictUsageTelemetry === true
6650
+ ? (event) =>
6651
+ _notifyStrictUsageSettlement(
6652
+ onUsageSettlement,
6653
+ projectSkillUsage(event),
6654
+ )
6655
+ : null;
6656
+ const projectSkillTool = (event) => ({
6657
+ ...event,
6658
+ attribution: event?.attribution || projectSkillUsage({}).attribution,
6659
+ });
6660
+ const observeSkillToolBoundary =
6661
+ strictUsageTelemetry === true
6662
+ ? (event) =>
6663
+ _notifyStrictToolObserver(
6664
+ onToolCallBoundary,
6665
+ projectSkillTool(event),
6666
+ "boundary",
6667
+ )
6668
+ : null;
6669
+ const observeSkillToolSettlement =
6670
+ strictUsageTelemetry === true
6671
+ ? (event) =>
6672
+ _notifyStrictToolObserver(
6673
+ onToolCallSettlement,
6674
+ projectSkillTool(event),
6675
+ "settlement",
6676
+ )
6677
+ : null;
6678
+ const skillLlmOptions = { ...(llmOptions || {}) };
6679
+ if (typeof llmOptions?.onStreamRetry === "function") {
6680
+ skillLlmOptions.onStreamRetry = (attempt, error, telemetry = {}) =>
6681
+ _notifyStrictRetryObserver(llmOptions.onStreamRetry, [
6682
+ attempt,
6683
+ error,
6684
+ projectSkillUsage(telemetry),
6685
+ ]);
6686
+ }
6601
6687
  // Run skill through isolated sub-agent context
6602
6688
  let subCtx;
6603
6689
  try {
@@ -6613,7 +6699,7 @@ async function executeToolInner(
6613
6699
  signal: skillExecutionSignal,
6614
6700
  toolAdmission,
6615
6701
  cwd,
6616
- llmOptions: llmOptions || null,
6702
+ llmOptions: skillLlmOptions,
6617
6703
  ...(sessionBudget ? { sessionBudget } : {}),
6618
6704
  ...(permissionRules ? { permissionRules } : {}),
6619
6705
  ...(hostManagedToolPolicy
@@ -6637,29 +6723,29 @@ async function executeToolInner(
6637
6723
  ...(mcpConflictScheduler ? { mcpConflictScheduler } : {}),
6638
6724
  onUsage: skillUsageSink
6639
6725
  ? (u) => {
6726
+ const forwarded = projectSkillUsage(u);
6640
6727
  try {
6641
- skillUsageSink.push(
6642
- u && u.attribution
6643
- ? u
6644
- : {
6645
- provider: u?.provider ?? null,
6646
- model: u?.model ?? null,
6647
- usage: u?.usage || null,
6648
- ...(u?.source ? { source: u.source } : {}),
6649
- attribution: {
6650
- origin: "skill",
6651
- skill: args.skill_name,
6652
- subagentId: skillSubRef?.id || null,
6653
- parentSessionId: sessionId || null,
6654
- depth: (subAgentDepth || 0) + 1,
6655
- },
6656
- },
6657
- );
6658
- } catch (_e) {
6659
- // usage forwarding is best-effort
6728
+ skillUsageSink.push(forwarded);
6729
+ } catch (error) {
6730
+ if (strictUsageTelemetry === true) {
6731
+ throw _runtimeUsageBoundaryFailure(
6732
+ error,
6733
+ "CC_USAGE_FORWARDING_FAILED",
6734
+ "Isolated skill usage forwarding failed",
6735
+ );
6736
+ }
6660
6737
  }
6661
6738
  }
6662
6739
  : null,
6740
+ ...(strictUsageTelemetry === true
6741
+ ? {
6742
+ strictUsageTelemetry: true,
6743
+ onUsageBoundary: observeSkillBoundary,
6744
+ onUsageSettlement: observeSkillSettlement,
6745
+ onToolCallBoundary: observeSkillToolBoundary,
6746
+ onToolCallSettlement: observeSkillToolSettlement,
6747
+ }
6748
+ : {}),
6663
6749
  });
6664
6750
  } catch (error) {
6665
6751
  skillExecutionLease?.release?.();
@@ -6704,6 +6790,7 @@ async function executeToolInner(
6704
6790
  toolsUsed: result.toolsUsed,
6705
6791
  });
6706
6792
  } catch (err) {
6793
+ if (err?.runtimeLedgerPersistence === true) throw err;
6707
6794
  if (
6708
6795
  signal?.aborted ||
6709
6796
  skillExecutionSignal?.aborted ||
@@ -7839,6 +7926,34 @@ function _backgroundSubAgentResultText(entry) {
7839
7926
  );
7840
7927
  }
7841
7928
 
7929
+ function _throwBackgroundSubAgentUsageFailure(entry) {
7930
+ const error = entry?.outcome?.fatalError;
7931
+ if (error?.runtimeLedgerPersistence === true) throw error;
7932
+ }
7933
+
7934
+ function _throwSettledBackgroundUsageFailure(map) {
7935
+ if (!(map instanceof Map)) return;
7936
+ for (const entry of map.values()) {
7937
+ if (entry?.settled === true) {
7938
+ _throwBackgroundSubAgentUsageFailure(entry);
7939
+ }
7940
+ }
7941
+ }
7942
+
7943
+ function _throwBackgroundUsageFailureState(state) {
7944
+ const error = state?.error;
7945
+ if (error?.runtimeLedgerPersistence === true) throw error;
7946
+ }
7947
+
7948
+ async function _awaitBackgroundUsageSettlement(map, state) {
7949
+ _throwBackgroundUsageFailureState(state);
7950
+ if (map instanceof Map && map.size > 0) {
7951
+ await Promise.all([...map.values()].map((entry) => entry.promise));
7952
+ }
7953
+ _throwBackgroundUsageFailureState(state);
7954
+ _throwSettledBackgroundUsageFailure(map);
7955
+ }
7956
+
7842
7957
  /**
7843
7958
  * Drain the run's attributed child-loop usage sink (spawn_sub_agent /
7844
7959
  * isolated run_skill — see toolContext.subAgentUsageSink), re-yielding each
@@ -7850,13 +7965,48 @@ function _backgroundSubAgentResultText(entry) {
7850
7965
  function* _drainSubAgentUsage(sink) {
7851
7966
  while (Array.isArray(sink) && sink.length > 0) {
7852
7967
  const u = sink.shift();
7968
+ if (u?.type === "model-usage-unknown") {
7969
+ const boundary = {
7970
+ callId: u.callId,
7971
+ provider: u.provider ?? null,
7972
+ model: u.model ?? null,
7973
+ source: u.source || "subagent",
7974
+ attribution: u.attribution || null,
7975
+ };
7976
+ if (u.callId && u.boundaryNotified !== true) {
7977
+ yield { type: "model-usage-started", ...boundary };
7978
+ }
7979
+ yield {
7980
+ type: "model-usage-unknown",
7981
+ ...boundary,
7982
+ code: u.code || "provider_transport_outcome_unknown",
7983
+ ...(u.ledgerPersisted === true ? { ledgerPersisted: true } : {}),
7984
+ };
7985
+ continue;
7986
+ }
7987
+ if (u?.callId && u.boundaryNotified !== true) {
7988
+ yield {
7989
+ type: "model-usage-started",
7990
+ callId: u.callId,
7991
+ provider: u.provider ?? null,
7992
+ model: u.model ?? null,
7993
+ source: u.source || "subagent",
7994
+ attribution: u.attribution || null,
7995
+ };
7996
+ }
7853
7997
  yield {
7854
7998
  type: "token-usage",
7999
+ ...(u?.callId ? { callId: u.callId } : {}),
7855
8000
  provider: u?.provider ?? null,
7856
8001
  model: u?.model ?? null,
7857
8002
  usage: u?.usage || {},
7858
- ...(u?.source ? { source: u.source } : {}),
8003
+ ...(u?.source
8004
+ ? { source: u.source }
8005
+ : u?.callId
8006
+ ? { source: "subagent" }
8007
+ : {}),
7859
8008
  attribution: u?.attribution || null,
8009
+ ...(u?.ledgerPersisted === true ? { ledgerPersisted: true } : {}),
7860
8010
  };
7861
8011
  }
7862
8012
  }
@@ -8313,32 +8463,126 @@ async function _executeSpawnSubAgent(args, ctx) {
8313
8463
  const usageSink = Array.isArray(ctx.subAgentUsageSink)
8314
8464
  ? ctx.subAgentUsageSink
8315
8465
  : null;
8466
+ if (ctx.strictUsageTelemetry === true) {
8467
+ _assertStrictUsageObserver(ctx.onUsageBoundary, "boundary");
8468
+ _assertStrictUsageObserver(ctx.onUsageSettlement, "settlement");
8469
+ }
8316
8470
  let subCtxRef = null;
8471
+ const latchBackgroundUsageFailure = (error) => {
8472
+ if (
8473
+ error?.runtimeLedgerPersistence === true &&
8474
+ ctx.backgroundUsageFailureState &&
8475
+ !ctx.backgroundUsageFailureState.error
8476
+ ) {
8477
+ ctx.backgroundUsageFailureState.error = error;
8478
+ }
8479
+ return error;
8480
+ };
8481
+ const projectSubagentUsage = (u) =>
8482
+ u && u.attribution
8483
+ ? u
8484
+ : {
8485
+ ...u,
8486
+ source:
8487
+ u?.source === "semantic-compaction"
8488
+ ? "semantic-compaction"
8489
+ : "subagent",
8490
+ attribution: {
8491
+ origin: "subagent",
8492
+ subagentId: subCtxRef?.id || null,
8493
+ role: subCtxRef?.role || role || null,
8494
+ parentSessionId,
8495
+ depth: currentDepth + 1,
8496
+ },
8497
+ };
8317
8498
  const onUsage = usageSink
8318
8499
  ? (u) => {
8500
+ const forwarded = projectSubagentUsage(u);
8319
8501
  try {
8320
- usageSink.push(
8321
- u && u.attribution
8322
- ? u
8323
- : {
8324
- provider: u?.provider ?? null,
8325
- model: u?.model ?? null,
8326
- usage: u?.usage || null,
8327
- ...(u?.source ? { source: u.source } : {}),
8328
- attribution: {
8329
- origin: "subagent",
8330
- subagentId: subCtxRef?.id || null,
8331
- role: subCtxRef?.role || role || null,
8332
- parentSessionId,
8333
- depth: currentDepth + 1,
8334
- },
8335
- },
8336
- );
8337
- } catch (_e) {
8338
- // usage forwarding is best-effort
8502
+ usageSink.push(forwarded);
8503
+ } catch (error) {
8504
+ if (ctx.strictUsageTelemetry === true) {
8505
+ throw _runtimeUsageBoundaryFailure(
8506
+ error,
8507
+ "CC_USAGE_FORWARDING_FAILED",
8508
+ "Sub-agent usage forwarding failed",
8509
+ );
8510
+ }
8339
8511
  }
8340
8512
  }
8341
8513
  : null;
8514
+ const observeSubagentBoundary =
8515
+ ctx.strictUsageTelemetry === true
8516
+ ? (event) => {
8517
+ try {
8518
+ return _notifyStrictUsageBoundary(
8519
+ ctx.onUsageBoundary,
8520
+ projectSubagentUsage(event),
8521
+ );
8522
+ } catch (error) {
8523
+ throw latchBackgroundUsageFailure(error);
8524
+ }
8525
+ }
8526
+ : null;
8527
+ const observeSubagentSettlement =
8528
+ ctx.strictUsageTelemetry === true
8529
+ ? (event) => {
8530
+ try {
8531
+ return _notifyStrictUsageSettlement(
8532
+ ctx.onUsageSettlement,
8533
+ projectSubagentUsage(event),
8534
+ );
8535
+ } catch (error) {
8536
+ throw latchBackgroundUsageFailure(error);
8537
+ }
8538
+ }
8539
+ : null;
8540
+ const projectSubagentTool = (event) => ({
8541
+ ...event,
8542
+ attribution:
8543
+ event?.attribution || projectSubagentUsage({}).attribution || null,
8544
+ });
8545
+ const observeSubagentToolBoundary =
8546
+ ctx.strictUsageTelemetry === true
8547
+ ? (event) => {
8548
+ try {
8549
+ return _notifyStrictToolObserver(
8550
+ ctx.onToolCallBoundary,
8551
+ projectSubagentTool(event),
8552
+ "boundary",
8553
+ );
8554
+ } catch (error) {
8555
+ throw latchBackgroundUsageFailure(error);
8556
+ }
8557
+ }
8558
+ : null;
8559
+ const observeSubagentToolSettlement =
8560
+ ctx.strictUsageTelemetry === true
8561
+ ? (event) => {
8562
+ try {
8563
+ return _notifyStrictToolObserver(
8564
+ ctx.onToolCallSettlement,
8565
+ projectSubagentTool(event),
8566
+ "settlement",
8567
+ );
8568
+ } catch (error) {
8569
+ throw latchBackgroundUsageFailure(error);
8570
+ }
8571
+ }
8572
+ : null;
8573
+ if (typeof parentLlm.onStreamRetry === "function") {
8574
+ subLlmOptions.onStreamRetry = (attempt, error, telemetry = {}) => {
8575
+ try {
8576
+ return _notifyStrictRetryObserver(parentLlm.onStreamRetry, [
8577
+ attempt,
8578
+ error,
8579
+ projectSubagentUsage(telemetry),
8580
+ ]);
8581
+ } catch (observerError) {
8582
+ throw latchBackgroundUsageFailure(observerError);
8583
+ }
8584
+ };
8585
+ }
8342
8586
 
8343
8587
  // Worktree sparse-checkout + dependency symlink (large-monorepo lever): when
8344
8588
  // the child runs in an isolated worktree, only materialize the packages it
@@ -8394,6 +8638,15 @@ async function _executeSpawnSubAgent(args, ctx) {
8394
8638
  subAgentBudget: ctx.subAgentBudget || null,
8395
8639
  ...(ctx.sessionBudget ? { sessionBudget: ctx.sessionBudget } : {}),
8396
8640
  onUsage,
8641
+ ...(ctx.strictUsageTelemetry === true
8642
+ ? {
8643
+ strictUsageTelemetry: true,
8644
+ onUsageBoundary: observeSubagentBoundary,
8645
+ onUsageSettlement: observeSubagentSettlement,
8646
+ onToolCallBoundary: observeSubagentToolBoundary,
8647
+ onToolCallSettlement: observeSubagentToolSettlement,
8648
+ }
8649
+ : {}),
8397
8650
  // Extended contract (gap 2026-07-11): per-agent iteration cap + opt-in
8398
8651
  // worktree isolation. undefined keeps the profile/flag defaults intact.
8399
8652
  ...(subMaxTurns ? { maxIterations: subMaxTurns } : {}),
@@ -8574,7 +8827,13 @@ async function _executeSpawnSubAgent(args, ctx) {
8574
8827
  (result) => ({ result, error: null }),
8575
8828
  (err) => {
8576
8829
  subCtx.forceComplete(err.message);
8577
- return { result: subCtx.result, error: err.message };
8830
+ return {
8831
+ result: subCtx.result,
8832
+ error: err.message,
8833
+ ...(err?.runtimeLedgerPersistence === true
8834
+ ? { fatalError: err }
8835
+ : {}),
8836
+ };
8578
8837
  },
8579
8838
  )
8580
8839
  .then((outcome) => {
@@ -8704,6 +8963,8 @@ async function _executeSpawnSubAgent(args, ctx) {
8704
8963
  completedAt: subCtx.completedAt,
8705
8964
  });
8706
8965
 
8966
+ if (err?.runtimeLedgerPersistence === true) throw err;
8967
+
8707
8968
  return {
8708
8969
  error: `Sub-agent failed: ${err.message}`,
8709
8970
  subAgentId: subCtx.id,
@@ -8739,6 +9000,49 @@ function getEffectiveToolDefinitions(options = {}) {
8739
9000
  });
8740
9001
  }
8741
9002
 
9003
+ function _isNonNegativeSafeInteger(value) {
9004
+ return Number.isSafeInteger(value) && value >= 0;
9005
+ }
9006
+
9007
+ function _providerUsageCount(usage, canonical, alias) {
9008
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
9009
+ const hasCanonical = Object.prototype.hasOwnProperty.call(usage, canonical);
9010
+ const hasAlias = Object.prototype.hasOwnProperty.call(usage, alias);
9011
+ if (!hasCanonical && !hasAlias) return null;
9012
+ if (hasCanonical && hasAlias && usage[canonical] !== usage[alias])
9013
+ return null;
9014
+ const value = hasCanonical ? usage[canonical] : usage[alias];
9015
+ return _isNonNegativeSafeInteger(value) ? value : null;
9016
+ }
9017
+
9018
+ function _optionalProviderUsageCount(usage, canonical, alias) {
9019
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
9020
+ const hasCanonical = Object.prototype.hasOwnProperty.call(usage, canonical);
9021
+ const hasAlias = Object.prototype.hasOwnProperty.call(usage, alias);
9022
+ if (!hasCanonical && !hasAlias) return 0;
9023
+ if (hasCanonical && hasAlias && usage[canonical] !== usage[alias])
9024
+ return null;
9025
+ const value = hasCanonical ? usage[canonical] : usage[alias];
9026
+ return _isNonNegativeSafeInteger(value) ? value : null;
9027
+ }
9028
+
9029
+ function _hasCompleteProviderUsage(usage) {
9030
+ return (
9031
+ _providerUsageCount(usage, "input_tokens", "prompt_tokens") != null &&
9032
+ _providerUsageCount(usage, "output_tokens", "completion_tokens") != null &&
9033
+ _optionalProviderUsageCount(
9034
+ usage,
9035
+ "cache_read_input_tokens",
9036
+ "cache_read_tokens",
9037
+ ) != null &&
9038
+ _optionalProviderUsageCount(
9039
+ usage,
9040
+ "cache_creation_input_tokens",
9041
+ "cache_creation_tokens",
9042
+ ) != null
9043
+ );
9044
+ }
9045
+
8742
9046
  /**
8743
9047
  * Send a chat completion request with tool definitions.
8744
9048
  * Supports 8 providers: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral, volcengine
@@ -8802,12 +9106,17 @@ export async function chatWithTools(rawMessages, options) {
8802
9106
  ),
8803
9107
  {
8804
9108
  signal,
8805
- onRetry: (attempt, error, telemetry) =>
8806
- options.onStreamRetry?.(attempt, error, {
8807
- ...telemetry,
8808
- provider: "ollama",
8809
- model: model || null,
8810
- }),
9109
+ strictRetryObserver: options.strictUsageTelemetry === true,
9110
+ ...(typeof options.onStreamRetry === "function"
9111
+ ? {
9112
+ onRetry: (attempt, error, telemetry) =>
9113
+ options.onStreamRetry(attempt, error, {
9114
+ ...telemetry,
9115
+ provider: "ollama",
9116
+ model: model || null,
9117
+ }),
9118
+ }
9119
+ : {}),
8811
9120
  },
8812
9121
  );
8813
9122
  }
@@ -8829,11 +9138,16 @@ export async function chatWithTools(rawMessages, options) {
8829
9138
  throw new Error(await formatProviderResponseError("ollama", response));
8830
9139
  }
8831
9140
  const data = await response.json();
8832
- if (data.prompt_eval_count || data.eval_count) {
9141
+ if (
9142
+ _isNonNegativeSafeInteger(data.prompt_eval_count) &&
9143
+ _isNonNegativeSafeInteger(data.eval_count)
9144
+ ) {
8833
9145
  data.usage = {
8834
- input_tokens: data.prompt_eval_count || 0,
8835
- output_tokens: data.eval_count || 0,
9146
+ input_tokens: data.prompt_eval_count,
9147
+ output_tokens: data.eval_count,
8836
9148
  };
9149
+ } else {
9150
+ delete data.usage;
8837
9151
  }
8838
9152
  return data;
8839
9153
  }
@@ -8941,12 +9255,17 @@ export async function chatWithTools(rawMessages, options) {
8941
9255
  ),
8942
9256
  {
8943
9257
  signal,
8944
- onRetry: (attempt, error, telemetry) =>
8945
- options.onStreamRetry?.(attempt, error, {
8946
- ...telemetry,
8947
- provider: "anthropic",
8948
- model: effModel,
8949
- }),
9258
+ strictRetryObserver: options.strictUsageTelemetry === true,
9259
+ ...(typeof options.onStreamRetry === "function"
9260
+ ? {
9261
+ onRetry: (attempt, error, telemetry) =>
9262
+ options.onStreamRetry(attempt, error, {
9263
+ ...telemetry,
9264
+ provider: "anthropic",
9265
+ model: effModel,
9266
+ }),
9267
+ }
9268
+ : {}),
8950
9269
  },
8951
9270
  );
8952
9271
  }
@@ -8968,17 +9287,32 @@ export async function chatWithTools(rawMessages, options) {
8968
9287
 
8969
9288
  const data = await response.json();
8970
9289
  const normalized = _normalizeAnthropicResponse(data);
8971
- if (data.usage) {
9290
+ if (_hasCompleteProviderUsage(data.usage)) {
8972
9291
  normalized.usage = {
8973
9292
  // With prompt caching, Anthropic splits input into uncached
8974
9293
  // `input_tokens` + cache read/write — capture all three so cost
8975
9294
  // accounting prices the cached prefix correctly (read ≈ 0.1×,
8976
9295
  // write ≈ 1.25× input). Absent (caching off) → 0, byte-identical.
8977
- input_tokens: data.usage.input_tokens || 0,
8978
- output_tokens: data.usage.output_tokens || 0,
8979
- cache_read_input_tokens: data.usage.cache_read_input_tokens || 0,
8980
- cache_creation_input_tokens:
8981
- data.usage.cache_creation_input_tokens || 0,
9296
+ input_tokens: _providerUsageCount(
9297
+ data.usage,
9298
+ "input_tokens",
9299
+ "prompt_tokens",
9300
+ ),
9301
+ output_tokens: _providerUsageCount(
9302
+ data.usage,
9303
+ "output_tokens",
9304
+ "completion_tokens",
9305
+ ),
9306
+ cache_read_input_tokens: _optionalProviderUsageCount(
9307
+ data.usage,
9308
+ "cache_read_input_tokens",
9309
+ "cache_read_tokens",
9310
+ ),
9311
+ cache_creation_input_tokens: _optionalProviderUsageCount(
9312
+ data.usage,
9313
+ "cache_creation_input_tokens",
9314
+ "cache_creation_tokens",
9315
+ ),
8982
9316
  };
8983
9317
  }
8984
9318
  return normalized;
@@ -9056,12 +9390,17 @@ export async function chatWithTools(rawMessages, options) {
9056
9390
  ),
9057
9391
  {
9058
9392
  signal,
9059
- onRetry: (attempt, error, telemetry) =>
9060
- options.onStreamRetry?.(attempt, error, {
9061
- ...telemetry,
9062
- provider,
9063
- model: model || defaultModels[provider] || "gpt-4o-mini",
9064
- }),
9393
+ strictRetryObserver: options.strictUsageTelemetry === true,
9394
+ ...(typeof options.onStreamRetry === "function"
9395
+ ? {
9396
+ onRetry: (attempt, error, telemetry) =>
9397
+ options.onStreamRetry(attempt, error, {
9398
+ ...telemetry,
9399
+ provider,
9400
+ model: model || defaultModels[provider] || "gpt-4o-mini",
9401
+ }),
9402
+ }
9403
+ : {}),
9065
9404
  },
9066
9405
  );
9067
9406
  }
@@ -9093,16 +9432,28 @@ export async function chatWithTools(rawMessages, options) {
9093
9432
  }
9094
9433
  const choice = data.choices[0];
9095
9434
  const out = { message: choice.message };
9096
- if (data.usage) {
9435
+ if (_hasCompleteProviderUsage(data.usage)) {
9097
9436
  // OpenAI/DeepSeek/volcengine report cached prompt tokens AS PART of
9098
9437
  // prompt_tokens — split them out so cost prices the cached prefix at the
9099
9438
  // provider's cache rate, not full input. 0 (or absent) → input unchanged.
9439
+ const promptTokens = _providerUsageCount(
9440
+ data.usage,
9441
+ "input_tokens",
9442
+ "prompt_tokens",
9443
+ );
9444
+ const completionTokens = _providerUsageCount(
9445
+ data.usage,
9446
+ "output_tokens",
9447
+ "completion_tokens",
9448
+ );
9100
9449
  const cached = _openaiCachedTokens(data.usage);
9101
- out.usage = {
9102
- input_tokens: Math.max(0, (data.usage.prompt_tokens || 0) - cached),
9103
- output_tokens: data.usage.completion_tokens || 0,
9104
- cache_read_input_tokens: cached,
9105
- };
9450
+ if (cached != null && cached <= promptTokens) {
9451
+ out.usage = {
9452
+ input_tokens: promptTokens - cached,
9453
+ output_tokens: completionTokens,
9454
+ cache_read_input_tokens: cached,
9455
+ };
9456
+ }
9106
9457
  }
9107
9458
  return out;
9108
9459
  }
@@ -9121,8 +9472,9 @@ function _ollamaInitState() {
9121
9472
  role: "assistant",
9122
9473
  content: "",
9123
9474
  toolCalls: null,
9124
- promptEval: 0,
9125
- evalCount: 0,
9475
+ promptEval: null,
9476
+ evalCount: null,
9477
+ usageInvalid: false,
9126
9478
  };
9127
9479
  }
9128
9480
 
@@ -9188,8 +9540,20 @@ function _ollamaReduceLine(state, line, onToken) {
9188
9540
  state.toolCalls = (state.toolCalls || []).concat(msg.tool_calls);
9189
9541
  }
9190
9542
  }
9191
- if (obj.prompt_eval_count) state.promptEval = obj.prompt_eval_count;
9192
- if (obj.eval_count) state.evalCount = obj.eval_count;
9543
+ if (Object.prototype.hasOwnProperty.call(obj, "prompt_eval_count")) {
9544
+ if (_isNonNegativeSafeInteger(obj.prompt_eval_count)) {
9545
+ state.promptEval = obj.prompt_eval_count;
9546
+ } else {
9547
+ state.usageInvalid = true;
9548
+ }
9549
+ }
9550
+ if (Object.prototype.hasOwnProperty.call(obj, "eval_count")) {
9551
+ if (_isNonNegativeSafeInteger(obj.eval_count)) {
9552
+ state.evalCount = obj.eval_count;
9553
+ } else {
9554
+ state.usageInvalid = true;
9555
+ }
9556
+ }
9193
9557
  return state;
9194
9558
  }
9195
9559
 
@@ -9199,7 +9563,11 @@ function _ollamaFinalize(state) {
9199
9563
  message.tool_calls = state.toolCalls;
9200
9564
  }
9201
9565
  const data = { message };
9202
- if (state.promptEval || state.evalCount) {
9566
+ if (
9567
+ state.usageInvalid !== true &&
9568
+ _isNonNegativeSafeInteger(state.promptEval) &&
9569
+ _isNonNegativeSafeInteger(state.evalCount)
9570
+ ) {
9203
9571
  data.usage = {
9204
9572
  input_tokens: state.promptEval,
9205
9573
  output_tokens: state.evalCount,
@@ -9292,7 +9660,7 @@ const _STREAM_STALL = Symbol("stream-stall");
9292
9660
  * last error — strictly better than today (one drop → instant error).
9293
9661
  *
9294
9662
  * @param {() => Promise<any>} streamFn invokes one `_chat*Streaming` attempt
9295
- * @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, sleep? }
9663
+ * @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, strictRetryObserver?, sleep? }
9296
9664
  */
9297
9665
  export async function _retryStreamingChat(streamFn, opts = {}) {
9298
9666
  // Default budget honors CC_MAX_RETRIES / CLAUDE_CODE_MAX_RETRIES (capped 15);
@@ -9311,13 +9679,46 @@ export async function _retryStreamingChat(streamFn, opts = {}) {
9311
9679
  if (attempt >= retries || !_isRetryableStreamError(err, signal))
9312
9680
  throw err;
9313
9681
  attempt++;
9682
+ if (
9683
+ opts.strictRetryObserver === true &&
9684
+ typeof opts.onRetry !== "function"
9685
+ ) {
9686
+ throw _runtimeUsageBoundaryFailure(
9687
+ null,
9688
+ "CC_RETRY_OBSERVER_REQUIRED",
9689
+ "Strict streaming retry requires a synchronous persistence observer",
9690
+ );
9691
+ }
9314
9692
  if (typeof opts.onRetry === "function") {
9315
9693
  try {
9316
- opts.onRetry(attempt, err, {
9694
+ const observation = opts.onRetry(attempt, err, {
9317
9695
  durationMs: Math.max(0, now() - attemptStartedAt),
9318
9696
  });
9319
- } catch {
9320
- /* the retry notice is best-effort; never let it mask the retry */
9697
+ if (observation && typeof observation.then === "function") {
9698
+ void Promise.resolve(observation).catch(() => {});
9699
+ if (opts.strictRetryObserver === true) {
9700
+ const observerError = new Error(
9701
+ "Strict retry observer must be synchronous",
9702
+ );
9703
+ observerError.code = "CC_RETRY_OBSERVER_ASYNC";
9704
+ throw observerError;
9705
+ }
9706
+ }
9707
+ } catch (observerError) {
9708
+ // A persisted usage ledger must record the failed attempt before a
9709
+ // second billable transport attempt starts. Strict hosts opt into a
9710
+ // fail-closed observer; legacy/unpersisted callers retain the former
9711
+ // best-effort notification behavior.
9712
+ if (
9713
+ opts.strictRetryObserver === true ||
9714
+ observerError?.code === "CC_SESSION_PERSISTENCE_FAILED"
9715
+ ) {
9716
+ throw _runtimeUsageBoundaryFailure(
9717
+ observerError,
9718
+ observerError?.code || "CC_RETRY_PERSISTENCE_FAILED",
9719
+ "Streaming retry persistence failed",
9720
+ );
9721
+ }
9321
9722
  }
9322
9723
  }
9323
9724
  await sleep(base * Math.pow(2, attempt - 1));
@@ -9489,10 +9890,11 @@ function _anthropicInitState() {
9489
9890
  return {
9490
9891
  text: "",
9491
9892
  blocks: {},
9492
- inputTokens: 0,
9493
- outputTokens: 0,
9893
+ inputTokens: null,
9894
+ outputTokens: null,
9494
9895
  cacheReadTokens: 0,
9495
9896
  cacheCreationTokens: 0,
9897
+ usageInvalid: false,
9496
9898
  };
9497
9899
  }
9498
9900
 
@@ -9508,13 +9910,35 @@ function _anthropicReduceLine(state, raw, onToken, onThinking) {
9508
9910
  return state;
9509
9911
  }
9510
9912
  if (obj.type === "message_start") {
9511
- const u = obj.message?.usage || {};
9512
- state.inputTokens = Number(u.input_tokens) || state.inputTokens;
9513
- // Prompt-cache token counts ride message_start.usage (Anthropic caching).
9514
- state.cacheReadTokens =
9515
- Number(u.cache_read_input_tokens) || state.cacheReadTokens;
9516
- state.cacheCreationTokens =
9517
- Number(u.cache_creation_input_tokens) || state.cacheCreationTokens;
9913
+ const usage = obj.message?.usage;
9914
+ if (usage != null) {
9915
+ const inputTokens = _providerUsageCount(
9916
+ usage,
9917
+ "input_tokens",
9918
+ "prompt_tokens",
9919
+ );
9920
+ const cacheReadTokens = _optionalProviderUsageCount(
9921
+ usage,
9922
+ "cache_read_input_tokens",
9923
+ "cache_read_tokens",
9924
+ );
9925
+ const cacheCreationTokens = _optionalProviderUsageCount(
9926
+ usage,
9927
+ "cache_creation_input_tokens",
9928
+ "cache_creation_tokens",
9929
+ );
9930
+ if (
9931
+ inputTokens == null ||
9932
+ cacheReadTokens == null ||
9933
+ cacheCreationTokens == null
9934
+ ) {
9935
+ state.usageInvalid = true;
9936
+ } else {
9937
+ state.inputTokens = inputTokens;
9938
+ state.cacheReadTokens = cacheReadTokens;
9939
+ state.cacheCreationTokens = cacheCreationTokens;
9940
+ }
9941
+ }
9518
9942
  } else if (obj.type === "content_block_start") {
9519
9943
  const cb = obj.content_block || {};
9520
9944
  state.blocks[obj.index] =
@@ -9553,7 +9977,18 @@ function _anthropicReduceLine(state, raw, onToken, onThinking) {
9553
9977
  (state.blocks[obj.index].signature || "") + (d.signature || "");
9554
9978
  }
9555
9979
  } else if (obj.type === "message_delta") {
9556
- state.outputTokens = Number(obj.usage?.output_tokens) || state.outputTokens;
9980
+ if (obj.usage != null) {
9981
+ const outputTokens = _providerUsageCount(
9982
+ obj.usage,
9983
+ "output_tokens",
9984
+ "completion_tokens",
9985
+ );
9986
+ if (outputTokens == null) {
9987
+ state.usageInvalid = true;
9988
+ } else {
9989
+ state.outputTokens = outputTokens;
9990
+ }
9991
+ }
9557
9992
  }
9558
9993
  return state;
9559
9994
  }
@@ -9594,10 +10029,9 @@ function _anthropicFinalize(state) {
9594
10029
  if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
9595
10030
  const out = { message };
9596
10031
  if (
9597
- state.inputTokens ||
9598
- state.outputTokens ||
9599
- state.cacheReadTokens ||
9600
- state.cacheCreationTokens
10032
+ state.usageInvalid !== true &&
10033
+ _isNonNegativeSafeInteger(state.inputTokens) &&
10034
+ _isNonNegativeSafeInteger(state.outputTokens)
9601
10035
  ) {
9602
10036
  out.usage = {
9603
10037
  input_tokens: state.inputTokens,
@@ -9683,21 +10117,49 @@ async function _chatAnthropicStreaming(
9683
10117
  // cached prefix twice. Verified live against volcengine (field present, 0 when
9684
10118
  // the provider does not auto-cache).
9685
10119
  function _openaiCachedTokens(usage) {
9686
- if (!usage || typeof usage !== "object") return 0;
9687
- const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
9688
- if (Number.isFinite(detailed) && detailed > 0) return detailed;
9689
- const deepseek = Number(usage.prompt_cache_hit_tokens);
9690
- if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
9691
- return 0;
10120
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
10121
+ const candidates = [];
10122
+ const hasDirect =
10123
+ Object.prototype.hasOwnProperty.call(usage, "cache_read_input_tokens") ||
10124
+ Object.prototype.hasOwnProperty.call(usage, "cache_read_tokens");
10125
+ if (hasDirect) {
10126
+ const direct = _optionalProviderUsageCount(
10127
+ usage,
10128
+ "cache_read_input_tokens",
10129
+ "cache_read_tokens",
10130
+ );
10131
+ if (direct == null) return null;
10132
+ candidates.push(direct);
10133
+ }
10134
+
10135
+ if (Object.prototype.hasOwnProperty.call(usage, "prompt_tokens_details")) {
10136
+ const details = usage.prompt_tokens_details;
10137
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
10138
+ return null;
10139
+ }
10140
+ if (Object.prototype.hasOwnProperty.call(details, "cached_tokens")) {
10141
+ if (!_isNonNegativeSafeInteger(details.cached_tokens)) return null;
10142
+ candidates.push(details.cached_tokens);
10143
+ }
10144
+ }
10145
+
10146
+ if (Object.prototype.hasOwnProperty.call(usage, "prompt_cache_hit_tokens")) {
10147
+ if (!_isNonNegativeSafeInteger(usage.prompt_cache_hit_tokens)) return null;
10148
+ candidates.push(usage.prompt_cache_hit_tokens);
10149
+ }
10150
+
10151
+ if (candidates.some((value) => value !== candidates[0])) return null;
10152
+ return candidates[0] ?? 0;
9692
10153
  }
9693
10154
 
9694
10155
  function _openaiInitState() {
9695
10156
  return {
9696
10157
  text: "",
9697
10158
  tools: [],
9698
- inputTokens: 0,
9699
- outputTokens: 0,
10159
+ inputTokens: null,
10160
+ outputTokens: null,
9700
10161
  cacheReadTokens: 0,
10162
+ usageInvalid: false,
9701
10163
  };
9702
10164
  }
9703
10165
 
@@ -9734,14 +10196,26 @@ function _openaiReduceLine(state, raw, onToken) {
9734
10196
  state.tools[idx].args += tc.function.arguments;
9735
10197
  }
9736
10198
  }
9737
- if (obj.usage) {
10199
+ if (obj.usage != null) {
10200
+ const usage = obj.usage;
10201
+ const prompt = _providerUsageCount(usage, "input_tokens", "prompt_tokens");
10202
+ const completion = _providerUsageCount(
10203
+ usage,
10204
+ "output_tokens",
10205
+ "completion_tokens",
10206
+ );
9738
10207
  const cached = _openaiCachedTokens(obj.usage);
9739
- const prompt = Number(obj.usage.prompt_tokens);
9740
- if (Number.isFinite(prompt))
10208
+ if (
10209
+ !_hasCompleteProviderUsage(usage) ||
10210
+ cached == null ||
10211
+ cached > prompt
10212
+ ) {
10213
+ state.usageInvalid = true;
10214
+ } else {
9741
10215
  state.inputTokens = Math.max(0, prompt - cached);
9742
- if (cached) state.cacheReadTokens = cached;
9743
- state.outputTokens =
9744
- Number(obj.usage.completion_tokens) || state.outputTokens;
10216
+ state.outputTokens = completion;
10217
+ state.cacheReadTokens = cached;
10218
+ }
9745
10219
  }
9746
10220
  return state;
9747
10221
  }
@@ -9755,11 +10229,15 @@ function _openaiFinalize(state) {
9755
10229
  const message = { role: "assistant", content: state.text };
9756
10230
  if (toolCalls.length) message.tool_calls = toolCalls;
9757
10231
  const out = { message };
9758
- if (state.inputTokens || state.outputTokens || state.cacheReadTokens) {
10232
+ if (
10233
+ state.usageInvalid !== true &&
10234
+ _isNonNegativeSafeInteger(state.inputTokens) &&
10235
+ _isNonNegativeSafeInteger(state.outputTokens)
10236
+ ) {
9759
10237
  out.usage = {
9760
10238
  input_tokens: state.inputTokens,
9761
10239
  output_tokens: state.outputTokens,
9762
- cache_read_input_tokens: state.cacheReadTokens || 0,
10240
+ cache_read_input_tokens: state.cacheReadTokens,
9763
10241
  };
9764
10242
  }
9765
10243
  return out;
@@ -10119,11 +10597,293 @@ function _managedCheckpointUncoveredWriterReason(toolContext) {
10119
10597
  * { type: "checkpoint", id, tool } — auto-checkpoint before a mutating tool
10120
10598
  * { type: "tool-executing", tool, args }
10121
10599
  * { type: "tool-result", tool, result, error }
10600
+ * { type: "model-usage-started", callId, provider, model, source }
10601
+ * { type: "token-usage", callId, provider, model, usage, source? }
10602
+ * { type: "model-usage-unknown", callId, provider, model, source, code }
10122
10603
  * { type: "response-complete", content }
10123
10604
  *
10124
10605
  * @param {Array} messages - mutable messages array (will be appended to)
10125
10606
  * @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd, slotFiller, interaction
10126
10607
  */
10608
+ const _autoCompactionUsageStates = new WeakMap();
10609
+ const _autoCompactorInstrumentation = new WeakMap();
10610
+
10611
+ function _newModelUsageCallId(source = "model") {
10612
+ // UUIDs are generated locally and contain no session/run/user material. The
10613
+ // fixed prefixes keep the identifier useful in diagnostics while remaining
10614
+ // far below the persisted ledger's 128-character ceiling.
10615
+ const prefix =
10616
+ source === "semantic-compaction"
10617
+ ? "cmp"
10618
+ : source === "subagent"
10619
+ ? "sub"
10620
+ : "mdl";
10621
+ return `${prefix}-${randomUUID()}`;
10622
+ }
10623
+
10624
+ function _runtimeUsageBoundaryFailure(error, code, fallbackMessage) {
10625
+ const failure =
10626
+ error && (typeof error === "object" || typeof error === "function")
10627
+ ? error
10628
+ : new Error(fallbackMessage);
10629
+ try {
10630
+ failure.runtimeLedgerPersistence = true;
10631
+ if (!failure.code && code) failure.code = code;
10632
+ } catch {
10633
+ const wrapped = new Error(fallbackMessage, { cause: error });
10634
+ wrapped.runtimeLedgerPersistence = true;
10635
+ wrapped.code = code;
10636
+ return wrapped;
10637
+ }
10638
+ return failure;
10639
+ }
10640
+
10641
+ /**
10642
+ * Strict child loops are consumed inside executeTool, so their yielded model
10643
+ * boundaries cannot reach the parent host before provider work. Require the
10644
+ * parent's synchronous persistence observer and prewrite one conservative
10645
+ * subagent call. The matching unknown event is forwarded after child settlement.
10646
+ */
10647
+ function _notifyStrictUsageBoundary(observer, boundary) {
10648
+ if (typeof observer !== "function") {
10649
+ throw _runtimeUsageBoundaryFailure(
10650
+ null,
10651
+ "CC_USAGE_BOUNDARY_OBSERVER_REQUIRED",
10652
+ "Strict child usage telemetry requires a synchronous boundary observer",
10653
+ );
10654
+ }
10655
+ let observation;
10656
+ try {
10657
+ observation = observer(boundary);
10658
+ } catch (error) {
10659
+ throw _runtimeUsageBoundaryFailure(
10660
+ error,
10661
+ "CC_USAGE_BOUNDARY_PERSISTENCE_FAILED",
10662
+ "Child usage boundary persistence failed",
10663
+ );
10664
+ }
10665
+ let isThenable = false;
10666
+ try {
10667
+ isThenable = Boolean(observation && typeof observation.then === "function");
10668
+ } catch (error) {
10669
+ throw _runtimeUsageBoundaryFailure(
10670
+ error,
10671
+ "CC_USAGE_BOUNDARY_OBSERVER_ASYNC",
10672
+ "Child usage boundary observer must be synchronous",
10673
+ );
10674
+ }
10675
+ if (isThenable) {
10676
+ void Promise.resolve(observation).catch(() => {});
10677
+ throw _runtimeUsageBoundaryFailure(
10678
+ null,
10679
+ "CC_USAGE_BOUNDARY_OBSERVER_ASYNC",
10680
+ "Child usage boundary observer must be synchronous",
10681
+ );
10682
+ }
10683
+ }
10684
+
10685
+ function _notifyStrictUsageSettlement(observer, settlement) {
10686
+ if (typeof observer !== "function") {
10687
+ throw _runtimeUsageBoundaryFailure(
10688
+ null,
10689
+ "CC_USAGE_SETTLEMENT_OBSERVER_REQUIRED",
10690
+ "Strict child usage telemetry requires a synchronous settlement observer",
10691
+ );
10692
+ }
10693
+ let observation;
10694
+ try {
10695
+ observation = observer(settlement);
10696
+ } catch (error) {
10697
+ throw _runtimeUsageBoundaryFailure(
10698
+ error,
10699
+ "CC_USAGE_SETTLEMENT_PERSISTENCE_FAILED",
10700
+ "Child usage settlement persistence failed",
10701
+ );
10702
+ }
10703
+ let isThenable = false;
10704
+ try {
10705
+ isThenable = Boolean(observation && typeof observation.then === "function");
10706
+ } catch (error) {
10707
+ throw _runtimeUsageBoundaryFailure(
10708
+ error,
10709
+ "CC_USAGE_SETTLEMENT_OBSERVER_ASYNC",
10710
+ "Child usage settlement observer must be synchronous",
10711
+ );
10712
+ }
10713
+ if (isThenable) {
10714
+ void Promise.resolve(observation).catch(() => {});
10715
+ throw _runtimeUsageBoundaryFailure(
10716
+ null,
10717
+ "CC_USAGE_SETTLEMENT_OBSERVER_ASYNC",
10718
+ "Child usage settlement observer must be synchronous",
10719
+ );
10720
+ }
10721
+ }
10722
+
10723
+ function _notifyStrictToolObserver(observer, event, phase) {
10724
+ const upper = phase.toUpperCase();
10725
+ if (typeof observer !== "function") {
10726
+ throw _runtimeUsageBoundaryFailure(
10727
+ null,
10728
+ `CC_TOOL_${upper}_OBSERVER_REQUIRED`,
10729
+ `Strict child tool ${phase} requires a synchronous persistence observer`,
10730
+ );
10731
+ }
10732
+ let observation;
10733
+ try {
10734
+ observation = observer(event);
10735
+ } catch (error) {
10736
+ throw _runtimeUsageBoundaryFailure(
10737
+ error,
10738
+ `CC_TOOL_${upper}_PERSISTENCE_FAILED`,
10739
+ `Child tool ${phase} persistence failed`,
10740
+ );
10741
+ }
10742
+ if (observation && typeof observation.then === "function") {
10743
+ void Promise.resolve(observation).catch(() => {});
10744
+ throw _runtimeUsageBoundaryFailure(
10745
+ null,
10746
+ `CC_TOOL_${upper}_OBSERVER_ASYNC`,
10747
+ `Child tool ${phase} observer must be synchronous`,
10748
+ );
10749
+ }
10750
+ }
10751
+
10752
+ function _notifyStrictRetryObserver(observer, args) {
10753
+ if (typeof observer !== "function") {
10754
+ throw _runtimeUsageBoundaryFailure(
10755
+ null,
10756
+ "CC_RETRY_OBSERVER_REQUIRED",
10757
+ "Strict child streaming retry requires a synchronous persistence observer",
10758
+ );
10759
+ }
10760
+ let observation;
10761
+ try {
10762
+ observation = observer(...args);
10763
+ } catch (error) {
10764
+ throw _runtimeUsageBoundaryFailure(
10765
+ error,
10766
+ "CC_RETRY_PERSISTENCE_FAILED",
10767
+ "Child streaming retry persistence failed",
10768
+ );
10769
+ }
10770
+ if (observation && typeof observation.then === "function") {
10771
+ void Promise.resolve(observation).catch(() => {});
10772
+ throw _runtimeUsageBoundaryFailure(
10773
+ null,
10774
+ "CC_RETRY_OBSERVER_ASYNC",
10775
+ "Child streaming retry observer must be synchronous",
10776
+ );
10777
+ }
10778
+ }
10779
+
10780
+ function _assertStrictUsageObserver(observer, phase) {
10781
+ if (typeof observer !== "function") {
10782
+ throw _runtimeUsageBoundaryFailure(
10783
+ null,
10784
+ `CC_USAGE_${phase.toUpperCase()}_OBSERVER_REQUIRED`,
10785
+ `Strict child usage telemetry requires a synchronous ${phase} observer`,
10786
+ );
10787
+ }
10788
+ }
10789
+
10790
+ function _autoCompactionUsageState(options) {
10791
+ let state = _autoCompactionUsageStates.get(options);
10792
+ if (!state) {
10793
+ state = { calls: [] };
10794
+ _autoCompactionUsageStates.set(options, state);
10795
+ }
10796
+ return state;
10797
+ }
10798
+
10799
+ /**
10800
+ * Wrap only the compressor's actual provider-query seam. `shouldAutoCompact`,
10801
+ * microcompact, and extractive/count-only compaction never enter this wrapper,
10802
+ * so they cannot manufacture a billable-call boundary.
10803
+ */
10804
+ function _instrumentAutoCompactorUsage(compactor, options) {
10805
+ if (!compactor || typeof compactor.llmQuery !== "function") return compactor;
10806
+ const installed = _autoCompactorInstrumentation.get(compactor);
10807
+ if (
10808
+ installed?.options === options &&
10809
+ compactor.llmQuery === installed.wrapper
10810
+ ) {
10811
+ return compactor;
10812
+ }
10813
+
10814
+ const original = installed?.original || compactor.llmQuery;
10815
+ const state = _autoCompactionUsageState(options);
10816
+ const wrapper = async function instrumentedCompactionQuery(...args) {
10817
+ const call = {
10818
+ callId: _newModelUsageCallId("semantic-compaction"),
10819
+ provider: options.provider || null,
10820
+ model: options.model || null,
10821
+ source: "semantic-compaction",
10822
+ observerError: null,
10823
+ observerFailed: false,
10824
+ boundaryNotified: false,
10825
+ };
10826
+ state.calls.push(call);
10827
+ try {
10828
+ if (typeof options.onUsageBoundary === "function") {
10829
+ // Synchronous and before `original`: a host can durably append the
10830
+ // start row, and any append failure prevents provider spend.
10831
+ const observation = options.onUsageBoundary({
10832
+ type: "model-usage-started",
10833
+ callId: call.callId,
10834
+ provider: call.provider,
10835
+ model: call.model,
10836
+ source: call.source,
10837
+ });
10838
+ if (observation && typeof observation.then === "function") {
10839
+ void Promise.resolve(observation).catch(() => {});
10840
+ const error = new Error(
10841
+ "Automatic compaction usage boundary observer must be synchronous",
10842
+ );
10843
+ error.code = "CC_USAGE_BOUNDARY_OBSERVER_ASYNC";
10844
+ throw error;
10845
+ }
10846
+ call.boundaryNotified = true;
10847
+ }
10848
+ } catch (error) {
10849
+ call.observerError = error;
10850
+ call.observerFailed = true;
10851
+ throw error;
10852
+ }
10853
+
10854
+ return await original.apply(this, args);
10855
+ };
10856
+
10857
+ try {
10858
+ compactor.llmQuery = wrapper;
10859
+ } catch (error) {
10860
+ if (options.strictUsageTelemetry === true) {
10861
+ throw _runtimeUsageBoundaryFailure(
10862
+ error,
10863
+ "CC_COMPACTION_USAGE_INSTRUMENTATION_REQUIRED",
10864
+ "Strict semantic compaction requires an instrumentable provider boundary",
10865
+ );
10866
+ }
10867
+ // A frozen injected compactor cannot be instrumented safely. Production
10868
+ // PromptCompressor instances are mutable; legacy injected fakes stay as-is.
10869
+ return compactor;
10870
+ }
10871
+ if (compactor.llmQuery === wrapper) {
10872
+ _autoCompactorInstrumentation.set(compactor, {
10873
+ original,
10874
+ options,
10875
+ wrapper,
10876
+ });
10877
+ } else if (options.strictUsageTelemetry === true) {
10878
+ throw _runtimeUsageBoundaryFailure(
10879
+ null,
10880
+ "CC_COMPACTION_USAGE_INSTRUMENTATION_REQUIRED",
10881
+ "Strict semantic compaction requires an instrumentable provider boundary",
10882
+ );
10883
+ }
10884
+ return compactor;
10885
+ }
10886
+
10127
10887
  /**
10128
10888
  * Lazily build (and cache on `options`) the PromptCompressor used for in-loop
10129
10889
  * auto-compaction. Returns null when the feature is off or the module can't be
@@ -10132,7 +10892,7 @@ function _managedCheckpointUncoveredWriterReason(toolContext) {
10132
10892
  */
10133
10893
  async function _getAutoCompactor(options) {
10134
10894
  if (Object.prototype.hasOwnProperty.call(options, "_autoCompactor")) {
10135
- return options._autoCompactor;
10895
+ return _instrumentAutoCompactorUsage(options._autoCompactor, options);
10136
10896
  }
10137
10897
  let compressor = null;
10138
10898
  try {
@@ -10180,6 +10940,7 @@ async function _getAutoCompactor(options) {
10180
10940
  llmQuery,
10181
10941
  summaryInputMaxChars: options.compactionInputMaxChars,
10182
10942
  });
10943
+ compressor = _instrumentAutoCompactorUsage(compressor, options);
10183
10944
  }
10184
10945
  } catch {
10185
10946
  compressor = null;
@@ -10194,19 +10955,38 @@ async function _getAutoCompactor(options) {
10194
10955
 
10195
10956
  function _compactionTokenUsage(stats) {
10196
10957
  const summaryUsage = stats?.summaryUsage;
10197
- if (!summaryUsage || typeof summaryUsage !== "object") return null;
10198
- const tokenCount = (value) => {
10199
- const number = Number(value);
10200
- return Number.isFinite(number) && number > 0 ? Math.trunc(number) : 0;
10958
+ if (
10959
+ !summaryUsage ||
10960
+ typeof summaryUsage !== "object" ||
10961
+ Array.isArray(summaryUsage) ||
10962
+ !_isNonNegativeSafeInteger(summaryUsage.inputTokens) ||
10963
+ !_isNonNegativeSafeInteger(summaryUsage.outputTokens)
10964
+ ) {
10965
+ return null;
10966
+ }
10967
+ const optionalTokenCount = (key) => {
10968
+ if (!Object.prototype.hasOwnProperty.call(summaryUsage, key)) return 0;
10969
+ return _isNonNegativeSafeInteger(summaryUsage[key])
10970
+ ? summaryUsage[key]
10971
+ : null;
10201
10972
  };
10973
+ const cacheReadTokens = optionalTokenCount("cacheReadTokens");
10974
+ const cacheCreationTokens = optionalTokenCount("cacheCreationTokens");
10975
+ if (cacheReadTokens == null || cacheCreationTokens == null) return null;
10202
10976
  return {
10203
- input_tokens: tokenCount(summaryUsage.inputTokens),
10204
- output_tokens: tokenCount(summaryUsage.outputTokens),
10205
- cache_read_input_tokens: tokenCount(summaryUsage.cacheReadTokens),
10206
- cache_creation_input_tokens: tokenCount(summaryUsage.cacheCreationTokens),
10977
+ input_tokens: summaryUsage.inputTokens,
10978
+ output_tokens: summaryUsage.outputTokens,
10979
+ cache_read_input_tokens: cacheReadTokens,
10980
+ cache_creation_input_tokens: cacheCreationTokens,
10207
10981
  };
10208
10982
  }
10209
10983
 
10984
+ function _compactionUsageUnknownReason(stats) {
10985
+ return stats?.summaryUsageUnknownReason === "provider_usage_not_reported"
10986
+ ? "provider_usage_not_reported"
10987
+ : "provider_transport_outcome_unknown";
10988
+ }
10989
+
10210
10990
  function _sameMessageSnapshot(messages, expectedMessages) {
10211
10991
  return (
10212
10992
  messages.length === expectedMessages.length &&
@@ -10519,7 +11299,30 @@ export async function* agentLoop(messages, options) {
10519
11299
  model: options.model || null,
10520
11300
  baseUrl: options.baseUrl || null,
10521
11301
  apiKey: options.apiKey || null,
11302
+ ...(typeof options.onUsageBoundary === "function"
11303
+ ? { onUsageBoundary: options.onUsageBoundary }
11304
+ : {}),
11305
+ ...(typeof options.onUsageSettlement === "function"
11306
+ ? { onUsageSettlement: options.onUsageSettlement }
11307
+ : {}),
11308
+ ...(typeof options.onStreamRetry === "function"
11309
+ ? { onStreamRetry: options.onStreamRetry }
11310
+ : {}),
11311
+ ...(typeof options.onToolCallBoundary === "function"
11312
+ ? { onToolCallBoundary: options.onToolCallBoundary }
11313
+ : {}),
11314
+ ...(typeof options.onToolCallSettlement === "function"
11315
+ ? { onToolCallSettlement: options.onToolCallSettlement }
11316
+ : {}),
11317
+ ...(options.strictUsageTelemetry === true
11318
+ ? { strictUsageTelemetry: true }
11319
+ : {}),
10522
11320
  },
11321
+ strictUsageTelemetry: options.strictUsageTelemetry === true,
11322
+ onUsageBoundary: options.onUsageBoundary || null,
11323
+ onUsageSettlement: options.onUsageSettlement || null,
11324
+ onToolCallBoundary: options.onToolCallBoundary || null,
11325
+ onToolCallSettlement: options.onToolCallSettlement || null,
10523
11326
  parentMessages: messages, // pass parent messages for sub-agent auto-condensation
10524
11327
  interaction: options.interaction || null,
10525
11328
  shellPolicyOverrides: options.shellPolicyOverrides || null,
@@ -10583,6 +11386,10 @@ export async function* agentLoop(messages, options) {
10583
11386
  // any are still running (it waits, injects, and gives the model one more
10584
11387
  // turn) so a background result can never be silently lost.
10585
11388
  backgroundSubAgents: new Map(),
11389
+ // A background child reports strict observer failure synchronously here,
11390
+ // before its promise rejection necessarily reaches the entry's `settled`
11391
+ // continuation. Provider/tool admission fences consult this latch directly.
11392
+ backgroundUsageFailureState: { error: null },
10586
11393
  // 用量归因: per-run sink for child-loop (spawn_sub_agent / isolated
10587
11394
  // run_skill) token usage. Child loops consume their own generator events,
10588
11395
  // so their real usage never reaches this loop's consumers — the spawn
@@ -10594,6 +11401,7 @@ export async function* agentLoop(messages, options) {
10594
11401
  : [],
10595
11402
  };
10596
11403
  const backgroundSubAgents = toolContext.backgroundSubAgents;
11404
+ const backgroundUsageFailureState = toolContext.backgroundUsageFailureState;
10597
11405
  const subAgentUsageSink = toolContext.subAgentUsageSink;
10598
11406
  const lifecycleHookEmitter = hermeticExecution ? () => {} : emitHooksV2Event;
10599
11407
 
@@ -10658,7 +11466,15 @@ export async function* agentLoop(messages, options) {
10658
11466
  // wrong / expired, self-heal to a provider we can actually run (endpoint-
10659
11467
  // inferred, then env-keyed) instead of failing the turn. Opt out with
10660
11468
  // `runnableProviderFallback: false`. Transparent on the happy path.
10661
- if (options.runnableProviderFallback !== false) {
11469
+ // A runnable fallback may perform an additional (and cross-provider) model
11470
+ // attempt inside one logical `llmCall`, where this generator cannot durably
11471
+ // bracket each transport attempt. Strict call-ledger sessions therefore
11472
+ // disable that recovery path and surface the configured provider's failure
11473
+ // as unknown; legacy sessions keep the existing self-healing behavior.
11474
+ if (
11475
+ options.runnableProviderFallback !== false &&
11476
+ options.strictUsageTelemetry !== true
11477
+ ) {
10662
11478
  const { makeRunnableProviderFallback } =
10663
11479
  await import("../lib/runnable-provider.js");
10664
11480
  llmCall = makeRunnableProviderFallback(llmCall, {
@@ -10742,6 +11558,12 @@ export async function* agentLoop(messages, options) {
10742
11558
  budget.consume();
10743
11559
  throwIfAborted(signal);
10744
11560
 
11561
+ // A detached child may fail its synchronous ledger write between parent
11562
+ // turns. Surface that original marked error before any further paid parent
11563
+ // provider call is admitted.
11564
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
11565
+ _throwSettledBackgroundUsageFailure(backgroundSubAgents);
11566
+
10745
11567
  // Surface attributed child-loop usage collected since the last boundary
10746
11568
  // (blocking spawns push during executeTool; background spawns push live).
10747
11569
  yield* _drainSubAgentUsage(subAgentUsageSink);
@@ -10787,9 +11609,14 @@ export async function* agentLoop(messages, options) {
10787
11609
  !automaticCompactionSettlementBlocked &&
10788
11610
  messages.length > 4
10789
11611
  ) {
11612
+ let compactionUsageState = null;
11613
+ let compactionUsageCall = null;
11614
+ let compactionObserverFailure = null;
10790
11615
  try {
10791
11616
  const compactor = await _getAutoCompactor(options);
10792
11617
  if (compactor && compactor.shouldAutoCompact(messages)) {
11618
+ compactionUsageState = _autoCompactionUsageState(options);
11619
+ compactionUsageState.calls = [];
10793
11620
  // This is the canonical pre-provider authority snapshot. A local
10794
11621
  // micro-compact may change the working input, but the durable CAS must
10795
11622
  // still compare against the transcript state that existed before any
@@ -10876,36 +11703,88 @@ export async function* agentLoop(messages, options) {
10876
11703
  preserveToolPairs: true,
10877
11704
  ...pinOpts,
10878
11705
  });
11706
+ const observerFailure = compactionUsageState.calls.find(
11707
+ (call) => call.observerFailed,
11708
+ );
11709
+ if (observerFailure) {
11710
+ compactionObserverFailure = observerFailure.observerError;
11711
+ compactionUsageState.calls = [];
11712
+ throw compactionObserverFailure;
11713
+ }
11714
+ compactionUsageCall = compactionUsageState.calls.at(-1) || null;
10879
11715
  const compactionUsage = _compactionTokenUsage(stats);
10880
11716
  if (stats.degraded === true) {
11717
+ const usageOutcomeUnknown = stats.summaryUsageUnknown === true;
11718
+ const projectedStats = usageOutcomeUnknown
11719
+ ? {
11720
+ ...stats,
11721
+ degradedReason: "semantic-summary-provider-outcome-unknown",
11722
+ summaryUsageUnknownReason:
11723
+ _compactionUsageUnknownReason(stats),
11724
+ }
11725
+ : stats;
10881
11726
  yield {
10882
11727
  type: "compaction-degraded",
10883
11728
  runId,
10884
- reason: stats.degradedReason || "semantic-summary-degraded",
11729
+ reason: usageOutcomeUnknown
11730
+ ? "semantic-summary-provider-outcome-unknown"
11731
+ : stats.degradedReason || "semantic-summary-degraded",
10885
11732
  summaryMode: stats.summaryMode || "extractive-fallback",
10886
- stats,
11733
+ stats: projectedStats,
10887
11734
  };
10888
11735
  }
11736
+ const compactionUsageMissing =
11737
+ stats.summaryUsageUnknown !== true &&
11738
+ compactionUsageCall != null &&
11739
+ compactionUsage == null;
11740
+ if (stats.summaryUsageUnknown === true || compactionUsageMissing) {
11741
+ automaticCompactionSettlementBlocked = true;
11742
+ const unknownReason = compactionUsageMissing
11743
+ ? "provider_usage_not_reported"
11744
+ : _compactionUsageUnknownReason(stats);
11745
+ if (
11746
+ options.strictUsageTelemetry === true &&
11747
+ !compactionUsageCall?.callId
11748
+ ) {
11749
+ throw _runtimeUsageBoundaryFailure(
11750
+ null,
11751
+ "CC_COMPACTION_USAGE_BOUNDARY_MISSING",
11752
+ "Strict semantic compaction produced usage without a real call boundary",
11753
+ );
11754
+ }
11755
+ yield {
11756
+ type: "compaction-usage-unknown",
11757
+ callId:
11758
+ compactionUsageCall?.callId ||
11759
+ _newModelUsageCallId("semantic-compaction"),
11760
+ runId,
11761
+ provider:
11762
+ compactionUsageCall?.boundaryNotified === true
11763
+ ? compactionUsageCall.provider
11764
+ : stats.summaryProvider || options.provider || null,
11765
+ model:
11766
+ compactionUsageCall?.boundaryNotified === true
11767
+ ? compactionUsageCall.model
11768
+ : stats.summaryModel || options.model || null,
11769
+ reason: unknownReason,
11770
+ code:
11771
+ unknownReason === "provider_usage_not_reported"
11772
+ ? "provider_usage_missing"
11773
+ : "provider_call_failed",
11774
+ source: "semantic-compaction",
11775
+ };
11776
+ // Unknown compaction spend makes every hard budget unverifiable;
11777
+ // never apply the fallback or initiate the main model call.
11778
+ await _awaitBackgroundUsageSettlement(
11779
+ backgroundSubAgents,
11780
+ backgroundUsageFailureState,
11781
+ );
11782
+ return;
11783
+ }
10889
11784
  if (
10890
11785
  stats.saved > 0 &&
10891
11786
  !_sameMessageSnapshot(compacted, authorityExpectedMessages)
10892
11787
  ) {
10893
- if (stats.summaryUsageUnknown === true) {
10894
- automaticCompactionSettlementBlocked = true;
10895
- yield {
10896
- type: "compaction-usage-unknown",
10897
- runId,
10898
- provider: stats.summaryProvider || options.provider || null,
10899
- model: stats.summaryModel || options.model || null,
10900
- reason:
10901
- stats.summaryUsageUnknownReason ||
10902
- "provider_transport_outcome_unknown",
10903
- source: "semantic-compaction",
10904
- };
10905
- // Do not apply the fallback or start another paid model request.
10906
- // A transport failure may already have incurred unknown usage.
10907
- return;
10908
- }
10909
11788
  const liveExpectedMessages = [...messages];
10910
11789
  try {
10911
11790
  await _settleAutomaticCompaction({
@@ -10931,8 +11810,17 @@ export async function* agentLoop(messages, options) {
10931
11810
  if (compactionUsage) {
10932
11811
  yield {
10933
11812
  type: "token-usage",
10934
- provider: stats.summaryProvider || options.provider || null,
10935
- model: stats.summaryModel || options.model || null,
11813
+ ...(compactionUsageCall
11814
+ ? { callId: compactionUsageCall.callId }
11815
+ : {}),
11816
+ provider:
11817
+ compactionUsageCall?.boundaryNotified === true
11818
+ ? compactionUsageCall.provider
11819
+ : stats.summaryProvider || options.provider || null,
11820
+ model:
11821
+ compactionUsageCall?.boundaryNotified === true
11822
+ ? compactionUsageCall.model
11823
+ : stats.summaryModel || options.model || null,
10936
11824
  usage: compactionUsage,
10937
11825
  source: "semantic-compaction",
10938
11826
  runId,
@@ -10940,6 +11828,10 @@ export async function* agentLoop(messages, options) {
10940
11828
  }
10941
11829
  // The paid summary call is never retried, and an unknown/stale
10942
11830
  // canonical settlement cannot be followed by another model call.
11831
+ await _awaitBackgroundUsageSettlement(
11832
+ backgroundSubAgents,
11833
+ backgroundUsageFailureState,
11834
+ );
10943
11835
  return;
10944
11836
  }
10945
11837
  if (!hermeticExecution) {
@@ -10967,8 +11859,17 @@ export async function* agentLoop(messages, options) {
10967
11859
  // next model call, without entering the normal response path.
10968
11860
  yield {
10969
11861
  type: "token-usage",
10970
- provider: stats.summaryProvider || options.provider || null,
10971
- model: stats.summaryModel || options.model || null,
11862
+ ...(compactionUsageCall
11863
+ ? { callId: compactionUsageCall.callId }
11864
+ : {}),
11865
+ provider:
11866
+ compactionUsageCall?.boundaryNotified === true
11867
+ ? compactionUsageCall.provider
11868
+ : stats.summaryProvider || options.provider || null,
11869
+ model:
11870
+ compactionUsageCall?.boundaryNotified === true
11871
+ ? compactionUsageCall.model
11872
+ : stats.summaryModel || options.model || null,
10972
11873
  usage: compactionUsage,
10973
11874
  source: "semantic-compaction",
10974
11875
  runId,
@@ -10977,6 +11878,35 @@ export async function* agentLoop(messages, options) {
10977
11878
  }
10978
11879
  } catch (_e) {
10979
11880
  if (isAbortError(_e) || signal?.aborted) throw _e;
11881
+ if (_e?.runtimeLedgerPersistence === true) throw _e;
11882
+ if (compactionObserverFailure === _e) throw _e;
11883
+ const observerFailure = compactionUsageState?.calls?.find(
11884
+ (call) => call.observerFailed && call.observerError === _e,
11885
+ );
11886
+ if (observerFailure) {
11887
+ compactionUsageState.calls = [];
11888
+ throw _e;
11889
+ }
11890
+ compactionUsageCall =
11891
+ compactionUsageCall || compactionUsageState?.calls?.at(-1) || null;
11892
+ if (compactionUsageCall) {
11893
+ automaticCompactionSettlementBlocked = true;
11894
+ yield {
11895
+ type: "compaction-usage-unknown",
11896
+ callId: compactionUsageCall.callId,
11897
+ runId,
11898
+ provider: compactionUsageCall.provider,
11899
+ model: compactionUsageCall.model,
11900
+ reason: "provider_transport_outcome_unknown",
11901
+ code: "provider_call_failed",
11902
+ source: "semantic-compaction",
11903
+ };
11904
+ await _awaitBackgroundUsageSettlement(
11905
+ backgroundSubAgents,
11906
+ backgroundUsageFailureState,
11907
+ );
11908
+ return;
11909
+ }
10980
11910
  yield {
10981
11911
  type: "compaction-degraded",
10982
11912
  runId,
@@ -10994,6 +11924,7 @@ export async function* agentLoop(messages, options) {
10994
11924
  for (const entry of _takeSettledBackgroundSubAgents(
10995
11925
  backgroundSubAgents,
10996
11926
  )) {
11927
+ _throwBackgroundSubAgentUsageFailure(entry);
10997
11928
  // SubagentStop fires at RESULT time for background spawns (the
10998
11929
  // spawn-time handle skips it); a block reason rides along as feedback.
10999
11930
  if (!hermeticExecution && options.settingsHooks) {
@@ -11033,6 +11964,12 @@ export async function* agentLoop(messages, options) {
11033
11964
  }
11034
11965
  }
11035
11966
 
11967
+ // Result-delivery hooks and context preparation can yield long enough for
11968
+ // a detached child's synchronous ledger failure to settle. Keep the final
11969
+ // dispatch fence adjacent to construction of the paid parent request.
11970
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
11971
+ _throwSettledBackgroundUsageFailure(backgroundSubAgents);
11972
+
11036
11973
  // Turn-scoped context injection (open-agents prepareCall parity).
11037
11974
  // prepareCall runs fresh each iteration and returns an ephemeral
11038
11975
  // system-message supplement that is NOT persisted to messages history.
@@ -11071,67 +12008,139 @@ export async function* agentLoop(messages, options) {
11071
12008
  promptId: `${runId}:t${budget.consumed}:p`,
11072
12009
  })
11073
12010
  : null;
11074
- const result = await _withSpan(
11075
- recorder,
11076
- "agent.model",
11077
- {
11078
- "gen_ai.system": options.provider || "ollama",
11079
- "gen_ai.request.model": options.model || "unknown",
11080
- "agent.iteration": budget.consumed,
11081
- ...(modelIdAttrs || {}),
11082
- },
11083
- () => llmCall(callMessages, effectiveToolOptions),
11084
- (span, r) => {
11085
- const t = _usageTokens(r?.usage);
11086
- if (t) {
11087
- if (t.input != null)
11088
- span.setAttribute("gen_ai.usage.input_tokens", t.input);
11089
- if (t.output != null)
11090
- span.setAttribute("gen_ai.usage.output_tokens", t.output);
11091
- // Cache read/write tokens — the prompt-caching hit rate the plan's
11092
- // reliability telemetry cares about.
11093
- if (t.cacheRead != null)
11094
- span.setAttribute("gen_ai.usage.cache_read_tokens", t.cacheRead);
11095
- if (t.cacheWrite != null)
11096
- span.setAttribute("gen_ai.usage.cache_write_tokens", t.cacheWrite);
11097
- }
11098
- span.setAttribute(
11099
- "agent.has_tool_calls",
11100
- Array.isArray(r?.message?.tool_calls) &&
11101
- r.message.tool_calls.length > 0,
11102
- );
11103
- // response CONTENT is opt-in (--otlp-content): stamped only when
11104
- // enabled, redacted + length-capped through the same normalizer; the
11105
- // field is entirely absent by default so default OTLP stays unchanged.
11106
- if (options.otlpIncludeContent === true && r?.message?.content) {
11107
- const respAttrs = buildTelemetryAttributes(
11108
- { response: r.message.content },
11109
- { includeContent: true },
12011
+ throwIfAborted(signal);
12012
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12013
+ _throwSettledBackgroundUsageFailure(backgroundSubAgents);
12014
+ const modelUsageCall = {
12015
+ type: "model-usage-started",
12016
+ callId: _newModelUsageCallId("model"),
12017
+ provider: options.provider || "ollama",
12018
+ model: options.model || "unknown",
12019
+ source: "model",
12020
+ };
12021
+ // The consumer receives this boundary before `.next()` resumes into the
12022
+ // provider call, so a durable append failure can prevent spend.
12023
+ yield modelUsageCall;
12024
+ // A background child can fail its synchronous settlement write while the
12025
+ // parent boundary is being persisted by the host. Re-check at the last
12026
+ // synchronous admission point before dispatching this paid call.
12027
+ try {
12028
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12029
+ _throwSettledBackgroundUsageFailure(backgroundSubAgents);
12030
+ } catch (backgroundFailure) {
12031
+ // The host has already persisted this parent boundary. Close it with the
12032
+ // same id before surfacing the detached child's authoritative failure.
12033
+ yield {
12034
+ type: "model-usage-unknown",
12035
+ callId: modelUsageCall.callId,
12036
+ provider: modelUsageCall.provider,
12037
+ model: modelUsageCall.model,
12038
+ source: modelUsageCall.source,
12039
+ code: "provider_call_failed",
12040
+ };
12041
+ throw backgroundFailure;
12042
+ }
12043
+
12044
+ let result;
12045
+ try {
12046
+ result = await _withSpan(
12047
+ recorder,
12048
+ "agent.model",
12049
+ {
12050
+ "gen_ai.system": options.provider || "ollama",
12051
+ "gen_ai.request.model": options.model || "unknown",
12052
+ "agent.iteration": budget.consumed,
12053
+ ...(modelIdAttrs || {}),
12054
+ },
12055
+ () => llmCall(callMessages, effectiveToolOptions),
12056
+ (span, r) => {
12057
+ const t = _usageTokens(r?.usage);
12058
+ if (t) {
12059
+ if (t.input != null)
12060
+ span.setAttribute("gen_ai.usage.input_tokens", t.input);
12061
+ if (t.output != null)
12062
+ span.setAttribute("gen_ai.usage.output_tokens", t.output);
12063
+ // Cache read/write tokens — the prompt-caching hit rate the plan's
12064
+ // reliability telemetry cares about.
12065
+ if (t.cacheRead != null)
12066
+ span.setAttribute("gen_ai.usage.cache_read_tokens", t.cacheRead);
12067
+ if (t.cacheWrite != null)
12068
+ span.setAttribute(
12069
+ "gen_ai.usage.cache_write_tokens",
12070
+ t.cacheWrite,
12071
+ );
12072
+ }
12073
+ span.setAttribute(
12074
+ "agent.has_tool_calls",
12075
+ Array.isArray(r?.message?.tool_calls) &&
12076
+ r.message.tool_calls.length > 0,
11110
12077
  );
11111
- if (respAttrs["content.response"] != null) {
11112
- span.setAttribute(
11113
- "content.response",
11114
- respAttrs["content.response"],
12078
+ // response CONTENT is opt-in (--otlp-content): stamped only when
12079
+ // enabled, redacted + length-capped through the same normalizer; the
12080
+ // field is entirely absent by default so default OTLP stays unchanged.
12081
+ if (options.otlpIncludeContent === true && r?.message?.content) {
12082
+ const respAttrs = buildTelemetryAttributes(
12083
+ { response: r.message.content },
12084
+ { includeContent: true },
11115
12085
  );
12086
+ if (respAttrs["content.response"] != null) {
12087
+ span.setAttribute(
12088
+ "content.response",
12089
+ respAttrs["content.response"],
12090
+ );
12091
+ }
11116
12092
  }
11117
- }
11118
- },
11119
- "model_error",
11120
- );
12093
+ },
12094
+ "model_error",
12095
+ );
12096
+ } catch (error) {
12097
+ // Never retain provider error text in the usage ledger. Pause on a fixed
12098
+ // unknown settlement, then rethrow only after the consumer resumes.
12099
+ yield {
12100
+ type: "model-usage-unknown",
12101
+ callId: modelUsageCall.callId,
12102
+ provider: modelUsageCall.provider,
12103
+ model: modelUsageCall.model,
12104
+ source: modelUsageCall.source,
12105
+ code: "provider_call_failed",
12106
+ };
12107
+ await _awaitBackgroundUsageSettlement(
12108
+ backgroundSubAgents,
12109
+ backgroundUsageFailureState,
12110
+ );
12111
+ throw error;
12112
+ }
11121
12113
  if (recorder) recorder.counter("agent.model.calls", 1);
11122
- throwIfAborted(signal);
11123
12114
  const msg = result?.message;
11124
12115
 
11125
- if (result?.usage) {
12116
+ // Close the already-persisted call boundary before honoring an abort that
12117
+ // raced with a successful provider response. Once the transport returned,
12118
+ // dropping this settlement would strand a durable `started` row forever.
12119
+ if (_hasCompleteProviderUsage(result?.usage)) {
11126
12120
  yield {
11127
12121
  type: "token-usage",
11128
- provider: options.provider || "ollama",
11129
- model: options.model || "unknown",
12122
+ callId: modelUsageCall.callId,
12123
+ provider: modelUsageCall.provider,
12124
+ model: modelUsageCall.model,
11130
12125
  usage: result.usage,
11131
12126
  };
12127
+ } else {
12128
+ yield {
12129
+ type: "model-usage-unknown",
12130
+ callId: modelUsageCall.callId,
12131
+ provider: modelUsageCall.provider,
12132
+ model: modelUsageCall.model,
12133
+ source: modelUsageCall.source,
12134
+ code: "provider_usage_missing",
12135
+ };
11132
12136
  }
12137
+ throwIfAborted(signal);
11133
12138
 
11134
12139
  if (!msg) {
12140
+ await _awaitBackgroundUsageSettlement(
12141
+ backgroundSubAgents,
12142
+ backgroundUsageFailureState,
12143
+ );
11135
12144
  yield { type: "response-complete", content: "(No response from LLM)" };
11136
12145
  yield { type: "run-ended", runId, reason: "no-response" };
11137
12146
  return;
@@ -11154,11 +12163,13 @@ export async function* agentLoop(messages, options) {
11154
12163
  await Promise.all(
11155
12164
  [...backgroundSubAgents.values()].map((e) => e.promise),
11156
12165
  );
12166
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
11157
12167
  throwIfAborted(signal);
11158
12168
  messages.push({ role: "assistant", content: msg.content || "" });
11159
12169
  for (const entry of _takeSettledBackgroundSubAgents(
11160
12170
  backgroundSubAgents,
11161
12171
  )) {
12172
+ _throwBackgroundSubAgentUsageFailure(entry);
11162
12173
  messages.push({
11163
12174
  role: "user",
11164
12175
  content:
@@ -11298,9 +12309,9 @@ export async function* agentLoop(messages, options) {
11298
12309
  // no process spawn, no shared-state writes) and there is no interactive
11299
12310
  // confirmer in play, run them concurrently so wall-clock drops from the SUM
11300
12311
  // of the reads to their MAX — the common "read these N files" case. Crucially,
11301
- // `tool-executing`/`tool-result` events are still YIELDED, and tool results
11302
- // still PUSHED, in the ORIGINAL order, so the output stream and tool_call_id
11303
- // correlation are byte-identical to the sequential path — only timing changes.
12312
+ // Every `tool-executing` boundary is YIELDED before dispatch, giving the
12313
+ // host a chance to durably record every call. Tool results stay in call order;
12314
+ // correlation stays deterministic even though the reads themselves overlap.
11304
12315
  // Any mutating/unknown/MCP tool in the batch, a single call, an interactive
11305
12316
  // session (concurrent prompts would race), or `parallelReadOnlyTools: false`
11306
12317
  // falls through to the strictly sequential loop below.
@@ -11317,10 +12328,7 @@ export async function* agentLoop(messages, options) {
11317
12328
  throwIfAborted(signal);
11318
12329
  const turnId = `${runId}:t${budget.consumed}`;
11319
12330
  const toolBatchRecords = [];
11320
- // Kick every read off now; settle into {result,error} so an unawaited
11321
- // promise can never surface as an unhandled rejection if the loop aborts
11322
- // before it is consumed.
11323
- const inflight = toolCalls.map((call) => {
12331
+ const prepared = toolCalls.map((call) => {
11324
12332
  let toolArgs;
11325
12333
  try {
11326
12334
  toolArgs =
@@ -11330,6 +12338,44 @@ export async function* agentLoop(messages, options) {
11330
12338
  } catch {
11331
12339
  toolArgs = {};
11332
12340
  }
12341
+ return { call, toolArgs };
12342
+ });
12343
+ // Yield every start boundary before dispatch. Async-generator consumers
12344
+ // persist each boundary before asking for the next item, so by the time
12345
+ // this loop finishes all parallel calls have durable started records.
12346
+ const startedPrepared = [];
12347
+ try {
12348
+ throwIfAborted(signal);
12349
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12350
+ for (const { call, toolArgs } of prepared) {
12351
+ yield {
12352
+ type: "tool-executing",
12353
+ tool: call.function.name,
12354
+ args: toolArgs,
12355
+ tool_use_id: call.id,
12356
+ turn_id: `${runId}:t${budget.consumed}`,
12357
+ };
12358
+ startedPrepared.push({ call, toolArgs });
12359
+ throwIfAborted(signal);
12360
+ }
12361
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12362
+ } catch (admissionFailure) {
12363
+ for (const { call } of startedPrepared) {
12364
+ yield {
12365
+ type: "tool-result",
12366
+ tool: call.function.name,
12367
+ result: { error: "Tool execution cancelled before dispatch" },
12368
+ error: "tool execution cancelled before dispatch",
12369
+ tool_use_id: call.id,
12370
+ turn_id: `${runId}:t${budget.consumed}`,
12371
+ };
12372
+ }
12373
+ throw admissionFailure;
12374
+ }
12375
+ // Only after every start has been consumed do we kick every read off.
12376
+ // Settle each promise into {result,error} so an abort before its ordered
12377
+ // result is consumed can never surface an unhandled rejection.
12378
+ const inflight = prepared.map(({ call, toolArgs }) => {
11333
12379
  const promise = executeTool(call.function.name, toolArgs, {
11334
12380
  ...toolContext,
11335
12381
  toolCallId: call.id,
@@ -11342,13 +12388,7 @@ export async function* agentLoop(messages, options) {
11342
12388
  });
11343
12389
  for (const { call, toolArgs, promise } of inflight) {
11344
12390
  throwIfAborted(signal);
11345
- yield {
11346
- type: "tool-executing",
11347
- tool: call.function.name,
11348
- args: toolArgs,
11349
- tool_use_id: call.id,
11350
- turn_id: `${runId}:t${budget.consumed}`,
11351
- };
12391
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
11352
12392
  const { result: toolResult, error: toolError } = await promise;
11353
12393
  throwIfAborted(signal);
11354
12394
  const failed = emitToolHookLifecycle({
@@ -11410,6 +12450,7 @@ export async function* agentLoop(messages, options) {
11410
12450
  const toolBatchRecords = [];
11411
12451
  for (const call of toolCalls) {
11412
12452
  throwIfAborted(signal);
12453
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
11413
12454
  const fn = call?.function;
11414
12455
  const toolName = fn?.name;
11415
12456
  // A malformed tool call (no `function` / no `name` — a provider quirk or a
@@ -11592,6 +12633,25 @@ export async function* agentLoop(messages, options) {
11592
12633
  tool_use_id: call.id,
11593
12634
  turn_id: `${runId}:t${budget.consumed}`,
11594
12635
  };
12636
+ // The event yield is a host persistence boundary. A detached child's
12637
+ // fatal usage write can settle while the consumer handles it; fence
12638
+ // the next (possibly provider-backed) tool immediately before start.
12639
+ try {
12640
+ _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12641
+ } catch (backgroundFailure) {
12642
+ yield {
12643
+ type: "tool-result",
12644
+ tool: toolName,
12645
+ result: {
12646
+ error:
12647
+ "Tool execution blocked by an authoritative background usage persistence failure",
12648
+ },
12649
+ error: "authoritative background usage persistence failure",
12650
+ tool_use_id: call.id,
12651
+ turn_id: `${runId}:t${budget.consumed}`,
12652
+ };
12653
+ throw backgroundFailure;
12654
+ }
11595
12655
  }
11596
12656
 
11597
12657
  let toolResult;
@@ -11676,6 +12736,7 @@ export async function* agentLoop(messages, options) {
11676
12736
  "tool_error",
11677
12737
  );
11678
12738
  } catch (err) {
12739
+ if (err?.runtimeLedgerPersistence === true) throw err;
11679
12740
  toolResult = { error: err.message };
11680
12741
  toolError = err.message;
11681
12742
  }
@@ -11845,6 +12906,10 @@ export async function* agentLoop(messages, options) {
11845
12906
 
11846
12907
  // Budget exhausted — flush any child usage the final iteration produced,
11847
12908
  // then yield exhaustion event + final message
12909
+ await _awaitBackgroundUsageSettlement(
12910
+ backgroundSubAgents,
12911
+ backgroundUsageFailureState,
12912
+ );
11848
12913
  yield* _drainSubAgentUsage(subAgentUsageSink);
11849
12914
  yield { type: "iteration-budget-exhausted", budget: budget.toSummary() };
11850
12915
  yield {