chainlesschain 0.166.2 → 0.166.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 (173) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BnZ24WRk.js → AIOps-HcOh0ZbH.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-ivXY7MGb.js → ActionButton-DWjDqBhd.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-BQS4OD-k.js → Analytics-C6FS-y3W.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-wOBuOKTO.js → AppLayout-DCN5cldF.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-BYuC4nw-.js → Artifacts-Crd-KE8P.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-BIeVfBt6.js → Audit-BexuTQZc.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-hIuyhmJ7.js → BackgroundAgents-Dydkh-b5.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-B6ohdgPQ.js → Backup-BYq21wMP.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-BzgYCw6h.js → BaseInput-CmNZP6yM.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CwahvZU1.js → Chat-J6bD0zCl.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-B-1IZ_k8.js → ChatBubbleRenderer-MEoaygnL.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-26EDP2x0.js → Checkbox-Ce-9vNmK.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-DYLw1Zvd.js → Codegen-zf7-Y-Eh.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-DH_zAvD-.js → Col-D-TkY0FD.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DURrTXJu.js → Community-l300NA-1.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CQyBqN0p.js → Compact-D-9Q_hxc.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-a0DfgKHD.js → Compliance-DAJ8lLiQ.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-C6fY_5Ke.js → Cowork-xmzZ967S.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DOwmpbTe.js → Cron-CeGqO5W4.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DEPOIEUs.js → Crosschain-DXN0AE-4.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-DaKc-CEV.js → DID-CtIKASPr.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-C-0KLXq5.js → Dashboard-DqRwT8NX.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-CnsDukzr.js → Dropdown-ddIVljO1.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-Bi_ACD4N.js → EmailListRenderer-CcsFzV4y.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZASpnx3.js → FamilyGuardDashboard-PnlAUGcG.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-43oTtzyp.js → Federation-BlmLuQrL.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-oyVadnkZ.js → FormItemContext-dDovBj1Y.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-Cwvfd6TZ.js → GenericCardRenderer-DTvGshFr.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-Cgi2wvU0.js → Git-DuFR2xgA.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-l8TWDKZ0.js → Governance-9EaNcJuq.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-Dc8j6TaO.js → Inference-B-iYWH_r.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-DgcO-Fwz.js → KnowledgeGraph-BGBaKiLX.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DjiZpiwa.js → Logs-G3wmXzfa.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-B5MFCi0N.js → MarkdownRenderer-hk0YwjTR.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-BeDn1hFc.js → Marketplace-BNARBeAv.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-PGdVfWK6.js → McpTools-DST2cSSK.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-e76lY3a1.js → Memory-piwDLsVH.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-DKlQqot_.js → MobileBridge-D-W8zHdm.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-C4g6WOEr.js → MobileProjects-CwaYHlVP.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-1j3NpRQq.js → Mtc-C2dxnN7Q.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-DgwiVd1C.js → MtcAudit-BwPZsV3N.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-Dzg5lZZY.js → Multisig-CUt8w2al.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-D7GYhst-.js → NLProgramming-DwxErx27.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-Blcfvaic.js → Notes-B-mXAZ84.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-Dc4Y6vfr.js → NotificationSettings-Bd1fBAzd.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-CCInnavw.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-DBXq024L.js → Organization-DTTONYZT.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-CFPbdx1Y.js → Overflow-H6lCZ1rE.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-w7z61NR5.js → P2P-DyBZzeYW.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-B7eGGZOa.js → PdhVaultBrowser-jj7LhT04.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-Df5Cl50D.js → Permissions-B5nSQwsr.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-C-We7RF6.js → PersonalDataHub-CACaQtlY.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-B6fHPnKy.js → Pipeline-5y-DR_Bw.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-B85BdiIc.js → Privacy-BMSA3Ehd.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-CcOyazSe.js → ProjectInit-VCOj1dTa.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-BE53KfuU.js → ProjectSettings-CJaXO_iE.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-DOvMOoe_.js → Projects-C21-MQ3H.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-5l7sT9lq.js → Providers-DZ1FNKUR.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DE37RXy4.js → QrScannerModal-6DKc9UBX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-BGmGCo7j.js → QuickAsk-Dyd0Ckrk.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-BFdx8Nrm.js → Recommend-CYW4-GJH.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-C1w-j_hB.js → RemoteSession-DLrfPy4F.js} +3 -3
  65. package/src/assets/web-panel/assets/{Reputation-BOvn-10y.js → Reputation-DfqPo1FI.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-db-a1YO9.js → Row-CR89H2x5.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-DeS43ZGM.js → RssFeed-B9QY7R-F.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-BaQYNfeb.js → Search-CUDnzu4w.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-yt1ESHDg.js → Security-CJ0CtM_Z.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-BcNqPUsS.js → Services-CyrQ6F5U.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-X3ceUQWf.js → Skeleton-DcwnKatQ.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-DmYeax6C.js → Skills-BhKkYF3_.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-C_fdfbuJ.js → Sla-CWh-awh7.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-CDqohXXQ.js → SpeechSettings-BaUMNzx7.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-B25maYEY.js → SyncSettings-D1Ijwwxl.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-DFqjHi3T.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-DUH0iuc4.js → Templates-Ct-9_ki3.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DrvhHf9T.js → Tenant-zBkgyilh.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-C2lJVYHA.js → Terminal-VVO2Hjfi.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-C1Hf_jYn.js → TimelineRenderer-CX9pIkx3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-DniS1NpS.js → Tokens-DHaD6e_m.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-4jrcb8oA.js → Trigger-CFq9j3WV.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DiBte55w.js → Trust-DuF7gIYX.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-CZRtc4VB.js → UkeySign-DbuidXJX.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-BtYGJDmg.js → VideoEditing-B8IA9_qE.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-D1nrTm7H.js → Wallet-DdCglSIK.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-DI91acpx.js → WebAuthn-CeteRsA8.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BtQjI50d.js → WorkflowEditor-DJhIbG__.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-I4A-f4FC.js → chat-srOi_Wef.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-QZkDkFVt.js → colors-HpVHsNGK.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item--FtCdzpr.js → compact-item-DsLhrqWe.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CaIKIKxP.js → createContext-aOasIUuE.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-B_acr36n.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DLYUII6-.js → hasIn-D5BVMimS.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DGLHSCHM.js → index-B615KD2o.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-3rVtgqa2.js → index-BCp70IlO.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Deg2S9ZJ.js → index-BD64r0Tx.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-yayaayEA.js → index-BGBvkNN3.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B65QxJaG.js → index-BNbsjeEc.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BxPoPofq.js → index-BTwDhhvl.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C3PnIEOH.js → index-BigtYchO.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BnLYpdox.js +1 -0
  103. package/src/assets/web-panel/assets/{index-BLGJmgmx.js → index-Bu6TVdgQ.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CL4c5D6G.js → index-C4tPtHU0.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Du2wUavy.js → index-CGVHW0nF.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DA_TPdzX.js → index-CQEoGqMG.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Ch0IFpGy.js → index-CQXp_uNd.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CBtII-nl.js → index-CSxJ4oRd.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-21I79XSf.js → index-CUUs12L9.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DAuf8r5o.js → index-CVp_dHCs.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Bs0qPPFb.js → index-ChGUPEwS.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DVitI3UQ.js → index-CvhVCHk-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DK2r6Bv7.js → index-D1xZMQrN.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Ooo_5jHo.js → index-D2XRXkZU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DJrd_CR3.js → index-DQ5j1VKg.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CXaeZCRP.js → index-DRCIPsdm.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CQ7OBHka.js → index-DV4y5rUj.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-eTqL91aV.js → index-DcLBzIiI.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CisYjy88.js → index-DiXsis7m.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-D4CXFU-w.js → index-Dpj-rEoV.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BebTT7iR.js → index-Dum_r_oY.js} +1 -1
  122. package/src/assets/web-panel/assets/index-Dzmwsl_v.js +1 -0
  123. package/src/assets/web-panel/assets/{index-vfiDxdN2.js → index-LSz0ZOB8.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Ygi2rYL1.js → index-Ow3SRXRX.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-hfiZ-i2b.js → index-P6IMLmLT.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CgMm85BY.js → index-V0gpVTbS.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-0DYBQ-6e.js → index-b0Y0hDhX.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BGWSmE3G.js → index-dPFzlII1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-OQPIS_am.js → index-fZnlPk7H.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BkJgWW_L.js → index-kYqi6zYp.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BvdwNnHC.js → index-lFOfvZDn.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-BcgtvJbC.js → index-q--LtRh9.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D2xETCkj.js → index-vQ6jy8Ew.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-DDkTvw8k.js → initDefaultProps-1NU9yNpr.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-fkr96tN5.js → motion-BZk2hQ6c.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-CGBV0SV2.js → move-BNvublME.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-DAsCliFt.js → mtc-parser-DWk5BUoL.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BM8PMmKN.js → omit-DztYxX3x.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-C6SdWoTK.js → pickAttrs-DURL8mtg.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BG0EVu-d.js → placementArrow-B820R56z.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-L10IvfYC.js → responsiveObserve-DUAkGJL1.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-TddIkbrn.js → slide-uZQeNSo6.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-FlveF2Yn.js → statusUtils-CGR1wFVq.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BrXRyebg.js → styleChecker-BIIPOMsQ.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-B7_oiDXZ.js → useFlexGapSupport-rIB9c0g2.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CQYeRYWb.js → useFs-CrJe1BJi.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DspIkhVi.js → usePersonalDataHub-DyEoD3Qm.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-BCwmwMAW.js → vnode-CjohPxI0.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-B0cEGluV.js → zoom-C5POL1_6.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/team.js +165 -53
  152. package/src/data/changelog.json +26 -0
  153. package/src/gateways/ws/remote-session-protocol.js +20 -15
  154. package/src/generated/cc-agent-protocol.schema.json +67 -1
  155. package/src/lib/agent-team/task-lease.js +718 -6
  156. package/src/lib/agent-team/team-graph-projection.js +303 -0
  157. package/src/lib/agent-team/team-message-bridge.js +83 -5
  158. package/src/lib/agent-team/team-message-tools.js +34 -1
  159. package/src/lib/agent-team/team-runner.js +1131 -9
  160. package/src/lib/agent-team/team-session-message-adapter.js +720 -0
  161. package/src/lib/app-server/cli-agent-kernel-adapter.js +1 -0
  162. package/src/lib/app-server/protocol.js +44 -8
  163. package/src/lib/app-server/server.js +8 -11
  164. package/src/lib/approval-grant-ledger.js +187 -0
  165. package/src/lib/remote-approval-bridge.js +8 -12
  166. package/src/lib/remote-approval-decision.js +87 -0
  167. package/src/lib/session-message-fabric.js +265 -3
  168. package/src/runtime/headless-stream.js +184 -19
  169. package/src/assets/web-panel/assets/OrderTableRenderer-dgM87XIn.js +0 -1
  170. package/src/assets/web-panel/assets/Tasks-3ILwauYw.js +0 -1
  171. package/src/assets/web-panel/assets/devWarning-D7qVrkrO.js +0 -1
  172. package/src/assets/web-panel/assets/index-D9Y_ZwZn.js +0 -1
  173. package/src/assets/web-panel/assets/index-D_cPes4G.js +0 -1
@@ -38,11 +38,15 @@ export const SESSION_MESSAGE_RECEIPT_STATUSES = Object.freeze([
38
38
  "full",
39
39
  "rate_limited",
40
40
  "expired",
41
+ "read",
42
+ "processed",
43
+ "dead_letter",
41
44
  ]);
42
45
 
43
46
  export const SESSION_MESSAGE_FABRIC_LIMITS = Object.freeze({
44
47
  maxMessageBytes: 256 * 1024,
45
48
  maxPendingPerRecipient: 100,
49
+ maxPendingBytes: 32 * 1024 * 1024,
46
50
  maxReceiptHistory: 5_000,
47
51
  maxMessagesPerSenderWindow: 120,
48
52
  senderRateWindowMs: 60_000,
@@ -62,9 +66,21 @@ export const SESSION_MESSAGE_FABRIC_ERROR_CODES = Object.freeze({
62
66
  });
63
67
 
64
68
  const PENDING_STATUSES = new Set(["held", "delivered"]);
69
+ const MESSAGE_STATUSES = new Set([
70
+ "delivered",
71
+ "held",
72
+ "refused",
73
+ "full",
74
+ "rate_limited",
75
+ "expired",
76
+ ]);
77
+ const PROCESSING_STATUSES = new Set(["read", "processed", "dead_letter"]);
78
+ const TERMINAL_PROCESSING_STATUSES = new Set(["processed", "dead_letter"]);
65
79
  const RECEIPT_STATUSES = new Set(SESSION_MESSAGE_RECEIPT_STATUSES);
66
80
  const POLICIES = new Set(SESSION_MESSAGE_POLICIES);
67
81
  const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
82
+ // Bounded ids explicitly reject ASCII control characters.
83
+ // eslint-disable-next-line no-control-regex
68
84
  const ID_PATTERN = /^[^\u0000-\u001f\u007f]{1,256}$/u;
69
85
  const MESSAGE_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/u;
70
86
 
@@ -324,6 +340,11 @@ function updateReceipt(state, message, status, reason, now) {
324
340
  updatedAt: now,
325
341
  idleNotifiedAt: null,
326
342
  readAt: null,
343
+ processedAt: null,
344
+ deadLetteredAt: null,
345
+ deliveryCount: 0,
346
+ consumerKey: null,
347
+ recipientAttempt: null,
327
348
  };
328
349
  state.receipts.push(receipt);
329
350
  } else {
@@ -402,6 +423,7 @@ function compactSettledMessages(state) {
402
423
  state.messages = state.messages.filter((message) => {
403
424
  if (!PENDING_STATUSES.has(message.status)) return false;
404
425
  if (message.acknowledgedAt == null) return true;
426
+ if (message.retainAfterAck === true) return true;
405
427
  if (message.notifyWhenIdle !== true) return false;
406
428
  const receipt = receiptFor(
407
429
  state,
@@ -413,6 +435,30 @@ function compactSettledMessages(state) {
413
435
  return state.messages.length !== before;
414
436
  }
415
437
 
438
+ function trimRetainedSettledMessages(state, limit) {
439
+ const retained = state.messages
440
+ .filter(
441
+ (message) =>
442
+ message.retainAfterAck === true && message.acknowledgedAt != null,
443
+ )
444
+ .sort(
445
+ (left, right) =>
446
+ left.acknowledgedAt - right.acknowledgedAt ||
447
+ left.createdAt - right.createdAt ||
448
+ left.messageId.localeCompare(right.messageId),
449
+ );
450
+ let changed = false;
451
+ while (retained.length > limit) {
452
+ const target = retained.shift();
453
+ const index = state.messages.indexOf(target);
454
+ if (index >= 0) {
455
+ state.messages.splice(index, 1);
456
+ changed = true;
457
+ }
458
+ }
459
+ return changed;
460
+ }
461
+
416
462
  function applyExpectedMessage(state, channel, message, recipient, now) {
417
463
  if (recipient.policy === "refuse") {
418
464
  updateReceipt(state, message, "refused", "policy_refuse", now);
@@ -556,7 +602,7 @@ function validateState(state) {
556
602
  !MESSAGE_ID_PATTERN.test(message?.messageId || "") ||
557
603
  !Number.isSafeInteger(message?.sequence) ||
558
604
  message.sequence < 1 ||
559
- !RECEIPT_STATUSES.has(message?.status) ||
605
+ !MESSAGE_STATUSES.has(message?.status) ||
560
606
  !channelIds.has(message?.channelId) ||
561
607
  typeof message?.payloadDigest !== "string"
562
608
  ) {
@@ -572,6 +618,13 @@ function validateState(state) {
572
618
  ) {
573
619
  fail("invalid receipt");
574
620
  }
621
+ if (
622
+ receipt.deliveryCount != null &&
623
+ (!Number.isSafeInteger(receipt.deliveryCount) ||
624
+ receipt.deliveryCount < 0)
625
+ ) {
626
+ fail("invalid receipt delivery count");
627
+ }
575
628
  }
576
629
  const bucketAuthorities = new Set();
577
630
  for (const bucket of state.rateBuckets || []) {
@@ -606,6 +659,7 @@ export class SessionMessageFabric {
606
659
  lockTimeoutMs = 10_000,
607
660
  maxMessageBytes = SESSION_MESSAGE_FABRIC_LIMITS.maxMessageBytes,
608
661
  maxPendingPerRecipient = SESSION_MESSAGE_FABRIC_LIMITS.maxPendingPerRecipient,
662
+ maxPendingBytes = SESSION_MESSAGE_FABRIC_LIMITS.maxPendingBytes,
609
663
  maxReceiptHistory = SESSION_MESSAGE_FABRIC_LIMITS.maxReceiptHistory,
610
664
  maxMessagesPerSenderWindow = SESSION_MESSAGE_FABRIC_LIMITS.maxMessagesPerSenderWindow,
611
665
  senderRateWindowMs = SESSION_MESSAGE_FABRIC_LIMITS.senderRateWindowMs,
@@ -636,6 +690,10 @@ export class SessionMessageFabric {
636
690
  maxPendingPerRecipient,
637
691
  SESSION_MESSAGE_FABRIC_LIMITS.maxPendingPerRecipient,
638
692
  ),
693
+ maxPendingBytes: positiveInteger(
694
+ maxPendingBytes,
695
+ SESSION_MESSAGE_FABRIC_LIMITS.maxPendingBytes,
696
+ ),
639
697
  maxReceiptHistory: positiveInteger(
640
698
  maxReceiptHistory,
641
699
  SESSION_MESSAGE_FABRIC_LIMITS.maxReceiptHistory,
@@ -770,6 +828,10 @@ export class SessionMessageFabric {
770
828
  );
771
829
  const outcome = mutator(state, now) || { value: undefined };
772
830
  const compacted = compactSettledMessages(state);
831
+ const trimmedSettled = trimRetainedSettledMessages(
832
+ state,
833
+ this._limits.maxReceiptHistory,
834
+ );
773
835
  const trimmed = trimReceiptHistory(
774
836
  state,
775
837
  this._limits.maxReceiptHistory,
@@ -778,6 +840,7 @@ export class SessionMessageFabric {
778
840
  swept ||
779
841
  prunedRateBuckets ||
780
842
  compacted ||
843
+ trimmedSettled ||
781
844
  trimmed ||
782
845
  outcome.changed === true
783
846
  ) {
@@ -985,6 +1048,7 @@ export class SessionMessageFabric {
985
1048
  sequence = null,
986
1049
  ttlMs = this._limits.defaultTtlMs,
987
1050
  notifyWhenIdle = false,
1051
+ retainAfterAck = false,
988
1052
  } = {}) {
989
1053
  const id = messageId(requestedMessageId, this._createId);
990
1054
  const cleanSubject = subject == null ? null : String(subject).slice(0, 512);
@@ -1108,6 +1172,7 @@ export class SessionMessageFabric {
1108
1172
  status: "held",
1109
1173
  reason: "out_of_order",
1110
1174
  notifyWhenIdle: notifyWhenIdle === true,
1175
+ retainAfterAck: retainAfterAck === true,
1111
1176
  createdAt: now,
1112
1177
  updatedAt: now,
1113
1178
  deliveredAt: null,
@@ -1137,10 +1202,17 @@ export class SessionMessageFabric {
1137
1202
  value: clone(receiptFor(state, sender.authorityId, id)),
1138
1203
  };
1139
1204
  }
1205
+ const pendingBytes = state.messages
1206
+ .filter(
1207
+ (entry) =>
1208
+ PENDING_STATUSES.has(entry.status) && entry.acknowledgedAt == null,
1209
+ )
1210
+ .reduce((sum, entry) => sum + (entry.messageBytes || 0), 0);
1140
1211
  if (
1141
1212
  recipient.policy !== "refuse" &&
1142
- pendingForRecipient(state, recipient).length >=
1143
- this._limits.maxPendingPerRecipient
1213
+ (pendingForRecipient(state, recipient).length >=
1214
+ this._limits.maxPendingPerRecipient ||
1215
+ pendingBytes + messageBytes > this._limits.maxPendingBytes)
1144
1216
  ) {
1145
1217
  updateReceipt(state, message, "full", "queue_capacity", now);
1146
1218
  return {
@@ -1194,6 +1266,196 @@ export class SessionMessageFabric {
1194
1266
  });
1195
1267
  }
1196
1268
 
1269
+ /**
1270
+ * Deliver an at-least-once batch without retiring it. This is deliberately
1271
+ * separate from `inbox(..., { acknowledge: true })`, whose historical API
1272
+ * means "read and remove". Processing callers must explicitly settle each
1273
+ * message with `acknowledge()`.
1274
+ */
1275
+ receive(selector, { limit = 100, markRead = false } = {}) {
1276
+ const boundedLimit = positiveInteger(limit, 100, 100);
1277
+ return this._transaction((state, now) => {
1278
+ const endpoint = requireEndpoint(state, selector);
1279
+ const messages = pendingForRecipient(state, endpoint)
1280
+ .filter((message) => message.status === "delivered")
1281
+ .sort(
1282
+ (left, right) =>
1283
+ (left.senderAuthority === right.senderAuthority
1284
+ ? left.sequence - right.sequence
1285
+ : left.createdAt - right.createdAt) ||
1286
+ left.senderAuthority.localeCompare(right.senderAuthority),
1287
+ )
1288
+ .slice(0, boundedLimit);
1289
+ for (const message of messages) {
1290
+ const receipt = receiptFor(
1291
+ state,
1292
+ message.senderAuthority,
1293
+ message.messageId,
1294
+ );
1295
+ if (!receipt) continue;
1296
+ receipt.deliveryCount = (receipt.deliveryCount || 0) + 1;
1297
+ receipt.deliveredAt ||= message.deliveredAt || now;
1298
+ if (markRead) {
1299
+ receipt.status = "read";
1300
+ receipt.readAt ||= now;
1301
+ } else if (!PROCESSING_STATUSES.has(receipt.status)) {
1302
+ receipt.status = "delivered";
1303
+ }
1304
+ receipt.updatedAt = now;
1305
+ }
1306
+ return {
1307
+ changed: messages.length > 0,
1308
+ value: clone(
1309
+ messages.map((message) => ({
1310
+ ...message,
1311
+ delivery: receiptFor(
1312
+ state,
1313
+ message.senderAuthority,
1314
+ message.messageId,
1315
+ ),
1316
+ })),
1317
+ ),
1318
+ };
1319
+ });
1320
+ }
1321
+
1322
+ /** Persist read/processed/dead-letter evidence for explicit physical ids. */
1323
+ acknowledge(
1324
+ selector,
1325
+ {
1326
+ messageIds = [],
1327
+ consumerKey,
1328
+ status = "processed",
1329
+ reason = null,
1330
+ recipientAttempt = null,
1331
+ } = {},
1332
+ ) {
1333
+ if (
1334
+ !Array.isArray(messageIds) ||
1335
+ messageIds.length === 0 ||
1336
+ messageIds.length > 100 ||
1337
+ messageIds.some((id) => !MESSAGE_ID_PATTERN.test(String(id || ""))) ||
1338
+ new Set(messageIds.map(String)).size !== messageIds.length
1339
+ ) {
1340
+ throw fabricError(
1341
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
1342
+ "messageIds must contain 1-100 unique message identifiers",
1343
+ );
1344
+ }
1345
+ if (!PROCESSING_STATUSES.has(status)) {
1346
+ throw fabricError(
1347
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
1348
+ "processing status must be read, processed or dead_letter",
1349
+ );
1350
+ }
1351
+ const safeConsumerKey = boundedId(consumerKey, "consumerKey");
1352
+ const safeAttempt =
1353
+ recipientAttempt == null ? null : jsonBody(recipientAttempt).value;
1354
+ const safeReason = String(reason || "poison_message").slice(0, 1024);
1355
+ const ids = messageIds.map(String);
1356
+ return this._transaction((state, now) => {
1357
+ const endpoint = requireEndpoint(state, selector);
1358
+ const plans = ids.map((id) => {
1359
+ const receipt = state.receipts.find(
1360
+ (candidate) =>
1361
+ candidate.messageId === id &&
1362
+ candidate.recipientAuthority === endpoint.authorityId &&
1363
+ candidate.recipientEpoch === endpoint.epoch,
1364
+ );
1365
+ const message = state.messages.find(
1366
+ (candidate) =>
1367
+ candidate.messageId === id &&
1368
+ candidate.recipientAuthority === endpoint.authorityId &&
1369
+ candidate.recipientEpoch === endpoint.epoch,
1370
+ );
1371
+ if (
1372
+ !receipt ||
1373
+ (!message && !TERMINAL_PROCESSING_STATUSES.has(receipt.status))
1374
+ ) {
1375
+ throw fabricError(
1376
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
1377
+ `message is not addressed to the active recipient: ${id}`,
1378
+ );
1379
+ }
1380
+ if (TERMINAL_PROCESSING_STATUSES.has(receipt.status)) {
1381
+ if (receipt.consumerKey !== safeConsumerKey) {
1382
+ throw fabricError(
1383
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.MESSAGE_ID_CONFLICT,
1384
+ `message was settled by another consumer: ${id}`,
1385
+ );
1386
+ }
1387
+ if (receipt.status !== status) {
1388
+ throw fabricError(
1389
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.MESSAGE_ID_CONFLICT,
1390
+ `message already has terminal status ${receipt.status}: ${id}`,
1391
+ );
1392
+ }
1393
+ return { receipt, message, replay: true };
1394
+ }
1395
+ return { receipt, message, replay: false };
1396
+ });
1397
+ const receipts = [];
1398
+ for (const plan of plans) {
1399
+ const { receipt, message, replay } = plan;
1400
+ if (replay) {
1401
+ receipts.push(clone(receipt));
1402
+ continue;
1403
+ }
1404
+ receipt.status = status;
1405
+ receipt.consumerKey = safeConsumerKey;
1406
+ receipt.recipientAttempt = safeAttempt;
1407
+ receipt.updatedAt = now;
1408
+ if (status === "read") receipt.readAt ||= now;
1409
+ if (status === "processed") {
1410
+ receipt.readAt ||= now;
1411
+ receipt.processedAt ||= now;
1412
+ message.acknowledgedAt = now;
1413
+ }
1414
+ if (status === "dead_letter") {
1415
+ receipt.deadLetteredAt ||= now;
1416
+ receipt.reason = safeReason;
1417
+ message.acknowledgedAt = now;
1418
+ }
1419
+ receipts.push(clone(receipt));
1420
+ }
1421
+ return { changed: plans.some((plan) => !plan.replay), value: receipts };
1422
+ });
1423
+ }
1424
+
1425
+ /** Bounded retained history for authoritative adapters and audit reducers. */
1426
+ history() {
1427
+ return this._transaction((state) => ({
1428
+ value: clone(
1429
+ state.messages.sort(
1430
+ (left, right) =>
1431
+ left.createdAt - right.createdAt ||
1432
+ left.messageId.localeCompare(right.messageId),
1433
+ ),
1434
+ ),
1435
+ }));
1436
+ }
1437
+
1438
+ /**
1439
+ * One-revision authority view for host adapters and deterministic reducers.
1440
+ * Unlike a sequence of projection/history/receipt reads, this cannot combine
1441
+ * fields from different concurrent commits.
1442
+ */
1443
+ auditSnapshot() {
1444
+ return this._transaction((state) => ({
1445
+ value: {
1446
+ schema: SESSION_MESSAGE_FABRIC_SCHEMA,
1447
+ version: state.version,
1448
+ authority: "cli",
1449
+ revision: state.revision,
1450
+ digest: state.digest,
1451
+ limits: clone(this._limits),
1452
+ endpoints: clone(state.endpoints),
1453
+ messages: clone(state.messages),
1454
+ receipts: clone(state.receipts),
1455
+ },
1456
+ }));
1457
+ }
1458
+
1197
1459
  receipts(selector) {
1198
1460
  return this._transaction((state) => {
1199
1461
  const endpoint = requireEndpoint(state, selector);
@@ -22,6 +22,12 @@ import {
22
22
  approvalBindingDigest,
23
23
  verifyApprovalBinding,
24
24
  } from "../lib/agent-authority.js";
25
+ import { validateApprovalDecision } from "../lib/app-server/protocol.js";
26
+ import {
27
+ APPROVAL_GRANTS_EVENT,
28
+ ApprovalGrantLedger,
29
+ approvalPermissionForContext,
30
+ } from "../lib/approval-grant-ledger.js";
25
31
  import {
26
32
  normalizeInteractionBinding,
27
33
  sameInteractionBinding,
@@ -572,14 +578,57 @@ export function parseInputEvent(line) {
572
578
  if (obj && typeof obj === "object" && obj.type === "compact") {
573
579
  return { compact: true };
574
580
  }
575
- // Approval verdicts (panel Approve/Deny for --interactive-approvals):
576
- // {"type":"approval","id":"appr-1","approve":true|false}
581
+ // Approval verdicts (panel/SDK for --interactive-approvals). New clients
582
+ // send the canonical structured decision and echo the request binding. The
583
+ // boolean remains an N-1 migration field for older clients only.
584
+ // {"type":"approval","id":"appr-1","decision":{"kind":"acceptOnce"},"binding":"..."}
585
+ // {"type":"approval","id":"appr-1","approve":true|false} // legacy
577
586
  if (obj && typeof obj === "object" && obj.type === "approval") {
578
587
  if (!obj.id) return null;
588
+ const structured = obj.decision !== undefined;
589
+ let decision;
590
+ let invalidReason = null;
591
+ if (structured) {
592
+ const validation = validateApprovalDecision(obj.decision);
593
+ if (validation.ok) {
594
+ decision = obj.decision;
595
+ } else {
596
+ invalidReason = "invalid-decision";
597
+ decision = {
598
+ kind: "decline",
599
+ reason: "Invalid structured approval decision",
600
+ };
601
+ }
602
+ } else {
603
+ decision =
604
+ obj.approve === true
605
+ ? { kind: "acceptOnce" }
606
+ : { kind: "decline", reason: "Legacy boolean denial" };
607
+ }
608
+ const approve = [
609
+ "acceptOnce",
610
+ "acceptForTurn",
611
+ "acceptForSession",
612
+ ].includes(decision.kind);
613
+ if (
614
+ !invalidReason &&
615
+ structured &&
616
+ typeof obj.approve === "boolean" &&
617
+ obj.approve !== approve
618
+ ) {
619
+ invalidReason = "decision-boolean-mismatch";
620
+ decision = {
621
+ kind: "decline",
622
+ reason: "Structured and legacy approval verdicts disagree",
623
+ };
624
+ }
579
625
  return {
580
626
  approval: {
581
627
  id: String(obj.id),
582
- approve: obj.approve === true,
628
+ approve: invalidReason ? false : approve,
629
+ decision,
630
+ structured,
631
+ invalidReason,
583
632
  // Optional approval binding (authority §"权限来源与跨 Agent 授权边界"):
584
633
  // when present it must match the digest the matching approval_request
585
634
  // advertised, so a stale/mis-routed/param-substituted verdict can't
@@ -2035,12 +2084,61 @@ async function runAgentHeadlessStreamInWorkspace(
2035
2084
  }
2036
2085
  };
2037
2086
 
2087
+ // Structured approval grants are exact tool+args+cwd capabilities. Turn
2088
+ // grants are memory-only and reset before every model turn; session grants
2089
+ // are restored only from a verified authority event and are appended under
2090
+ // the same anchored transcript contract as other session authority state.
2091
+ let approvalGrantLedger = new ApprovalGrantLedger({ sessionId });
2092
+ let approvalGrantPersistenceError = null;
2093
+ if (persist) {
2094
+ try {
2095
+ const event = store.findLatestEvent(
2096
+ sessionId,
2097
+ APPROVAL_GRANTS_EVENT,
2098
+ (candidate) => candidate?.data && typeof candidate.data === "object",
2099
+ );
2100
+ if (event) {
2101
+ approvalGrantLedger = ApprovalGrantLedger.fromJSON(event.data, {
2102
+ sessionId,
2103
+ });
2104
+ }
2105
+ } catch (error) {
2106
+ approvalGrantPersistenceError = error;
2107
+ approvalGrantLedger = new ApprovalGrantLedger({ sessionId });
2108
+ emit({
2109
+ type: "recovery_degraded",
2110
+ component: "approval_grants",
2111
+ session_id: sessionId,
2112
+ error:
2113
+ "Persisted approval grants could not be verified; all grants were discarded",
2114
+ });
2115
+ }
2116
+ }
2117
+ const persistApprovalGrants = () => {
2118
+ if (!persist || approvalGrantPersistenceError) return false;
2119
+ try {
2120
+ const persisted = store.appendAuthorityEvent(
2121
+ sessionId,
2122
+ APPROVAL_GRANTS_EVENT,
2123
+ approvalGrantLedger.toJSON(),
2124
+ );
2125
+ if (persisted === false) {
2126
+ throw new Error("session store rejected approval grants");
2127
+ }
2128
+ return true;
2129
+ } catch (error) {
2130
+ approvalGrantPersistenceError = error;
2131
+ return false;
2132
+ }
2133
+ };
2134
+
2038
2135
  // ── Interactive approvals (--interactive-approvals; chat-panel UX) ────────
2039
2136
  // CONFIRM-tier decisions (risky shell via the ApprovalGate, settings/hook
2040
2137
  // `ask`) normally fail closed in headless. With this opt-in they become a
2041
2138
  // structured round-trip instead: emit `approval_request`, BLOCK the tool
2042
- // until a {"type":"approval",id,approve} arrives on stdin (handled by the
2043
- // concurrent pump below, so the wait never deadlocks), fail closed on
2139
+ // until a canonical structured decision (or an N-1 boolean response) arrives
2140
+ // on stdin (handled by the concurrent pump below, so the wait never
2141
+ // deadlocks), fail closed on
2044
2142
  // timeout (CC_APPROVAL_TIMEOUT_MS, default 120s) or stdin close. The
2045
2143
  // resolution is echoed as `approval_resolved` so UIs can settle their cards.
2046
2144
  const interactive = options.interactiveApprovals === true;
@@ -2050,45 +2148,87 @@ async function runAgentHeadlessStreamInWorkspace(
2050
2148
  Number(process.env.CC_APPROVAL_TIMEOUT_MS) > 0
2051
2149
  ? Number(process.env.CC_APPROVAL_TIMEOUT_MS)
2052
2150
  : 120000;
2053
- const settleApproval = (id, approve, via, incomingBinding = null) => {
2151
+ const settleApproval = (
2152
+ id,
2153
+ decision,
2154
+ via,
2155
+ incomingBinding = null,
2156
+ { structured = false, invalidReason = null } = {},
2157
+ ) => {
2054
2158
  const p = pendingApprovals.get(id);
2055
2159
  if (!p) return;
2160
+ const approve = [
2161
+ "acceptOnce",
2162
+ "acceptForTurn",
2163
+ "acceptForSession",
2164
+ ].includes(decision?.kind);
2056
2165
  // Approval binding (authority §"权限来源与跨 Agent 授权边界"): an *approve*
2057
2166
  // verdict that carries a binding which does NOT match the one the request
2058
2167
  // advertised is stale / mis-routed / argument-tampered — reject it (deny,
2059
2168
  // fail closed) instead of green-lighting a different or changed tool call.
2060
- // A verdict with no binding stays backward-compatible; a deny always wins.
2169
+ // A legacy boolean verdict with no binding stays backward-compatible. A
2170
+ // structured approval must echo the binding; a deny always wins.
2061
2171
  if (
2062
2172
  approve === true &&
2063
- incomingBinding &&
2064
2173
  p.binding &&
2065
- !verifyApprovalBinding(p.binding, incomingBinding)
2174
+ ((structured && !incomingBinding) ||
2175
+ (incomingBinding && !verifyApprovalBinding(p.binding, incomingBinding)))
2066
2176
  ) {
2067
2177
  pendingApprovals.delete(id);
2068
2178
  clearTimeout(p.timer);
2179
+ const rejectionReason = incomingBinding
2180
+ ? "binding-mismatch"
2181
+ : "binding-missing";
2069
2182
  emit({
2070
2183
  type: "approval_resolved",
2071
2184
  id,
2072
2185
  approved: false,
2073
- via: "binding-mismatch",
2186
+ decision: {
2187
+ kind: "decline",
2188
+ reason: `Approval ${rejectionReason}`,
2189
+ },
2190
+ via: rejectionReason,
2074
2191
  session_id: sessionId,
2075
2192
  });
2076
2193
  p.resolve(false);
2077
2194
  return;
2078
2195
  }
2196
+ if (
2197
+ approve &&
2198
+ (decision.kind === "acceptForTurn" ||
2199
+ decision.kind === "acceptForSession")
2200
+ ) {
2201
+ const priorSessionGrants = new Map(approvalGrantLedger.sessionGrants);
2202
+ const priorRevision = approvalGrantLedger.revision;
2203
+ const applied = approvalGrantLedger.applyDecision(
2204
+ decision,
2205
+ p.requiredPermission,
2206
+ p.binding,
2207
+ );
2208
+ decision = applied.decision;
2209
+ if (applied.persistedScope && persist && !persistApprovalGrants()) {
2210
+ approvalGrantLedger.sessionGrants = priorSessionGrants;
2211
+ approvalGrantLedger.revision = priorRevision;
2212
+ decision = { kind: "acceptOnce" };
2213
+ via = "session-grant-persistence-failed";
2214
+ }
2215
+ }
2079
2216
  pendingApprovals.delete(id);
2080
2217
  clearTimeout(p.timer);
2081
2218
  emit({
2082
2219
  type: "approval_resolved",
2083
2220
  id,
2084
- approved: approve === true,
2085
- via,
2221
+ approved: invalidReason ? false : approve,
2222
+ decision,
2223
+ via: invalidReason || via,
2086
2224
  session_id: sessionId,
2087
2225
  });
2088
- p.resolve(approve === true);
2226
+ p.resolve(!invalidReason && approve);
2089
2227
  };
2090
- const interactiveConfirm = (ctx = {}) =>
2091
- new Promise((resolve) => {
2228
+ const interactiveConfirm = (ctx = {}) => {
2229
+ const requiredPermission = approvalPermissionForContext(ctx, { cwd });
2230
+ if (approvalGrantLedger.allows(requiredPermission)) return true;
2231
+ return new Promise((resolve) => {
2092
2232
  const id = `appr-${++approvalSeq}`;
2093
2233
  // Bind this approval request to the exact call it authorizes: the request
2094
2234
  // id, its normalized arguments, and the policy/rule in force. The digest
@@ -2101,11 +2241,21 @@ async function runAgentHeadlessStreamInWorkspace(
2101
2241
  policyDigest: ctx.rule || ctx.riskLevel || ctx.risk || null,
2102
2242
  });
2103
2243
  const timer = setTimeout(
2104
- () => settleApproval(id, false, "timeout"),
2244
+ () =>
2245
+ settleApproval(
2246
+ id,
2247
+ { kind: "decline", reason: "Approval timed out" },
2248
+ "timeout",
2249
+ ),
2105
2250
  approvalTimeoutMs,
2106
2251
  );
2107
2252
  timer.unref?.();
2108
- pendingApprovals.set(id, { resolve, timer, binding });
2253
+ pendingApprovals.set(id, {
2254
+ resolve,
2255
+ timer,
2256
+ binding,
2257
+ requiredPermission,
2258
+ });
2109
2259
  emit({
2110
2260
  type: "approval_request",
2111
2261
  id,
@@ -2116,8 +2266,10 @@ async function runAgentHeadlessStreamInWorkspace(
2116
2266
  rule: ctx.rule || null,
2117
2267
  reason: ctx.reason || null,
2118
2268
  binding,
2269
+ requested_permissions: [requiredPermission],
2119
2270
  });
2120
2271
  });
2272
+ };
2121
2273
 
2122
2274
  let approvalGate = null;
2123
2275
  try {
@@ -2206,6 +2358,10 @@ async function runAgentHeadlessStreamInWorkspace(
2206
2358
  type: "approval_resolved",
2207
2359
  id,
2208
2360
  approved: false,
2361
+ decision: {
2362
+ kind: "decline",
2363
+ reason: `Approval ${via}`,
2364
+ },
2209
2365
  via,
2210
2366
  session_id: sessionId,
2211
2367
  });
@@ -3260,9 +3416,13 @@ async function runAgentHeadlessStreamInWorkspace(
3260
3416
  // Approval verdicts settle a BLOCKED tool — never queued.
3261
3417
  settleApproval(
3262
3418
  parsed.approval.id,
3263
- parsed.approval.approve,
3419
+ parsed.approval.decision,
3264
3420
  parsed.approval.approve ? "user-approve" : "user-deny",
3265
3421
  parsed.approval.binding,
3422
+ {
3423
+ structured: parsed.approval.structured,
3424
+ invalidReason: parsed.approval.invalidReason,
3425
+ },
3266
3426
  );
3267
3427
  continue;
3268
3428
  }
@@ -3321,7 +3481,11 @@ async function runAgentHeadlessStreamInWorkspace(
3321
3481
  // stdin closed while approvals were pending → fail closed so the blocked
3322
3482
  // turn can finish and the process can exit.
3323
3483
  for (const id of [...pendingApprovals.keys()]) {
3324
- settleApproval(id, false, "stdin-closed");
3484
+ settleApproval(
3485
+ id,
3486
+ { kind: "decline", reason: "Approval input closed" },
3487
+ "stdin-closed",
3488
+ );
3325
3489
  }
3326
3490
  // stdin closed while a question was pending → user_timeout (model proceeds).
3327
3491
  for (const id of [...pendingQuestions.keys()]) {
@@ -4112,6 +4276,7 @@ async function runAgentHeadlessStreamInWorkspace(
4112
4276
  }
4113
4277
  }
4114
4278
  turns += 1;
4279
+ approvalGrantLedger.beginTurn(turns);
4115
4280
  if (persistenceFailure) {
4116
4281
  terminalPersistenceFailure = persistenceFailure;
4117
4282
  sawError = true;