chainlesschain 0.162.160 → 0.162.162

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 (199) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
  29. package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
  30. package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
  86. package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
  97. package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
  98. package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
  110. package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
  111. package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
  119. package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +15 -1
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +16 -2
  153. package/src/commands/review.js +6 -1
  154. package/src/data/changelog.json +12 -2
  155. package/src/gateways/ws/background-agent-protocol.js +88 -14
  156. package/src/lib/agent-authority.js +251 -0
  157. package/src/lib/agents.js +19 -0
  158. package/src/lib/background-agent-phase.js +100 -0
  159. package/src/lib/background-agent-supervisor.js +322 -15
  160. package/src/lib/backpressure-policy.js +105 -0
  161. package/src/lib/capability-negotiation.js +212 -0
  162. package/src/lib/credential-guard.js +11 -0
  163. package/src/lib/credential-proxy.js +182 -0
  164. package/src/lib/docs-drift.js +166 -0
  165. package/src/lib/doctor-checkup.js +76 -0
  166. package/src/lib/event-seq-replay.js +167 -0
  167. package/src/lib/goal-condition-engine.js +369 -0
  168. package/src/lib/headless-manifest.js +13 -0
  169. package/src/lib/hook-event-bus.cjs +0 -0
  170. package/src/lib/ide-context.js +53 -14
  171. package/src/lib/json-schema-output.js +30 -1
  172. package/src/lib/json-schema-validate.js +691 -0
  173. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  174. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  175. package/src/lib/operation-fingerprint.js +95 -0
  176. package/src/lib/plugin-runtime/capabilities.js +371 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/project-instructions.js +98 -1
  179. package/src/lib/remote-approval-bridge.js +16 -0
  180. package/src/lib/remote-path-mapping.js +229 -0
  181. package/src/lib/review-pipeline.js +227 -0
  182. package/src/lib/runtime-checkup.js +318 -0
  183. package/src/lib/schedule-planner.js +136 -0
  184. package/src/lib/session-lifecycle.js +305 -0
  185. package/src/lib/settings-hook-events.cjs +39 -13
  186. package/src/lib/subagent-contract.js +383 -0
  187. package/src/lib/telemetry-ids.js +190 -0
  188. package/src/lib/turn-binding.js +397 -0
  189. package/src/repl/bg-dashboard.js +13 -12
  190. package/src/runtime/agent-core.js +24 -10
  191. package/src/runtime/headless-stream.js +195 -11
  192. package/src/runtime/system-prompt.js +3 -0
  193. package/src/workers/background-agent-worker.js +37 -2
  194. package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
  195. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
  196. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
  197. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
  198. package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
  199. package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
@@ -8,17 +8,37 @@
8
8
  * Routes (request → reply):
9
9
  * bg-list {all?} → {type:"bg-list", sessions}
10
10
  * bg-view {bgId, lines?} → {type:"bg-view", session, log}
11
- * bg-attach {bgId, lines?} → {type:"bg-attach", bgId, hello, log}
11
+ * bg-attach {bgId, lines?, sinceSeq?} → {type:"bg-attach", bgId, hello,
12
+ * log, latestSeq, [reattached, replayed,
13
+ * replayTruncated]}
12
14
  * bg-prompt {bgId, text} → {type:"bg-prompt", bgId, sent}
13
15
  * bg-stop-turn {bgId} → {type:"bg-stop-turn", bgId, sent}
14
16
  * bg-detach {bgId} → {type:"bg-detach", bgId}
15
17
  * bg-stop {bgId} → {type:"bg-stop", session} (kill whole session)
16
18
  *
