chainlesschain 0.162.148 → 0.162.149
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/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DwpCCp64.js → AIOps-BJif14yR.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C9pBYocA.js → ActionButton-CXek6gJY.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BWAkeXxK.js → Analytics-BgQhdAJi.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CivFGH6B.js → AppLayout-DTYl5NtR.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-Cm8hRpZm.js → Audit-BzATQAeR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DXdFMsE8.js → Backup-BzdPEQhL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uAwSTeql.js → BaseInput-BM0KVh8E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-KU8eeJJE.js → Chat-D7fHXHkz.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Wm34RR-b.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6AMDkjd.js → Checkbox-CAiSQ9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-t2moDxcO.js → Codegen-Df40CrEe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DCcsRRhN.js → Col-DIT2fNFU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DtB-8SAC.js → Community-CSTIbW2k.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CZm8THYA.js → Compact-DtqXZZUi.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-LiQgC7g1.js → Compliance-CUtzw6o7.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CA8SAxht.js → Cowork-CezmlnmU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CtRaF1wD.js → Cron-P4BITarg.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-6-br6Hub.js → Crosschain-BRRiYwvq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Do2EccrL.js → DID-L5ijB9i4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-wrXcbzGe.js → Dashboard-CFxro4ob.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-p3FsT245.js → Dropdown-BmAFCQ71.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D95A2L8q.js → EmailListRenderer-DJBCEuon.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-cLuJW2zo.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Brgq_cbN.js → Federation-DsVCpHMF.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DWLCkNgh.js → FormItemContext-DP3bP5th.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBqUfQd6.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8F090w4I.js → Git-I3g_bAw9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CCogEbxb.js → Governance-jHS5-ioS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfLD7v7C.js → Inference-CPL7sfx9.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DDuHJewd.js → KnowledgeGraph-Jf236h4C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CE8KSEVC.js → Logs-Cm2tcSKg.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DmwpZmhT.js → Marketplace-CZK82rhi.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Bf7AazU8.js → McpTools-CZCeji7a.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D0QU_00S.js → Memory-3l2KVS9k.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-yXS9xdhx.js → MobileBridge-VYdpoLh6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BuDUoGUP.js → MobileProjects-CiB9cmm1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BYyHy28p.js → Mtc-wCFZbBuL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CK0aqpiZ.js → MtcAudit-1a3THIyG.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-mNimKYeb.js → Multisig-DMzLB2hG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-AioAJ0YA.js → NLProgramming-D9wZbf6l.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVVNCLHE.js → Notes-hrFe2ZM-.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C4ABj-TK.js → NotificationSettings-CHGX5Jwm.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-uozl_gWK.js → Organization-By7FkhtY.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BCZOM2hK.js → Overflow--khGSzJn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CArcaiaj.js → P2P-CAG9dH35.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Sgq2Srr8.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BYrQrXnH.js → Permissions-D35ec6JA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BzHStAbz.js → PersonalDataHub-CxBF7hW_.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-k7KqxX1M.js → Pipeline-s6EtOO1s.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-IumTUWqx.js → Privacy-BbJYmWmO.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BRyImYTf.js → ProjectInit-VGQq1jMT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFIqTBFk.js → ProjectSettings-B1ew3Teh.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CyyFnUPs.js → Providers-mrO47FIK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C82cRx-X.js → QrScannerModal-IJF2mHyw.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DGxDF521.js → QuickAsk-CU0wN_Z0.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cns-Am1y.js → Recommend-DhN37R6G.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-R7OqAIlg.js → RemoteSession-D_xOX_hu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D7mgPIYV.js → Reputation-D_Ssv7uH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B3lEURyd.js → Row-Kx5dKxX7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-9_UVrpBt.js → RssFeed-BlGwqZkY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-ClZeO80q.js → Search-hLFQzxpb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BNNJczEp.js → Security-DMlvsVt4.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5SjrWUj.js → Services-Bd0Qsj2m.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Ci_obQ-g.js → Skeleton-DFclq9X3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DdebN15P.js → Skills-BNtln2qY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-OinZP2vi.js → Sla-Dch5H19G.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CxzbNF51.js → SpeechSettings-2UfQcU1g.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D06N_66b.js → SyncSettings-CD9YQr4i.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BdEdW0AZ.js → Tasks-BNu-7MzI.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-F1ctKmPa.js → Templates-D1n9CaIR.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CVz1Urot.js → Tenant-DenL3iBW.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Doitzjmf.js → TimelineRenderer-D87IfukO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpyVRpVA.js → Tokens-BkiZc8E3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nWhWYTbU.js → Trigger-BNRSytGN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DK_G-wLx.js → Trust-Bwbd4X2m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B5yBUojO.js → UkeySign-B8O2bs0o.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5D51qAe.js → VideoEditing-C2fL8zmH.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CLbL9K7v.js → Wallet-BORYDdE1.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DZUelI3V.js → WebAuthn-C0V5S2FM.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-8RPxt4KW.js → WorkflowEditor-2dR0fcHL.js} +1 -1
- package/src/assets/web-panel/assets/{chat-4N4tOA3d.js → chat-BmuAFAn8.js} +1 -1
- package/src/assets/web-panel/assets/{colors-zbbGVrua.js → colors-CN3smMcK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D7iMkbnE.js → compact-item-DCnxb4kW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CBzPJv-w.js → createContext-vlR43pHn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-bHdrQSqZ.js → hasIn-D04tHYFd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbE5D0WL.js → index-0DnaZGkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-308gCGh7.js → index-430S68MN.js} +1 -1
- package/src/assets/web-panel/assets/{index-kmh1TxRa.js → index-B78xL3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QFObYeo1.js → index-BTPEZTk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZb8F8YG.js → index-BXRg8GFQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE9j5YgT.js → index-BasvsWDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiG1IZao.js → index-BeGDEXFs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqmx24kh.js → index-BqhASEL4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCr0geV9.js → index-BudvKQfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-S74FpAIn.js → index-C-6NO5il.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyoNgbXl.js → index-C5Sxb1sE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtbbdLnf.js → index-CP9m9Ggr.js} +3 -3
- package/src/assets/web-panel/assets/{index-CkxK86vf.js → index-CTOCnIQ7.js} +1 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
- package/src/assets/web-panel/assets/{index-BTIjjXry.js → index-ClhAHDK3.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7b1hGry.js → index-CnXebZLL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPxkoKgA.js → index-CngCC8hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci009ijp.js → index-Cr-A0FYJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdmMoYN1.js → index-CsAsCPJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBNr77E1.js → index-D28DeAAB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1s0Bz9O.js → index-D5LZVZ0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-oDKNgCsP.js → index-D65_XseV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QNAG4JtR.js → index-D8vwp4qp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQfu1U78.js → index-DDwLgQY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnBOpqv3.js → index-DIpY0qM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ6NcfWz.js → index-DTWg-18U.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4XKpj6c.js → index-DTtRscUa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1YDh7Wr.js → index-DYAtmqiv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvRMA9uT.js → index-D_n8_vfI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahryTX0.js → index-DbADHwhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ4ZGyW0.js → index-DbVsQBOH.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
- package/src/assets/web-panel/assets/{index-xYCm6W2h.js → index-HhNjnCfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BS4_Mwk6.js → index-ToUh2b1-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DISWAYce.js → index-TslMTwNy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmqlJed-.js → index-_FIkN3jd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqdPyWm5.js → index-hpvvtAZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsKehmmS.js → index-jzR7Ujuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUyjUkY7.js → index-ljuBiQW9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BD55yNBt.js → initDefaultProps-C3wUb4je.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B5Bbe77U.js → motion-D-jYFUNA.js} +1 -1
- package/src/assets/web-panel/assets/{move-CTvIqHEH.js → move-tqb8nwJ2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BkNyPQKB.js → mtc-parser-DSOjMJMz.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BaXJXgHA.js → omit-CJ7VimIW.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bw6-YTxH.js → pickAttrs-qXiG1iT3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLdbkLqJ.js → placementArrow-CkGBcy1Z.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-IVYkzntU.js → responsiveObserve-D3WvTlLO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DFMFwwtY.js → slide-BEE2ftge.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CURYRtZ1.js → statusUtils-CsfBpxiG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cfz63s1r.js → styleChecker-xPrIiJPI.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-COJL0KE3.js → useFlexGapSupport-C5Yd-SCR.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-YLLojQQf.js → useFs-D_MDS8HI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BF_21qUC.js → usePersonalDataHub-BqJh-usA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-9ZpkA7bb.js → vnode-chm_p-gz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DhtAfhl8.js → zoom-3-YDNz0Z.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/assets/web-panel/remote-session-sw.js +43 -0
- package/src/command-manifest.json +22 -1
- package/src/commands/agent.js +44 -5
- package/src/commands/codeintel.js +252 -0
- package/src/commands/eval.js +248 -0
- package/src/commands/plugin.js +354 -0
- package/src/commands/team.js +456 -0
- package/src/gateways/ws/message-dispatcher.js +45 -2
- package/src/gateways/ws/remote-session-protocol.js +37 -0
- package/src/gateways/ws/ws-server.js +1 -1
- package/src/harness/prompt-compressor.js +27 -1
- package/src/harness/remote-command-ledger.js +158 -0
- package/src/harness/remote-session-push-apns.js +246 -0
- package/src/harness/remote-session-push-errors.js +15 -0
- package/src/harness/remote-session-push-fcm.js +9 -25
- package/src/harness/remote-session-push-huawei.js +174 -0
- package/src/harness/remote-session-push-oppo.js +171 -0
- package/src/harness/remote-session-push-senders.js +42 -0
- package/src/harness/remote-session-push-vivo.js +179 -0
- package/src/harness/remote-session-push-web.js +299 -0
- package/src/harness/remote-session-push-xiaomi.js +99 -0
- package/src/index.js +6 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +161 -5
- package/src/lib/agent-team/task-lease.js +434 -0
- package/src/lib/agent-team/team-budget.js +165 -0
- package/src/lib/agent-team/team-mailbox.js +106 -0
- package/src/lib/agent-team/team-runner.js +266 -0
- package/src/lib/agent-team/team-worktree.js +204 -0
- package/src/lib/agents.js +18 -0
- package/src/lib/async-hook-supervisor.cjs +305 -0
- package/src/lib/eval/runner.js +163 -0
- package/src/lib/eval/tasks.js +456 -0
- package/src/lib/eval/trend.js +185 -0
- package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
- package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
- package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
- package/src/lib/lsp/code-intelligence.js +356 -0
- package/src/lib/lsp/jsonrpc-stream.js +139 -0
- package/src/lib/lsp/lsp-client.js +318 -0
- package/src/lib/lsp/lsp-manager.js +323 -0
- package/src/lib/lsp/lsp-server-registry.js +196 -0
- package/src/lib/plugin-monitor-supervisor.js +238 -0
- package/src/lib/plugin-runtime/agents.js +51 -0
- package/src/lib/plugin-runtime/bin.js +100 -0
- package/src/lib/plugin-runtime/hooks.js +100 -0
- package/src/lib/plugin-runtime/install.js +388 -0
- package/src/lib/plugin-runtime/lsp.js +75 -0
- package/src/lib/plugin-runtime/manifest.js +458 -0
- package/src/lib/plugin-runtime/mcp.js +89 -0
- package/src/lib/plugin-runtime/monitors.js +100 -0
- package/src/lib/plugin-runtime/policy.js +144 -0
- package/src/lib/plugin-runtime/reload.js +79 -0
- package/src/lib/plugin-runtime/scopes.js +197 -0
- package/src/lib/plugin-runtime/settings.js +119 -0
- package/src/lib/plugin-runtime/signature.js +107 -0
- package/src/lib/plugin-runtime/skills.js +43 -0
- package/src/lib/plugin-runtime/trust.js +140 -0
- package/src/lib/sandbox-egress-proxy.js +162 -0
- package/src/lib/sandbox-network-policy.js +134 -0
- package/src/lib/sandbox-v2.js +10 -4
- package/src/lib/settings-hook-events.cjs +78 -6
- package/src/lib/settings-hooks.cjs +29 -3
- package/src/lib/settings-loader.cjs +35 -1
- package/src/lib/skill-loader.js +18 -0
- package/src/lib/telemetry/span-recorder.js +237 -0
- package/src/repl/agent-repl.js +400 -4
- package/src/runtime/agent-core.js +546 -52
- package/src/runtime/coding-agent-contract-shared.cjs +82 -8
- package/src/runtime/coding-agent-policy.cjs +32 -7
- package/src/runtime/fallback-model.js +134 -7
- package/src/runtime/headless-runner.js +353 -108
- package/src/runtime/mcp-config.js +46 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DIp8Xcbd.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dpid9CDg.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +0 -1
- package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +0 -1
- package/src/assets/web-panel/assets/index-BXiw9dQf.js +0 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskLeaseRegistry (Phase 4 — Agent Team) — the two capabilities the existing
|
|
3
|
+
* shared task list lacks, layered on top of it rather than reinventing it:
|
|
4
|
+
*
|
|
5
|
+
* 1. EXCLUSIVE LEASES WITH TTL. session-core's `SharedTaskList.claim()` is
|
|
6
|
+
* documented as "不互斥" (non-mutex) — it just stamps an assignee, so two
|
|
7
|
+
* teammates can claim the same task and duplicate the work. Here a task can
|
|
8
|
+
* be leased by AT MOST ONE holder at a time, the lease carries an expiry,
|
|
9
|
+
* and a crashed teammate's expired lease becomes reclaimable — satisfying
|
|
10
|
+
* the Phase 4 acceptance "多 Agent 不会重复处理已被有效 lease 的任务" and
|
|
11
|
+
* "teammate 崩溃后任务可回收并重新分配".
|
|
12
|
+
*
|
|
13
|
+
* 2. DEPENDENCY EDGES (a DAG). A task declares `dependsOn: [key,…]`; it is not
|
|
14
|
+
* claimable until every dependency is COMPLETED. Cycles are rejected at add
|
|
15
|
+
* time (a cyclic dep would deadlock — it could never become claimable).
|
|
16
|
+
*
|
|
17
|
+
* It COMPOSES a real `SharedTaskList` (optimistic-lock task store, status
|
|
18
|
+
* vocabulary, snapshot/restore, terminal-state enforcement) — the lease + edges
|
|
19
|
+
* live in each task's metadata, so persistence/recovery come for free. The clock
|
|
20
|
+
* is injected (`now`) so lease expiry is fully deterministic in tests.
|
|
21
|
+
*
|
|
22
|
+
* This is the scheduling brain; the claim→execute handoff to a real agent
|
|
23
|
+
* (SubAgentContext.run in a per-teammate worktree) is wired separately.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { SharedTaskList, TASK_STATUS } from "@chainlesschain/session-core";
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_LEASE_TTL_MS = 60000;
|
|
29
|
+
export const DEFAULT_MAX_ATTEMPTS = 3;
|
|
30
|
+
|
|
31
|
+
export class TaskLeaseRegistry {
|
|
32
|
+
constructor({
|
|
33
|
+
groupId = null,
|
|
34
|
+
now = () => Date.now(),
|
|
35
|
+
defaultTtlMs = DEFAULT_LEASE_TTL_MS,
|
|
36
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
37
|
+
} = {}) {
|
|
38
|
+
this._now = typeof now === "function" ? now : () => now;
|
|
39
|
+
this.defaultTtlMs = defaultTtlMs > 0 ? defaultTtlMs : DEFAULT_LEASE_TTL_MS;
|
|
40
|
+
this.maxAttempts = maxAttempts > 0 ? maxAttempts : DEFAULT_MAX_ATTEMPTS;
|
|
41
|
+
this._tasks = new SharedTaskList({ groupId, now: this._now });
|
|
42
|
+
this._byKey = new Map(); // stable user key → internal SharedTaskList id
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add a task to the graph. `key` is a stable, user-chosen identifier so
|
|
47
|
+
* `dependsOn` can reference tasks regardless of insertion order. Rejects a
|
|
48
|
+
* duplicate key or a dependency edge that would create a cycle.
|
|
49
|
+
*
|
|
50
|
+
* @returns {{ ok:boolean, key?:string, reason?:string, cycle?:string[] }}
|
|
51
|
+
*/
|
|
52
|
+
addTask({
|
|
53
|
+
key,
|
|
54
|
+
title,
|
|
55
|
+
dependsOn = [],
|
|
56
|
+
priority = "normal",
|
|
57
|
+
metadata = {},
|
|
58
|
+
createdBy = null,
|
|
59
|
+
} = {}) {
|
|
60
|
+
const k = key || `t_${this._tasks.size() + 1}`;
|
|
61
|
+
if (this._byKey.has(k)) {
|
|
62
|
+
return { ok: false, reason: `duplicate task key "${k}"` };
|
|
63
|
+
}
|
|
64
|
+
if (!title || typeof title !== "string") {
|
|
65
|
+
return { ok: false, reason: "title required" };
|
|
66
|
+
}
|
|
67
|
+
const deps = Array.isArray(dependsOn) ? dependsOn.filter(Boolean) : [];
|
|
68
|
+
// Self-dependency and (once inserted) any back-edge that closes a cycle
|
|
69
|
+
// would deadlock the task. Check against the edges already present PLUS the
|
|
70
|
+
// proposed ones.
|
|
71
|
+
const cycle = this._detectCycleIfAdded(k, deps);
|
|
72
|
+
if (cycle) {
|
|
73
|
+
return { ok: false, reason: "dependency cycle", cycle };
|
|
74
|
+
}
|
|
75
|
+
const created = this._tasks.add({
|
|
76
|
+
title,
|
|
77
|
+
priority,
|
|
78
|
+
createdBy,
|
|
79
|
+
metadata: {
|
|
80
|
+
...metadata,
|
|
81
|
+
key: k,
|
|
82
|
+
dependsOn: deps,
|
|
83
|
+
lease: null,
|
|
84
|
+
attempts: 0,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
this._byKey.set(k, created.id);
|
|
88
|
+
return { ok: true, key: k, id: created.id };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve a stable key → the current task object (or null). */
|
|
92
|
+
getTask(key) {
|
|
93
|
+
const id = this._byKey.get(key);
|
|
94
|
+
if (!id) return null;
|
|
95
|
+
const t = this._tasks.get(id);
|
|
96
|
+
if (!t) return null;
|
|
97
|
+
return this._view(t);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_view(t) {
|
|
101
|
+
const lease = t.metadata?.lease || null;
|
|
102
|
+
return {
|
|
103
|
+
key: t.metadata?.key,
|
|
104
|
+
id: t.id,
|
|
105
|
+
title: t.title,
|
|
106
|
+
status: t.status,
|
|
107
|
+
priority: t.priority,
|
|
108
|
+
dependsOn: t.metadata?.dependsOn || [],
|
|
109
|
+
attempts: t.metadata?.attempts || 0,
|
|
110
|
+
lease: lease ? { ...lease } : null,
|
|
111
|
+
assignee: t.assignee,
|
|
112
|
+
rev: t.rev,
|
|
113
|
+
// Full metadata is exposed so executors can read user payload
|
|
114
|
+
// (e.g. a task's shell `command` or agent `prompt`) — it carries the
|
|
115
|
+
// internal lease/dependsOn/key too, which callers simply ignore.
|
|
116
|
+
metadata: { ...t.metadata },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
list() {
|
|
121
|
+
return this._tasks.list().map((t) => this._view(t));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_statusOf(key) {
|
|
125
|
+
const id = this._byKey.get(key);
|
|
126
|
+
const t = id ? this._tasks.get(id) : null;
|
|
127
|
+
return t ? t.status : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Unmet (not-yet-completed) dependency keys for a task. */
|
|
131
|
+
unmetDependencies(key) {
|
|
132
|
+
const t = this.getTask(key);
|
|
133
|
+
if (!t) return [];
|
|
134
|
+
return t.dependsOn.filter(
|
|
135
|
+
(d) => this._statusOf(d) !== TASK_STATUS.COMPLETED,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_leaseValid(lease, now) {
|
|
140
|
+
return !!(lease && lease.expiresAt > now);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Keys that could be leased right now: PENDING (or a task whose lease has
|
|
145
|
+
* expired) with all dependencies COMPLETED and no currently-valid lease.
|
|
146
|
+
*/
|
|
147
|
+
claimable({ now = this._now() } = {}) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const t of this._tasks.list()) {
|
|
150
|
+
const v = this._view(t);
|
|
151
|
+
if (
|
|
152
|
+
v.status === TASK_STATUS.COMPLETED ||
|
|
153
|
+
v.status === TASK_STATUS.CANCELLED
|
|
154
|
+
) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (this._leaseValid(v.lease, now)) continue; // held by a live teammate
|
|
158
|
+
if (this.unmetDependencies(v.key).length > 0) continue; // blocked by deps
|
|
159
|
+
out.push(v.key);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Acquire (or renew, or steal an expired) exclusive lease.
|
|
166
|
+
* - blocked by unmet deps → { ok:false, reason:"blocked_by_deps", unmet }
|
|
167
|
+
* - terminal task → { ok:false, reason:"terminal" }
|
|
168
|
+
* - validly leased by someone else → { ok:false, reason:"leased", holder, expiresAt }
|
|
169
|
+
* - same holder → renew (extend expiry, bump renewals)
|
|
170
|
+
* - expired lease held by another → steal (stolen:true)
|
|
171
|
+
* @returns {{ ok:boolean, reason?:string, lease?:object, unmet?:string[], holder?:string, expiresAt?:number }}
|
|
172
|
+
*/
|
|
173
|
+
acquire(key, { holder, ttlMs, now = this._now() } = {}) {
|
|
174
|
+
if (!holder) return { ok: false, reason: "holder required" };
|
|
175
|
+
const id = this._byKey.get(key);
|
|
176
|
+
const task = id ? this._tasks.get(id) : null;
|
|
177
|
+
if (!task) return { ok: false, reason: "not_found" };
|
|
178
|
+
if (
|
|
179
|
+
task.status === TASK_STATUS.COMPLETED ||
|
|
180
|
+
task.status === TASK_STATUS.CANCELLED
|
|
181
|
+
) {
|
|
182
|
+
return { ok: false, reason: "terminal" };
|
|
183
|
+
}
|
|
184
|
+
const unmet = this.unmetDependencies(key);
|
|
185
|
+
if (unmet.length > 0) {
|
|
186
|
+
return { ok: false, reason: "blocked_by_deps", unmet };
|
|
187
|
+
}
|
|
188
|
+
const existing = task.metadata?.lease || null;
|
|
189
|
+
if (this._leaseValid(existing, now) && existing.holder !== holder) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "leased",
|
|
193
|
+
holder: existing.holder,
|
|
194
|
+
expiresAt: existing.expiresAt,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const ttl = ttlMs > 0 ? ttlMs : this.defaultTtlMs;
|
|
198
|
+
const sameHolder = existing && existing.holder === holder;
|
|
199
|
+
const lease = {
|
|
200
|
+
holder,
|
|
201
|
+
acquiredAt: sameHolder ? existing.acquiredAt : now,
|
|
202
|
+
expiresAt: now + ttl,
|
|
203
|
+
renewals: sameHolder ? (existing.renewals || 0) + 1 : 0,
|
|
204
|
+
stolen: !!(existing && existing.holder !== holder),
|
|
205
|
+
};
|
|
206
|
+
return this._write(task, {
|
|
207
|
+
status: TASK_STATUS.IN_PROGRESS,
|
|
208
|
+
assignee: holder,
|
|
209
|
+
metadata: { ...task.metadata, lease },
|
|
210
|
+
})
|
|
211
|
+
? { ok: true, lease }
|
|
212
|
+
: { ok: false, reason: "concurrent" };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Extend the lease you hold. Fails if you're not the current valid holder. */
|
|
216
|
+
renew(key, { holder, ttlMs, now = this._now() } = {}) {
|
|
217
|
+
const id = this._byKey.get(key);
|
|
218
|
+
const task = id ? this._tasks.get(id) : null;
|
|
219
|
+
if (!task) return { ok: false, reason: "not_found" };
|
|
220
|
+
const lease = task.metadata?.lease || null;
|
|
221
|
+
if (!this._leaseValid(lease, now) || lease.holder !== holder) {
|
|
222
|
+
return { ok: false, reason: "not_holder_or_expired" };
|
|
223
|
+
}
|
|
224
|
+
const ttl = ttlMs > 0 ? ttlMs : this.defaultTtlMs;
|
|
225
|
+
const next = {
|
|
226
|
+
...lease,
|
|
227
|
+
expiresAt: now + ttl,
|
|
228
|
+
renewals: (lease.renewals || 0) + 1,
|
|
229
|
+
};
|
|
230
|
+
return this._write(task, { metadata: { ...task.metadata, lease: next } })
|
|
231
|
+
? { ok: true, lease: next }
|
|
232
|
+
: { ok: false, reason: "concurrent" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Voluntarily give up a lease — the task returns to PENDING for re-claim. */
|
|
236
|
+
release(key, { holder, now = this._now() } = {}) {
|
|
237
|
+
const id = this._byKey.get(key);
|
|
238
|
+
const task = id ? this._tasks.get(id) : null;
|
|
239
|
+
if (!task) return { ok: false, reason: "not_found" };
|
|
240
|
+
const lease = task.metadata?.lease || null;
|
|
241
|
+
if (!lease || lease.holder !== holder) {
|
|
242
|
+
return { ok: false, reason: "not_holder" };
|
|
243
|
+
}
|
|
244
|
+
return this._write(task, {
|
|
245
|
+
status: TASK_STATUS.PENDING,
|
|
246
|
+
assignee: null,
|
|
247
|
+
metadata: { ...task.metadata, lease: null },
|
|
248
|
+
})
|
|
249
|
+
? { ok: true }
|
|
250
|
+
: { ok: false, reason: "concurrent" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Complete a task you hold the lease on → terminal COMPLETED. */
|
|
254
|
+
complete(key, { holder, result = null, now = this._now() } = {}) {
|
|
255
|
+
const id = this._byKey.get(key);
|
|
256
|
+
const task = id ? this._tasks.get(id) : null;
|
|
257
|
+
if (!task) return { ok: false, reason: "not_found" };
|
|
258
|
+
const lease = task.metadata?.lease || null;
|
|
259
|
+
// Only the valid holder may complete — prevents a stale teammate (whose
|
|
260
|
+
// lease already expired and was reassigned) from marking another's work done.
|
|
261
|
+
if (!this._leaseValid(lease, now) || lease.holder !== holder) {
|
|
262
|
+
return { ok: false, reason: "not_holder_or_expired" };
|
|
263
|
+
}
|
|
264
|
+
return this._write(task, {
|
|
265
|
+
status: TASK_STATUS.COMPLETED,
|
|
266
|
+
metadata: { ...task.metadata, lease: null, result },
|
|
267
|
+
})
|
|
268
|
+
? { ok: true }
|
|
269
|
+
: { ok: false, reason: "concurrent" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Report a failed attempt. Under the attempt cap the task returns to PENDING
|
|
274
|
+
* (reclaimable for retry); at the cap it is CANCELLED (terminal) so the team
|
|
275
|
+
* doesn't loop forever on a doomed task.
|
|
276
|
+
* @returns {{ ok:boolean, retry?:boolean, attempts?:number, reason?:string }}
|
|
277
|
+
*/
|
|
278
|
+
fail(key, { holder, error = null, now = this._now() } = {}) {
|
|
279
|
+
const id = this._byKey.get(key);
|
|
280
|
+
const task = id ? this._tasks.get(id) : null;
|
|
281
|
+
if (!task) return { ok: false, reason: "not_found" };
|
|
282
|
+
const lease = task.metadata?.lease || null;
|
|
283
|
+
if (!lease || lease.holder !== holder) {
|
|
284
|
+
return { ok: false, reason: "not_holder" };
|
|
285
|
+
}
|
|
286
|
+
const attempts = (task.metadata?.attempts || 0) + 1;
|
|
287
|
+
const willRetry = attempts < this.maxAttempts;
|
|
288
|
+
const ok = this._write(task, {
|
|
289
|
+
status: willRetry ? TASK_STATUS.PENDING : TASK_STATUS.CANCELLED,
|
|
290
|
+
assignee: null,
|
|
291
|
+
metadata: {
|
|
292
|
+
...task.metadata,
|
|
293
|
+
lease: null,
|
|
294
|
+
attempts,
|
|
295
|
+
lastError: error ? String(error) : null,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
if (!ok) return { ok: false, reason: "concurrent" };
|
|
299
|
+
return { ok: true, retry: willRetry, attempts };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Reclaim every task whose lease has EXPIRED (a crashed/lost teammate never
|
|
304
|
+
* renewed) back to PENDING so it can be re-assigned. Returns the reclaimed
|
|
305
|
+
* keys. This is the crash-recovery sweep.
|
|
306
|
+
*/
|
|
307
|
+
reclaimExpired({ now = this._now() } = {}) {
|
|
308
|
+
const reclaimed = [];
|
|
309
|
+
for (const t of this._tasks.list()) {
|
|
310
|
+
const lease = t.metadata?.lease || null;
|
|
311
|
+
if (lease && !this._leaseValid(lease, now)) {
|
|
312
|
+
const fresh = this._tasks.get(t.id); // re-read for current rev
|
|
313
|
+
if (
|
|
314
|
+
this._write(fresh, {
|
|
315
|
+
status: TASK_STATUS.PENDING,
|
|
316
|
+
assignee: null,
|
|
317
|
+
metadata: { ...fresh.metadata, lease: null },
|
|
318
|
+
})
|
|
319
|
+
) {
|
|
320
|
+
reclaimed.push(fresh.metadata?.key);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return reclaimed;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
stats({ now = this._now() } = {}) {
|
|
328
|
+
const base = this._tasks.stats();
|
|
329
|
+
let leased = 0;
|
|
330
|
+
let expired = 0;
|
|
331
|
+
for (const t of this._tasks.list()) {
|
|
332
|
+
const lease = t.metadata?.lease || null;
|
|
333
|
+
if (lease) {
|
|
334
|
+
if (this._leaseValid(lease, now)) leased++;
|
|
335
|
+
else expired++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
...base,
|
|
340
|
+
leased,
|
|
341
|
+
expiredLeases: expired,
|
|
342
|
+
claimable: this.claimable({ now }).length,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Whether the graph is fully done (all terminal). */
|
|
347
|
+
allDone() {
|
|
348
|
+
return this._tasks
|
|
349
|
+
.list()
|
|
350
|
+
.every(
|
|
351
|
+
(t) =>
|
|
352
|
+
t.status === TASK_STATUS.COMPLETED ||
|
|
353
|
+
t.status === TASK_STATUS.CANCELLED,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
snapshot() {
|
|
358
|
+
return {
|
|
359
|
+
registry: {
|
|
360
|
+
defaultTtlMs: this.defaultTtlMs,
|
|
361
|
+
maxAttempts: this.maxAttempts,
|
|
362
|
+
byKey: Array.from(this._byKey.entries()),
|
|
363
|
+
},
|
|
364
|
+
tasks: this._tasks.snapshot(),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
static restore(snapshot, { now = () => Date.now() } = {}) {
|
|
369
|
+
const reg = new TaskLeaseRegistry({
|
|
370
|
+
now,
|
|
371
|
+
defaultTtlMs: snapshot?.registry?.defaultTtlMs,
|
|
372
|
+
maxAttempts: snapshot?.registry?.maxAttempts,
|
|
373
|
+
});
|
|
374
|
+
reg._tasks = SharedTaskList.restore(snapshot.tasks, { now: reg._now });
|
|
375
|
+
reg._byKey = new Map(snapshot?.registry?.byKey || []);
|
|
376
|
+
return reg;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// --- internals -----------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/** Optimistic write via the underlying list; false on rev conflict. */
|
|
382
|
+
_write(task, patch) {
|
|
383
|
+
try {
|
|
384
|
+
this._tasks.update(task.id, {
|
|
385
|
+
rev: task.rev,
|
|
386
|
+
patch,
|
|
387
|
+
actor: patch.assignee,
|
|
388
|
+
});
|
|
389
|
+
return true;
|
|
390
|
+
} catch {
|
|
391
|
+
// ConcurrencyError (rev changed under us) — caller re-reads and retries.
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Would adding edges `key → dependsOn[i]` create a cycle? Walks the existing
|
|
398
|
+
* edge set augmented with the proposed edges. Returns the offending path or
|
|
399
|
+
* null. A self-edge counts as a cycle.
|
|
400
|
+
*/
|
|
401
|
+
_detectCycleIfAdded(key, deps) {
|
|
402
|
+
// Build adjacency from current tasks, then add the candidate.
|
|
403
|
+
const adj = new Map();
|
|
404
|
+
for (const t of this._tasks.list()) {
|
|
405
|
+
adj.set(t.metadata?.key, t.metadata?.dependsOn || []);
|
|
406
|
+
}
|
|
407
|
+
adj.set(key, deps);
|
|
408
|
+
const WHITE = 0,
|
|
409
|
+
GRAY = 1,
|
|
410
|
+
BLACK = 2;
|
|
411
|
+
const color = new Map();
|
|
412
|
+
const stack = [];
|
|
413
|
+
let found = null;
|
|
414
|
+
const visit = (node) => {
|
|
415
|
+
color.set(node, GRAY);
|
|
416
|
+
stack.push(node);
|
|
417
|
+
for (const next of adj.get(node) || []) {
|
|
418
|
+
if (!adj.has(next)) continue; // edge to a not-yet-added task: no cycle via it
|
|
419
|
+
const c = color.get(next) || WHITE;
|
|
420
|
+
if (c === GRAY) {
|
|
421
|
+
found = [...stack.slice(stack.indexOf(next)), next];
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (c === WHITE && visit(next)) return true;
|
|
425
|
+
}
|
|
426
|
+
stack.pop();
|
|
427
|
+
color.set(node, BLACK);
|
|
428
|
+
return false;
|
|
429
|
+
};
|
|
430
|
+
// Only need to start from the candidate — a new cycle must pass through it.
|
|
431
|
+
if (visit(key)) return found;
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamBudget (Phase 4 — Agent Team) — a team-wide spend cap across four
|
|
3
|
+
* independent dimensions, consulted by TeamRunner BEFORE it hands a teammate a
|
|
4
|
+
* new task so a runaway graph is bounded and a resumed session keeps counting
|
|
5
|
+
* from where it left off (Phase 4 acceptance "支持 token、时间、并发数和费用预算"
|
|
6
|
+
* + "团队会话恢复后…预算保持一致").
|
|
7
|
+
*
|
|
8
|
+
* - maxTasks total task EXECUTIONS across all teammates (task-count cap)
|
|
9
|
+
* - maxTokens total (input+output) LLM tokens folded from task results
|
|
10
|
+
* - maxUsd total estimated USD spend — delegated to the audited CostBudget
|
|
11
|
+
* (same pricing/cache-token rules as `--max-budget-usd`)
|
|
12
|
+
* - maxWallMs wall-clock since the first task started (time cap)
|
|
13
|
+
*
|
|
14
|
+
* Because a task's cost is only known once it returns, the team may overshoot a
|
|
15
|
+
* token/USD cap by at most one task — like CostBudget, it never STARTS a task
|
|
16
|
+
* once already over. A dimension left null is simply inactive. The clock is
|
|
17
|
+
* injected so the wall-clock cap is deterministic in tests, and snapshot/restore
|
|
18
|
+
* carries the running totals so `cc team run --resume` doesn't reset the budget.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { CostBudget } from "../cost-budget.js";
|
|
22
|
+
|
|
23
|
+
const pos = (n) => {
|
|
24
|
+
const v = Number(n);
|
|
25
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class TeamBudget {
|
|
29
|
+
constructor({
|
|
30
|
+
maxTasks = null,
|
|
31
|
+
maxTokens = null,
|
|
32
|
+
maxUsd = null,
|
|
33
|
+
maxWallMs = null,
|
|
34
|
+
table = undefined,
|
|
35
|
+
now = () => Date.now(),
|
|
36
|
+
} = {}) {
|
|
37
|
+
this._now = typeof now === "function" ? now : () => now;
|
|
38
|
+
this.maxTasks = pos(maxTasks);
|
|
39
|
+
this.maxTokens = pos(maxTokens);
|
|
40
|
+
this.maxWallMs = pos(maxWallMs);
|
|
41
|
+
this.cost = new CostBudget({ limitUsd: maxUsd, table });
|
|
42
|
+
this.tasks = 0;
|
|
43
|
+
this.tokens = 0;
|
|
44
|
+
this.startedAt = null; // set on the first recorded task
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Any dimension active? (an all-null budget never stops anything). */
|
|
48
|
+
enabled() {
|
|
49
|
+
return (
|
|
50
|
+
this.maxTasks != null ||
|
|
51
|
+
this.maxTokens != null ||
|
|
52
|
+
this.maxWallMs != null ||
|
|
53
|
+
this.cost.enabled()
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Begin the wall-clock window (idempotent — first call wins). */
|
|
58
|
+
start(now = this._now()) {
|
|
59
|
+
if (this.startedAt == null) this.startedAt = now;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fold one settled task into the running totals. `usage` (an LLM token-usage
|
|
65
|
+
* event) is optional — a plain `--exec` shell task carries none, so only the
|
|
66
|
+
* task-count / wall-clock dimensions move for it.
|
|
67
|
+
* @returns {this}
|
|
68
|
+
*/
|
|
69
|
+
record({ usage = null, provider, model } = {}, now = this._now()) {
|
|
70
|
+
this.start(now);
|
|
71
|
+
this.tasks += 1;
|
|
72
|
+
if (usage) {
|
|
73
|
+
this.tokens +=
|
|
74
|
+
(Number(usage.input_tokens) || 0) + (Number(usage.output_tokens) || 0);
|
|
75
|
+
this.cost.add({ provider, model, usage });
|
|
76
|
+
}
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Which cap (if any) is now reached — checked BEFORE starting a new task.
|
|
82
|
+
* @returns {null|"max-tasks"|"max-tokens"|"max-usd"|"max-wall-ms"}
|
|
83
|
+
*/
|
|
84
|
+
reason(now = this._now()) {
|
|
85
|
+
if (this.maxTasks != null && this.tasks >= this.maxTasks) {
|
|
86
|
+
return "max-tasks";
|
|
87
|
+
}
|
|
88
|
+
if (this.maxTokens != null && this.tokens >= this.maxTokens) {
|
|
89
|
+
return "max-tokens";
|
|
90
|
+
}
|
|
91
|
+
if (this.cost.exceeded()) return "max-usd";
|
|
92
|
+
if (
|
|
93
|
+
this.maxWallMs != null &&
|
|
94
|
+
this.startedAt != null &&
|
|
95
|
+
now - this.startedAt >= this.maxWallMs
|
|
96
|
+
) {
|
|
97
|
+
return "max-wall-ms";
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** True once any active cap has been reached. */
|
|
103
|
+
shouldStop(now = this._now()) {
|
|
104
|
+
return this.reason(now) != null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
status(now = this._now()) {
|
|
108
|
+
return {
|
|
109
|
+
tasks: this.tasks,
|
|
110
|
+
maxTasks: this.maxTasks,
|
|
111
|
+
tokens: this.tokens,
|
|
112
|
+
maxTokens: this.maxTokens,
|
|
113
|
+
spentUsd: this.cost.spentUsd,
|
|
114
|
+
maxUsd: this.cost.limitUsd,
|
|
115
|
+
elapsedMs: this.startedAt == null ? 0 : now - this.startedAt,
|
|
116
|
+
maxWallMs: this.maxWallMs,
|
|
117
|
+
reason: this.reason(now),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
snapshot() {
|
|
122
|
+
return {
|
|
123
|
+
limits: {
|
|
124
|
+
maxTasks: this.maxTasks,
|
|
125
|
+
maxTokens: this.maxTokens,
|
|
126
|
+
maxUsd: this.cost.limitUsd,
|
|
127
|
+
maxWallMs: this.maxWallMs,
|
|
128
|
+
},
|
|
129
|
+
totals: {
|
|
130
|
+
tasks: this.tasks,
|
|
131
|
+
tokens: this.tokens,
|
|
132
|
+
spentUsd: this.cost.spentUsd,
|
|
133
|
+
startedAt: this.startedAt,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Restore running totals + caps from a snapshot. `overrides` lets the current
|
|
140
|
+
* CLI invocation RAISE/lower a cap on resume — a provided (non-nullish) value
|
|
141
|
+
* wins over the snapshot's; an omitted one keeps the prior cap (so a resume
|
|
142
|
+
* with no flags can't silently drop a safety cap).
|
|
143
|
+
*/
|
|
144
|
+
static restore(snap, { now = () => Date.now(), table, overrides = {} } = {}) {
|
|
145
|
+
const pick = (o, s) => (o == null ? s : o);
|
|
146
|
+
const b = new TeamBudget({
|
|
147
|
+
maxTasks: pick(overrides.maxTasks, snap?.limits?.maxTasks),
|
|
148
|
+
maxTokens: pick(overrides.maxTokens, snap?.limits?.maxTokens),
|
|
149
|
+
maxUsd: pick(overrides.maxUsd, snap?.limits?.maxUsd),
|
|
150
|
+
maxWallMs: pick(overrides.maxWallMs, snap?.limits?.maxWallMs),
|
|
151
|
+
table,
|
|
152
|
+
now,
|
|
153
|
+
});
|
|
154
|
+
b.tasks = Number(snap?.totals?.tasks) || 0;
|
|
155
|
+
b.tokens = Number(snap?.totals?.tokens) || 0;
|
|
156
|
+
// Preserve prior USD spend so the cap keeps counting across a resume.
|
|
157
|
+
const spent = Number(snap?.totals?.spentUsd);
|
|
158
|
+
b.cost.spentUsd = Number.isFinite(spent) && spent >= 0 ? spent : 0;
|
|
159
|
+
if (b.cost.spentUsd > 0) b.cost.priced = true;
|
|
160
|
+
// A time cap must NOT keep counting wall-clock across a crash gap (the
|
|
161
|
+
// process was down); restart the window on the next recorded task.
|
|
162
|
+
b.startedAt = null;
|
|
163
|
+
return b;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamMailbox (Phase 4 — Agent Team) — directed + broadcast messaging between
|
|
3
|
+
* teammates so a coordinating agent can hand off context, flag a finding, or ask
|
|
4
|
+
* a peer to redo work (Phase 4 work-item "支持 Agent 间直接消息和定向通知",
|
|
5
|
+
* acceptance "团队会话恢复后…消息…保持一致").
|
|
6
|
+
*
|
|
7
|
+
* A message is addressed to a specific recipient (`to`) or broadcast (`to:"*"`).
|
|
8
|
+
* `drain(recipient)` returns that recipient's undelivered messages (direct +
|
|
9
|
+
* broadcasts it hasn't seen) and marks them delivered FOR THAT RECIPIENT — a
|
|
10
|
+
* broadcast is delivered independently to each teammate, so two teammates each
|
|
11
|
+
* receive it exactly once. Delivery cursors + the log are in the snapshot, so a
|
|
12
|
+
* resumed session re-delivers only what a teammate hadn't yet drained.
|
|
13
|
+
*
|
|
14
|
+
* Pure + deterministic: ids come from a monotonic counter (not a clock) and the
|
|
15
|
+
* clock is injected only for timestamps, so ordering is stable in tests.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class TeamMailbox {
|
|
19
|
+
constructor({ now = () => Date.now() } = {}) {
|
|
20
|
+
this._now = typeof now === "function" ? now : () => now;
|
|
21
|
+
this._log = []; // ordered [{ id, from, to, subject, body, ts }]
|
|
22
|
+
this._seq = 0; // monotonic message id source
|
|
23
|
+
this._delivered = new Map(); // recipient → highest message id already drained
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Post a message. `to` is a teammate id or "*" (broadcast). Returns the stored
|
|
28
|
+
* message (with its assigned id).
|
|
29
|
+
*/
|
|
30
|
+
send({ from = null, to, subject = null, body = null } = {}) {
|
|
31
|
+
if (!to || typeof to !== "string") {
|
|
32
|
+
throw new Error("TeamMailbox.send: `to` recipient (or '*') is required");
|
|
33
|
+
}
|
|
34
|
+
const msg = {
|
|
35
|
+
id: ++this._seq,
|
|
36
|
+
from,
|
|
37
|
+
to,
|
|
38
|
+
subject,
|
|
39
|
+
body,
|
|
40
|
+
ts: this._now(),
|
|
41
|
+
};
|
|
42
|
+
this._log.push(msg);
|
|
43
|
+
return { ...msg };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_isFor(msg, recipient) {
|
|
47
|
+
return msg.to === recipient || msg.to === "*";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Peek at a recipient's undelivered messages WITHOUT marking them delivered. */
|
|
51
|
+
peek(recipient) {
|
|
52
|
+
const cursor = this._delivered.get(recipient) || 0;
|
|
53
|
+
return this._log
|
|
54
|
+
.filter(
|
|
55
|
+
(m) =>
|
|
56
|
+
m.id > cursor && m.from !== recipient && this._isFor(m, recipient),
|
|
57
|
+
)
|
|
58
|
+
.map((m) => ({ ...m }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Return a recipient's undelivered messages and advance its delivery cursor so
|
|
63
|
+
* they are not returned again. A teammate never receives its own broadcast.
|
|
64
|
+
*/
|
|
65
|
+
drain(recipient) {
|
|
66
|
+
const pending = this.peek(recipient);
|
|
67
|
+
if (pending.length > 0) {
|
|
68
|
+
const highest = pending[pending.length - 1].id;
|
|
69
|
+
const cursor = this._delivered.get(recipient) || 0;
|
|
70
|
+
this._delivered.set(recipient, Math.max(cursor, highest));
|
|
71
|
+
}
|
|
72
|
+
return pending;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** How many messages a recipient has yet to drain. */
|
|
76
|
+
pendingCount(recipient) {
|
|
77
|
+
return this.peek(recipient).length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The full ordered message log (for a status panel / audit). */
|
|
81
|
+
log() {
|
|
82
|
+
return this._log.map((m) => ({ ...m }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
size() {
|
|
86
|
+
return this._log.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
snapshot() {
|
|
90
|
+
return {
|
|
91
|
+
log: this._log.map((m) => ({ ...m })),
|
|
92
|
+
seq: this._seq,
|
|
93
|
+
delivered: Array.from(this._delivered.entries()),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static restore(snap, { now = () => Date.now() } = {}) {
|
|
98
|
+
const mb = new TeamMailbox({ now });
|
|
99
|
+
mb._log = Array.isArray(snap?.log) ? snap.log.map((m) => ({ ...m })) : [];
|
|
100
|
+
mb._seq =
|
|
101
|
+
Number(snap?.seq) ||
|
|
102
|
+
mb._log.reduce((max, m) => Math.max(max, m.id || 0), 0);
|
|
103
|
+
mb._delivered = new Map(snap?.delivered || []);
|
|
104
|
+
return mb;
|
|
105
|
+
}
|
|
106
|
+
}
|