chainlesschain 0.165.2 → 0.165.4

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 (210) hide show
  1. package/README.md +151 -29
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/assets/{AIOps-lS1R0Cza.js → AIOps-C__nnraW.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BmlgBPsj.js → ActionButton-CsKWHEZ-.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-CNKczv1S.js → Analytics-BhIjfjWO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-BuZGJARQ.js → AppLayout-BjuXFzWZ.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-Dm73Sl-O.js → Artifacts-k6whDkZJ.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-BGMW7A5e.js → Audit-C4FBMYRh.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BQ6M54JR.js → BackgroundAgents-C_vRrSXk.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-DBzGP0bR.js → Backup-CngFZeVp.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-D1mLCoOV.js → BaseInput-B_H9EBRm.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-BUfcN7p8.js → Chat-DLEC5kt7.js} +4 -4
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-xijcWkJe.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-Du0Yg5Pi.js → Checkbox-m3ClR4xh.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-B2RXziHW.js → Codegen-CmLSFrgp.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-DWhekOrR.js → Col-DFGQteHN.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-HQhrkon4.js → Community-EUUVORHE.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-izHNb4uz.js → Compact-2PSg0Yr7.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BDwLy41-.js → Compliance-CcKur7gB.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DFJhLa7K.js → Cowork-DK_Fezfa.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DYY9RG7-.js → Cron-Dbw89xl7.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-rr7tJTtv.js → Crosschain-CH66h_Ec.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-D2sfHRLi.js → DID-D1igRb17.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-q_eGdhha.js → Dashboard-DVAsrobk.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-Cufrh2Bz.js → Dropdown-Dl7W8AQs.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-B-kdv10f.js → EmailListRenderer-D5tpaEQC.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D21XjgfG.js → FamilyGuardDashboard-O_VQf8gq.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-CAwRTLZH.js → Federation-BTVcgS9n.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-vx3daNDF.js → FormItemContext-BRnkJAIz.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-CRWY2wU4.js → GenericCardRenderer-8OWSCkDj.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-Q6SwauyD.js → Git-WXLYTQj2.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-BJ1q4IQZ.js → Governance-BezaUhtc.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CSWjROgD.js → Inference-CuRay3Gu.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-fzuQsWuY.js → KnowledgeGraph-DfWLDW1m.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DVF8Mnns.js → Logs-C55y2pPY.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-hDpWdiBc.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-BQP7LOA4.js → Marketplace-Dk08Uycg.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-BNlJCmNc.js → McpTools-C11fSB5V.js} +4 -4
  39. package/src/assets/web-panel/assets/{Memory-DLLg5WQ4.js → Memory-mr0eZc97.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CR2skNAF.js → MobileBridge-C0iCSRbb.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-BxVavObl.js → MobileProjects-CY-xMG8Z.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-B17pgqMI.js → Mtc-Bplqc_Wc.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-4UmV5cIL.js → MtcAudit-BlzwsoSM.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-DoMEXEcU.js → Multisig-BBLmTty6.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-h9DMXFX4.js → NLProgramming-DtT1uf1W.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-Cihn-xX8.js → Notes-DcqQAxTK.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-BE6ZGa3R.js → NotificationSettings-cb0kWoZe.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-CnwojoSI.js → OrderTableRenderer-kw9HZQNN.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-B2vpwmjb.js → Organization-DlTlbkUN.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-C4z28R0-.js → Overflow-pp1qs-oY.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BVlFkwhW.js → P2P-BESLnyYz.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-BXbEdL5B.js → PdhVaultBrowser-B-J1mGuf.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-C28xs4lp.js → Permissions-B36PeT1_.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-Dgl_rBjM.js → PersonalDataHub-CTK2ad-3.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-3QUIhrKI.js → Pipeline-zJNGXsac.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-8Gh33OZ5.js → Privacy-b9fz0Qf0.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-BNmg4tZi.js → ProjectInit-DjZbZgiB.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-vgMfeCf9.js → ProjectSettings-C-hHJXlD.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-BC9KSDM6.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-CXoOa8eX.js → Providers-Dz-CAYYU.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-BhfTF0oS.js → QrScannerModal-A0yoQ3-3.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-C746HEAj.js → QuickAsk-DrFgFJNC.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-BSej8Eo0.js → Recommend-KC8hF1g4.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-f3gmnlG3.js → RemoteSession-DGTPIk6u.js} +3 -3
  65. package/src/assets/web-panel/assets/{Reputation-CyvybpkN.js → Reputation-QmbwM7Yk.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DwV2PeYn.js → Row-BxjJylia.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-11GrzjZy.js → RssFeed-B7Wda1J6.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-BjKXkrAY.js → Search-_plQ_YSA.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-eWiFb9Yk.js → Security-CD2YJZfx.js} +2 -2
  70. package/src/assets/web-panel/assets/{Services-CdCsMAtt.js → Services-qQE7NmwE.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-4Q2UTKQ5.js → Skeleton-DgQqkFf7.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-v4rtYrRj.js → Skills-C4C0oFFt.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-CRhsFvd_.js → Sla-Px2N2B5c.js} +1 -1
  74. package/src/assets/web-panel/assets/SpeechSettings-DN6C3lQd.js +1 -0
  75. package/src/assets/web-panel/assets/{SyncSettings-D16zonl7.js → SyncSettings-jm5aH_m7.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-Xxozz0i_.js → Tasks-DlkJcv67.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-CXJpPtCT.js → Templates-DyrTHUBs.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-XRV0L1ZF.js → Tenant-mC434PtL.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-CYuO1i_w.js → Terminal-o0_s3NgK.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-BXqitWsu.js → TimelineRenderer-DAGlAXOw.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-BvPrDHIy.js → Tokens-BnulS_bs.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DhCrSTf5.js → Trigger-suQbVXYM.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DmsuZD6K.js → Trust-DbIJAPo5.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-D_yrts5C.js → UkeySign-CQNpbIHi.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-B01ihPrm.js → VideoEditing-D8aWCERk.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-DpUjKa-N.js → Wallet-CuLpYfZc.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CL07yON6.js → WebAuthn-CsfG8ZMc.js} +2 -2
  88. package/src/assets/web-panel/assets/{WorkflowEditor-CLMflnRr.js → WorkflowEditor-BYsEAZbd.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DtUqe8h_.js → chat-FT4Um3-I.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-DPKrgVfy.js → colors-CKbS_Wlt.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BTt2GtOm.js → compact-item-CFJtzuJM.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-D_VeUc1J.js → createContext-B9kSRW6K.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DJZ0gAMu.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-1kndiylj.js → hasIn-DR-x0Xpi.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-dVc-CXY4.js → index-7jHr_A_k.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DFIZ_1Sf.js → index-B3ZX81mK.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-fk2Rjacw.js → index-B7osrXqy.js} +3 -3
  98. package/src/assets/web-panel/assets/{index-CHG-KcS4.js → index-BGbPnC5x.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dbbymxpj.js → index-BI5hP_qy.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BaZAbw9A.js → index-BK3TiKVq.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CzjkKUmR.js → index-BVPU43b1.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BAXfRKfG.js → index-BW-t9n-q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-8nz_fmlj.js → index-BfFCQHL3.js} +1 -1
  104. package/src/assets/web-panel/assets/index-Bizb4hkw.js +1 -0
  105. package/src/assets/web-panel/assets/{index-DdkplL9R.js → index-Bt4fS2eL.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DG2MZQ3f.js → index-BwR7yc2h.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DfKBRdSS.js → index-Bzz0C1BZ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-GsoUQ2M7.js → index-C2JvmYRc.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-l1BETrDb.js → index-CGaXUsIs.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-4bna2_SR.js → index-CJmIQ9XR.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DfGl8CqG.js → index-C_enk9I_.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D95OFht7.js → index-Cr2FLUsD.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DsJCDUJG.js → index-CvrCCqJt.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-sDoMHTNA.js → index-D4LfPvWA.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-By5n8up9.js → index-DMGmg497.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-ocf06EVQ.js → index-DShSkww-.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bhudc_Ez.js → index-DTxNy0ZU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CX81qSM4.js → index-DW4BWjTk.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BlDNVgBt.js → index-DWdqx7zI.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CcO04eDn.js → index-Dap9c3eD.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Dwqmtk65.js → index-DcfxhKXR.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-2eyAXE3d.js → index-DdkIhfi-.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-brCInrzq.js → index-DeSgw-WV.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DJ8vXGOa.js → index-DiNUEpSQ.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BHpBMWCh.js → index-DjEP7WkH.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-m31Ka8hZ.js → index-DtkFqPVA.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CtF7EN49.js → index-KqDaCApw.js} +1 -1
  128. package/src/assets/web-panel/assets/index-OHSl-xCh.js +1 -0
  129. package/src/assets/web-panel/assets/{index-B9KvcS7R.js → index-OSzjTzGM.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CauoBQkH.js → index-QBSeNVO3.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-FzdwcSy2.js → index-WE08gMJP.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DxdTdBix.js → index-r2VI1Ikk.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BepLZwgB.js → index-tqSEHPRT.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CvyYJd7z.js → initDefaultProps-Dd7n7VgI.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DYZkhUdz.js → motion-BwxouRw1.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-slnJRVT1.js → move-DObN0PG9.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-DVc0CKmy.js → mtc-parser-B3nOUQc7.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BCXAtHCq.js → omit-o9WU2Nbl.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-DBsemz_K.js → pickAttrs-CL_vvfQ_.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BRtkJ-nv.js → placementArrow-CYrM2tyK.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-Cebq7EZn.js → responsiveObserve-k_06cQ96.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-DT7qvId2.js → slide-CPKG0pxS.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-pQiITLkG.js → statusUtils-BDDEeLTw.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-c-19BVKQ.js → styleChecker-Cmb7RTh7.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-DsS5-0bE.js → useFlexGapSupport-e2IedC4N.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CxGiMe5n.js → useFs-CE_gxVnm.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-D9btVID5.js → usePersonalDataHub-BkMIIKMz.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-CUYFsFEN.js → vnode-DW886XV1.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-CXmn_35j.js → zoom-Dwo_adaf.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +3 -3
  152. package/src/commands/agent.js +105 -35
  153. package/src/commands/artifacts.js +363 -33
  154. package/src/commands/cowork.js +202 -36
  155. package/src/commands/session-budget.js +155 -0
  156. package/src/commands/session-location.js +948 -0
  157. package/src/commands/session.js +72 -46
  158. package/src/data/changelog.json +26 -0
  159. package/src/gateways/ws/artifact-protocol.js +77 -13
  160. package/src/gateways/ws/session-protocol.js +391 -24
  161. package/src/gateways/ws/ws-agent-handler.js +191 -14
  162. package/src/gateways/ws/ws-server.js +14 -3
  163. package/src/gateways/ws/ws-session-gateway.js +101 -5
  164. package/src/harness/jsonl-session-store.js +1210 -0
  165. package/src/lib/agent-team/team-runner.js +43 -13
  166. package/src/lib/artifact-access-ledger.js +461 -0
  167. package/src/lib/artifact-cleanup-ledger.js +861 -0
  168. package/src/lib/artifact-deletion-ledger.js +677 -0
  169. package/src/lib/artifact-store.js +256 -51
  170. package/src/lib/background-agent-keeper-protocol.js +75 -4
  171. package/src/lib/background-agent-launch-barrier.js +74 -0
  172. package/src/lib/background-agent-supervisor.js +149 -5
  173. package/src/lib/background-turn-bootstrap-protocol.js +4 -0
  174. package/src/lib/command-lifecycle-report.js +8 -4
  175. package/src/lib/cowork-adapter.js +9 -5
  176. package/src/lib/cowork-task-runner.js +20 -0
  177. package/src/lib/cowork-workflow.js +178 -20
  178. package/src/lib/direct-model-usage.js +62 -22
  179. package/src/lib/dynamic-workflow-runtime.js +3184 -111
  180. package/src/lib/execution-location-result-apply.js +339 -0
  181. package/src/lib/execution-location-result-artifact.js +271 -0
  182. package/src/lib/execution-location-result-review.js +253 -0
  183. package/src/lib/execution-location-result-store.js +331 -0
  184. package/src/lib/execution-location-result.js +617 -0
  185. package/src/lib/execution-location-target.js +236 -1
  186. package/src/lib/managed-tool-checkpoint.js +47 -4
  187. package/src/lib/process-execution-broker/workspace-transaction.js +3 -0
  188. package/src/lib/session-budget-production-root.js +376 -0
  189. package/src/lib/session-budget-runtime.js +109 -18
  190. package/src/lib/session-budget-usage.js +63 -0
  191. package/src/lib/session-host-snapshot.js +19 -2
  192. package/src/lib/session-resource-budget.js +766 -105
  193. package/src/lib/sub-agent-context.js +69 -22
  194. package/src/lib/web-ui-server.js +12 -1
  195. package/src/lib/with-file-lock.js +175 -26
  196. package/src/repl/agent-repl.js +237 -16
  197. package/src/runtime/agent-core.js +123 -0
  198. package/src/runtime/headless-runner.js +135 -46
  199. package/src/runtime/headless-stream.js +133 -25
  200. package/src/runtime/policies/agent-policy.js +10 -0
  201. package/src/workers/background-agent-keeper.js +112 -19
  202. package/src/workers/background-agent-worker.js +383 -134
  203. package/src/workers/background-turn-bootstrap.js +13 -8
  204. package/src/assets/web-panel/assets/ChatBubbleRenderer-CY5m8ZHg.js +0 -1
  205. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +0 -1
  206. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +0 -1
  207. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +0 -1
  208. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +0 -1
  209. package/src/assets/web-panel/assets/index-CPcM-eqo.js +0 -1
  210. package/src/assets/web-panel/assets/index-xeSyVoRe.js +0 -1
@@ -15,10 +15,14 @@
15
15
  */
16
16
 
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
18
- import { randomUUID } from "node:crypto";
18
+ import { createHash, randomUUID } from "node:crypto";
19
19
  import { CostBudget } from "./cost-budget.js";
20
20
 
21
21
  export const SESSION_RESOURCE_BUDGET_VERSION = 1;
22
+ export const SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA =
23
+ "chainlesschain.session-budget-recovery-adjudication/v1";
24
+ export const SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA =
25
+ "chainlesschain.session-budget-recovery-receipt-history/v1";
22
26
 
23
27
  const INTEGER_LIMITS = new Set([
24
28
  "maxConcurrent",
@@ -56,6 +60,8 @@ const USAGE_TOKEN_FIELDS = Object.freeze([
56
60
 
57
61
  const MAX_TIMER_DELAY = 2_147_483_647;
58
62
  const RESOURCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/;
63
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
64
+ const MAX_RECOVERY_ADJUDICATION_RECEIPTS = 2048;
59
65
  const OPAQUE_AUTHORITY_ID_PATTERNS = Object.freeze({
60
66
  work: /^work-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
61
67
  tool: /^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
@@ -95,6 +101,35 @@ function isRecord(value) {
95
101
  return proto === Object.prototype || proto === null;
96
102
  }
97
103
 
104
+ function canonicalJson(value) {
105
+ if (Array.isArray(value)) {
106
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
107
+ }
108
+ if (isRecord(value)) {
109
+ return `{${Object.keys(value)
110
+ .sort()
111
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
112
+ .join(",")}}`;
113
+ }
114
+ return JSON.stringify(value);
115
+ }
116
+
117
+ function sha256(value) {
118
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
119
+ }
120
+
121
+ function normalizeRecoveryIdentity(field, raw) {
122
+ if (
123
+ typeof raw !== "string" ||
124
+ raw.length < 1 ||
125
+ raw.length > 256 ||
126
+ /[\u0000-\u001f\u007f]/u.test(raw)
127
+ ) {
128
+ throw new TypeError(`invalid session budget recovery ${field}`);
129
+ }
130
+ return raw;
131
+ }
132
+
98
133
  function normalizeLimit(field, raw) {
99
134
  if (raw === undefined || raw === null || raw === "") return null;
100
135
  const value = Number(raw);
@@ -150,6 +185,405 @@ function normalizeUsageTokens(usage) {
150
185
  return { fields, total };
151
186
  }
152
187
 
188
+ function normalizeUsageAccounting({
189
+ usage = null,
190
+ usageRecords = null,
191
+ provider,
192
+ model,
193
+ } = {}) {
194
+ if (usageRecords != null && !Array.isArray(usageRecords)) {
195
+ throw new TypeError("invalid session budget usage records");
196
+ }
197
+ if (Array.isArray(usageRecords) && usageRecords.length > 0) {
198
+ const detailedFields = Object.fromEntries(
199
+ USAGE_TOKEN_FIELDS.map((field) => [field, 0]),
200
+ );
201
+ let detailedTokenCount = 0;
202
+ const pricedRecords = usageRecords.map((record, index) => {
203
+ if (!isRecord(record) || !isRecord(record.usage)) {
204
+ throw new TypeError(
205
+ `invalid session budget usage record at index ${index}`,
206
+ );
207
+ }
208
+ const normalized = normalizeUsageTokens(record.usage);
209
+ for (const field of USAGE_TOKEN_FIELDS) {
210
+ detailedFields[field] += normalized.fields[field];
211
+ if (!Number.isSafeInteger(detailedFields[field])) {
212
+ throw new TypeError(
213
+ `session budget usage record aggregate exceeds safe integer: ${field}`,
214
+ );
215
+ }
216
+ }
217
+ detailedTokenCount += normalized.total;
218
+ if (!Number.isSafeInteger(detailedTokenCount)) {
219
+ throw new TypeError("session budget token total exceeds safe integer");
220
+ }
221
+ return { record, tokens: normalized.total };
222
+ });
223
+ if (usage != null) {
224
+ const aggregate = normalizeUsageTokens(usage);
225
+ for (const field of USAGE_TOKEN_FIELDS) {
226
+ if (aggregate.fields[field] !== detailedFields[field]) {
227
+ throw new TypeError(
228
+ `session budget usage records do not match aggregate usage: ${field}`,
229
+ );
230
+ }
231
+ }
232
+ return { tokenCount: aggregate.total, pricedRecords };
233
+ }
234
+ return { tokenCount: detailedTokenCount, pricedRecords };
235
+ }
236
+ const normalized = normalizeUsageTokens(usage);
237
+ return {
238
+ tokenCount: normalized.total,
239
+ pricedRecords:
240
+ usage == null
241
+ ? []
242
+ : [{ record: { provider, model, usage }, tokens: normalized.total }],
243
+ };
244
+ }
245
+
246
+ function cloneCostBudget(cost) {
247
+ const clone = new CostBudget({
248
+ limitUsd: cost.limitUsd,
249
+ table: cost.table,
250
+ });
251
+ clone.spentUsd = cost.spentUsd;
252
+ clone.priced = cost.priced;
253
+ clone.sawUnpriced = cost.sawUnpriced;
254
+ clone.sawFree = cost.sawFree;
255
+ clone._warned = cost._warned;
256
+ return clone;
257
+ }
258
+
259
+ function calculateUsageAccounting(
260
+ { tokens, cost, unpricedUsage },
261
+ usageInput,
262
+ ) {
263
+ const { tokenCount, pricedRecords } = normalizeUsageAccounting(usageInput);
264
+ const nextTokens = tokens + tokenCount;
265
+ if (!Number.isSafeInteger(nextTokens)) {
266
+ throw new TypeError("session budget token total exceeds safe integer");
267
+ }
268
+ const nextCost = cloneCostBudget(cost);
269
+ let nextUnpricedUsage = unpricedUsage;
270
+ for (const { record, tokens: recordTokens } of pricedRecords) {
271
+ const estimate = nextCost.add(record);
272
+ const price = Number(estimate?.totalCost);
273
+ if (
274
+ nextCost.enabled() &&
275
+ recordTokens > 0 &&
276
+ estimate?.free !== true &&
277
+ (estimate?.matched !== true || !Number.isFinite(price) || price < 0)
278
+ ) {
279
+ nextUnpricedUsage = true;
280
+ }
281
+ }
282
+ return {
283
+ tokens: nextTokens,
284
+ cost: nextCost,
285
+ unpricedUsage: nextUnpricedUsage,
286
+ };
287
+ }
288
+
289
+ function canonicalRecoveryUsageRecord(record, field) {
290
+ if (!isRecord(record) || !isRecord(record.usage)) {
291
+ throw new TypeError(`invalid session budget recovery ${field}`);
292
+ }
293
+ return {
294
+ provider: normalizeRecoveryIdentity(`${field} provider`, record.provider),
295
+ model: normalizeRecoveryIdentity(`${field} model`, record.model),
296
+ usage: normalizeUsageTokens(record.usage).fields,
297
+ };
298
+ }
299
+
300
+ function canonicalRecoverySettlement(record, index) {
301
+ const base = { authorityId: record.authorityId };
302
+ if (Array.isArray(record.usageRecords) && record.usageRecords.length > 0) {
303
+ const usageRecords = record.usageRecords
304
+ .map((entry, recordIndex) =>
305
+ canonicalRecoveryUsageRecord(
306
+ entry,
307
+ `settlement ${index} usage record ${recordIndex}`,
308
+ ),
309
+ )
310
+ .sort((left, right) =>
311
+ canonicalJson(left).localeCompare(canonicalJson(right)),
312
+ );
313
+ return {
314
+ ...base,
315
+ ...(record.usage != null
316
+ ? { usage: normalizeUsageTokens(record.usage).fields }
317
+ : {}),
318
+ usageRecords,
319
+ };
320
+ }
321
+ return {
322
+ ...base,
323
+ ...canonicalRecoveryUsageRecord(record, `settlement ${index}`),
324
+ };
325
+ }
326
+
327
+ function cloneCanonicalRecord(value) {
328
+ return JSON.parse(canonicalJson(value));
329
+ }
330
+
331
+ function isOpaqueAuthorityId(value) {
332
+ return Object.values(OPAQUE_AUTHORITY_ID_PATTERNS).some((pattern) =>
333
+ pattern.test(value),
334
+ );
335
+ }
336
+
337
+ function normalizeRecoveryReceipt(value, index) {
338
+ if (
339
+ !isRecord(value) ||
340
+ value.schema !== SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA ||
341
+ !Number.isSafeInteger(value.sequence) ||
342
+ value.sequence < 1 ||
343
+ (value.previousDigest !== null &&
344
+ !SHA256_PATTERN.test(value.previousDigest)) ||
345
+ !Array.isArray(value.abandoned) ||
346
+ !Array.isArray(value.settled) ||
347
+ !isRecord(value.totalsBefore) ||
348
+ !isRecord(value.totalsAfter)
349
+ ) {
350
+ throw new TypeError(
351
+ `invalid session budget recovery receipt at index ${index}`,
352
+ );
353
+ }
354
+ const abandoned = value.abandoned.map((raw) => String(raw)).sort();
355
+ const settled = value.settled
356
+ .map((record, settlementIndex) => {
357
+ if (!isRecord(record)) {
358
+ throw new TypeError(
359
+ `invalid session budget recovery receipt settlement at index ${index}:${settlementIndex}`,
360
+ );
361
+ }
362
+ const authorityId = String(record.authorityId || "");
363
+ if (!OPAQUE_AUTHORITY_ID_PATTERNS.usage.test(authorityId)) {
364
+ throw new TypeError(
365
+ `invalid session budget recovery receipt authority at index ${index}:${settlementIndex}`,
366
+ );
367
+ }
368
+ return canonicalRecoverySettlement(
369
+ { ...record, authorityId },
370
+ settlementIndex,
371
+ );
372
+ })
373
+ .sort((left, right) => left.authorityId.localeCompare(right.authorityId));
374
+ const allAuthorityIds = [
375
+ ...abandoned,
376
+ ...settled.map((record) => record.authorityId),
377
+ ];
378
+ if (
379
+ abandoned.some((authorityId) => !isOpaqueAuthorityId(authorityId)) ||
380
+ new Set(allAuthorityIds).size !== allAuthorityIds.length ||
381
+ canonicalJson(abandoned) !== canonicalJson(value.abandoned) ||
382
+ canonicalJson(settled) !== canonicalJson(value.settled)
383
+ ) {
384
+ throw new TypeError(
385
+ `invalid session budget recovery receipt authorities at index ${index}`,
386
+ );
387
+ }
388
+ const totalsBefore = {
389
+ tokens: normalizeNonNegativeInteger(
390
+ "recovery receipt tokens before",
391
+ value.totalsBefore.tokens,
392
+ ),
393
+ spentUsd: normalizeNonNegativeNumber(
394
+ "recovery receipt USD before",
395
+ value.totalsBefore.spentUsd,
396
+ ),
397
+ };
398
+ const totalsAfter = {
399
+ tokens: normalizeNonNegativeInteger(
400
+ "recovery receipt tokens after",
401
+ value.totalsAfter.tokens,
402
+ ),
403
+ spentUsd: normalizeNonNegativeNumber(
404
+ "recovery receipt USD after",
405
+ value.totalsAfter.spentUsd,
406
+ ),
407
+ };
408
+ if (
409
+ totalsAfter.tokens < totalsBefore.tokens ||
410
+ totalsAfter.spentUsd < totalsBefore.spentUsd
411
+ ) {
412
+ throw new TypeError(
413
+ `invalid session budget recovery receipt totals at index ${index}`,
414
+ );
415
+ }
416
+ const core = {
417
+ schema: SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA,
418
+ sequence: value.sequence,
419
+ previousDigest: value.previousDigest,
420
+ abandoned,
421
+ settled,
422
+ totalsBefore,
423
+ totalsAfter,
424
+ };
425
+ const digest = sha256(canonicalJson(core));
426
+ if (value.digest !== digest) {
427
+ throw new TypeError(
428
+ `invalid session budget recovery receipt digest at index ${index}`,
429
+ );
430
+ }
431
+ return { ...core, digest };
432
+ }
433
+
434
+ function normalizeRecoveryReceiptHistory(value, adjudication, totals) {
435
+ if (
436
+ !isRecord(value) ||
437
+ value.schema !== SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA ||
438
+ !Number.isSafeInteger(value.baseSequence) ||
439
+ value.baseSequence < 0 ||
440
+ (value.baseDigest !== null && !SHA256_PATTERN.test(value.baseDigest)) ||
441
+ (value.baseSequence === 0) !== (value.baseDigest === null) ||
442
+ !Array.isArray(value.entries) ||
443
+ value.entries.length < 1 ||
444
+ value.entries.length > MAX_RECOVERY_ADJUDICATION_RECEIPTS ||
445
+ value.baseSequence + value.entries.length !== adjudication.count
446
+ ) {
447
+ throw new TypeError(
448
+ "invalid session budget recovery adjudication receipt history",
449
+ );
450
+ }
451
+ const entries = value.entries.map(normalizeRecoveryReceipt);
452
+ let previousDigest = value.baseDigest;
453
+ let previousTotals = null;
454
+ for (let index = 0; index < entries.length; index += 1) {
455
+ const entry = entries[index];
456
+ if (
457
+ entry.sequence !== value.baseSequence + index + 1 ||
458
+ entry.previousDigest !== previousDigest ||
459
+ (previousTotals !== null &&
460
+ (entry.totalsBefore.tokens !== previousTotals.tokens ||
461
+ entry.totalsBefore.spentUsd !== previousTotals.spentUsd))
462
+ ) {
463
+ throw new TypeError(
464
+ "invalid session budget recovery adjudication receipt chain",
465
+ );
466
+ }
467
+ previousDigest = entry.digest;
468
+ previousTotals = entry.totalsAfter;
469
+ }
470
+ const last = entries.at(-1);
471
+ if (
472
+ last.sequence !== adjudication.count ||
473
+ last.digest !== adjudication.headDigest ||
474
+ last.digest !== adjudication.last.digest ||
475
+ last.previousDigest !== adjudication.last.previousDigest ||
476
+ last.settled.length !== adjudication.last.settledCount ||
477
+ last.abandoned.length !== adjudication.last.abandonedCount ||
478
+ last.totalsAfter.tokens - last.totalsBefore.tokens !==
479
+ adjudication.last.tokenDelta ||
480
+ last.totalsAfter.spentUsd - last.totalsBefore.spentUsd !==
481
+ adjudication.last.spentUsdDelta ||
482
+ last.totalsAfter.tokens !== totals.tokens ||
483
+ last.totalsAfter.spentUsd !== totals.spentUsd
484
+ ) {
485
+ throw new TypeError(
486
+ "invalid session budget recovery adjudication receipt head",
487
+ );
488
+ }
489
+ return {
490
+ schema: SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA,
491
+ baseSequence: value.baseSequence,
492
+ baseDigest: value.baseDigest,
493
+ entries,
494
+ };
495
+ }
496
+
497
+ function normalizeRecoveryAdjudicationState(value, totals) {
498
+ if (value === undefined || value === null) return null;
499
+ if (
500
+ !isRecord(value) ||
501
+ value.schema !== SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA ||
502
+ !Number.isSafeInteger(value.count) ||
503
+ value.count < 1 ||
504
+ !SHA256_PATTERN.test(value.headDigest) ||
505
+ !isRecord(value.last) ||
506
+ value.last.sequence !== value.count ||
507
+ value.last.digest !== value.headDigest ||
508
+ !SHA256_PATTERN.test(value.last.digest) ||
509
+ (value.last.previousDigest !== null &&
510
+ !SHA256_PATTERN.test(value.last.previousDigest)) ||
511
+ !Number.isSafeInteger(value.last.settledCount) ||
512
+ value.last.settledCount < 0 ||
513
+ !Number.isSafeInteger(value.last.abandonedCount) ||
514
+ value.last.abandonedCount < 0 ||
515
+ !Number.isSafeInteger(value.last.tokenDelta) ||
516
+ value.last.tokenDelta < 0 ||
517
+ !Number.isFinite(value.last.spentUsdDelta) ||
518
+ value.last.spentUsdDelta < 0
519
+ ) {
520
+ throw new TypeError("invalid session budget recovery adjudication state");
521
+ }
522
+ if (
523
+ (value.count === 1 && value.last.previousDigest !== null) ||
524
+ (value.count > 1 && value.last.previousDigest === null)
525
+ ) {
526
+ throw new TypeError("invalid session budget recovery adjudication chain");
527
+ }
528
+ const adjudication = {
529
+ schema: SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA,
530
+ count: value.count,
531
+ headDigest: value.headDigest,
532
+ last: {
533
+ sequence: value.last.sequence,
534
+ digest: value.last.digest,
535
+ previousDigest: value.last.previousDigest,
536
+ settledCount: value.last.settledCount,
537
+ abandonedCount: value.last.abandonedCount,
538
+ tokenDelta: value.last.tokenDelta,
539
+ spentUsdDelta: value.last.spentUsdDelta,
540
+ },
541
+ };
542
+ if (value.history !== undefined) {
543
+ adjudication.history = normalizeRecoveryReceiptHistory(
544
+ value.history,
545
+ adjudication,
546
+ totals,
547
+ );
548
+ }
549
+ return adjudication;
550
+ }
551
+
552
+ function cloneRecoveryAdjudication(value, { includeReceipts = true } = {}) {
553
+ if (!value) return null;
554
+ const cloned = {
555
+ schema: value.schema,
556
+ count: value.count,
557
+ headDigest: value.headDigest,
558
+ last: { ...value.last },
559
+ };
560
+ if (value.history) {
561
+ cloned.history = includeReceipts
562
+ ? {
563
+ schema: value.history.schema,
564
+ baseSequence: value.history.baseSequence,
565
+ baseDigest: value.history.baseDigest,
566
+ entries: value.history.entries.map(cloneCanonicalRecord),
567
+ }
568
+ : {
569
+ schema: value.history.schema,
570
+ baseSequence: value.history.baseSequence,
571
+ baseDigest: value.history.baseDigest,
572
+ retainedCount: value.history.entries.length,
573
+ complete: value.history.baseSequence === 0,
574
+ };
575
+ }
576
+ return cloned;
577
+ }
578
+
579
+ function normalizeUsageSettlementLabel(raw) {
580
+ const value = String(raw || "");
581
+ if (!RESOURCE_ID_PATTERN.test(value)) {
582
+ throw new TypeError("invalid session budget usage settlement id");
583
+ }
584
+ return value;
585
+ }
586
+
153
587
  function safeReason(reason, fallback = "session-aborted") {
154
588
  if (reason instanceof Error) return reason;
155
589
  const text = String(reason || fallback).slice(0, 512);
@@ -258,6 +692,10 @@ function validateSnapshot(snapshot) {
258
692
  }
259
693
  const work = validateResourceList(snapshot.inFlight.work, "work");
260
694
  const tools = validateResourceList(snapshot.inFlight.tools, "tools");
695
+ const recoveryAdjudication = normalizeRecoveryAdjudicationState(
696
+ snapshot.state.recoveryAdjudication,
697
+ totals,
698
+ );
261
699
  const workIds = new Set(work.map((entry) => entry.id));
262
700
  if (tools.some((entry) => workIds.has(entry.id))) {
263
701
  throw new TypeError(
@@ -270,6 +708,7 @@ function validateSnapshot(snapshot) {
270
708
  state: {
271
709
  started: snapshot.state.started,
272
710
  abort: snapshot.state.abort,
711
+ recoveryAdjudication,
273
712
  },
274
713
  inFlight: {
275
714
  work,
@@ -340,6 +779,13 @@ export function normalizeSessionResourceBudgetSnapshot(snapshot) {
340
779
  message: `Session resource budget stopped: ${abortReason}`,
341
780
  }
342
781
  : null,
782
+ ...(normalized.state.recoveryAdjudication
783
+ ? {
784
+ recoveryAdjudication: cloneRecoveryAdjudication(
785
+ normalized.state.recoveryAdjudication,
786
+ ),
787
+ }
788
+ : {}),
343
789
  },
344
790
  };
345
791
  }
@@ -423,9 +869,11 @@ export class SessionResourceBudget {
423
869
  this._activeWorkLabels = new Map();
424
870
  this._activeToolLabels = new Map();
425
871
  this._pendingUsage = new Map();
872
+ this._pendingUsageLabels = new Map();
426
873
  this._issuedAuthorityIds = new Set();
427
874
  this._abortables = new Map();
428
875
  this._recoveryUnknown = new Map();
876
+ this._recoveryAdjudication = null;
429
877
  this._deadlineTimer = null;
430
878
  this._abortController = new AbortController();
431
879
  this._abortState = null;
@@ -701,7 +1149,6 @@ export class SessionResourceBudget {
701
1149
 
702
1150
  reason(now = this._now()) {
703
1151
  if (this._abortState) return this._abortState.reason;
704
- if (this._pendingUsage.size > 0) return "usage-settlement-pending";
705
1152
  if (this._recoveryUnknown.size > 0) return "recovery-required";
706
1153
  const continuous = this._continuousReason(now);
707
1154
  if (continuous) return continuous;
@@ -733,9 +1180,6 @@ export class SessionResourceBudget {
733
1180
  if (this.signal.aborted) {
734
1181
  return this._admissionFailure(this.reason() || "session-aborted", { id });
735
1182
  }
736
- if (this._pendingUsage.size > 0) {
737
- return this._admissionFailure("usage-settlement-pending", { id });
738
- }
739
1183
  if (this._recoveryUnknown.size > 0) {
740
1184
  return this._admissionFailure("recovery-required", { id });
741
1185
  }
@@ -794,9 +1238,6 @@ export class SessionResourceBudget {
794
1238
  id: key,
795
1239
  });
796
1240
  }
797
- if (this._pendingUsage.size > 0) {
798
- return this._admissionFailure("usage-settlement-pending", { id: key });
799
- }
800
1241
  if (this._recoveryUnknown.size > 0) {
801
1242
  return this._admissionFailure("recovery-required", { id: key });
802
1243
  }
@@ -917,9 +1358,6 @@ export class SessionResourceBudget {
917
1358
  id: key,
918
1359
  });
919
1360
  }
920
- if (this._pendingUsage.size > 0) {
921
- return this._admissionFailure("usage-settlement-pending", { id: key });
922
- }
923
1361
  if (this._recoveryUnknown.size > 0) {
924
1362
  return this._admissionFailure("recovery-required", { id: key });
925
1363
  }
@@ -997,112 +1435,143 @@ export class SessionResourceBudget {
997
1435
  return true;
998
1436
  }
999
1437
 
1000
- recordUsage({ usage = null, usageRecords = null, provider, model } = {}) {
1438
+ beginUsageSettlement({ id, settleAfterAbort = false } = {}) {
1001
1439
  this._assertAuthorityMutationAllowed();
1002
- this.start();
1003
- if (this._pendingUsage.size > 0) {
1004
- throw new SessionBudgetError("usage-settlement-pending");
1005
- }
1006
- let tokenCount;
1007
- let pricedRecords;
1440
+ const label = normalizeUsageSettlementLabel(id);
1008
1441
  try {
1009
- if (usageRecords != null && !Array.isArray(usageRecords)) {
1010
- throw new TypeError("invalid session budget usage records");
1011
- }
1012
- if (Array.isArray(usageRecords) && usageRecords.length > 0) {
1013
- const detailedFields = Object.fromEntries(
1014
- USAGE_TOKEN_FIELDS.map((field) => [field, 0]),
1015
- );
1016
- let detailedTokenCount = 0;
1017
- pricedRecords = usageRecords.map((record, index) => {
1018
- if (!isRecord(record) || !isRecord(record.usage)) {
1019
- throw new TypeError(
1020
- `invalid session budget usage record at index ${index}`,
1021
- );
1022
- }
1023
- const normalized = normalizeUsageTokens(record.usage);
1024
- for (const field of USAGE_TOKEN_FIELDS) {
1025
- detailedFields[field] += normalized.fields[field];
1026
- if (!Number.isSafeInteger(detailedFields[field])) {
1027
- throw new TypeError(
1028
- `session budget usage record aggregate exceeds safe integer: ${field}`,
1029
- );
1030
- }
1031
- }
1032
- detailedTokenCount += normalized.total;
1033
- if (!Number.isSafeInteger(detailedTokenCount)) {
1034
- throw new TypeError(
1035
- "session budget token total exceeds safe integer",
1036
- );
1037
- }
1038
- return { record, tokens: normalized.total };
1039
- });
1040
- if (usage != null) {
1041
- const aggregate = normalizeUsageTokens(usage);
1042
- for (const field of USAGE_TOKEN_FIELDS) {
1043
- if (aggregate.fields[field] !== detailedFields[field]) {
1044
- throw new TypeError(
1045
- `session budget usage records do not match aggregate usage: ${field}`,
1046
- );
1047
- }
1048
- }
1049
- tokenCount = aggregate.total;
1050
- } else {
1051
- tokenCount = detailedTokenCount;
1052
- }
1053
- } else {
1054
- tokenCount = normalizeUsageTokens(usage).total;
1055
- pricedRecords =
1056
- usage == null
1057
- ? []
1058
- : [{ record: { provider, model, usage }, tokens: tokenCount }];
1059
- }
1060
- if (!Number.isSafeInteger(tokenCount)) {
1061
- throw new TypeError("session budget token total exceeds safe integer");
1062
- }
1442
+ this.start();
1063
1443
  } catch (error) {
1064
- this.abort(error, { reason: "invalid-usage" });
1065
- throw error;
1444
+ return this._persistenceAdmissionFailure(error, { id: label });
1066
1445
  }
1067
- const nextTokens = this.tokens + tokenCount;
1068
- if (!Number.isSafeInteger(nextTokens)) {
1069
- const error = new TypeError(
1070
- "session budget token total exceeds safe integer",
1071
- );
1072
- this.abort(error, { reason: "invalid-usage" });
1073
- throw error;
1446
+ if (this.signal.aborted && settleAfterAbort !== true) {
1447
+ return this._admissionFailure(this.reason() || "session-aborted", {
1448
+ id: label,
1449
+ });
1450
+ }
1451
+ if (this._recoveryUnknown.size > 0) {
1452
+ return this._admissionFailure("recovery-required", { id: label });
1074
1453
  }
1075
- const settlementId = this._nextUsageSettlementId();
1076
- this._pendingUsage.set(settlementId, {
1077
- id: settlementId,
1454
+ if (this._pendingUsageLabels.has(label)) {
1455
+ return this._admissionFailure("duplicate-usage-settlement-id", {
1456
+ id: label,
1457
+ });
1458
+ }
1459
+ const authorityId = this._nextUsageSettlementId();
1460
+ const entry = {
1461
+ id: authorityId,
1462
+ label,
1078
1463
  kind: "usage-settlement",
1079
1464
  depth: 0,
1080
1465
  startedAt: this._now(),
1466
+ };
1467
+ this._pendingUsage.set(authorityId, entry);
1468
+ this._pendingUsageLabels.set(label, authorityId);
1469
+ try {
1470
+ this._authorityChanged(
1471
+ "budget:usage-settlement-started",
1472
+ {
1473
+ pendingUsage: this._pendingUsage.size,
1474
+ },
1475
+ {
1476
+ rollback: () => {
1477
+ this._pendingUsage.delete(authorityId);
1478
+ this._pendingUsageLabels.delete(label);
1479
+ },
1480
+ },
1481
+ );
1482
+ } catch (error) {
1483
+ return this._persistenceAdmissionFailure(error, { id: label });
1484
+ }
1485
+ return { ok: true, id: label, authorityId };
1486
+ }
1487
+
1488
+ _pendingUsageSettlement(callId) {
1489
+ if (callId !== undefined && callId !== null) {
1490
+ const label = normalizeUsageSettlementLabel(callId);
1491
+ const authorityId = this._pendingUsageLabels.get(label);
1492
+ if (!authorityId) {
1493
+ throw new SessionBudgetError("usage-settlement-not-started");
1494
+ }
1495
+ return { label, authorityId };
1496
+ }
1497
+ if (this._pendingUsage.size > 0) {
1498
+ throw new SessionBudgetError("usage-settlement-pending");
1499
+ }
1500
+ const label = `inline:${randomUUID()}`;
1501
+ const admission = this.beginUsageSettlement({
1502
+ id: label,
1503
+ settleAfterAbort: true,
1081
1504
  });
1082
- this._authorityChanged("budget:usage-settlement-started", {
1083
- pendingUsage: this._pendingUsage.size,
1084
- });
1085
- this.tokens = nextTokens;
1505
+ if (!admission?.ok) {
1506
+ if (admission?.reason === "persistence-failed") {
1507
+ this._assertAuthorityMutationAllowed();
1508
+ }
1509
+ throw new SessionBudgetError(
1510
+ admission?.reason || "usage-settlement-not-started",
1511
+ );
1512
+ }
1513
+ return { label, authorityId: admission.authorityId };
1514
+ }
1515
+
1516
+ markUsageUnknown({ callId } = {}) {
1517
+ this._assertAuthorityMutationAllowed();
1518
+ const { label, authorityId } = this._pendingUsageSettlement(callId);
1519
+ const pending = this._pendingUsage.get(authorityId);
1520
+ this._pendingUsage.delete(authorityId);
1521
+ this._pendingUsageLabels.delete(label);
1522
+ const recovery = { ...pending, resourceType: "work" };
1523
+ this._recoveryUnknown.set(authorityId, recovery);
1524
+ this._authorityChanged(
1525
+ "budget:usage-unknown",
1526
+ {
1527
+ pendingUsage: this._pendingUsage.size,
1528
+ pendingRecovery: this._recoveryUnknown.size,
1529
+ },
1530
+ {
1531
+ rollback: () => {
1532
+ this._recoveryUnknown.delete(authorityId);
1533
+ this._pendingUsage.set(authorityId, pending);
1534
+ this._pendingUsageLabels.set(label, authorityId);
1535
+ },
1536
+ },
1537
+ );
1538
+ return this.status();
1539
+ }
1086
1540
 
1541
+ recordUsage({
1542
+ callId = null,
1543
+ usage = null,
1544
+ usageRecords = null,
1545
+ provider,
1546
+ model,
1547
+ } = {}) {
1548
+ this._assertAuthorityMutationAllowed();
1549
+ this.start();
1550
+ let accounting;
1087
1551
  try {
1088
- for (const { record, tokens: recordTokens } of pricedRecords) {
1089
- const estimate = this.cost.add(record);
1090
- const price = Number(estimate?.totalCost);
1091
- if (
1092
- this.cost.enabled() &&
1093
- recordTokens > 0 &&
1094
- estimate?.free !== true &&
1095
- (estimate?.matched !== true || !Number.isFinite(price) || price < 0)
1096
- ) {
1097
- this.unpricedUsage = true;
1098
- }
1099
- }
1552
+ accounting = calculateUsageAccounting(
1553
+ {
1554
+ tokens: this.tokens,
1555
+ cost: this.cost,
1556
+ unpricedUsage: this.unpricedUsage,
1557
+ },
1558
+ { usage, usageRecords, provider, model },
1559
+ );
1100
1560
  } catch (error) {
1101
1561
  this.abort(error, { reason: "invalid-usage" });
1102
1562
  throw error;
1103
1563
  }
1564
+ const { label, authorityId: settlementId } =
1565
+ this._pendingUsageSettlement(callId);
1104
1566
  const pendingSettlement = this._pendingUsage.get(settlementId);
1567
+ const previousTokens = this.tokens;
1568
+ const previousCost = this.cost;
1569
+ const previousUnpricedUsage = this.unpricedUsage;
1570
+ this.tokens = accounting.tokens;
1571
+ this.cost = accounting.cost;
1572
+ this.unpricedUsage = accounting.unpricedUsage;
1105
1573
  this._pendingUsage.delete(settlementId);
1574
+ this._pendingUsageLabels.delete(label);
1106
1575
  this._authorityChanged(
1107
1576
  "budget:usage-recorded",
1108
1577
  {
@@ -1111,7 +1580,11 @@ export class SessionResourceBudget {
1111
1580
  },
1112
1581
  {
1113
1582
  rollback: () => {
1583
+ this.tokens = previousTokens;
1584
+ this.cost = previousCost;
1585
+ this.unpricedUsage = previousUnpricedUsage;
1114
1586
  this._pendingUsage.set(settlementId, pendingSettlement);
1587
+ this._pendingUsageLabels.set(label, settlementId);
1115
1588
  },
1116
1589
  },
1117
1590
  );
@@ -1223,6 +1696,7 @@ export class SessionResourceBudget {
1223
1696
  cleanupErrors,
1224
1697
  activeWork: this._activeWork.size,
1225
1698
  activeTools: this._activeTools.size,
1699
+ pendingUsage: this._pendingUsage.size,
1226
1700
  };
1227
1701
  if (skipAuthorityChange) this._emit("budget:aborted", detail);
1228
1702
  else this._authorityChanged("budget:aborted", detail);
@@ -1276,15 +1750,50 @@ export class SessionResourceBudget {
1276
1750
  });
1277
1751
  }
1278
1752
 
1279
- adjudicateRecovery({ abandoned = [] } = {}) {
1753
+ adjudicateRecovery({ abandoned = [], settled = [] } = {}) {
1280
1754
  this._assertAuthorityMutationAllowed();
1281
1755
  if (!Array.isArray(abandoned)) {
1282
1756
  throw new TypeError(
1283
1757
  "session budget abandoned recovery ids must be an array",
1284
1758
  );
1285
1759
  }
1286
- const supplied = new Set(abandoned.map((id) => String(id)));
1760
+ if (!Array.isArray(settled)) {
1761
+ throw new TypeError(
1762
+ "session budget settled recovery records must be an array",
1763
+ );
1764
+ }
1765
+ const abandonedIds = abandoned.map((id) => String(id));
1766
+ const normalizedSettlements = settled.map((record, index) => {
1767
+ if (!isRecord(record)) {
1768
+ throw new TypeError(
1769
+ `invalid session budget recovery settlement at index ${index}`,
1770
+ );
1771
+ }
1772
+ const authorityId = String(record.authorityId || "");
1773
+ if (!OPAQUE_AUTHORITY_ID_PATTERNS.usage.test(authorityId)) {
1774
+ throw new TypeError(
1775
+ `invalid session budget recovery settlement authority at index ${index}`,
1776
+ );
1777
+ }
1778
+ const normalized = { ...record, authorityId };
1779
+ return {
1780
+ ...normalized,
1781
+ canonical: canonicalRecoverySettlement(normalized, index),
1782
+ };
1783
+ });
1784
+ const settledIds = normalizedSettlements.map(
1785
+ (record) => record.authorityId,
1786
+ );
1787
+ const suppliedIds = [...abandonedIds, ...settledIds];
1788
+ const supplied = new Set(suppliedIds);
1287
1789
  const expected = new Set(this._recoveryUnknown.keys());
1790
+ if (supplied.size !== suppliedIds.length) {
1791
+ return {
1792
+ ok: false,
1793
+ reason: "recovery-adjudication-duplicate",
1794
+ pending: this.pendingRecovery(),
1795
+ };
1796
+ }
1288
1797
  if (
1289
1798
  supplied.size !== expected.size ||
1290
1799
  [...expected].some((id) => !supplied.has(id))
@@ -1295,23 +1804,160 @@ export class SessionResourceBudget {
1295
1804
  pending: this.pendingRecovery(),
1296
1805
  };
1297
1806
  }
1807
+ for (const record of normalizedSettlements) {
1808
+ const pending = this._recoveryUnknown.get(record.authorityId);
1809
+ if (
1810
+ pending?.resourceType !== "work" ||
1811
+ pending?.kind !== "usage-settlement"
1812
+ ) {
1813
+ return {
1814
+ ok: false,
1815
+ reason: "recovery-settlement-not-usage",
1816
+ authorityId: record.authorityId,
1817
+ pending: this.pendingRecovery(),
1818
+ };
1819
+ }
1820
+ }
1821
+ let accounting = {
1822
+ tokens: this.tokens,
1823
+ cost: this.cost,
1824
+ unpricedUsage: this.unpricedUsage,
1825
+ };
1826
+ for (const record of normalizedSettlements) {
1827
+ accounting = calculateUsageAccounting(accounting, record);
1828
+ }
1829
+ const previousAdjudication = this._recoveryAdjudication;
1830
+ const sequence = (previousAdjudication?.count || 0) + 1;
1831
+ const previousDigest = previousAdjudication?.headDigest || null;
1832
+ const sortedAbandoned = [...abandonedIds].sort();
1833
+ const sortedSettlements = normalizedSettlements
1834
+ .map((record) => record.canonical)
1835
+ .sort((left, right) =>
1836
+ left.authorityId.localeCompare(right.authorityId),
1837
+ );
1838
+ const tokenDelta = accounting.tokens - this.tokens;
1839
+ const spentUsdDelta = accounting.cost.spentUsd - this.cost.spentUsd;
1840
+ const adjudicationCore = {
1841
+ schema: SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA,
1842
+ sequence,
1843
+ previousDigest,
1844
+ abandoned: sortedAbandoned,
1845
+ settled: sortedSettlements,
1846
+ totalsBefore: {
1847
+ tokens: this.tokens,
1848
+ spentUsd: this.cost.spentUsd,
1849
+ },
1850
+ totalsAfter: {
1851
+ tokens: accounting.tokens,
1852
+ spentUsd: accounting.cost.spentUsd,
1853
+ },
1854
+ };
1855
+ const adjudicationDigest = sha256(canonicalJson(adjudicationCore));
1856
+ const receipt = {
1857
+ ...adjudicationCore,
1858
+ digest: adjudicationDigest,
1859
+ };
1860
+ const adjudicationState = {
1861
+ sequence,
1862
+ digest: adjudicationDigest,
1863
+ previousDigest,
1864
+ settledCount: settledIds.length,
1865
+ abandonedCount: abandonedIds.length,
1866
+ tokenDelta,
1867
+ spentUsdDelta,
1868
+ };
1869
+ const adjudication = {
1870
+ schema: SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA,
1871
+ ...adjudicationState,
1872
+ };
1298
1873
  const previousRecovery = [...this._recoveryUnknown.entries()];
1874
+ const previousTokens = this.tokens;
1875
+ const previousCost = this.cost;
1876
+ const previousUnpricedUsage = this.unpricedUsage;
1877
+ this.tokens = accounting.tokens;
1878
+ this.cost = accounting.cost;
1879
+ this.unpricedUsage = accounting.unpricedUsage;
1880
+ this._recoveryAdjudication = {
1881
+ schema: SESSION_BUDGET_RECOVERY_ADJUDICATION_SCHEMA,
1882
+ count: sequence,
1883
+ headDigest: adjudicationDigest,
1884
+ last: adjudicationState,
1885
+ history: {
1886
+ schema: SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA,
1887
+ baseSequence:
1888
+ previousAdjudication?.history?.baseSequence ??
1889
+ previousAdjudication?.count ??
1890
+ 0,
1891
+ baseDigest:
1892
+ previousAdjudication?.history !== undefined
1893
+ ? previousAdjudication.history.baseDigest
1894
+ : previousAdjudication?.headDigest || null,
1895
+ entries: [
1896
+ ...(previousAdjudication?.history?.entries || []).map(
1897
+ cloneCanonicalRecord,
1898
+ ),
1899
+ receipt,
1900
+ ],
1901
+ },
1902
+ };
1299
1903
  this._recoveryUnknown.clear();
1300
1904
  this._authorityChanged(
1301
1905
  "budget:recovery-adjudicated",
1302
1906
  {
1303
- abandonedCount: supplied.size,
1907
+ abandonedCount: abandonedIds.length,
1908
+ settledCount: settledIds.length,
1909
+ tokens: this.tokens,
1910
+ spentUsd: this.cost.spentUsd,
1911
+ adjudicationDigest,
1304
1912
  },
1305
1913
  {
1306
1914
  rollback: () => {
1915
+ this.tokens = previousTokens;
1916
+ this.cost = previousCost;
1917
+ this.unpricedUsage = previousUnpricedUsage;
1918
+ this._recoveryAdjudication = previousAdjudication;
1307
1919
  for (const [id, entry] of previousRecovery) {
1308
1920
  this._recoveryUnknown.set(id, entry);
1309
1921
  }
1310
1922
  },
1311
1923
  },
1312
1924
  );
1313
- this._armDeadline();
1314
- return { ok: true, abandoned: [...supplied].sort() };
1925
+ const reason = this._continuousReason();
1926
+ if (reason) this._abortFor(reason);
1927
+ else this._armDeadline();
1928
+ return {
1929
+ ok: true,
1930
+ abandoned: [...abandonedIds].sort(),
1931
+ settled: [...settledIds].sort(),
1932
+ adjudication: { ...adjudication },
1933
+ receipt: cloneCanonicalRecord(receipt),
1934
+ };
1935
+ }
1936
+
1937
+ recoveryAdjudicationReceipts() {
1938
+ if (!this._recoveryAdjudication) {
1939
+ return {
1940
+ schema: SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA,
1941
+ count: 0,
1942
+ headDigest: null,
1943
+ baseSequence: 0,
1944
+ baseDigest: null,
1945
+ complete: true,
1946
+ entries: [],
1947
+ };
1948
+ }
1949
+ const history = this._recoveryAdjudication.history;
1950
+ return {
1951
+ schema: SESSION_BUDGET_RECOVERY_RECEIPT_HISTORY_SCHEMA,
1952
+ count: this._recoveryAdjudication.count,
1953
+ headDigest: this._recoveryAdjudication.headDigest,
1954
+ baseSequence: history?.baseSequence ?? this._recoveryAdjudication.count,
1955
+ baseDigest: history
1956
+ ? history.baseDigest
1957
+ : this._recoveryAdjudication.headDigest,
1958
+ complete: history ? history.baseSequence === 0 : false,
1959
+ entries: history?.entries.map(cloneCanonicalRecord) || [],
1960
+ };
1315
1961
  }
1316
1962
 
1317
1963
  status(now = this._now()) {
@@ -1332,9 +1978,14 @@ export class SessionResourceBudget {
1332
1978
  toolMs: this._toolMsAt(now),
1333
1979
  maxToolMs: this.maxToolMs,
1334
1980
  activeTools: this._activeTools.size,
1981
+ pendingUsage: this._pendingUsage.size,
1335
1982
  resources: this._abortables.size,
1336
1983
  recoveryRequired: this._recoveryUnknown.size > 0,
1337
1984
  pendingRecovery: this._recoveryUnknown.size,
1985
+ recoveryAdjudication: cloneRecoveryAdjudication(
1986
+ this._recoveryAdjudication,
1987
+ { includeReceipts: false },
1988
+ ),
1338
1989
  unpricedUsage: this.unpricedUsage,
1339
1990
  aborted: this.signal.aborted,
1340
1991
  reason: this.reason(now),
@@ -1386,6 +2037,13 @@ export class SessionResourceBudget {
1386
2037
  state: {
1387
2038
  started: this._startedAt !== null,
1388
2039
  abort: this._abortState ? { ...this._abortState } : null,
2040
+ ...(this._recoveryAdjudication
2041
+ ? {
2042
+ recoveryAdjudication: cloneRecoveryAdjudication(
2043
+ this._recoveryAdjudication,
2044
+ ),
2045
+ }
2046
+ : {}),
1389
2047
  },
1390
2048
  };
1391
2049
  }
@@ -1442,6 +2100,9 @@ export class SessionResourceBudget {
1442
2100
  budget._toolMs = normalized.totals.toolMs;
1443
2101
  const restoredAt = budget._now();
1444
2102
  budget._startedAt = normalized.state.started ? restoredAt : null;
2103
+ budget._recoveryAdjudication = cloneRecoveryAdjudication(
2104
+ normalized.state.recoveryAdjudication,
2105
+ );
1445
2106
 
1446
2107
  if (recoverUnsettled === "require-adjudication") {
1447
2108
  for (const entry of normalized.inFlight.work) {