17
- * While attached, the server pushes unsolicited frames to that client:
18
- * {type:"bg-event", bgId, event} worker lifecycle events (turn-started /
19
+ * While attached, the server pushes unsolicited frames to that client, each
20
+ * stamped with a per-attachment monotonic `seq` (additive; older clients
21
+ * ignore it) so the client can detect gaps and request a replay:
22
+ * {type:"bg-event", bgId, seq, event} worker lifecycle events (turn-started /
19
23
  * turn-ended / idle / accepted / error /
20
24
  * transport-closed)
21
- * {type:"bg-log", bgId, chunk} log-file delta (500ms poll)
25
+ * {type:"bg-log", bgId, seq, chunk} log-file delta (500ms poll)
26
+ *
27
+ * Reconnect resilience: a panel whose WS blipped re-sends bg-attach with the
28
+ * last `seq` it saw (`sinceSeq`); the still-live relay re-pushes the frames it
29
+ * missed (bounded buffer) and reports `latestSeq`. If the missed range was
30
+ * already evicted, `replayTruncated:true` tells the client to full-resync from
31
+ * the log. If the relay was fully torn down (WS closed → cleanup), the fresh
32
+ * attach returns hello + log tail as before and the client resets its tracker.
33
+ *
34
+ * Backpressure: if the WS client drains too slowly (ws.bufferedAmount over the
35
+ * high water), DROPPABLE frames (bg-log) are shed while CRITICAL ones (bg-event
36
+ * lifecycle) are always delivered. Dropped frames are still recorded (seq
37
+ * advances), so the gap on the next delivered frame trips the client's
38
+ * replaySince recovery. A one-shot {type:"bg-lag", bgId, lagging:true} marks
39
+ * the start and {type:"bg-lag", bgId, lagging:false, dropped, latestSeq} the
40
+ * recovery — the UI shows "catching up" instead of freezing. bg-lag frames
41
+ * carry no seq (out-of-band control; the gap tracker ignores them).
22
42
  *
23
43
  * The state file's `transport.token` is the local takeover capability — it is
24
44
  * NEVER sent over WS. Sessions are serialized with `interactive: true/false`
@@ -26,6 +46,12 @@
26
46
  */
27
47
 
28
48
  import { existsSync, readFileSync } from "node:fs";
49
+ import { EventReplayBuffer } from "../../lib/event-seq-replay.js";
50
+ import {
51
+ BackpressurePolicy,
52
+ CRITICAL,
53
+ DROPPABLE,
54
+ } from "../../lib/backpressure-policy.js";
29
55
 
30
56
  const LOG_POLL_MS = 500;
31
57
 
@@ -139,6 +165,20 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
139
165
  if (existing) {
140
166
  // Idempotent re-attach: reply with the live handshake instead of
141
167
  // stacking a second relay (a reconnecting panel re-sends bg-attach).
168
+ // If the panel reports the last seq it saw (`sinceSeq`), replay the
169
+ // push frames it missed while the WS was blipped — re-sent as normal
170
+ // bg-event/bg-log frames (their original seq), so the client's gap
171
+ // tracker sees a contiguous run. `replayTruncated` tells the client the
172
+ // hole predates our buffer and it must full-resync (re-read the log).
173
+ const sinceSeq = Number(message.sinceSeq);
174
+ let replayTruncated = false;
175
+ let replayed = 0;
176
+ if (Number.isFinite(sinceSeq)) {
177
+ const { frames, truncated } = existing.buffer.replaySince(sinceSeq);
178
+ replayTruncated = truncated;
179
+ replayed = frames.length;
180
+ for (const frame of frames) server._send(ws, frame);
181
+ }
142
182
  server._send(ws, {
143
183
  id,
144
184
  type: "bg-attach",
@@ -146,6 +186,9 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
146
186
  hello: existing.hello,
147
187
  log: "",
148
188
  reattached: true,
189
+ latestSeq: existing.buffer.latestSeq,
190
+ replayed,
191
+ replayTruncated,
149
192
  });
150
193
  return;
151
194
  }
@@ -172,6 +215,42 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
172
215
  );
173
216
  }
174
217
 
218
+ // Per-attachment replay buffer: stamps a monotonic seq on every push frame
219
+ // and retains a bounded tail so a reconnecting client can fill gaps
220
+ // (bg-attach { sinceSeq }). One buffer per relay instance — seq restarts
221
+ // at 1 on a fresh attach, so the client resets its tracker then.
222
+ const buffer = new EventReplayBuffer();
223
+ // Backpressure: if the WS client drains too slowly (ws.bufferedAmount grows
224
+ // past the high water), shed DROPPABLE frames (bg-log — recoverable) while
225
+ // still delivering CRITICAL ones (bg-event lifecycle). Dropped frames are
226
+ // still RECORDED (seq advances), so the seq gap on the next delivered frame
227
+ // trips the client's replaySince recovery; a one-shot bg-lag notice lets
228
+ // the UI show "catching up" instead of freezing. Buffer stays 0 in the
229
+ // normal case → byte-identical legacy behavior.
230
+ const backpressure = new BackpressurePolicy();
231
+ const bufferedBytes = () =>
232
+ ws && Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
233
+ const pushFrame = (base, frameClass) => {
234
+ const frame = buffer.record(base); // record always → seq advances, replay recovers drops
235
+ const d = backpressure.evaluate(bufferedBytes(), frameClass);
236
+ if (d.lagStarted)
237
+ server._send(ws, { type: "bg-lag", bgId, lagging: true });
238
+ if (d.send) server._send(ws, frame);
239
+ if (d.lagEnded) {
240
+ server._send(ws, {
241
+ type: "bg-lag",
242
+ bgId,
243
+ lagging: false,
244
+ dropped: d.dropped,
245
+ latestSeq: buffer.latestSeq,
246
+ });
247
+ }
248
+ };
249
+ const pushEvent = (event) =>
250
+ pushFrame({ type: "bg-event", bgId, event }, CRITICAL);
251
+ const pushLog = (chunk) =>
252
+ pushFrame({ type: "bg-log", bgId, chunk }, DROPPABLE);
253
+
175
254
  const { connectBackgroundSession } =
