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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/commands/review.js +6 -1
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/agents.js +19 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +322 -15
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/docs-drift.js +166 -0
- package/src/lib/doctor-checkup.js +76 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/hook-event-bus.cjs +0 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/json-schema-output.js +30 -1
- package/src/lib/json-schema-validate.js +691 -0
- package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
- package/src/lib/lsp/diagnostics-scheduler.js +127 -0
- package/src/lib/operation-fingerprint.js +95 -0
- package/src/lib/plugin-runtime/capabilities.js +371 -0
- package/src/lib/plugin-runtime/manifest.js +30 -0
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-approval-bridge.js +16 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/review-pipeline.js +227 -0
- package/src/lib/runtime-checkup.js +318 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/session-lifecycle.js +305 -0
- package/src/lib/settings-hook-events.cjs +39 -13
- package/src/lib/subagent-contract.js +383 -0
- package/src/lib/telemetry-ids.js +190 -0
- package/src/lib/turn-binding.js +397 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +24 -10
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- 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?}
|
|
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
|
-
*
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|