chainlesschain 0.162.158 → 0.162.160
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/assets/{AIOps-BwpTJTN_.js → AIOps-CkRHjW8e.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-BP3MeNi2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-CjbjQsOr.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-i0adH1M8.js} +5 -5
- package/src/assets/web-panel/assets/Artifacts-CCCG2GOU.js +1 -0
- package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DrsgvwHC.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-DkXNkbZM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-DarTWCex.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DaDzhcto.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-C4EzGw6V.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-gddNgaQi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-BKldFWf9.js} +1 -1
- package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-ChR-brDs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DlHtZX86.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-BAhfcBpC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-gIrKkm6m.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BYnzLBO3.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-ByUDIljI.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DOdNbHFp.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-BJHaNWD2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-Drm_tClz.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-CA1WLzGN.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-Di1SKjNO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-CubPwv_N.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-CsUFe5bU.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +1 -0
- package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-D-Qx9dhO.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-_YMYaU4n.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-J7Yzh-K_.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-oMTSYfGY.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-DKUqYDaF.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-BEv3Gx61.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-CtA8XD2y.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-ug7c1mMj.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-WJaMs4i2.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-DXnK_-ru.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-C4Ev7726.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-BMFb9_zk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Bl93nvBX.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-BN2W5cl6.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-h1A0XB2a.js → OrderTableRenderer-Dxc9oN4o.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-CAs48637.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-Bhq27Zoc.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-D61-2542.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C1j4iUxz.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-rJ2-fScM.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-DFIlTok6.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-BjIgEWoZ.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-BQoH5wcX.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-CYR_-_Ub.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-nkatCq-9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-BnKM9fsF.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Cfe82dbz.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-r1RbPufV.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-CjCbM2WE.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-BuMGPH6V.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-Bmj3NY1F.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-BIsNcE_1.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-BObmojD2.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-BRmbkQNS.js} +1 -1
- package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-_rF3DRpY.js} +2 -2
- package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D0If9ZV4.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-o7zIksUg.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-oUR7zi9u.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-fqKyGi03.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-B1wZ_gxx.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-B3qJWYXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-BH5_dUfr.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-CCIVpjRP.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-2GZtTGnB.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-BlMkfs_u.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-U6hi5OMc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-C-_BerGJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DaKzBgN3.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-CLvcsco3.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-BFn_Y1zZ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-B0V18dLT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-BbmP7Imu.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-Dewj_0kt.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-B1OIpZnh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-CSGNVdri.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-8S0vcg0u.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-vZyc27TN.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DWw3-UhA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-B4pP2ycr.js} +1 -1
- package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-BDHyovnK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-BKEmQQvR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BQssf0EQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-BZnauCqN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-BdyokEau.js} +1 -1
- package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-BeBv896m.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-BjC-TrOb.js} +1 -1
- package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-BkWd6YL7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-BoIPHP4R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-BuB5P0TR.js} +1 -1
- package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-BwKrTVV0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C6r27A-o.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-CB2MOS_8.js} +3 -3
- package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CFQWZvak.js} +1 -1
- package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-CI3slBKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCyP071m.js → index-CJgKP2cm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-CLPr2vC_.js} +1 -1
- package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-CbZqw7pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-CfEvacz7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-CwXBblm-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-D1i_8oop.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-D8qXVfoV.js} +1 -1
- package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-DKwl1AkQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D23L1V76.js → index-DLAvUtOH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-DLgkAhEE.js} +1 -1
- package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-DPgY5GLy.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-Dc40pX3F.js} +1 -1
- package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-DcfLXAW7.js} +1 -1
- package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-DeCS12Mh.js} +1 -1
- package/src/assets/web-panel/assets/{index-B30NvR21.js → index-Dg0Izipr.js} +1 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
- package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-DtHWHJnj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-Q6oyLnKC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-dxocDVD_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-t-q3rkVm.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-tr1x96W9.js} +1 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
- package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-xx5M5Rbj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-Di5YFId0.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-BVSN0aeL.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-DHNiaP2l.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-xJPf3gQD.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-C7qzUdkN.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-pSpjg3Le.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-cs_jL_UC.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-DkS_Zs4r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-B5O1Kpxo.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-BX02GnZp.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-EmBYRftd.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-CfP7M1m7.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-7xh0If_O.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-DmitvTR2.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +72 -6
- package/src/commands/background-session.js +173 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +107 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +396 -0
- package/src/data/changelog.json +12 -2
- package/src/harness/jsonl-session-store.js +152 -3
- package/src/harness/mcp-client.js +50 -0
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/agent-sandbox.js +68 -0
- package/src/lib/agents.js +12 -4
- package/src/lib/api-key-helper.js +62 -0
- package/src/lib/background-agent-supervisor.js +158 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +386 -0
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/dependency-install-policy.js +130 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/exit-codes.cjs +77 -0
- package/src/lib/headless-manifest.js +175 -0
- package/src/lib/hook-runner.cjs +70 -4
- package/src/lib/ide-context-redaction.js +130 -0
- package/src/lib/ide-context.js +89 -13
- package/src/lib/jsonl-session-store.js +6 -0
- package/src/lib/llm-config-defaults.js +24 -8
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/project-mcp-trust.js +87 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/sensitive-file-guard.js +51 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +30 -3
- package/src/repl/agent-repl.js +361 -3
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +344 -7
- package/src/runtime/coding-agent-contract-shared.cjs +107 -2
- package/src/runtime/coding-agent-policy.cjs +17 -0
- package/src/runtime/headless-runner.js +137 -14
- package/src/runtime/headless-stream.js +43 -9
- package/src/runtime/mcp-config.js +32 -1
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BAV9V-t-.js +0 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-DxxEdn2r.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-CVtTqgtg.js +0 -1
- package/src/assets/web-panel/assets/Projects-COATmHxF.js +0 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
- package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript integrity — hash-chained JSONL session transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Every event appended to a session transcript carries `prevHash` (the hash of
|
|
5
|
+
* the previous event, or null for the first chained event) and `hash` — a
|
|
6
|
+
* SHA-256 over the previous hash plus the event's canonical core
|
|
7
|
+
* (`{type, timestamp, data}`). Editing, deleting, inserting, or reordering any
|
|
8
|
+
* chained line breaks the chain and is detected by `verifyTranscriptLines`.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints:
|
|
11
|
+
* - Pure module: no fs, no paths — the store layer (jsonl-session-store.js)
|
|
12
|
+
* owns file IO so this stays cycle-free and unit-testable in isolation.
|
|
13
|
+
* - Backward compatible: transcripts written before chaining have no hash
|
|
14
|
+
* fields. A fully legacy file verifies as `legacy`; a legacy prefix followed
|
|
15
|
+
* by a valid chain is `partial` (chaining starts on the first upgraded
|
|
16
|
+
* append). An UNchained record appearing AFTER chained records is flagged as
|
|
17
|
+
* tampered — our writer never does that, so it is either a downgrade-write
|
|
18
|
+
* or a manual append, both of which the verifier must surface.
|
|
19
|
+
* - Honest limitation: truncating the file tail (deleting the newest N chained
|
|
20
|
+
* lines) is undetectable without an external anchor — the chain is
|
|
21
|
+
* self-contained. A half-written LAST line (crash mid-append) is reported as
|
|
22
|
+
* `truncatedTail: true` rather than tampered.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
export const TRANSCRIPT_CHAIN_STATUS = Object.freeze({
|
|
28
|
+
VERIFIED: "verified", // every record chained + chain valid
|
|
29
|
+
PARTIAL: "partial", // legacy prefix, then a valid chain
|
|
30
|
+
LEGACY: "legacy", // no chained records at all (pre-chaining transcript)
|
|
31
|
+
TAMPERED: "tampered", // chain broken: edit / delete / insert / reorder
|
|
32
|
+
EMPTY: "empty", // no records
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hash one event's canonical core against the previous chain hash.
|
|
37
|
+
* The core is rebuilt field-by-field (not the raw line) so the hash is
|
|
38
|
+
* independent of the presence/position of the prevHash/hash fields themselves;
|
|
39
|
+
* JSON.parse → JSON.stringify preserves the writer's key insertion order for
|
|
40
|
+
* `data`, so writer and verifier serialize identically.
|
|
41
|
+
*/
|
|
42
|
+
export function computeEventHash(prevHash, core) {
|
|
43
|
+
const canonical = JSON.stringify({
|
|
44
|
+
type: core.type,
|
|
45
|
+
timestamp: core.timestamp,
|
|
46
|
+
data: core.data,
|
|
47
|
+
});
|
|
48
|
+
return createHash("sha256")
|
|
49
|
+
.update(`${prevHash || "genesis"}\n${canonical}`, "utf-8")
|
|
50
|
+
.digest("hex");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The hash the NEXT appended event must chain from: the `hash` of the last
|
|
55
|
+
* parseable line, or null when the file is empty / fully legacy / the last
|
|
56
|
+
* parseable line is unchained. A trailing half-written line (crash mid-append)
|
|
57
|
+
* is skipped so the writer chains from the last intact record.
|
|
58
|
+
*/
|
|
59
|
+
export function latestChainHash(text) {
|
|
60
|
+
const lines = String(text || "").split("\n");
|
|
61
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
if (!line.trim()) continue;
|
|
64
|
+
try {
|
|
65
|
+
const event = JSON.parse(line);
|
|
66
|
+
return typeof event?.hash === "string" ? event.hash : null;
|
|
67
|
+
} catch {
|
|
68
|
+
// half-written tail line — chain from the previous intact record
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verify a transcript's hash chain.
|
|
77
|
+
*
|
|
78
|
+
* @param {string[]} lines raw transcript lines (as split on "\n")
|
|
79
|
+
* @returns {{
|
|
80
|
+
* status: string, // TRANSCRIPT_CHAIN_STATUS value
|
|
81
|
+
* chainedEvents: number,
|
|
82
|
+
* legacyEvents: number,
|
|
83
|
+
* malformedLines: number,
|
|
84
|
+
* truncatedTail: boolean, // last non-empty line is half-written JSON
|
|
85
|
+
* firstInvalidLine: number|null, // 1-based line number of the first break
|
|
86
|
+
* reason: string|null,
|
|
87
|
+
* }}
|
|
88
|
+
*/
|
|
89
|
+
export function verifyTranscriptLines(lines) {
|
|
90
|
+
const input = Array.isArray(lines) ? lines : [];
|
|
91
|
+
let lastNonEmpty = -1;
|
|
92
|
+
for (let i = input.length - 1; i >= 0; i--) {
|
|
93
|
+
if (input[i] && input[i].trim()) {
|
|
94
|
+
lastNonEmpty = i;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = {
|
|
100
|
+
status: TRANSCRIPT_CHAIN_STATUS.EMPTY,
|
|
101
|
+
chainedEvents: 0,
|
|
102
|
+
legacyEvents: 0,
|
|
103
|
+
malformedLines: 0,
|
|
104
|
+
truncatedTail: false,
|
|
105
|
+
firstInvalidLine: null,
|
|
106
|
+
reason: null,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let lastHash = null;
|
|
110
|
+
let sawChain = false;
|
|
111
|
+
|
|
112
|
+
const tampered = (lineNo, reason) => {
|
|
113
|
+
result.status = TRANSCRIPT_CHAIN_STATUS.TAMPERED;
|
|
114
|
+
result.firstInvalidLine = lineNo;
|
|
115
|
+
result.reason = reason;
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < input.length; i++) {
|
|
120
|
+
const line = input[i];
|
|
121
|
+
if (!line || !line.trim()) continue;
|
|
122
|
+
const lineNo = i + 1;
|
|
123
|
+
|
|
124
|
+
let event;
|
|
125
|
+
try {
|
|
126
|
+
event = JSON.parse(line);
|
|
127
|
+
} catch {
|
|
128
|
+
result.malformedLines++;
|
|
129
|
+
if (i === lastNonEmpty) {
|
|
130
|
+
// Crash mid-append: the newest line is half-written. Not tampering —
|
|
131
|
+
// report it so callers can surface a warning.
|
|
132
|
+
result.truncatedTail = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (sawChain) {
|
|
136
|
+
return tampered(lineNo, "malformed line inside hash chain");
|
|
137
|
+
}
|
|
138
|
+
continue; // junk inside the legacy region — validate reports it
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (event && typeof event.hash === "string") {
|
|
142
|
+
const expectedPrev = sawChain ? lastHash : null;
|
|
143
|
+
if ((event.prevHash ?? null) !== expectedPrev) {
|
|
144
|
+
return tampered(
|
|
145
|
+
lineNo,
|
|
146
|
+
sawChain
|
|
147
|
+
? "hash chain linkage broken (record deleted, inserted, or reordered)"
|
|
148
|
+
: "chain does not start at a genesis record (head records removed)",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (computeEventHash(expectedPrev, event) !== event.hash) {
|
|
152
|
+
return tampered(lineNo, "event content does not match its hash");
|
|
153
|
+
}
|
|
154
|
+
sawChain = true;
|
|
155
|
+
lastHash = event.hash;
|
|
156
|
+
result.chainedEvents++;
|
|
157
|
+
} else {
|
|
158
|
+
if (sawChain) {
|
|
159
|
+
return tampered(
|
|
160
|
+
lineNo,
|
|
161
|
+
"unchained record after hash chain started (manual append or downgrade write)",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
result.legacyEvents++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.chainedEvents > 0) {
|
|
169
|
+
result.status =
|
|
170
|
+
result.legacyEvents > 0
|
|
171
|
+
? TRANSCRIPT_CHAIN_STATUS.PARTIAL
|
|
172
|
+
: TRANSCRIPT_CHAIN_STATUS.VERIFIED;
|
|
173
|
+
} else if (result.legacyEvents > 0 || result.malformedLines > 0) {
|
|
174
|
+
result.status = TRANSCRIPT_CHAIN_STATUS.LEGACY;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Convenience wrapper for whole-file text. */
|
|
180
|
+
export function verifyTranscriptText(text) {
|
|
181
|
+
return verifyTranscriptLines(String(text || "").split("\n"));
|
|
182
|
+
}
|
package/src/index.js
CHANGED
|
@@ -159,6 +159,12 @@ import { registerRemoteControlCommand } from "./commands/remote-control.js";
|
|
|
159
159
|
// Agenda: consumer for agent-scheduled wakeups/crons/monitors (第四阶段 #3)
|
|
160
160
|
import { registerAgendaCommand } from "./commands/agenda.js";
|
|
161
161
|
|
|
162
|
+
// Routines: named scheduled/triggered agent tasks with run history (gap-2026-07-11 P1#8)
|
|
163
|
+
import { registerRoutineCommand } from "./commands/routine.js";
|
|
164
|
+
|
|
165
|
+
// Cloud: self-hosted cloud handoff — offload a task to a private runner (gap-2026-07-11 P1#7)
|
|
166
|
+
import { registerCloudCommand } from "./commands/cloud.js";
|
|
167
|
+
|
|
162
168
|
// Batch: dynamic worktree batch — split a change into parallel units (第四阶段 #4)
|
|
163
169
|
import { registerBatchCommand } from "./commands/batch.js";
|
|
164
170
|
|
|
@@ -597,6 +603,8 @@ export function createProgram(opts = {}) {
|
|
|
597
603
|
|
|
598
604
|
// Agenda: fire agent-scheduled wakeups/crons/monitors
|
|
599
605
|
registerAgendaCommand(program);
|
|
606
|
+
registerRoutineCommand(program);
|
|
607
|
+
registerCloudCommand(program);
|
|
600
608
|
|
|
601
609
|
// Batch: dynamic worktree batch
|
|
602
610
|
registerBatchCommand(program);
|
package/src/lib/agent-sandbox.js
CHANGED
|
@@ -215,11 +215,79 @@ function executeBubblewrapShell(command, sandbox, options, hostCwd, policy) {
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The run's TRUE isolation level (gap-analysis 2026-07-11 P0 "OS 级沙箱"):
|
|
220
|
+
* - "os-sandbox" : bubblewrap — kernel namespaces confine the child
|
|
221
|
+
* - "container" : docker — container boundary confines the child
|
|
222
|
+
* - "policy-only" : no sandbox — permission rules/shell policy are advisory
|
|
223
|
+
* for already-spawned subprocesses
|
|
224
|
+
* Surfaced in the headless init event and sandboxSummary so a caller can see
|
|
225
|
+
* what actually confines tool subprocesses instead of assuming.
|
|
226
|
+
*/
|
|
227
|
+
export function isolationLevel(sandbox) {
|
|
228
|
+
if (!sandbox) return "policy-only";
|
|
229
|
+
if (sandbox.engine === "bubblewrap") return "os-sandbox";
|
|
230
|
+
if (sandbox.engine === "docker") return "container";
|
|
231
|
+
return "policy-only";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Probe whether the configured sandbox ENGINE is actually runnable on this
|
|
236
|
+
* host (binary present + responds to a version query). Cheap: one spawnSync
|
|
237
|
+
* with a short timeout; no container is started.
|
|
238
|
+
* @returns {{available:boolean, reason:string|null}}
|
|
239
|
+
*/
|
|
240
|
+
export function probeSandboxAvailability(sandbox, deps = _deps) {
|
|
241
|
+
if (!sandbox) return { available: true, reason: null };
|
|
242
|
+
const probeArgs =
|
|
243
|
+
sandbox.engine === "bubblewrap"
|
|
244
|
+
? ["bwrap", ["--version"]]
|
|
245
|
+
: ["docker", ["version", "--format", "{{.Server.Version}}"]];
|
|
246
|
+
const result = deps.spawnSync(probeArgs[0], probeArgs[1], {
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
timeout: 10000,
|
|
249
|
+
windowsHide: true,
|
|
250
|
+
});
|
|
251
|
+
if (result.error) {
|
|
252
|
+
return {
|
|
253
|
+
available: false,
|
|
254
|
+
reason:
|
|
255
|
+
result.error.code === "ENOENT"
|
|
256
|
+
? `${probeArgs[0]} is not installed`
|
|
257
|
+
: result.error.message,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
261
|
+
return {
|
|
262
|
+
available: false,
|
|
263
|
+
reason: `${probeArgs[0]} probe exited ${result.status}: ${(result.stderr || "").trim().slice(0, 200)}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { available: true, reason: null };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Strict mode (`sandbox.failIfUnavailable: true` in settings): refuse to
|
|
271
|
+
* START the agent when the configured sandbox engine is unavailable, instead
|
|
272
|
+
* of silently degrading per command. Throws with an actionable message; a
|
|
273
|
+
* no-op when the flag is unset or the sandbox is fine.
|
|
274
|
+
*/
|
|
275
|
+
export function assertSandboxAvailable(sandbox, deps = _deps) {
|
|
276
|
+
if (!sandbox || sandbox.policy?.failIfUnavailable !== true) return;
|
|
277
|
+
const probe = probeSandboxAvailability(sandbox, deps);
|
|
278
|
+
if (!probe.available) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`sandbox.failIfUnavailable: ${sandbox.engine} sandbox is unavailable (${probe.reason}) — refusing to start. Install/start ${sandbox.engine === "bubblewrap" ? "bubblewrap" : "Docker"}, or unset failIfUnavailable to allow per-command degradation.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
218
285
|
export function sandboxSummary(sandbox) {
|
|
219
286
|
if (!sandbox) return null;
|
|
220
287
|
return {
|
|
221
288
|
engine: sandbox.engine,
|
|
222
289
|
image: sandbox.image,
|
|
290
|
+
isolationLevel: isolationLevel(sandbox),
|
|
223
291
|
network: sandbox.network ? "enabled" : "disabled",
|
|
224
292
|
workspace: "read-write",
|
|
225
293
|
policy: {
|
package/src/lib/agents.js
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* `review/security.md` is the agent `review:security`.
|
|
9
9
|
*
|
|
10
10
|
* Frontmatter (all optional):
|
|
11
|
-
* name
|
|
12
|
-
* description
|
|
13
|
-
* tools
|
|
14
|
-
*
|
|
11
|
+
* name override the filename-derived name
|
|
12
|
+
* description one-line summary (when to use this agent)
|
|
13
|
+
* tools allow-list — comma string or YAML array; omit = inherit all
|
|
14
|
+
* disallowedTools deny-list — removed from whatever the allow-list resolves to
|
|
15
|
+
* model model override for runs of this agent
|
|
16
|
+
* maxTurns per-run iteration cap for this agent (positive integer)
|
|
17
|
+
* isolation "worktree" → run in a fresh git worktree (gap 2026-07-11)
|
|
15
18
|
*
|
|
16
19
|
* Project scope shadows personal on a name clash. Discovery + parse are pure
|
|
17
20
|
* (inject fs/path/home) so the whole thing is unit-testable.
|
|
@@ -132,13 +135,18 @@ export function parseAgentFile(file, scope, opts = {}) {
|
|
|
132
135
|
return null;
|
|
133
136
|
}
|
|
134
137
|
const { data, body } = parseFrontmatter(content);
|
|
138
|
+
const maxTurns = Number(data.maxTurns);
|
|
135
139
|
return {
|
|
136
140
|
file,
|
|
137
141
|
scope,
|
|
138
142
|
name: data.name || null, // resolved against the path in discoverAgents
|
|
139
143
|
description: data.description || "",
|
|
140
144
|
tools: normalizeTools(data.tools),
|
|
145
|
+
disallowedTools: normalizeTools(data.disallowedTools),
|
|
141
146
|
model: data.model || null,
|
|
147
|
+
maxTurns:
|
|
148
|
+
Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : null,
|
|
149
|
+
isolation: data.isolation === "worktree" ? "worktree" : null,
|
|
142
150
|
systemPrompt: body || "",
|
|
143
151
|
};
|
|
144
152
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `llm.apiKeyHelper` (gap-analysis 2026-07-11 P0 "依赖安装与凭据"): fetch the
|
|
3
|
+
* LLM API key from an external command instead of storing it in plaintext
|
|
4
|
+
* config.json. Point it at your OS credential store, e.g.:
|
|
5
|
+
*
|
|
6
|
+
* Windows : "powershell -NoProfile -Command \"...CredentialManager...\""
|
|
7
|
+
* macOS : "security find-generic-password -s cc-llm -w"
|
|
8
|
+
* Linux : "secret-tool lookup service cc-llm"
|
|
9
|
+
*
|
|
10
|
+
* Resolution precedence stays: --api-key > CC_API_KEY > config llm.apiKey >
|
|
11
|
+
* llm.apiKeyHelper. The helper's stdout (trimmed) is the key; a failing or
|
|
12
|
+
* empty helper resolves null so the run fails with the provider's own
|
|
13
|
+
* missing-key error (never a silent fallback — see the no-silent-substitution
|
|
14
|
+
* rule). Cached per process (5 min) so multi-call runs don't re-spawn it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
const _cache = new Map(); // helper command → { key, at }
|
|
21
|
+
let _warnedFailure = false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} helperCmd shell command printing the key on stdout
|
|
25
|
+
* @param {{exec?:Function, now?:()=>number, writeErr?:(s:string)=>void}} [opts]
|
|
26
|
+
* @returns {string|null}
|
|
27
|
+
*/
|
|
28
|
+
export function resolveApiKeyFromHelper(helperCmd, opts = {}) {
|
|
29
|
+
const cmd = typeof helperCmd === "string" ? helperCmd.trim() : "";
|
|
30
|
+
if (!cmd) return null;
|
|
31
|
+
const now = opts.now || Date.now;
|
|
32
|
+
const hit = _cache.get(cmd);
|
|
33
|
+
if (hit && now() - hit.at < TTL_MS) return hit.key;
|
|
34
|
+
const exec = opts.exec || execSync;
|
|
35
|
+
try {
|
|
36
|
+
const out = exec(cmd, {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
});
|
|
42
|
+
const key = String(out || "").trim();
|
|
43
|
+
if (!key) return null;
|
|
44
|
+
_cache.set(cmd, { key, at: now() });
|
|
45
|
+
return key;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Warn once per process, then stay quiet — the provider's missing-key
|
|
48
|
+
// error is the actionable surface.
|
|
49
|
+
if (!_warnedFailure) {
|
|
50
|
+
_warnedFailure = true;
|
|
51
|
+
const writeErr = opts.writeErr || ((s) => process.stderr.write(s));
|
|
52
|
+
writeErr(`[api-key-helper] llm.apiKeyHelper failed: ${err.message}\n`);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Test seam: drop the per-process cache + warn-once latch. */
|
|
59
|
+
export function clearApiKeyHelperCache() {
|
|
60
|
+
_cache.clear();
|
|
61
|
+
_warnedFailure = false;
|
|
62
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readdirSync,
|
|
9
9
|
renameSync,
|
|
10
10
|
rmSync,
|
|
11
|
+
statSync,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import { randomBytes } from "node:crypto";
|
|
@@ -61,12 +62,57 @@ export function logPath(id) {
|
|
|
61
62
|
return join(backgroundAgentsDir(), `${safeId(id)}.log`);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
|
|
66
|
+
|
|
64
67
|
export function writeBackgroundAgentState(state) {
|
|
65
68
|
const target = statePath(state.id);
|
|
69
|
+
// Field-aware merge against the freshest on-disk state. The state file has
|
|
70
|
+
// multiple concurrent read-modify-write writers (launcher, worker heartbeat/
|
|
71
|
+
// turn/finalize, rename/pin/stop from other processes) with last-writer-wins
|
|
72
|
+
// semantics — a writer holding a stale snapshot used to clobber a terminal
|
|
73
|
+
// status back to "running" (phantom session, real exit code lost) or roll a
|
|
74
|
+
// fresh rename back to the old title. Two invariants restore convergence:
|
|
75
|
+
// 1. a terminal status wins over a racing "running" snapshot — there is no
|
|
76
|
+
// legitimate same-id terminal→running transition (resume mints a NEW
|
|
77
|
+
// id, and the worker's own writeHeartbeat already refuses to resurrect);
|
|
78
|
+
// 2. the newest rename/pin (by renamedAt/pinnedAt) wins regardless of
|
|
79
|
+
// which writer's snapshot carries it.
|
|
80
|
+
// The read happens immediately before the atomic rename, shrinking the
|
|
81
|
+
// clobber window from "any caller's RMW span" to microseconds; rename/pin
|
|
82
|
+
// additionally verify-and-retry on top of this.
|
|
83
|
+
let next = state;
|
|
84
|
+
const current = readBackgroundAgentState(state.id);
|
|
85
|
+
if (current) {
|
|
86
|
+
if (TERMINAL_STATUSES.has(current.status) && next.status === "running") {
|
|
87
|
+
next = {
|
|
88
|
+
...next,
|
|
89
|
+
status: current.status,
|
|
90
|
+
endedAt: current.endedAt ?? next.endedAt ?? null,
|
|
91
|
+
exitCode: current.exitCode ?? next.exitCode ?? null,
|
|
92
|
+
...(current.signal !== undefined ? { signal: current.signal } : {}),
|
|
93
|
+
...(current.error !== undefined ? { error: current.error } : {}),
|
|
94
|
+
...(current.lostReason !== undefined
|
|
95
|
+
? { lostReason: current.lostReason }
|
|
96
|
+
: {}),
|
|
97
|
+
...(current.stoppedByUser !== undefined
|
|
98
|
+
? { stoppedByUser: current.stoppedByUser }
|
|
99
|
+
: {}),
|
|
100
|
+
// terminal sessions have no live phase or transport endpoint
|
|
101
|
+
phase: null,
|
|
102
|
+
transport: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (Number(current.renamedAt || 0) > Number(next.renamedAt || 0)) {
|
|
106
|
+
next = { ...next, title: current.title, renamedAt: current.renamedAt };
|
|
107
|
+
}
|
|
108
|
+
if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
|
|
109
|
+
next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
66
112
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
67
|
-
writeFileSync(tmp, JSON.stringify(
|
|
113
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
68
114
|
renameSync(tmp, target);
|
|
69
|
-
return
|
|
115
|
+
return next;
|
|
70
116
|
}
|
|
71
117
|
|
|
72
118
|
export function readBackgroundAgentState(id) {
|
|
@@ -177,6 +223,66 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
177
223
|
return next;
|
|
178
224
|
}
|
|
179
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Remove a background agent's RECORD (state file + log) — `cc daemon rm`
|
|
228
|
+
* (gap-analysis 2026-07-11 P0 "后台 Agent Supervisor" 补 rm 动词). Terminal
|
|
229
|
+
* sessions (completed/failed/stopped/lost) remove directly; a still-running
|
|
230
|
+
* one is refused unless `force`, which stops it first. The underlying JSONL
|
|
231
|
+
* conversation session is NOT touched — that's `cc session delete`.
|
|
232
|
+
*/
|
|
233
|
+
export function removeBackgroundAgent(id, options = {}) {
|
|
234
|
+
const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
235
|
+
now: options.now,
|
|
236
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
237
|
+
});
|
|
238
|
+
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
239
|
+
if (state.status === "running") {
|
|
240
|
+
if (options.force !== true) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`${id} is still running — stop it first (cc daemon stop ${id}) or pass --force`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
stopBackgroundAgent(id);
|
|
247
|
+
} catch {
|
|
248
|
+
/* best-effort — the record removal below is the point of rm --force */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
rmSync(statePath(id), { force: true });
|
|
252
|
+
if (options.keepLog !== true) {
|
|
253
|
+
rmSync(logPath(id), { force: true });
|
|
254
|
+
}
|
|
255
|
+
return { id, removed: true, status: state.status };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Pin/unpin a session for the dashboard (`cc daemon view`) — pinned sessions
|
|
260
|
+
* sort first inside their group. Same read-modify-write + verify-retry dance
|
|
261
|
+
* as rename: the worker's periodic state merges can clobber a write that
|
|
262
|
+
* lands inside its read→write window.
|
|
263
|
+
*/
|
|
264
|
+
export function setBackgroundAgentPinned(id, pinned, options = {}) {
|
|
265
|
+
const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
266
|
+
now: options.now,
|
|
267
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
268
|
+
});
|
|
269
|
+
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
270
|
+
const value = pinned === true;
|
|
271
|
+
const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
|
|
272
|
+
let next = { ...state, pinned: value, pinnedAt };
|
|
273
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
274
|
+
writeBackgroundAgentState(next);
|
|
275
|
+
sleepSyncMs(15);
|
|
276
|
+
const current = readBackgroundAgentState(id) || next;
|
|
277
|
+
if (current.pinned === value) {
|
|
278
|
+
return { ...current, pinned: value, pinnedAt };
|
|
279
|
+
}
|
|
280
|
+
next = { ...current, pinned: value, pinnedAt };
|
|
281
|
+
}
|
|
282
|
+
writeBackgroundAgentState(next);
|
|
283
|
+
return next;
|
|
284
|
+
}
|
|
285
|
+
|
|
180
286
|
/**
|
|
181
287
|
* Build the follow-up argv template for interactive attach turns: the launch
|
|
182
288
|
* argv minus every token that carried the FIRST turn's prompt (positional
|
|
@@ -258,6 +364,33 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
258
364
|
});
|
|
259
365
|
}
|
|
260
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Fail fast on an unusable cwd. Without this, spawn() surfaces a bad cwd as
|
|
369
|
+
* an ASYNC 'error' event on the detached child — which nothing listened to
|
|
370
|
+
* (uncaught exception with no context) — and the pre-written state/job files
|
|
371
|
+
* stayed behind as a phantom "running" session. Deleted, file-replaced, and
|
|
372
|
+
* unmounted paths all land here with one clear message.
|
|
373
|
+
*/
|
|
374
|
+
function assertUsableCwd(cwd) {
|
|
375
|
+
let reason = null;
|
|
376
|
+
if (!cwd) {
|
|
377
|
+
reason = "no working directory given";
|
|
378
|
+
} else if (!existsSync(cwd)) {
|
|
379
|
+
reason = "directory does not exist (deleted or unmounted?)";
|
|
380
|
+
} else {
|
|
381
|
+
try {
|
|
382
|
+
if (!statSync(cwd).isDirectory()) reason = "path is not a directory";
|
|
383
|
+
} catch (err) {
|
|
384
|
+
reason = `directory is not accessible (${err.code || err.message})`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (reason) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Cannot launch background agent: cwd "${cwd ?? ""}" — ${reason}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
261
394
|
export function launchBackgroundAgent({
|
|
262
395
|
argv,
|
|
263
396
|
cwd,
|
|
@@ -266,6 +399,7 @@ export function launchBackgroundAgent({
|
|
|
266
399
|
cliEntry,
|
|
267
400
|
followUpArgv,
|
|
268
401
|
}) {
|
|
402
|
+
assertUsableCwd(cwd);
|
|
269
403
|
const id = createBackgroundAgentId();
|
|
270
404
|
const dir = backgroundAgentsDir();
|
|
271
405
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
@@ -317,6 +451,28 @@ export function launchBackgroundAgent({
|
|
|
317
451
|
rmSync(statePath(id), { force: true });
|
|
318
452
|
throw error;
|
|
319
453
|
}
|
|
454
|
+
// Async spawn failures (EPERM, cwd raced away between the check and the
|
|
455
|
+
// spawn, …) arrive as an 'error' event on the detached child. Reap them
|
|
456
|
+
// into the state file instead of leaving a phantom "running" session and
|
|
457
|
+
// an uncaught exception.
|
|
458
|
+
if (typeof child.on === "function") {
|
|
459
|
+
child.on("error", (error) => {
|
|
460
|
+
try {
|
|
461
|
+
rmSync(jobFile, { force: true });
|
|
462
|
+
const current = readBackgroundAgentState(id);
|
|
463
|
+
if (current && current.status === "running") {
|
|
464
|
+
writeBackgroundAgentState({
|
|
465
|
+
...current,
|
|
466
|
+
status: "failed",
|
|
467
|
+
endedAt: Date.now(),
|
|
468
|
+
lostReason: `spawn-error: ${error.code || error.message}`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
/* best-effort */
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
320
476
|
child.unref();
|
|
321
477
|
const current = readBackgroundAgentState(id) || state;
|
|
322
478
|
const withPid = {
|