176
255
  await import("../../lib/background-session-transport.js");
177
256
  let conn;
@@ -179,16 +258,10 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
179
258
  conn = await connectBackgroundSession({
180
259
  pipePath: session.transport.pipe,
181
260
  token: session.transport.token,
182
- onEvent: (event) => {
183
- server._send(ws, { type: "bg-event", bgId, event });
184
- },
261
+ onEvent: (event) => pushEvent(event),
185
262
  onClose: () => {
186
263
  if (dropAttachment(client, bgId)) {
187
- server._send(ws, {
188
- type: "bg-event",
189
- bgId,
190
- event: { type: "transport-closed" },
191
- });
264
+ pushEvent({ type: "transport-closed" });
192
265
  }
193
266
  },
194
267
  });
@@ -212,7 +285,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
212
285
  if (text.length > offset) {
213
286
  const chunk = text.slice(offset);
214
287
  offset = text.length;
215
- server._send(ws, { type: "bg-log", bgId, chunk });
288
+ pushLog(chunk);
216
289
  }
217
290
  } catch {
218
291
  /* transient read failures skip one tick */
@@ -220,7 +293,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
220
293
  }, LOG_POLL_MS);
221
294
  poll.unref?.();
222
295
 
223
- attachments(client).set(bgId, { conn, poll, hello: conn.hello });
296
+ attachments(client).set(bgId, { conn, poll, hello: conn.hello, buffer });
224
297
  server._send(ws, {
225
298
  id,
226
299
  type: "bg-attach",
@@ -229,6 +302,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
229
302
  log: readBackgroundAgentLog(bgId, {
230
303
  lines: Number(message.lines) || 100,
231
304
  }),
305
+ latestSeq: buffer.latestSeq,
232
306
  });
233
307
  } catch (err) {
234
308
  sendError(server, id, ws, "BG_ATTACH_FAILED", err.message);
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Cross-agent authority envelope + approval binding (P0 security slice —
3
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"权限来源与跨 Agent
4
+ * 授权边界").
5
+ *
6
+ * When Subagent, Team, Remote Control, Inbound Channel, Hook and MCP all feed
7
+ * text into one session, "who said this" is a security boundary: another
8
+ * agent's "the user approved" MUST NOT gain the user's authority, and an
9
+ * external channel message MUST NOT silently widen the session's permissions.
10
+ *
11
+ * This module is the single source of truth for two invariants the runtime
12
+ * enforces at every trust seam:
13
+ *
14
+ * 1. **Who may approve.** Only the local user (UI-owned stdin), an explicit
15
+ * permission tool, or a *paired + authenticated + approve-scoped* remote
16
+ * device may answer a permission gate. Model / subagent / teammate /
17
+ * channel / hook messages top out at `steer` — the string "approved" in
18
+ * their payload is just text.
19
+ *
20
+ * 2. **What an approval authorizes.** An approval is bound to
21
+ * `tool_call_id + normalized_arguments + policy_digest`. A replayed or
22
+ * mis-routed approval, or one issued for arguments that have since
23
+ * changed, fails the binding check — so a stale "yes" can never green-light
24
+ * a different (or tampered) tool call.
25
+ *
26
+ * `origin` is always assigned by trusted dispatch code from HOW a message
27
+ * arrived (which socket / which authenticated device / which internal caller)
28
+ * — it is never read from untrusted message *content*, which is the whole
29
+ * point: a model cannot label its own text `origin:"user"`.
30
+ *
31
+ * Pure + dependency-light (only node:crypto) so every seam — remote-session
32
+ * protocol, channels, hooks, subagent dispatch — can share it.
33
+ */
34
+
35
+ import { createHash, timingSafeEqual } from "node:crypto";
36
+
37
+ /** Where a message / approval entered the runtime. Assigned by trusted code. */
38
+ export const ORIGIN = Object.freeze({
39
+ USER: "user", // the human at the local UI (owns stdin)
40
+ MODEL: "model", // the assistant's own output
41
+ SUBAGENT: "subagent", // a spawned sub-agent
42
+ TEAMMATE: "teammate", // another agent in a team run
43
+ CHANNEL: "channel", // an inbound channel (webhook / telegram)
44
+ HOOK: "hook", // a settings/plugin hook result
45
+ REMOTE: "remote", // a paired remote-control device
46
+ SYSTEM: "system", // internal runtime event
47
+ PERMISSION_TOOL: "permission_tool", // an explicit permission-answering tool
48
+ });
49
+
50
+ /** What a message is allowed to do. Ordered least → most privileged. */
51
+ export const AUTHORITY = Object.freeze({
52
+ NONE: "none", // ignored for control purposes
53
+ STEER: "steer", // may add a user turn / guide the conversation
54
+ APPROVE: "approve", // may answer a permission gate (approve/deny a tool)
55
+ MANAGE: "manage", // may also manage session lifecycle
56
+ });
57
+
58
+ const AUTHORITY_RANK = Object.freeze({
59
+ none: 0,
60
+ steer: 1,
61
+ approve: 2,
62
+ manage: 3,
63
+ });
64
+
65
+ /** Numeric rank for an authority string (unknown → 0). */
66
+ export function authorityRank(authority) {
67
+ return AUTHORITY_RANK[authority] ?? 0;
68
+ }
69
+
70
+ /** Remote scopes that already imply the ability to answer a permission gate. */
71
+ function scopeGrantsApprove(context) {
72
+ const scopes = normalizeScopes(context);
73
+ return scopes.has("approve") || scopes.has("manage");
74
+ }
75
+
76
+ function scopeGrantsSteer(context) {
77
+ const scopes = normalizeScopes(context);
78
+ return (
79
+ scopes.has("prompt") ||
80
+ scopes.has("interrupt") ||
81
+ scopes.has("observe") ||
82
+ scopeGrantsApprove(context)
83
+ );
84
+ }
85
+
86
+ function normalizeScopes(context = {}) {
87
+ const raw = Array.isArray(context.scopes)
88
+ ? context.scopes
89
+ : context.scope
90
+ ? [context.scope]
91
+ : [];
92
+ return new Set(raw.map((s) => String(s || "").toLowerCase()));
93
+ }
94
+
95
+ /**
96
+ * The MAXIMUM authority an origin may exercise given its authentication
97
+ * context. This is a ceiling — a caller still passes the specific action
98
+ * through `canApprove` / `assertCanApprove`.
99
+ *
100
+ * The only paths to `approve` are: the local user (`manage`), an explicit
101
+ * permission tool, or a remote device that is BOTH authenticated AND was
102
+ * granted the approve scope at pairing. Everything an agent or external
103
+ * message can say tops out at `steer`.
104
+ */
105
+ export function authorityForOrigin(origin, context = {}) {
106
+ switch (origin) {
107
+ case ORIGIN.USER:
108
+ return AUTHORITY.MANAGE;
109
+ case ORIGIN.PERMISSION_TOOL:
110
+ return AUTHORITY.APPROVE;
111
+ case ORIGIN.REMOTE:
112
+ // A paired Remote Approval Bridge acts for the user ONLY when the pairing
113
+ // is authenticated AND carries the approve scope. Never on the strength
114
+ // of the message alone.
115
+ if (context.authenticated === true && scopeGrantsApprove(context)) {
116
+ return normalizeScopes(context).has("manage")
117
+ ? AUTHORITY.MANAGE
118
+ : AUTHORITY.APPROVE;
119
+ }
120
+ if (context.authenticated === true && scopeGrantsSteer(context)) {
121
+ return AUTHORITY.STEER;
122
+ }
123
+ return AUTHORITY.NONE;
124
+ case ORIGIN.MODEL:
125
+ case ORIGIN.SUBAGENT:
126
+ case ORIGIN.TEAMMATE:
127
+ case ORIGIN.CHANNEL:
128
+ case ORIGIN.HOOK:
129
+ case ORIGIN.SYSTEM:
130
+ // May guide the conversation, never speak with the user's approval
131
+ // authority. "The user approved" in their text is just text.
132
+ return AUTHORITY.STEER;
133
+ default:
134
+ return AUTHORITY.NONE;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * True when this envelope may answer a permission gate. An envelope is
140
+ * `{ origin, authenticated?, scope?/scopes?, principalId?, sessionId?, … }`.
141
+ */
142
+ export function canApprove(envelope = {}) {
143
+ return (
144
+ authorityRank(authorityForOrigin(envelope.origin, envelope)) >=
145
+ AUTHORITY_RANK.approve
146
+ );
147
+ }
148
+
149
+ /** True when this envelope may manage session lifecycle. */
150
+ export function canManageSession(envelope = {}) {
151
+ return (
152
+ authorityRank(authorityForOrigin(envelope.origin, envelope)) >=
153
+ AUTHORITY_RANK.manage
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Throw a clear, log-safe error when an envelope is NOT allowed to approve.
159
+ * Callers gate every "resolve a permission" seam through this so the rule
160
+ * lives in exactly one place.
161
+ */
162
+ export function assertCanApprove(envelope = {}) {
163
+ if (canApprove(envelope)) return;
164
+ const who = envelope.origin || "unknown";
165
+ const got = authorityForOrigin(envelope.origin, envelope);
166
+ throw new Error(
167
+ `origin "${who}" (authority "${got}") is not authorized to approve tool calls — ` +
168
+ "only the local user, a paired approve-scoped remote device, or an explicit permission tool may",
169
+ );
170
+ }
171
+
172
+ // ── Approval binding ─────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Deterministic, key-order-independent serialization of tool arguments, so
176
+ * two structurally-equal argument objects hash identically and ANY real value
177
+ * change hashes differently. Non-objects serialize via JSON.
178
+ */
179
+ export function normalizeToolArgs(args) {
180
+ return stableStringify(args === undefined ? null : args);
181
+ }
182
+
183
+ function stableStringify(value) {
184
+ if (value === null || typeof value !== "object") {
185
+ return JSON.stringify(value === undefined ? null : value);
186
+ }
187
+ if (Array.isArray(value)) {
188
+ return "[" + value.map(stableStringify).join(",") + "]";
189
+ }
190
+ const keys = Object.keys(value).sort();
191
+ return (
192
+ "{" +
193
+ keys
194
+ .map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]))
195
+ .join(",") +
196
+ "}"
197
+ );
198
+ }
199
+
200
+ /**
201
+ * The identity an approval authorizes: the exact tool call (`toolCallId`), its
202
+ * normalized arguments, and the policy in force (`policyDigest`). Change any
203
+ * one and the digest changes, so a "yes" for one call can never be replayed
204
+ * onto another or onto tampered arguments.
205
+ */
206
+ export function approvalBindingDigest({ toolCallId, args, policyDigest } = {}) {
207
+ const h = createHash("sha256");
208
+ h.update("cc-approval-binding-v1\n");
209
+ h.update(String(toolCallId ?? "") + "\n");
210
+ h.update(normalizeToolArgs(args) + "\n");
211
+ h.update(String(policyDigest ?? "") + "\n");
212
+ return "ab_" + h.digest("hex").slice(0, 32);
213
+ }
214
+
215
+ function timingSafeEqualStr(a, b) {
216
+ const ba = Buffer.from(String(a), "utf8");
217
+ const bb = Buffer.from(String(b), "utf8");
218
+ if (ba.length !== bb.length) return false;
219
+ return timingSafeEqual(ba, bb);
220
+ }
221
+
222
+ /**
223
+ * Verify an incoming approval targets the expected pending request. Each side
224
+ * may be a precomputed digest string or a `{toolCallId, args, policyDigest}`
225
+ * descriptor. Returns false on any mismatch or missing input (fail closed).
226
+ * Comparison is constant-time.
227
+ */
228
+ export function verifyApprovalBinding(expected, provided) {
229
+ if (!expected || !provided) return false;
230
+ const e =
231
+ typeof expected === "string" ? expected : approvalBindingDigest(expected);
232
+ const p =
233
+ typeof provided === "string" ? provided : approvalBindingDigest(provided);
234
+ if (!e || !p) return false;
235
+ return timingSafeEqualStr(e, p);
236
+ }
237
+
238
+ /**
239
+ * A compact, log-safe provenance string for the permission/audit log so it is
240
+ * always clear WHICH principal, over WHICH session, with WHICH authority
241
+ * requested or answered a permission. Carries no secrets.
242
+ */
243
+ export function describeAuthorityChain(envelope = {}) {
244
+ const parts = [`origin=${envelope.origin || "unknown"}`];
245
+ if (envelope.principalId) parts.push(`principal=${envelope.principalId}`);
246
+ if (envelope.sessionId) parts.push(`session=${envelope.sessionId}`);
247
+ if (envelope.parentAgentId) parts.push(`parent=${envelope.parentAgentId}`);
248
+ if (envelope.correlationId) parts.push(`corr=${envelope.correlationId}`);
249
+ parts.push(`authority=${authorityForOrigin(envelope.origin, envelope)}`);
250
+ return parts.join(" ");
251
+ }
package/src/lib/agents.js CHANGED
@@ -15,6 +15,21 @@
15
15
  * model model override for runs of this agent
16
16
  * maxTurns per-run iteration cap for this agent (positive integer)
17
17
  * isolation "worktree" → run in a fresh git worktree (gap 2026-07-11)
18
+ * permission-mode child permission mode (tighten-only vs parent)
19
+ * skills capability allow-list — skills the child may use
20
+ * mcp-servers capability allow-list — MCP servers the child may use
21
+ * hooks capability allow-list — hooks the child may run
22
+ * memory whether the child may read permanent memory
23
+ * effort reasoning effort (low|medium|high|xhigh)
24
+ * background run detached
25
+ * context "fresh" (default, isolated) | "fork" (inherit parent)
26
+ * max-depth/max-children ceilings this agent imposes on its own descendants
27
+ * budget { tokens, cost, time } caps
28
+ *
29
+ * The extended P1 contract (permission/skills/mcp/hooks/memory/effort/
30
+ * background/context/depth/children/budget) is normalized into a `contract`
31
+ * field via subagent-contract.js; the top-level tools/maxTurns/isolation are
32
+ * kept as-is for the existing spawn path.
18
33
  *
19
34
  * Project scope shadows personal on a name clash. Discovery + parse are pure
20
35
  * (inject fs/path/home) so the whole thing is unit-testable.
@@ -26,6 +41,7 @@ import { homedir } from "node:os";
26
41
  import yaml from "js-yaml";
27
42
  import { projectRootBase } from "./project-root.cjs";
28
43
  import { discoverPluginAgentLayers } from "./plugin-runtime/agents.js";
44
+ import { normalizeSubagentContract } from "./subagent-contract.js";
29
45
 
30
46
  const _deps = { fs: fsDefault, path: pathDefault };
31
47
 
@@ -147,6 +163,9 @@ export function parseAgentFile(file, scope, opts = {}) {
147
163
  maxTurns:
148
164
  Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : null,
149
165
  isolation: data.isolation === "worktree" ? "worktree" : null,
166
+ // Extended P1 subagent contract (permission/skills/mcp/hooks/memory/effort/
167
+ // background/context/depth/children/budget), normalized + validated.
168
+ contract: normalizeSubagentContract(data),
150
169
  systemPrompt: body || "",
151
170
  };
152
171
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Background-agent phase vocabulary + dashboard grouping (P0 state-machine
3
+ * slice — CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"后台 Agent
4
+ * 状态机").
5
+ *
6
+ * A session has two orthogonal axes:
7
+ * - `status` — running | completed | failed | stopped | lost — owned by the
8
+ * supervisor's liveness reconciliation.
9
+ * - `phase` — the live sub-state WHILE running, persisted by the worker.
10
+ *
11
+ * Historically the worker only emitted "turn" (a turn is executing) and
12
+ * "idle" (between turns, transport open), and the dashboard mislabeled *idle*
13
+ * as "Needs input" — so an agent that had simply finished a turn and was
14
+ * parked shouted for attention it didn't need. Claude-Code's Agent View keeps
15
+ * these distinct (Working / Needs input / Idle / …).
16
+ *
17
+ * This module is the single source of truth that (a) names the fuller phase
18
+ * vocabulary the runtime is growing into and (b) classifies a session into a
19
+ * dashboard group, crucially separating `idle` (finished a turn, nothing
20
+ * blocking) from a genuine *needs-input* state (a human decision — a question
21
+ * or a pending permission — is blocking progress). Pure + zero-dependency so
22
+ * the worker, the supervisor and the REPL dashboard can all share it without a
23
+ * worker ever importing UI code.
24
+ */
25
+
26
+ /**
27
+ * Canonical running sub-phases. `working`/`idle` are the steady states;
28
+ * `needs_input`/`waiting_permission` mark a turn that cannot advance without a
29
+ * human. `starting` is the brief pre-first-turn window.
30
+ */
31
+ export const BACKGROUND_AGENT_PHASES = Object.freeze({
32
+ STARTING: "starting",
33
+ WORKING: "working",
34
+ IDLE: "idle",
35
+ NEEDS_INPUT: "needs_input",
36
+ WAITING_PERMISSION: "waiting_permission",
37
+ });
38
+
39
+ /**
40
+ * Aliases accepted from producers / older state schemas → canonical phase.
41
+ * The legacy worker emits "turn" for a live turn; map it to `working` so old
42
+ * on-disk states and new ones classify identically. Kebab and *_approval
43
+ * spellings are tolerated because different producers naturally reach for
44
+ * different casing.
45
+ */
46
+ const PHASE_ALIASES = new Map([
47
+ ["turn", BACKGROUND_AGENT_PHASES.WORKING],
48
+ ["working", BACKGROUND_AGENT_PHASES.WORKING],
49
+ ["starting", BACKGROUND_AGENT_PHASES.STARTING],
50
+ ["idle", BACKGROUND_AGENT_PHASES.IDLE],
51
+ ["needs_input", BACKGROUND_AGENT_PHASES.NEEDS_INPUT],
52
+ ["needs-input", BACKGROUND_AGENT_PHASES.NEEDS_INPUT],
53
+ ["awaiting_input", BACKGROUND_AGENT_PHASES.NEEDS_INPUT],
54
+ ["question", BACKGROUND_AGENT_PHASES.NEEDS_INPUT],
55
+ ["waiting_permission", BACKGROUND_AGENT_PHASES.WAITING_PERMISSION],
56
+ ["waiting-permission", BACKGROUND_AGENT_PHASES.WAITING_PERMISSION],
57
+ ["waiting_approval", BACKGROUND_AGENT_PHASES.WAITING_PERMISSION],
58
+ ["awaiting_approval", BACKGROUND_AGENT_PHASES.WAITING_PERMISSION],
59
+ ]);
60
+
61
+ /**
62
+ * Normalize a raw phase value to a canonical phase, or null when absent /
63
+ * unrecognized (callers treat null as "no declared phase"). Never throws.
64
+ */
65
+ export function normalizeBackgroundAgentPhase(value) {
66
+ if (typeof value !== "string") return null;
67
+ return PHASE_ALIASES.get(value.trim().toLowerCase()) || null;
68
+ }
69
+
70
+ /** Phases where a human decision is blocking progress → "Needs input". */
71
+ const BLOCKING_PHASES = new Set([
72
+ BACKGROUND_AGENT_PHASES.NEEDS_INPUT,
73
+ BACKGROUND_AGENT_PHASES.WAITING_PERMISSION,
74
+ ]);
75
+
76
+ /**
77
+ * Dashboard group for a session. Running sessions split by phase:
78
+ * - a pending approval OR a blocking phase (needs_input / waiting_permission)
79
+ * → "needs-input" (a human decision unblocks it)
80
+ * - "idle" (finished a turn, transport open, available but nothing blocking)
81
+ * → "idle"
82
+ * - everything else (working / turn / starting / unknown) → "working"
83
+ * Terminal statuses map to their own groups; lost/unknown → "failed".
84
+ *
85
+ * `pendingApprovals` wins over `phase` because an approval that is genuinely
86
+ * pending blocks progress regardless of the phase label the worker last wrote.
87
+ */
88
+ export function phaseGroupKey(session) {
89
+ const status = session?.status;
90
+ if (status === "running") {
91
+ if (Number(session?.pendingApprovals) > 0) return "needs-input";
92
+ const phase = normalizeBackgroundAgentPhase(session?.phase);
93
+ if (BLOCKING_PHASES.has(phase)) return "needs-input";
94
+ if (phase === BACKGROUND_AGENT_PHASES.IDLE) return "idle";
95
+ return "working";
96
+ }
97
+ if (status === "completed") return "completed";
98
+ if (status === "stopped") return "stopped";
99
+ return "failed"; // failed | lost | unknown
100
+ }