chainlesschain 0.162.188 → 0.162.193
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 +61 -11
- package/bin/chainlesschain.js +7 -52
- package/completions/_cc +209 -0
- package/completions/cc.bash +18 -0
- package/completions/cc.fish +13 -0
- package/completions/cc.ps1 +226 -0
- package/package.json +10 -2
- package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
- package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
- package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
- package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
- package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
- package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
- package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
- package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
- package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
- package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
- package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
- package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/cli-help.js +181 -0
- package/src/command-help-index.json +181 -0
- package/src/command-manifest.json +1107 -175
- package/src/command-surface-policy.js +197 -0
- package/src/commands/agent.js +98 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +950 -22
- package/src/commands/compact.js +101 -25
- package/src/commands/config.js +506 -20
- package/src/commands/doctor.js +4 -1
- package/src/commands/hub.js +1 -1
- package/src/commands/mcp.js +688 -24
- package/src/commands/pack.js +16 -2
- package/src/commands/plugin.js +20 -12
- package/src/commands/session-mcp-recovery.js +218 -0
- package/src/commands/session.js +470 -110
- package/src/commands/setup.js +36 -10
- package/src/commands/skill.js +181 -110
- package/src/commands/status.js +21 -5
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/commands/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/action-protocol.js +10 -0
- package/src/gateways/ws/session-protocol.js +555 -27
- package/src/gateways/ws/ws-agent-handler.js +541 -14
- package/src/gateways/ws/ws-session-gateway.js +129 -22
- package/src/gateways/ws/ws-session-state.js +431 -0
- package/src/harness/background-task-manager.js +233 -36
- package/src/harness/jsonl-session-store.js +2551 -167
- package/src/harness/mcp-client.js +802 -61
- package/src/harness/prompt-compressor.js +227 -27
- package/src/harness/session-index.js +89 -38
- package/src/harness/session-list-index.js +254 -0
- package/src/harness/structured-handoff.js +410 -0
- package/src/lazy-dispatch.js +747 -84
- package/src/lib/__tests__/logger.test.js +50 -2
- package/src/lib/advisor-runtime.js +1047 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +543 -24
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/artifact-store.js +131 -8
- package/src/lib/background-agent-supervisor.js +185 -13
- package/src/lib/background-interaction-journal.js +37 -9
- package/src/lib/background-launch-profile.js +1058 -0
- package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
- package/src/lib/checkpoint-restore-orchestrator.js +987 -0
- package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
- package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
- package/src/lib/checkpoint-restore-recovery.js +884 -0
- package/src/lib/checkpoint-restore-retention.js +126 -0
- package/src/lib/checkpoint-restore-saga.js +5079 -0
- package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
- package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
- package/src/lib/checkpoint-store.js +1688 -97
- package/src/lib/checkpoint-timeline-authority.js +522 -0
- package/src/lib/checkpoint-timeline.js +642 -0
- package/src/lib/config-keys.js +18 -119
- package/src/lib/config-manager.js +446 -27
- package/src/lib/config-redaction.js +57 -0
- package/src/lib/config-schema.js +853 -0
- package/src/lib/cowork-mcp-tools.js +5 -0
- package/src/lib/cowork-task-runner.js +458 -63
- package/src/lib/delivery-coordinator.js +1234 -0
- package/src/lib/delivery-evidence.js +450 -0
- package/src/lib/doctor-checkup.js +199 -14
- package/src/lib/executable-path.js +34 -0
- package/src/lib/fatal-handler.js +16 -0
- package/src/lib/file-checkpoint.js +4909 -186
- package/src/lib/file-lines.js +125 -0
- package/src/lib/headless-config-command.js +16 -5
- package/src/lib/hook-runner.cjs +139 -38
- package/src/lib/hooks-v2-workspace-context.js +10 -0
- package/src/lib/impacted-gate-selector.js +424 -0
- package/src/lib/interaction-adapter.js +62 -0
- package/src/lib/jsonl-session-store.js +1 -35
- package/src/lib/logger.js +66 -14
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/mcp-call-ledger-store.js +1069 -0
- package/src/lib/mcp-call-ledger.js +747 -0
- package/src/lib/mcp-client.js +5 -0
- package/src/lib/mcp-conflict-scheduler.js +468 -0
- package/src/lib/mcp-effect-contract.js +102 -0
- package/src/lib/mcp-headers-helper-trust.js +145 -0
- package/src/lib/mcp-headers-helper.js +616 -0
- package/src/lib/mcp-host-recovery-runtime.js +131 -0
- package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
- package/src/lib/mcp-recovery-adjudication.js +443 -0
- package/src/lib/output-context.js +86 -0
- package/src/lib/packer/file-identity.js +170 -0
- package/src/lib/packer/index.js +3 -0
- package/src/lib/packer/native-update-state.js +277 -0
- package/src/lib/packer/pack-update-applier.js +1983 -136
- package/src/lib/packer/pack-update-checker.js +34 -0
- package/src/lib/packer/pack-update-downloader.js +875 -86
- package/src/lib/packer/pack-update-signature.js +86 -0
- package/src/lib/packer/pkg-config-generator.js +6 -0
- package/src/lib/paths.js +65 -10
- package/src/lib/plan-mode.js +831 -14
- package/src/lib/plan-persistence.js +540 -0
- package/src/lib/plugin-runtime/hooks.js +227 -69
- package/src/lib/plugin-runtime/plugin-options.js +189 -44
- package/src/lib/pr-automation-policy.js +146 -8
- package/src/lib/process-execution-broker/credential-agent.js +7 -0
- package/src/lib/process-execution-broker/index.js +491 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
- package/src/lib/project-instructions.js +165 -29
- package/src/lib/project-mcp-trust.js +67 -0
- package/src/lib/public-ide-capability-manifest.js +272 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/secure-fs.js +1089 -0
- package/src/lib/security-audit.js +58 -0
- package/src/lib/service-manager.js +34 -13
- package/src/lib/session-budget-runtime.js +1452 -0
- package/src/lib/session-host-snapshot.js +388 -0
- package/src/lib/session-message-provenance.js +429 -0
- package/src/lib/session-projection.js +804 -0
- package/src/lib/session-resource-budget.js +1495 -0
- package/src/lib/settings-hooks.cjs +138 -22
- package/src/lib/settings-loader.cjs +25 -2
- package/src/lib/side-effect-ledger-store.js +45 -23
- package/src/lib/skill-execution-identity.js +217 -0
- package/src/lib/skill-loader.js +299 -18
- package/src/lib/sub-agent-context.js +236 -28
- package/src/lib/sub-agent-registry.js +5 -0
- package/src/lib/subagent-contract.js +94 -0
- package/src/lib/todo-manager.js +185 -18
- package/src/lib/todo-persistence.js +417 -0
- package/src/lib/turn-binding-store.js +41 -23
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +165 -24
- package/src/lib/ws-chat-handler.js +21 -0
- package/src/program-base.js +18 -0
- package/src/repl/advisor-command.js +103 -0
- package/src/repl/agent-repl.js +2175 -146
- package/src/repl/btw-command.js +93 -10
- package/src/repl/clipboard-image.js +119 -0
- package/src/repl/config-summary.js +64 -108
- package/src/repl/prompt-editor.js +233 -0
- package/src/repl/prompt-interactions.js +344 -0
- package/src/repl/prompt-stash.js +354 -0
- package/src/repl/prompt-suggestions.js +312 -0
- package/src/repl/repl-keybindings.js +227 -0
- package/src/repl/session-recap.js +217 -0
- package/src/repl/slash-command-registry.js +22 -0
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1630 -447
- package/src/runtime/coding-agent-contract-shared.cjs +13 -1
- package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
- package/src/runtime/diagnostics.js +32 -13
- package/src/runtime/headless-runner.js +310 -118
- package/src/runtime/headless-stream.js +356 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
package/src/repl/agent-repl.js
CHANGED
|
@@ -22,6 +22,8 @@ import chalk from "chalk";
|
|
|
22
22
|
import fs from "fs";
|
|
23
23
|
import os from "os";
|
|
24
24
|
import path from "path";
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { isPromise, isProxy } from "node:util/types";
|
|
25
27
|
import { logger } from "../lib/logger.js";
|
|
26
28
|
import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
|
|
27
29
|
import { createVimState, feedNormalKey } from "../lib/repl-vim.js";
|
|
@@ -45,15 +47,35 @@ import {
|
|
|
45
47
|
startSession as jsonlStartSession,
|
|
46
48
|
appendUserMessage,
|
|
47
49
|
appendAssistantMessage,
|
|
48
|
-
|
|
50
|
+
appendCompactEventIfMessagesMatch,
|
|
49
51
|
appendTokenUsage,
|
|
50
52
|
appendToolCallCompact,
|
|
51
53
|
appendLlmRetryCompact,
|
|
52
54
|
appendEvent,
|
|
53
|
-
|
|
55
|
+
appendAuthorityEvent,
|
|
56
|
+
isUnsafeSessionId,
|
|
54
57
|
sessionExists,
|
|
58
|
+
forkSession,
|
|
55
59
|
} from "../harness/jsonl-session-store.js";
|
|
56
60
|
import { classifyStreamRetryReason } from "../lib/stream-retry.js";
|
|
61
|
+
import {
|
|
62
|
+
createSessionMcpLedgerSink,
|
|
63
|
+
formatMcpLedgerRecoveryNotice,
|
|
64
|
+
loadMcpLedgerRecovery,
|
|
65
|
+
} from "../lib/mcp-call-ledger-store.js";
|
|
66
|
+
import { createMcpHostRecoveryRuntime } from "../lib/mcp-host-recovery-runtime.js";
|
|
67
|
+
import {
|
|
68
|
+
isVerifiedSessionHostSnapshot,
|
|
69
|
+
readSessionHostResumeState,
|
|
70
|
+
} from "../lib/session-host-snapshot.js";
|
|
71
|
+
import {
|
|
72
|
+
preserveDurableSystemMessageProvenance,
|
|
73
|
+
projectCanonicalResumeMessages,
|
|
74
|
+
} from "../lib/session-message-provenance.js";
|
|
75
|
+
import {
|
|
76
|
+
MCP_RECOVERY_INVALID_CODE,
|
|
77
|
+
classifyMcpRecoveryAdmission,
|
|
78
|
+
} from "../lib/mcp-ledger-recovery-admission.js";
|
|
57
79
|
import { CLISkillLoader } from "../lib/skill-loader.js";
|
|
58
80
|
import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
|
|
59
81
|
import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
|
|
@@ -87,10 +109,11 @@ import {
|
|
|
87
109
|
import { HookEvents } from "../lib/hook-manager.js";
|
|
88
110
|
import { IterationBudget } from "../lib/iteration-budget.js";
|
|
89
111
|
import { resolveAgentMcp } from "../runtime/mcp-config.js";
|
|
90
|
-
import {
|
|
112
|
+
import { mergeConsecutiveMessages } from "../runtime/message-roles.js";
|
|
91
113
|
import {
|
|
92
114
|
AGENT_TOOLS,
|
|
93
115
|
buildSystemPrompt,
|
|
116
|
+
chatWithTools,
|
|
94
117
|
executeTool as coreExecuteTool,
|
|
95
118
|
agentLoop as coreAgentLoop,
|
|
96
119
|
formatToolArgs,
|
|
@@ -111,8 +134,15 @@ import { resolveSlashMacro } from "./slash-macro.js";
|
|
|
111
134
|
import { expandMcpPrompt, renderMcpSurface } from "./mcp-prompt.js";
|
|
112
135
|
import { newCostStore, addUsage } from "./session-cost.js";
|
|
113
136
|
import { extractPluginUsageAttribution } from "../lib/plugin-usage-attribution.js";
|
|
137
|
+
import { formatManagedCheckpointEvent } from "../lib/managed-checkpoint-render.js";
|
|
114
138
|
import { parseThinkCommand, parseEffortCommand } from "./think-command.js";
|
|
115
|
-
import {
|
|
139
|
+
import {
|
|
140
|
+
parseBtwCommand,
|
|
141
|
+
parseNoteNextCommand,
|
|
142
|
+
runBtwQuestion,
|
|
143
|
+
buildAsideBlock,
|
|
144
|
+
applyAside,
|
|
145
|
+
} from "./btw-command.js";
|
|
116
146
|
import { shouldStreamLive } from "./stream-decision.js";
|
|
117
147
|
import { emptyTurnNotice } from "./empty-turn-notice.js";
|
|
118
148
|
import {
|
|
@@ -295,11 +325,11 @@ export async function agentLoop(messages, options) {
|
|
|
295
325
|
// IN PLACE: `coreAgentLoop` mutates this same array (appending assistant/tool
|
|
296
326
|
// turns), and the REPL reuses it across turns, so folding a copy would leave
|
|
297
327
|
// the degenerate `[user, user]` pair in the persistent history and re-break
|
|
298
|
-
// the next turn.
|
|
299
|
-
//
|
|
300
|
-
//
|
|
328
|
+
// the next turn. The tail helper validates the complete raw transcript first
|
|
329
|
+
// and folds only an exact plain `{role, content}` pair, so recovery authority
|
|
330
|
+
// can never disappear during this live one-shot normalization.
|
|
301
331
|
if (options.mergeRoles) {
|
|
302
|
-
|
|
332
|
+
collapseValidatedPlainReplTailInPlace(messages);
|
|
303
333
|
}
|
|
304
334
|
const usageEvents = [];
|
|
305
335
|
// Visible cross-vendor fallback notice: a silent switch from the configured
|
|
@@ -340,7 +370,14 @@ export async function agentLoop(messages, options) {
|
|
|
340
370
|
/* advisory — never break the turn */
|
|
341
371
|
}
|
|
342
372
|
}
|
|
343
|
-
|
|
373
|
+
const managedCheckpointLine = formatManagedCheckpointEvent(event);
|
|
374
|
+
if (managedCheckpointLine) {
|
|
375
|
+
const render =
|
|
376
|
+
event.type === "managed-checkpoint-error" || event.coverage === "none"
|
|
377
|
+
? chalk.yellow
|
|
378
|
+
: chalk.gray;
|
|
379
|
+
process.stdout.write(render(`${managedCheckpointLine}\n`));
|
|
380
|
+
} else if (event.type === "checkpoint") {
|
|
344
381
|
// Remember which file snapshot lines up with the live conversation so
|
|
345
382
|
// `/rewind <n>` can restore code + conversation together (Claude-Code
|
|
346
383
|
// parity). atMessageCount = messages.length at snapshot time; see
|
|
@@ -471,20 +508,1562 @@ export async function agentLoop(messages, options) {
|
|
|
471
508
|
return { content: event.content, usageEvents, thinking: event.thinking };
|
|
472
509
|
}
|
|
473
510
|
}
|
|
474
|
-
return { content: "", usageEvents };
|
|
511
|
+
return { content: "", usageEvents };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const REPL_MCP_RECOVERY_CANDIDATES = new WeakSet();
|
|
515
|
+
const REPL_JSONL_RESUME_CANDIDATES = new WeakSet();
|
|
516
|
+
const REPL_JSONL_RESUME_ABSENCE_CANDIDATES = new WeakSet();
|
|
517
|
+
const REPL_NATIVE_PROMISE = Promise;
|
|
518
|
+
const REPL_NATIVE_PROMISE_PROTOTYPE = Promise.prototype;
|
|
519
|
+
const REPL_NATIVE_PROMISE_THEN = Promise.prototype.then;
|
|
520
|
+
const REPL_NATIVE_PROMISE_CONSTRUCTOR_DESCRIPTOR = Object.freeze({
|
|
521
|
+
...Object.getOwnPropertyDescriptor(Promise.prototype, "constructor"),
|
|
522
|
+
});
|
|
523
|
+
const REPL_NATIVE_PROMISE_SPECIES_DESCRIPTOR = Object.freeze({
|
|
524
|
+
...Object.getOwnPropertyDescriptor(Promise, Symbol.species),
|
|
525
|
+
});
|
|
526
|
+
const REPL_SAFE_PROMISE_CONSTRUCTOR = Object.freeze(
|
|
527
|
+
Object.defineProperty(Object.create(null), Symbol.species, {
|
|
528
|
+
configurable: false,
|
|
529
|
+
enumerable: false,
|
|
530
|
+
writable: false,
|
|
531
|
+
value: null,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
const REPL_NATIVE_PROMISE_REJECTION_HANDLER = () => {};
|
|
535
|
+
const REPL_PAYLOAD_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
536
|
+
const REPL_AUTHORITY_HASH = /^[0-9a-f]{64}$/;
|
|
537
|
+
const REPL_RESUME_SESSION_ID_MAX_BYTES = 128;
|
|
538
|
+
const REPL_RESUME_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
|
|
539
|
+
const REPL_WINDOWS_DEVICE_NAME =
|
|
540
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
541
|
+
const REPL_RESUME_ROLES = new Set(["system", "user", "assistant", "tool"]);
|
|
542
|
+
const REPL_MCP_EFFECTS = new Set(["read", "unknown", "write", "destructive"]);
|
|
543
|
+
const REPL_REPLAY_DENY_FIELDS = new Set([
|
|
544
|
+
"ledgerId",
|
|
545
|
+
"serverName",
|
|
546
|
+
"toolName",
|
|
547
|
+
"inputBytes",
|
|
548
|
+
"replayDigest",
|
|
549
|
+
]);
|
|
550
|
+
const REPL_MCP_REMEDIATIONS = new Set([
|
|
551
|
+
"inspect_transcript",
|
|
552
|
+
"adjudicate_started_calls",
|
|
553
|
+
"exact_replay_denied",
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
function invalidReplRecovery(message, code = MCP_RECOVERY_INVALID_CODE) {
|
|
557
|
+
const error = new TypeError(message);
|
|
558
|
+
error.code = code;
|
|
559
|
+
return error;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function validateReplResumeSessionId(sessionId) {
|
|
563
|
+
if (
|
|
564
|
+
isUnsafeSessionId(sessionId) ||
|
|
565
|
+
Buffer.byteLength(sessionId, "utf8") > REPL_RESUME_SESSION_ID_MAX_BYTES ||
|
|
566
|
+
!REPL_RESUME_SESSION_ID_PATTERN.test(sessionId) ||
|
|
567
|
+
REPL_WINDOWS_DEVICE_NAME.test(sessionId) ||
|
|
568
|
+
sessionId.endsWith(".") ||
|
|
569
|
+
sessionId.normalize("NFC") !== sessionId
|
|
570
|
+
) {
|
|
571
|
+
throw invalidReplRecovery(
|
|
572
|
+
"REPL resume session id is invalid",
|
|
573
|
+
"CC_REPL_SESSION_ID_INVALID",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return sessionId;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function brandedFrozenCandidate(brand, value) {
|
|
580
|
+
const candidate = Object.freeze(value);
|
|
581
|
+
brand.add(candidate);
|
|
582
|
+
return candidate;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function safeRecoveryErrorValue(error, key, fallback) {
|
|
586
|
+
try {
|
|
587
|
+
if (error && !isProxy(error)) {
|
|
588
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
589
|
+
if (descriptor && "value" in descriptor) return descriptor.value;
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
// Malformed errors are diagnostic input, never recovery authority.
|
|
593
|
+
}
|
|
594
|
+
return fallback;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function replMcpRecoveryError(error, fallbackCode) {
|
|
598
|
+
const message = safeRecoveryErrorValue(
|
|
599
|
+
error,
|
|
600
|
+
"message",
|
|
601
|
+
"REPL session recovery failed",
|
|
602
|
+
);
|
|
603
|
+
let cause = new Error("REPL session recovery failed");
|
|
604
|
+
try {
|
|
605
|
+
if (!isProxy(error) && error instanceof Error) cause = error;
|
|
606
|
+
} catch {
|
|
607
|
+
// Keep the inert fallback cause for hostile diagnostic objects.
|
|
608
|
+
}
|
|
609
|
+
const wrapped = new Error(
|
|
610
|
+
typeof message === "string" && message
|
|
611
|
+
? message
|
|
612
|
+
: "REPL session recovery failed",
|
|
613
|
+
{ cause },
|
|
614
|
+
);
|
|
615
|
+
const code = safeRecoveryErrorValue(error, "code", fallbackCode);
|
|
616
|
+
wrapped.code = typeof code === "string" && code ? code : fallbackCode;
|
|
617
|
+
return wrapped;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function samePropertyDescriptor(actual, expected) {
|
|
621
|
+
if (!actual || !expected) return actual === expected;
|
|
622
|
+
if (
|
|
623
|
+
actual.configurable !== expected.configurable ||
|
|
624
|
+
actual.enumerable !== expected.enumerable
|
|
625
|
+
) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if ("value" in expected) {
|
|
629
|
+
return (
|
|
630
|
+
"value" in actual &&
|
|
631
|
+
actual.value === expected.value &&
|
|
632
|
+
actual.writable === expected.writable
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
return (
|
|
636
|
+
!("value" in actual) &&
|
|
637
|
+
actual.get === expected.get &&
|
|
638
|
+
actual.set === expected.set
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function unconsumableNativePromise(subject) {
|
|
643
|
+
return invalidReplRecovery(
|
|
644
|
+
`${subject} is a native Promise with an unsafe non-configurable constructor; ` +
|
|
645
|
+
"its producer must observe the rejection",
|
|
646
|
+
"CC_REPL_NATIVE_PROMISE_UNCONSUMABLE",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function consumeNativeReplPromise(value, subject) {
|
|
651
|
+
const prototype = Object.getPrototypeOf(value);
|
|
652
|
+
if (isProxy(prototype)) throw unconsumableNativePromise(subject);
|
|
653
|
+
|
|
654
|
+
const constructorDescriptor = Object.getOwnPropertyDescriptor(
|
|
655
|
+
value,
|
|
656
|
+
"constructor",
|
|
657
|
+
);
|
|
658
|
+
const intrinsicSpeciesIntact = samePropertyDescriptor(
|
|
659
|
+
Object.getOwnPropertyDescriptor(REPL_NATIVE_PROMISE, Symbol.species),
|
|
660
|
+
REPL_NATIVE_PROMISE_SPECIES_DESCRIPTOR,
|
|
661
|
+
);
|
|
662
|
+
const inheritedIntrinsicConstructor =
|
|
663
|
+
!constructorDescriptor &&
|
|
664
|
+
prototype === REPL_NATIVE_PROMISE_PROTOTYPE &&
|
|
665
|
+
samePropertyDescriptor(
|
|
666
|
+
Object.getOwnPropertyDescriptor(
|
|
667
|
+
REPL_NATIVE_PROMISE_PROTOTYPE,
|
|
668
|
+
"constructor",
|
|
669
|
+
),
|
|
670
|
+
REPL_NATIVE_PROMISE_CONSTRUCTOR_DESCRIPTOR,
|
|
671
|
+
) &&
|
|
672
|
+
intrinsicSpeciesIntact;
|
|
673
|
+
const ownIntrinsicConstructor =
|
|
674
|
+
constructorDescriptor &&
|
|
675
|
+
"value" in constructorDescriptor &&
|
|
676
|
+
(constructorDescriptor.value === undefined ||
|
|
677
|
+
(constructorDescriptor.value === REPL_NATIVE_PROMISE &&
|
|
678
|
+
intrinsicSpeciesIntact));
|
|
679
|
+
|
|
680
|
+
if (inheritedIntrinsicConstructor || ownIntrinsicConstructor) {
|
|
681
|
+
Reflect.apply(REPL_NATIVE_PROMISE_THEN, value, [
|
|
682
|
+
undefined,
|
|
683
|
+
REPL_NATIVE_PROMISE_REJECTION_HANDLER,
|
|
684
|
+
]);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const canShadow = constructorDescriptor
|
|
689
|
+
? constructorDescriptor.configurable === true
|
|
690
|
+
: Object.isExtensible(value);
|
|
691
|
+
if (!canShadow) {
|
|
692
|
+
// Promise.prototype.then necessarily performs SpeciesConstructor, which
|
|
693
|
+
// would execute this hostile constructor/accessor. ECMAScript exposes no
|
|
694
|
+
// separate hook-free operation that marks a rejection handled. Fail closed
|
|
695
|
+
// without pretending the Promise was consumed; the producer must have
|
|
696
|
+
// attached its own rejection observer before returning this invalid value.
|
|
697
|
+
throw unconsumableNativePromise(subject);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
Object.defineProperty(value, "constructor", {
|
|
701
|
+
configurable: true,
|
|
702
|
+
enumerable: constructorDescriptor?.enumerable ?? false,
|
|
703
|
+
writable: false,
|
|
704
|
+
value: REPL_SAFE_PROMISE_CONSTRUCTOR,
|
|
705
|
+
});
|
|
706
|
+
try {
|
|
707
|
+
Reflect.apply(REPL_NATIVE_PROMISE_THEN, value, [
|
|
708
|
+
undefined,
|
|
709
|
+
REPL_NATIVE_PROMISE_REJECTION_HANDLER,
|
|
710
|
+
]);
|
|
711
|
+
} finally {
|
|
712
|
+
if (constructorDescriptor) {
|
|
713
|
+
Object.defineProperty(value, "constructor", constructorDescriptor);
|
|
714
|
+
} else {
|
|
715
|
+
Reflect.deleteProperty(value, "constructor");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function synchronousReplRecoveryValue(value, subject) {
|
|
721
|
+
if (
|
|
722
|
+
value === null ||
|
|
723
|
+
value === undefined ||
|
|
724
|
+
(typeof value !== "object" && typeof value !== "function")
|
|
725
|
+
) {
|
|
726
|
+
return value;
|
|
727
|
+
}
|
|
728
|
+
if (isProxy(value)) {
|
|
729
|
+
throw invalidReplRecovery(`${subject} must not be a Proxy`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Consume a genuine Promise before inspecting any user-controlled `then`
|
|
733
|
+
// descriptor. A Promise can shadow `then` with an accessor or data property;
|
|
734
|
+
// reading that descriptor first would leave an already-rejected Promise
|
|
735
|
+
// unobserved even though the recovery boundary correctly rejected it.
|
|
736
|
+
if (isPromise(value)) {
|
|
737
|
+
consumeNativeReplPromise(value, subject);
|
|
738
|
+
throw invalidReplRecovery(`${subject} must be returned synchronously`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let prototype = value;
|
|
742
|
+
while (prototype) {
|
|
743
|
+
// An ordinary object can hide a Proxy in its prototype chain. Check every
|
|
744
|
+
// level before reflection so no getOwnPropertyDescriptor/getPrototypeOf
|
|
745
|
+
// trap is executed while rejecting untrusted recovery evidence.
|
|
746
|
+
if (isProxy(prototype)) {
|
|
747
|
+
throw invalidReplRecovery(`${subject} must not inherit from a Proxy`);
|
|
748
|
+
}
|
|
749
|
+
const thenDescriptor = Object.getOwnPropertyDescriptor(prototype, "then");
|
|
750
|
+
if (thenDescriptor) {
|
|
751
|
+
if (!("value" in thenDescriptor)) {
|
|
752
|
+
throw invalidReplRecovery(`${subject}.then must not be an accessor`);
|
|
753
|
+
}
|
|
754
|
+
if (typeof thenDescriptor.value === "function") {
|
|
755
|
+
// Never invoke an arbitrary user-supplied thenable capability.
|
|
756
|
+
throw invalidReplRecovery(`${subject} must be returned synchronously`);
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
761
|
+
}
|
|
762
|
+
return value;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function plainDataDescriptors(value, subject) {
|
|
766
|
+
synchronousReplRecoveryValue(value, subject);
|
|
767
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
768
|
+
throw invalidReplRecovery(`${subject} must be a plain data object`);
|
|
769
|
+
}
|
|
770
|
+
const prototype = Object.getPrototypeOf(value);
|
|
771
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
772
|
+
throw invalidReplRecovery(`${subject} must have a plain prototype`);
|
|
773
|
+
}
|
|
774
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
775
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
776
|
+
if (typeof key === "symbol" || !("value" in descriptors[key])) {
|
|
777
|
+
throw invalidReplRecovery(`${subject} contains an accessor or symbol`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return descriptors;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function dataDescriptorValue(
|
|
784
|
+
descriptors,
|
|
785
|
+
key,
|
|
786
|
+
subject,
|
|
787
|
+
{ required = true, fallback = null } = {},
|
|
788
|
+
) {
|
|
789
|
+
const descriptor = descriptors[key];
|
|
790
|
+
if (!descriptor) {
|
|
791
|
+
if (!required) return fallback;
|
|
792
|
+
throw invalidReplRecovery(`${subject}.${key} is missing`);
|
|
793
|
+
}
|
|
794
|
+
if (!("value" in descriptor)) {
|
|
795
|
+
throw invalidReplRecovery(`${subject}.${key} must be a data property`);
|
|
796
|
+
}
|
|
797
|
+
return descriptor.value;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function snapshotDataArray(value, subject, mapper) {
|
|
801
|
+
synchronousReplRecoveryValue(value, subject);
|
|
802
|
+
if (!Array.isArray(value) || isProxy(value)) {
|
|
803
|
+
throw invalidReplRecovery(`${subject} must be a non-Proxy array`);
|
|
804
|
+
}
|
|
805
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
806
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
807
|
+
if (typeof key === "symbol" || !("value" in descriptors[key])) {
|
|
808
|
+
throw invalidReplRecovery(`${subject} contains an accessor or symbol`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const length = descriptors.length?.value;
|
|
812
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
813
|
+
throw invalidReplRecovery(`${subject}.length is invalid`);
|
|
814
|
+
}
|
|
815
|
+
const snapshots = [];
|
|
816
|
+
for (let index = 0; index < length; index += 1) {
|
|
817
|
+
const descriptor = descriptors[String(index)];
|
|
818
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
819
|
+
throw invalidReplRecovery(`${subject}[${index}] is not plain data`);
|
|
820
|
+
}
|
|
821
|
+
snapshots.push(mapper(descriptor.value, index));
|
|
822
|
+
}
|
|
823
|
+
return Object.freeze(snapshots);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function requiredRecoveryString(value, subject, { nullable = false } = {}) {
|
|
827
|
+
if (value === null && nullable) return null;
|
|
828
|
+
if (typeof value !== "string" || (!nullable && !value)) {
|
|
829
|
+
throw invalidReplRecovery(`${subject} must be a string`);
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function snapshotReplMcpRecord(record, index) {
|
|
835
|
+
const subject = `MCP recovery unsettled[${index}]`;
|
|
836
|
+
const descriptors = plainDataDescriptors(record, subject);
|
|
837
|
+
const effectContract = dataDescriptorValue(
|
|
838
|
+
descriptors,
|
|
839
|
+
"effectContract",
|
|
840
|
+
subject,
|
|
841
|
+
);
|
|
842
|
+
const effectDescriptors = plainDataDescriptors(
|
|
843
|
+
effectContract,
|
|
844
|
+
`${subject}.effectContract`,
|
|
845
|
+
);
|
|
846
|
+
const effect = requiredRecoveryString(
|
|
847
|
+
dataDescriptorValue(
|
|
848
|
+
effectDescriptors,
|
|
849
|
+
"effect",
|
|
850
|
+
`${subject}.effectContract`,
|
|
851
|
+
),
|
|
852
|
+
`${subject}.effectContract.effect`,
|
|
853
|
+
);
|
|
854
|
+
if (!REPL_MCP_EFFECTS.has(effect)) {
|
|
855
|
+
throw invalidReplRecovery(`${subject}.effectContract.effect is invalid`);
|
|
856
|
+
}
|
|
857
|
+
return Object.freeze({
|
|
858
|
+
ledgerId: requiredRecoveryString(
|
|
859
|
+
dataDescriptorValue(descriptors, "ledgerId", subject),
|
|
860
|
+
`${subject}.ledgerId`,
|
|
861
|
+
),
|
|
862
|
+
serverName: requiredRecoveryString(
|
|
863
|
+
dataDescriptorValue(descriptors, "serverName", subject),
|
|
864
|
+
`${subject}.serverName`,
|
|
865
|
+
),
|
|
866
|
+
toolName: requiredRecoveryString(
|
|
867
|
+
dataDescriptorValue(descriptors, "toolName", subject),
|
|
868
|
+
`${subject}.toolName`,
|
|
869
|
+
),
|
|
870
|
+
effectContract: Object.freeze({ effect }),
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function snapshotReplMcpIncident(incident, index) {
|
|
875
|
+
const subject = `MCP recovery incidents[${index}]`;
|
|
876
|
+
const descriptors = plainDataDescriptors(incident, subject);
|
|
877
|
+
return Object.freeze({
|
|
878
|
+
code: requiredRecoveryString(
|
|
879
|
+
dataDescriptorValue(descriptors, "code", subject),
|
|
880
|
+
`${subject}.code`,
|
|
881
|
+
),
|
|
882
|
+
ledgerId: requiredRecoveryString(
|
|
883
|
+
dataDescriptorValue(descriptors, "ledgerId", subject, {
|
|
884
|
+
required: false,
|
|
885
|
+
fallback: null,
|
|
886
|
+
}),
|
|
887
|
+
`${subject}.ledgerId`,
|
|
888
|
+
{ nullable: true },
|
|
889
|
+
),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function snapshotReplReplayDeny(entry, index) {
|
|
894
|
+
const subject = `MCP recovery replayDenied[${index}]`;
|
|
895
|
+
const descriptors = plainDataDescriptors(entry, subject);
|
|
896
|
+
const fields = Object.keys(descriptors);
|
|
897
|
+
if (
|
|
898
|
+
fields.length !== REPL_REPLAY_DENY_FIELDS.size ||
|
|
899
|
+
fields.some((field) => !REPL_REPLAY_DENY_FIELDS.has(field))
|
|
900
|
+
) {
|
|
901
|
+
throw invalidReplRecovery(`${subject} has an invalid exact schema`);
|
|
902
|
+
}
|
|
903
|
+
const snapshot = Object.freeze({
|
|
904
|
+
ledgerId: requiredRecoveryString(
|
|
905
|
+
dataDescriptorValue(descriptors, "ledgerId", subject),
|
|
906
|
+
`${subject}.ledgerId`,
|
|
907
|
+
),
|
|
908
|
+
serverName: requiredRecoveryString(
|
|
909
|
+
dataDescriptorValue(descriptors, "serverName", subject),
|
|
910
|
+
`${subject}.serverName`,
|
|
911
|
+
),
|
|
912
|
+
toolName: requiredRecoveryString(
|
|
913
|
+
dataDescriptorValue(descriptors, "toolName", subject),
|
|
914
|
+
`${subject}.toolName`,
|
|
915
|
+
),
|
|
916
|
+
inputBytes: dataDescriptorValue(descriptors, "inputBytes", subject),
|
|
917
|
+
replayDigest: requiredRecoveryString(
|
|
918
|
+
dataDescriptorValue(descriptors, "replayDigest", subject),
|
|
919
|
+
`${subject}.replayDigest`,
|
|
920
|
+
),
|
|
921
|
+
});
|
|
922
|
+
if (
|
|
923
|
+
!Number.isInteger(snapshot.inputBytes) ||
|
|
924
|
+
snapshot.inputBytes < 0 ||
|
|
925
|
+
!REPL_PAYLOAD_DIGEST.test(snapshot.replayDigest)
|
|
926
|
+
) {
|
|
927
|
+
throw invalidReplRecovery(`${subject} has an invalid exact identity`);
|
|
928
|
+
}
|
|
929
|
+
return snapshot;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function strictReplMcpRecoverySnapshot(recovery, sessionId) {
|
|
933
|
+
const descriptors = plainDataDescriptors(recovery, "MCP recovery");
|
|
934
|
+
const recoverySessionId = requiredRecoveryString(
|
|
935
|
+
dataDescriptorValue(descriptors, "sessionId", "MCP recovery"),
|
|
936
|
+
"MCP recovery.sessionId",
|
|
937
|
+
);
|
|
938
|
+
if (recoverySessionId !== sessionId) {
|
|
939
|
+
throw invalidReplRecovery("MCP recovery session does not match the target");
|
|
940
|
+
}
|
|
941
|
+
if (dataDescriptorValue(descriptors, "verified", "MCP recovery") !== true) {
|
|
942
|
+
throw invalidReplRecovery("MCP recovery was not verified");
|
|
943
|
+
}
|
|
944
|
+
const unsettled = snapshotDataArray(
|
|
945
|
+
dataDescriptorValue(descriptors, "unsettled", "MCP recovery"),
|
|
946
|
+
"MCP recovery.unsettled",
|
|
947
|
+
snapshotReplMcpRecord,
|
|
948
|
+
);
|
|
949
|
+
const incidents = snapshotDataArray(
|
|
950
|
+
dataDescriptorValue(descriptors, "incidents", "MCP recovery"),
|
|
951
|
+
"MCP recovery.incidents",
|
|
952
|
+
snapshotReplMcpIncident,
|
|
953
|
+
);
|
|
954
|
+
// Exact-replay denies are authority, not presentation samples. Preserve and
|
|
955
|
+
// freeze every entry: truncation would silently re-enable a denied call.
|
|
956
|
+
const replayDenied = snapshotDataArray(
|
|
957
|
+
dataDescriptorValue(descriptors, "replayDenied", "MCP recovery"),
|
|
958
|
+
"MCP recovery.replayDenied",
|
|
959
|
+
snapshotReplReplayDeny,
|
|
960
|
+
);
|
|
961
|
+
const headHash = requiredRecoveryString(
|
|
962
|
+
dataDescriptorValue(descriptors, "headHash", "MCP recovery"),
|
|
963
|
+
"MCP recovery.headHash",
|
|
964
|
+
{ nullable: true },
|
|
965
|
+
);
|
|
966
|
+
const recoveryDigest = requiredRecoveryString(
|
|
967
|
+
dataDescriptorValue(descriptors, "recoveryDigest", "MCP recovery"),
|
|
968
|
+
"MCP recovery.recoveryDigest",
|
|
969
|
+
);
|
|
970
|
+
const remediation = requiredRecoveryString(
|
|
971
|
+
dataDescriptorValue(descriptors, "remediation", "MCP recovery"),
|
|
972
|
+
"MCP recovery.remediation",
|
|
973
|
+
{ nullable: true },
|
|
974
|
+
);
|
|
975
|
+
if (
|
|
976
|
+
(headHash !== null && !REPL_AUTHORITY_HASH.test(headHash)) ||
|
|
977
|
+
!REPL_PAYLOAD_DIGEST.test(recoveryDigest) ||
|
|
978
|
+
(remediation !== null && !REPL_MCP_REMEDIATIONS.has(remediation))
|
|
979
|
+
) {
|
|
980
|
+
throw invalidReplRecovery("MCP recovery authority metadata is invalid");
|
|
981
|
+
}
|
|
982
|
+
const expectedRemediation =
|
|
983
|
+
incidents.length > 0
|
|
984
|
+
? "inspect_transcript"
|
|
985
|
+
: unsettled.length > 0
|
|
986
|
+
? "adjudicate_started_calls"
|
|
987
|
+
: replayDenied.length > 0
|
|
988
|
+
? "exact_replay_denied"
|
|
989
|
+
: null;
|
|
990
|
+
if (remediation !== expectedRemediation) {
|
|
991
|
+
throw invalidReplRecovery("MCP recovery remediation is inconsistent");
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const snapshot = Object.freeze({
|
|
995
|
+
sessionId: recoverySessionId,
|
|
996
|
+
verified: true,
|
|
997
|
+
unsettled,
|
|
998
|
+
incidents,
|
|
999
|
+
replayDenied,
|
|
1000
|
+
headHash,
|
|
1001
|
+
recoveryDigest,
|
|
1002
|
+
remediation,
|
|
1003
|
+
});
|
|
1004
|
+
const admission = classifyMcpRecoveryAdmission(snapshot);
|
|
1005
|
+
if (admission.reasonCode) {
|
|
1006
|
+
throw invalidReplRecovery(
|
|
1007
|
+
"MCP recovery projection is malformed",
|
|
1008
|
+
admission.reasonCode,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return snapshot;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function dependencyDataFunction(dependencies, key, fallback) {
|
|
1015
|
+
if (dependencies == null) return fallback;
|
|
1016
|
+
if (typeof dependencies !== "object" || isProxy(dependencies)) {
|
|
1017
|
+
throw invalidReplRecovery("REPL recovery dependencies are invalid");
|
|
1018
|
+
}
|
|
1019
|
+
const descriptor = Object.getOwnPropertyDescriptor(dependencies, key);
|
|
1020
|
+
if (!descriptor) return fallback;
|
|
1021
|
+
if (!("value" in descriptor) || typeof descriptor.value !== "function") {
|
|
1022
|
+
throw invalidReplRecovery(`REPL recovery dependency ${key} is invalid`);
|
|
1023
|
+
}
|
|
1024
|
+
return descriptor.value;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function replMcpRecoveryFailureNotice(error, subject = "MCP call ledger") {
|
|
1028
|
+
return (
|
|
1029
|
+
`MCP recovery notice — the durable ${subject} could not be verified. ` +
|
|
1030
|
+
"Do not execute or retry MCP tools until the session transcript has " +
|
|
1031
|
+
`been inspected (${error?.code || "CC_MCP_LEDGER_EVENT_READ_FAILED"}).`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function failClosedReplMcpRecoveryCandidate(
|
|
1036
|
+
sessionId,
|
|
1037
|
+
error,
|
|
1038
|
+
fallbackCode = "CC_MCP_LEDGER_EVENT_READ_FAILED",
|
|
1039
|
+
subject = "MCP call ledger",
|
|
1040
|
+
) {
|
|
1041
|
+
const recoveryError = replMcpRecoveryError(error, fallbackCode);
|
|
1042
|
+
return brandedFrozenCandidate(REPL_MCP_RECOVERY_CANDIDATES, {
|
|
1043
|
+
sessionId,
|
|
1044
|
+
recovery: null,
|
|
1045
|
+
recoveryError,
|
|
1046
|
+
notice: replMcpRecoveryFailureNotice(recoveryError, subject),
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Read one target session's verified MCP recovery projection without mutating
|
|
1052
|
+
* the active REPL. Callers commit the returned candidate only after the rest
|
|
1053
|
+
* of a session switch (notably message rebuild) has succeeded.
|
|
1054
|
+
*/
|
|
1055
|
+
export function readReplMcpRecoveryCandidate(sessionId, dependencies = {}) {
|
|
1056
|
+
try {
|
|
1057
|
+
const loadRecovery = dependencyDataFunction(
|
|
1058
|
+
dependencies,
|
|
1059
|
+
"loadMcpLedgerRecovery",
|
|
1060
|
+
loadMcpLedgerRecovery,
|
|
1061
|
+
);
|
|
1062
|
+
const formatNotice = dependencyDataFunction(
|
|
1063
|
+
dependencies,
|
|
1064
|
+
"formatMcpLedgerRecoveryNotice",
|
|
1065
|
+
formatMcpLedgerRecoveryNotice,
|
|
1066
|
+
);
|
|
1067
|
+
return createReplMcpRecoveryCandidate(
|
|
1068
|
+
sessionId,
|
|
1069
|
+
synchronousReplRecoveryValue(
|
|
1070
|
+
loadRecovery(sessionId),
|
|
1071
|
+
"MCP recovery loader result",
|
|
1072
|
+
),
|
|
1073
|
+
formatNotice,
|
|
1074
|
+
);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return failClosedReplMcpRecoveryCandidate(sessionId, error);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function createReplMcpRecoveryCandidate(
|
|
1081
|
+
sessionId,
|
|
1082
|
+
recoveryValue,
|
|
1083
|
+
formatNotice = formatMcpLedgerRecoveryNotice,
|
|
1084
|
+
) {
|
|
1085
|
+
try {
|
|
1086
|
+
const recovery = strictReplMcpRecoverySnapshot(recoveryValue, sessionId);
|
|
1087
|
+
let notice = synchronousReplRecoveryValue(
|
|
1088
|
+
formatNotice(recovery),
|
|
1089
|
+
"MCP recovery formatter result",
|
|
1090
|
+
);
|
|
1091
|
+
if (notice !== null && typeof notice !== "string") {
|
|
1092
|
+
throw invalidReplRecovery(
|
|
1093
|
+
"MCP recovery formatter must return a string or null",
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
if (
|
|
1097
|
+
!notice &&
|
|
1098
|
+
(recovery.unsettled.length > 0 ||
|
|
1099
|
+
recovery.incidents.length > 0 ||
|
|
1100
|
+
recovery.replayDenied.length > 0)
|
|
1101
|
+
) {
|
|
1102
|
+
notice =
|
|
1103
|
+
"MCP recovery notice — interrupted MCP calls, ledger incidents, " +
|
|
1104
|
+
"or exact replay denies require explicit inspection before replay.";
|
|
1105
|
+
}
|
|
1106
|
+
return brandedFrozenCandidate(REPL_MCP_RECOVERY_CANDIDATES, {
|
|
1107
|
+
sessionId,
|
|
1108
|
+
recovery,
|
|
1109
|
+
recoveryError: null,
|
|
1110
|
+
notice: notice || null,
|
|
1111
|
+
});
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
return failClosedReplMcpRecoveryCandidate(sessionId, error);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function snapshotReplMessageValue(value, subject, ancestors = new WeakSet()) {
|
|
1118
|
+
if (
|
|
1119
|
+
value === null ||
|
|
1120
|
+
value === undefined ||
|
|
1121
|
+
typeof value === "string" ||
|
|
1122
|
+
typeof value === "boolean" ||
|
|
1123
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
1124
|
+
) {
|
|
1125
|
+
return value;
|
|
1126
|
+
}
|
|
1127
|
+
if (!value || typeof value !== "object" || isProxy(value)) {
|
|
1128
|
+
throw invalidReplRecovery(
|
|
1129
|
+
`${subject} contains a non-serializable value`,
|
|
1130
|
+
"CC_REPL_SESSION_REBUILD_FAILED",
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (ancestors.has(value)) {
|
|
1134
|
+
throw invalidReplRecovery(
|
|
1135
|
+
`${subject} contains a cycle`,
|
|
1136
|
+
"CC_REPL_SESSION_REBUILD_FAILED",
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
ancestors.add(value);
|
|
1140
|
+
try {
|
|
1141
|
+
if (Array.isArray(value)) {
|
|
1142
|
+
return snapshotDataArray(value, subject, (item, index) =>
|
|
1143
|
+
snapshotReplMessageValue(item, `${subject}[${index}]`, ancestors),
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const descriptors = plainDataDescriptors(value, subject);
|
|
1147
|
+
const snapshot = {};
|
|
1148
|
+
for (const key of Object.keys(descriptors)) {
|
|
1149
|
+
snapshot[key] = snapshotReplMessageValue(
|
|
1150
|
+
descriptors[key].value,
|
|
1151
|
+
`${subject}.${key}`,
|
|
1152
|
+
ancestors,
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return Object.freeze(snapshot);
|
|
1156
|
+
} finally {
|
|
1157
|
+
ancestors.delete(value);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function snapshotReplMessages(messages) {
|
|
1162
|
+
try {
|
|
1163
|
+
return snapshotDataArray(
|
|
1164
|
+
messages,
|
|
1165
|
+
"REPL session rebuild",
|
|
1166
|
+
(message, index) => {
|
|
1167
|
+
const snapshot = snapshotReplMessageValue(
|
|
1168
|
+
message,
|
|
1169
|
+
`REPL session rebuild[${index}]`,
|
|
1170
|
+
);
|
|
1171
|
+
if (
|
|
1172
|
+
!snapshot ||
|
|
1173
|
+
Array.isArray(snapshot) ||
|
|
1174
|
+
typeof snapshot.role !== "string"
|
|
1175
|
+
) {
|
|
1176
|
+
throw invalidReplRecovery(
|
|
1177
|
+
`REPL session rebuild[${index}] is not a message`,
|
|
1178
|
+
"CC_REPL_SESSION_REBUILD_FAILED",
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
return preserveDurableSystemMessageProvenance(message, snapshot);
|
|
1182
|
+
},
|
|
1183
|
+
);
|
|
1184
|
+
} catch (cause) {
|
|
1185
|
+
const error = invalidReplRecovery(
|
|
1186
|
+
safeRecoveryErrorValue(cause, "message", "REPL session rebuild failed"),
|
|
1187
|
+
"CC_REPL_SESSION_REBUILD_FAILED",
|
|
1188
|
+
);
|
|
1189
|
+
error.cause = cause;
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function canonicalPersistentReplMessages(messages) {
|
|
1195
|
+
return snapshotReplMessages(
|
|
1196
|
+
projectCanonicalResumeMessages(snapshotReplMessages(messages), {
|
|
1197
|
+
strict: true,
|
|
1198
|
+
}),
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Track the exact replay projection this REPL has persisted. Compact writes
|
|
1204
|
+
* compare it with the store's verified active messages under one lock, so a
|
|
1205
|
+
* second host's unseen turn can never be hidden by a local auto/session-end
|
|
1206
|
+
* checkpoint.
|
|
1207
|
+
*/
|
|
1208
|
+
export function createReplCompactPersistence(
|
|
1209
|
+
initialMessages = [],
|
|
1210
|
+
dependencies = {},
|
|
1211
|
+
) {
|
|
1212
|
+
const appendCompact = dependencyDataFunction(
|
|
1213
|
+
dependencies,
|
|
1214
|
+
"appendCompactEventIfMessagesMatch",
|
|
1215
|
+
appendCompactEventIfMessagesMatch,
|
|
1216
|
+
);
|
|
1217
|
+
let persistedMessages = canonicalPersistentReplMessages(initialMessages);
|
|
1218
|
+
const replace = (messages) => {
|
|
1219
|
+
persistedMessages = canonicalPersistentReplMessages(messages);
|
|
1220
|
+
return persistedMessages;
|
|
1221
|
+
};
|
|
1222
|
+
return Object.freeze({
|
|
1223
|
+
record(message) {
|
|
1224
|
+
const projected = canonicalPersistentReplMessages([message]);
|
|
1225
|
+
if (projected.length !== 1) {
|
|
1226
|
+
throw new TypeError("Persisted REPL message is not canonical");
|
|
1227
|
+
}
|
|
1228
|
+
persistedMessages = Object.freeze([...persistedMessages, projected[0]]);
|
|
1229
|
+
return projected[0];
|
|
1230
|
+
},
|
|
1231
|
+
replace,
|
|
1232
|
+
persist(sessionId, payload) {
|
|
1233
|
+
const canonicalMessages = canonicalPersistentReplMessages(
|
|
1234
|
+
payload?.messages || [],
|
|
1235
|
+
);
|
|
1236
|
+
const canonicalPayload = { ...payload, messages: canonicalMessages };
|
|
1237
|
+
const result = appendCompact(
|
|
1238
|
+
sessionId,
|
|
1239
|
+
canonicalPayload,
|
|
1240
|
+
persistedMessages,
|
|
1241
|
+
);
|
|
1242
|
+
replace(canonicalMessages);
|
|
1243
|
+
return result;
|
|
1244
|
+
},
|
|
1245
|
+
snapshot() {
|
|
1246
|
+
return snapshotReplMessages(persistedMessages);
|
|
1247
|
+
},
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function validateRawReplReplayMessages(rebuiltMessages) {
|
|
1252
|
+
for (let index = 0; index < rebuiltMessages.length; index += 1) {
|
|
1253
|
+
const message = rebuiltMessages[index];
|
|
1254
|
+
if (!REPL_RESUME_ROLES.has(message.role)) {
|
|
1255
|
+
throw invalidReplRecovery(
|
|
1256
|
+
`REPL session message[${index}] has an unsupported role`,
|
|
1257
|
+
"CC_REPL_SESSION_ROLE_INVALID",
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const hasToolCalls = Object.prototype.hasOwnProperty.call(
|
|
1262
|
+
message,
|
|
1263
|
+
"tool_calls",
|
|
1264
|
+
);
|
|
1265
|
+
const hasToolCallId = Object.prototype.hasOwnProperty.call(
|
|
1266
|
+
message,
|
|
1267
|
+
"tool_call_id",
|
|
1268
|
+
);
|
|
1269
|
+
if (message.role !== "assistant" && hasToolCalls) {
|
|
1270
|
+
throw invalidReplRecovery(
|
|
1271
|
+
`REPL session message[${index}] carries tool calls on the wrong role`,
|
|
1272
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
if (message.role !== "tool" && hasToolCallId) {
|
|
1276
|
+
throw invalidReplRecovery(
|
|
1277
|
+
`REPL session message[${index}] carries a tool result id on the wrong role`,
|
|
1278
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (message.role === "tool") {
|
|
1283
|
+
if (
|
|
1284
|
+
!hasToolCallId ||
|
|
1285
|
+
typeof message.tool_call_id !== "string" ||
|
|
1286
|
+
message.tool_call_id.length === 0
|
|
1287
|
+
) {
|
|
1288
|
+
throw invalidReplRecovery(
|
|
1289
|
+
`REPL session tool result[${index}] has an invalid call id`,
|
|
1290
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (message.role !== "assistant" || !hasToolCalls) continue;
|
|
1297
|
+
if (!Array.isArray(message.tool_calls)) {
|
|
1298
|
+
throw invalidReplRecovery(
|
|
1299
|
+
`REPL session assistant message[${index}] has invalid tool calls`,
|
|
1300
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const batchToolCallIds = new Set();
|
|
1304
|
+
for (const toolCall of message.tool_calls) {
|
|
1305
|
+
const toolCallId =
|
|
1306
|
+
toolCall && typeof toolCall === "object" && !Array.isArray(toolCall)
|
|
1307
|
+
? toolCall.id
|
|
1308
|
+
: null;
|
|
1309
|
+
if (
|
|
1310
|
+
typeof toolCallId !== "string" ||
|
|
1311
|
+
toolCallId.length === 0 ||
|
|
1312
|
+
batchToolCallIds.has(toolCallId)
|
|
1313
|
+
) {
|
|
1314
|
+
throw invalidReplRecovery(
|
|
1315
|
+
`REPL session assistant message[${index}] has ambiguous tool call ids`,
|
|
1316
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
batchToolCallIds.add(toolCallId);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function isPlainReplMergeMessage(message) {
|
|
1325
|
+
if (message.role !== "user" && message.role !== "assistant") return false;
|
|
1326
|
+
const keys = Object.keys(message);
|
|
1327
|
+
return (
|
|
1328
|
+
Object.prototype.hasOwnProperty.call(message, "content") &&
|
|
1329
|
+
keys.every((key) => key === "role" || key === "content")
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function mergePlainReplRoleRuns(rebuiltMessages) {
|
|
1334
|
+
const mergedMessages = [];
|
|
1335
|
+
for (const message of rebuiltMessages) {
|
|
1336
|
+
const previous = mergedMessages.at(-1);
|
|
1337
|
+
if (
|
|
1338
|
+
previous &&
|
|
1339
|
+
previous.role === message.role &&
|
|
1340
|
+
isPlainReplMergeMessage(previous) &&
|
|
1341
|
+
isPlainReplMergeMessage(message)
|
|
1342
|
+
) {
|
|
1343
|
+
// The shared role helper is safe only after raw authority validation and
|
|
1344
|
+
// only for exact {role, content} records. This prevents a later record's
|
|
1345
|
+
// tool/identity authority from disappearing behind object spread.
|
|
1346
|
+
mergedMessages[mergedMessages.length - 1] = mergeConsecutiveMessages([
|
|
1347
|
+
previous,
|
|
1348
|
+
message,
|
|
1349
|
+
])[0];
|
|
1350
|
+
} else {
|
|
1351
|
+
mergedMessages.push(message);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return snapshotReplMessages(mergedMessages);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function collapseValidatedPlainReplTailInPlace(messages) {
|
|
1358
|
+
const rawSnapshot = snapshotReplMessages(messages);
|
|
1359
|
+
validateRawReplReplayMessages(rawSnapshot);
|
|
1360
|
+
const previous = rawSnapshot.at(-2);
|
|
1361
|
+
const current = rawSnapshot.at(-1);
|
|
1362
|
+
if (
|
|
1363
|
+
!previous ||
|
|
1364
|
+
previous.role !== current?.role ||
|
|
1365
|
+
!isPlainReplMergeMessage(previous) ||
|
|
1366
|
+
!isPlainReplMergeMessage(current)
|
|
1367
|
+
) {
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
const merged = mergeConsecutiveMessages([previous, current]);
|
|
1371
|
+
if (merged.length !== 1) return false;
|
|
1372
|
+
messages.splice(messages.length - 2, 2, merged[0]);
|
|
1373
|
+
return true;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function normalizeReplReplayMessages(rebuiltMessages) {
|
|
1377
|
+
// Validate the immutable raw snapshot before any normalization. In
|
|
1378
|
+
// particular, never let a same-role merge hide malformed tool authority.
|
|
1379
|
+
validateRawReplReplayMessages(rebuiltMessages);
|
|
1380
|
+
|
|
1381
|
+
// Stable-partition verified canonical systems ahead of conversation. Their
|
|
1382
|
+
// relative order is preserved exactly, without inspecting content. This
|
|
1383
|
+
// keeps a trailing migration/fork marker from separating a dangling user
|
|
1384
|
+
// turn from the next live user turn after providers extract system prompts.
|
|
1385
|
+
const canonicalSystemMessages = snapshotReplMessages(
|
|
1386
|
+
rebuiltMessages.filter((message) => message.role === "system"),
|
|
1387
|
+
);
|
|
1388
|
+
const conversationMessages = mergePlainReplRoleRuns(
|
|
1389
|
+
rebuiltMessages.filter((message) => message.role !== "system"),
|
|
1390
|
+
);
|
|
1391
|
+
let phase = "expect-initial-conversation-message";
|
|
1392
|
+
let pendingToolCallIds = null;
|
|
1393
|
+
|
|
1394
|
+
for (let index = 0; index < conversationMessages.length; index += 1) {
|
|
1395
|
+
const message = conversationMessages[index];
|
|
1396
|
+
const hasToolCalls = Object.prototype.hasOwnProperty.call(
|
|
1397
|
+
message,
|
|
1398
|
+
"tool_calls",
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
if (message.role === "tool") {
|
|
1402
|
+
const toolCallId = message.tool_call_id;
|
|
1403
|
+
if (phase !== "expect-tools" || !pendingToolCallIds?.delete(toolCallId)) {
|
|
1404
|
+
throw invalidReplRecovery(
|
|
1405
|
+
`REPL session tool result[${index}] has no matching pending call`,
|
|
1406
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
if (pendingToolCallIds.size === 0) {
|
|
1410
|
+
pendingToolCallIds = null;
|
|
1411
|
+
phase = "expect-assistant-after-tools";
|
|
1412
|
+
}
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (message.role === "user") {
|
|
1417
|
+
if (
|
|
1418
|
+
phase !== "expect-user" &&
|
|
1419
|
+
phase !== "expect-initial-conversation-message"
|
|
1420
|
+
) {
|
|
1421
|
+
throw invalidReplRecovery(
|
|
1422
|
+
`REPL session user message[${index}] breaks role alternation`,
|
|
1423
|
+
"CC_REPL_SESSION_ROLE_ALTERNATION_INVALID",
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
phase = "expect-assistant";
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (
|
|
1431
|
+
phase !== "expect-assistant" &&
|
|
1432
|
+
phase !== "expect-assistant-after-tools" &&
|
|
1433
|
+
phase !== "expect-initial-conversation-message"
|
|
1434
|
+
) {
|
|
1435
|
+
throw invalidReplRecovery(
|
|
1436
|
+
`REPL session assistant message[${index}] breaks role alternation`,
|
|
1437
|
+
"CC_REPL_SESSION_ROLE_ALTERNATION_INVALID",
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const toolCalls = hasToolCalls ? message.tool_calls : [];
|
|
1442
|
+
if (toolCalls.length === 0) {
|
|
1443
|
+
phase = "expect-user";
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// IDs are unique only within this pending assistant batch. A later, fully
|
|
1448
|
+
// settled fallback round may legitimately reuse a deterministic ID such as
|
|
1449
|
+
// call_<toolName>.
|
|
1450
|
+
pendingToolCallIds = new Set(toolCalls.map((toolCall) => toolCall.id));
|
|
1451
|
+
phase = "expect-tools";
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (phase === "expect-tools" || phase === "expect-assistant-after-tools") {
|
|
1455
|
+
throw invalidReplRecovery(
|
|
1456
|
+
"REPL session ends with an incomplete tool call/result exchange",
|
|
1457
|
+
"CC_REPL_SESSION_TOOL_PAIR_INVALID",
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
return Object.freeze({
|
|
1461
|
+
canonicalSystemMessages,
|
|
1462
|
+
conversationMessages,
|
|
1463
|
+
replayMessages: Object.freeze([
|
|
1464
|
+
...canonicalSystemMessages,
|
|
1465
|
+
...conversationMessages,
|
|
1466
|
+
]),
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function failedReplJsonlResumeCandidate(sessionId, error, sessionSnapshot) {
|
|
1471
|
+
const observedCode = safeRecoveryErrorValue(
|
|
1472
|
+
error,
|
|
1473
|
+
"code",
|
|
1474
|
+
"CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
|
|
1475
|
+
);
|
|
1476
|
+
const resumeError = replMcpRecoveryError(
|
|
1477
|
+
error,
|
|
1478
|
+
typeof observedCode === "string" && observedCode
|
|
1479
|
+
? observedCode
|
|
1480
|
+
: "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
|
|
1481
|
+
);
|
|
1482
|
+
return brandedFrozenCandidate(REPL_JSONL_RESUME_CANDIDATES, {
|
|
1483
|
+
ok: false,
|
|
1484
|
+
sessionId,
|
|
1485
|
+
rebuiltMessages: null,
|
|
1486
|
+
replayMessages: null,
|
|
1487
|
+
canonicalSystemMessages: null,
|
|
1488
|
+
conversationMessages: null,
|
|
1489
|
+
sessionSnapshot: sessionSnapshot || null,
|
|
1490
|
+
mcp: failClosedReplMcpRecoveryCandidate(
|
|
1491
|
+
sessionId,
|
|
1492
|
+
resumeError,
|
|
1493
|
+
resumeError.code,
|
|
1494
|
+
"session transcript",
|
|
1495
|
+
),
|
|
1496
|
+
error: resumeError,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function absentReplJsonlResumeCandidate(sessionId) {
|
|
1501
|
+
const error = new Error(`Session not found: ${sessionId}`);
|
|
1502
|
+
error.code = "CC_REPL_SESSION_NOT_FOUND";
|
|
1503
|
+
const candidate = failedReplJsonlResumeCandidate(sessionId, error, null);
|
|
1504
|
+
REPL_JSONL_RESUME_ABSENCE_CANDIDATES.add(candidate);
|
|
1505
|
+
return candidate;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Prepare a JSONL resume transaction from exactly one fully verified sample.
|
|
1510
|
+
* Messages, the public snapshot, and MCP recovery authority are derived by the
|
|
1511
|
+
* shared reader from that same event array. A present-but-invalid transcript is
|
|
1512
|
+
* an inert structured refusal and cannot be committed into the active REPL.
|
|
1513
|
+
*/
|
|
1514
|
+
export function prepareReplJsonlResumeCandidate(sessionId, dependencies = {}) {
|
|
1515
|
+
let sessionSnapshot = null;
|
|
1516
|
+
try {
|
|
1517
|
+
validateReplResumeSessionId(sessionId);
|
|
1518
|
+
const readResumeState = dependencyDataFunction(
|
|
1519
|
+
dependencies,
|
|
1520
|
+
"readSessionHostResumeState",
|
|
1521
|
+
readSessionHostResumeState,
|
|
1522
|
+
);
|
|
1523
|
+
const formatNotice = dependencyDataFunction(
|
|
1524
|
+
dependencies,
|
|
1525
|
+
"formatMcpLedgerRecoveryNotice",
|
|
1526
|
+
formatMcpLedgerRecoveryNotice,
|
|
1527
|
+
);
|
|
1528
|
+
const state = synchronousReplRecoveryValue(
|
|
1529
|
+
readResumeState(sessionId),
|
|
1530
|
+
"REPL session host resume state",
|
|
1531
|
+
);
|
|
1532
|
+
if (state === null) return absentReplJsonlResumeCandidate(sessionId);
|
|
1533
|
+
const stateDescriptors = plainDataDescriptors(
|
|
1534
|
+
state,
|
|
1535
|
+
"REPL session host resume state",
|
|
1536
|
+
);
|
|
1537
|
+
sessionSnapshot = snapshotReplMessageValue(
|
|
1538
|
+
dataDescriptorValue(
|
|
1539
|
+
stateDescriptors,
|
|
1540
|
+
"snapshot",
|
|
1541
|
+
"REPL session host resume state",
|
|
1542
|
+
),
|
|
1543
|
+
"REPL session host snapshot",
|
|
1544
|
+
);
|
|
1545
|
+
if (
|
|
1546
|
+
!isVerifiedSessionHostSnapshot(sessionSnapshot) ||
|
|
1547
|
+
sessionSnapshot.sessionId !== sessionId
|
|
1548
|
+
) {
|
|
1549
|
+
const error = new Error(
|
|
1550
|
+
"Canonical JSONL session is not fully verified; resume was refused",
|
|
1551
|
+
);
|
|
1552
|
+
error.code = "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED";
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
const rawRebuiltMessages = snapshotReplMessages(
|
|
1556
|
+
dataDescriptorValue(
|
|
1557
|
+
stateDescriptors,
|
|
1558
|
+
"messages",
|
|
1559
|
+
"REPL session host resume state",
|
|
1560
|
+
),
|
|
1561
|
+
);
|
|
1562
|
+
// The injected host seam may supply plain persisted fields. Only the
|
|
1563
|
+
// runtime-only provenance already established by the verified store is
|
|
1564
|
+
// canonical; unmarked host prompts and forged wire tags are removed before
|
|
1565
|
+
// REPL normalization or any side effect.
|
|
1566
|
+
const rebuiltMessages = snapshotReplMessages(
|
|
1567
|
+
projectCanonicalResumeMessages(rawRebuiltMessages),
|
|
1568
|
+
);
|
|
1569
|
+
const normalizedReplay = normalizeReplReplayMessages(rebuiltMessages);
|
|
1570
|
+
const mcp = createReplMcpRecoveryCandidate(
|
|
1571
|
+
sessionId,
|
|
1572
|
+
dataDescriptorValue(
|
|
1573
|
+
stateDescriptors,
|
|
1574
|
+
"recovery",
|
|
1575
|
+
"REPL session host resume state",
|
|
1576
|
+
),
|
|
1577
|
+
formatNotice,
|
|
1578
|
+
);
|
|
1579
|
+
if (
|
|
1580
|
+
mcp.recoveryError ||
|
|
1581
|
+
mcp.recovery.headHash !== sessionSnapshot.head.hash ||
|
|
1582
|
+
mcp.recovery.recoveryDigest !==
|
|
1583
|
+
sessionSnapshot.recoveryAuthority.recoveryDigest
|
|
1584
|
+
) {
|
|
1585
|
+
const error = new Error(
|
|
1586
|
+
"Canonical JSONL messages and MCP recovery authority do not share one head",
|
|
1587
|
+
);
|
|
1588
|
+
error.code = "CC_REPL_SESSION_AUTHORITY_MISMATCH";
|
|
1589
|
+
throw error;
|
|
1590
|
+
}
|
|
1591
|
+
return brandedFrozenCandidate(REPL_JSONL_RESUME_CANDIDATES, {
|
|
1592
|
+
ok: true,
|
|
1593
|
+
sessionId,
|
|
1594
|
+
rebuiltMessages,
|
|
1595
|
+
replayMessages: normalizedReplay.replayMessages,
|
|
1596
|
+
canonicalSystemMessages: normalizedReplay.canonicalSystemMessages,
|
|
1597
|
+
conversationMessages: normalizedReplay.conversationMessages,
|
|
1598
|
+
sessionSnapshot,
|
|
1599
|
+
mcp,
|
|
1600
|
+
error: null,
|
|
1601
|
+
});
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
return failedReplJsonlResumeCandidate(sessionId, error, sessionSnapshot);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Resolve startup storage only after a requested canonical JSONL session has
|
|
1609
|
+
* been sampled and verified. A present canonical session is authoritative;
|
|
1610
|
+
* feature/config reads are reserved for new sessions or the legacy DB
|
|
1611
|
+
* fallback when no JSONL transcript exists.
|
|
1612
|
+
*/
|
|
1613
|
+
export function prepareReplStartupResume(sessionId, dependencies = {}) {
|
|
1614
|
+
if (sessionId !== null && sessionId !== undefined) {
|
|
1615
|
+
const candidate = prepareReplJsonlResumeCandidate(sessionId, dependencies);
|
|
1616
|
+
if (candidate.ok) {
|
|
1617
|
+
return Object.freeze({ useJsonl: true, candidate });
|
|
1618
|
+
}
|
|
1619
|
+
if (!REPL_JSONL_RESUME_ABSENCE_CANDIDATES.has(candidate)) {
|
|
1620
|
+
return Object.freeze({ useJsonl: true, candidate });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const readFeature = dependencyDataFunction(dependencies, "feature", feature);
|
|
1625
|
+
return Object.freeze({
|
|
1626
|
+
useJsonl: Boolean(readFeature("JSONL_SESSION")),
|
|
1627
|
+
candidate: null,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function assertReplStartupAdmission(value) {
|
|
1632
|
+
synchronousReplRecoveryValue(value, "REPL startup admission");
|
|
1633
|
+
const descriptors = plainDataDescriptors(value, "REPL startup admission");
|
|
1634
|
+
const useJsonl = dataDescriptorValue(
|
|
1635
|
+
descriptors,
|
|
1636
|
+
"useJsonl",
|
|
1637
|
+
"REPL startup admission",
|
|
1638
|
+
);
|
|
1639
|
+
const candidate = dataDescriptorValue(
|
|
1640
|
+
descriptors,
|
|
1641
|
+
"candidate",
|
|
1642
|
+
"REPL startup admission",
|
|
1643
|
+
);
|
|
1644
|
+
if (
|
|
1645
|
+
typeof useJsonl !== "boolean" ||
|
|
1646
|
+
!Object.isFrozen(value) ||
|
|
1647
|
+
(candidate !== null &&
|
|
1648
|
+
(!REPL_JSONL_RESUME_CANDIDATES.has(candidate) ||
|
|
1649
|
+
!Object.isFrozen(candidate))) ||
|
|
1650
|
+
(candidate !== null && useJsonl !== true)
|
|
1651
|
+
) {
|
|
1652
|
+
throw invalidReplRecovery(
|
|
1653
|
+
"REPL startup admission capability is invalid",
|
|
1654
|
+
"CC_REPL_STARTUP_ADMISSION_INVALID",
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
return value;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function refuseReplStartupResume(options, candidate) {
|
|
1661
|
+
const observedCode = safeRecoveryErrorValue(
|
|
1662
|
+
candidate?.error,
|
|
1663
|
+
"code",
|
|
1664
|
+
"CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
|
|
1665
|
+
);
|
|
1666
|
+
const refusal = Object.freeze({
|
|
1667
|
+
started: false,
|
|
1668
|
+
exitCode: 1,
|
|
1669
|
+
code:
|
|
1670
|
+
typeof observedCode === "string" && observedCode
|
|
1671
|
+
? observedCode
|
|
1672
|
+
: "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
|
|
1673
|
+
sessionId: options.sessionId,
|
|
1674
|
+
sessionSnapshot: candidate?.sessionSnapshot || null,
|
|
1675
|
+
});
|
|
1676
|
+
logger.error(
|
|
1677
|
+
JSON.stringify({
|
|
1678
|
+
type: "session.resume.refused",
|
|
1679
|
+
...refusal,
|
|
1680
|
+
}),
|
|
1681
|
+
);
|
|
1682
|
+
process.exitCode = 1;
|
|
1683
|
+
return refusal;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function commitPreparedReplResume(prepared, commit, rollback) {
|
|
1687
|
+
if (!prepared || typeof commit !== "function") return false;
|
|
1688
|
+
try {
|
|
1689
|
+
commit(prepared);
|
|
1690
|
+
return true;
|
|
1691
|
+
} catch (commitError) {
|
|
1692
|
+
if (typeof rollback === "function") {
|
|
1693
|
+
try {
|
|
1694
|
+
rollback(prepared, commitError);
|
|
1695
|
+
} catch (rollbackError) {
|
|
1696
|
+
const error = new AggregateError(
|
|
1697
|
+
[commitError, rollbackError],
|
|
1698
|
+
"REPL session resume and rollback both failed",
|
|
1699
|
+
);
|
|
1700
|
+
error.code = "CC_REPL_SESSION_RESUME_ROLLBACK_FAILED";
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
throw commitError;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/** Commit a fully prepared JSONL switch; forged/failed candidates are inert. */
|
|
1709
|
+
export function commitPreparedReplJsonlResume(candidate, commit, rollback) {
|
|
1710
|
+
if (
|
|
1711
|
+
!candidate ||
|
|
1712
|
+
!REPL_JSONL_RESUME_CANDIDATES.has(candidate) ||
|
|
1713
|
+
!candidate.ok
|
|
1714
|
+
) {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
return commitPreparedReplResume(candidate, commit, rollback);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/** Commit the already parsed and snapshotted legacy-DB resume state. */
|
|
1721
|
+
export function commitPreparedReplDbResume(preparedState, commit, rollback) {
|
|
1722
|
+
return commitPreparedReplResume(preparedState, commit, rollback);
|
|
475
1723
|
}
|
|
476
1724
|
|
|
477
1725
|
/**
|
|
478
|
-
*
|
|
1726
|
+
* Own the exact mutable state switched by the interactive `/session resume`
|
|
1727
|
+
* command. Both JSONL and legacy-DB paths use this controller, which also gives
|
|
1728
|
+
* tests a behavioral seam for failures at each real apply stage.
|
|
479
1729
|
*/
|
|
480
|
-
export
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
1730
|
+
export function createReplResumeStateController(bindings) {
|
|
1731
|
+
const snapshots = new WeakSet();
|
|
1732
|
+
const hostSystemMessagePrefixes = new WeakSet();
|
|
1733
|
+
let registeredHostSystemMessages = null;
|
|
1734
|
+
const replaceArray = (target, values) => {
|
|
1735
|
+
target.length = 0;
|
|
1736
|
+
for (const value of values) target.push(value);
|
|
1737
|
+
};
|
|
1738
|
+
const invalidHostPrefix = (message) =>
|
|
1739
|
+
invalidReplRecovery(message, "CC_REPL_HOST_SYSTEM_PREFIX_INVALID");
|
|
1740
|
+
const assertHostSystemMessages = (candidate) => {
|
|
1741
|
+
if (
|
|
1742
|
+
!candidate ||
|
|
1743
|
+
!hostSystemMessagePrefixes.has(candidate) ||
|
|
1744
|
+
!Array.isArray(candidate) ||
|
|
1745
|
+
!Object.isFrozen(candidate) ||
|
|
1746
|
+
candidate.length === 0 ||
|
|
1747
|
+
candidate.some(
|
|
1748
|
+
(message) =>
|
|
1749
|
+
!message || message.role !== "system" || !Object.isFrozen(message),
|
|
1750
|
+
)
|
|
1751
|
+
) {
|
|
1752
|
+
throw invalidHostPrefix("REPL host system prefix capability is invalid");
|
|
1753
|
+
}
|
|
1754
|
+
return candidate;
|
|
1755
|
+
};
|
|
1756
|
+
const createHostSystemMessages = (sourceMessages) => {
|
|
1757
|
+
const snapshot = snapshotReplMessages(sourceMessages);
|
|
1758
|
+
const leadingSystems = [];
|
|
1759
|
+
for (const message of snapshot) {
|
|
1760
|
+
if (message.role !== "system") break;
|
|
1761
|
+
leadingSystems.push(message);
|
|
1762
|
+
}
|
|
1763
|
+
if (leadingSystems.length === 0) {
|
|
1764
|
+
throw invalidHostPrefix("REPL host system prefix is empty");
|
|
1765
|
+
}
|
|
1766
|
+
const capability = Object.freeze(leadingSystems);
|
|
1767
|
+
hostSystemMessagePrefixes.add(capability);
|
|
1768
|
+
return capability;
|
|
1769
|
+
};
|
|
1770
|
+
const registerHostSystemMessages = () => {
|
|
1771
|
+
if (registeredHostSystemMessages !== null) {
|
|
1772
|
+
throw invalidHostPrefix("REPL host system prefix is already registered");
|
|
1773
|
+
}
|
|
1774
|
+
registeredHostSystemMessages = createHostSystemMessages(bindings.messages);
|
|
1775
|
+
return registeredHostSystemMessages;
|
|
1776
|
+
};
|
|
1777
|
+
const refreshHostSystemMessages = () => {
|
|
1778
|
+
const registered = assertHostSystemMessages(registeredHostSystemMessages);
|
|
1779
|
+
const currentBase = snapshotReplMessages([bindings.messages[0]])[0];
|
|
1780
|
+
if (currentBase.role !== "system") {
|
|
1781
|
+
throw invalidHostPrefix("REPL current host base system is invalid");
|
|
1782
|
+
}
|
|
1783
|
+
const capability = Object.freeze([currentBase, ...registered.slice(1)]);
|
|
1784
|
+
hostSystemMessagePrefixes.add(capability);
|
|
1785
|
+
registeredHostSystemMessages = capability;
|
|
1786
|
+
return capability;
|
|
1787
|
+
};
|
|
1788
|
+
const materializeHostSystemMessages = (candidate) =>
|
|
1789
|
+
assertHostSystemMessages(candidate).map((message) => ({ ...message }));
|
|
1790
|
+
|
|
1791
|
+
const capture = () => {
|
|
1792
|
+
const hostSystemMessages = assertHostSystemMessages(
|
|
1793
|
+
registeredHostSystemMessages,
|
|
1794
|
+
);
|
|
1795
|
+
const mcpRuntime = bindings.runtimeManager.current;
|
|
1796
|
+
if (!mcpRuntime) {
|
|
1797
|
+
const error = new Error("Active MCP recovery runtime is unavailable");
|
|
1798
|
+
error.code = "CC_REPL_MCP_RUNTIME_UNAVAILABLE";
|
|
1799
|
+
throw error;
|
|
1800
|
+
}
|
|
1801
|
+
const snapshot = Object.freeze({
|
|
1802
|
+
sessionId: bindings.sessionId,
|
|
1803
|
+
messages: Object.freeze(bindings.messages.slice()),
|
|
1804
|
+
recovery: bindings.recovery,
|
|
1805
|
+
recoveryError: bindings.recoveryError,
|
|
1806
|
+
sanitizeRolesNextTurn: bindings.sanitizeRolesNextTurn,
|
|
1807
|
+
turnBindingProducer: bindings.turnBindingProducer,
|
|
1808
|
+
turnBindingCriticalError: bindings.turnBindingCriticalError,
|
|
1809
|
+
checkpointMarks: Object.freeze(bindings.checkpointMarks.slice()),
|
|
1810
|
+
clearedConversation: bindings.clearedConversation,
|
|
1811
|
+
hostSystemMessages,
|
|
1812
|
+
mcpRuntime,
|
|
1813
|
+
});
|
|
1814
|
+
snapshots.add(snapshot);
|
|
1815
|
+
return snapshot;
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
const restore = (snapshot) => {
|
|
1819
|
+
if (!snapshot || !snapshots.has(snapshot)) {
|
|
1820
|
+
throw new TypeError("REPL resume snapshot is invalid");
|
|
1821
|
+
}
|
|
1822
|
+
bindings.sessionId = snapshot.sessionId;
|
|
1823
|
+
replaceArray(bindings.messages, snapshot.messages);
|
|
1824
|
+
bindings.recovery = snapshot.recovery;
|
|
1825
|
+
bindings.recoveryError = snapshot.recoveryError;
|
|
1826
|
+
bindings.sanitizeRolesNextTurn = snapshot.sanitizeRolesNextTurn;
|
|
1827
|
+
bindings.turnBindingProducer = snapshot.turnBindingProducer;
|
|
1828
|
+
bindings.turnBindingCriticalError = snapshot.turnBindingCriticalError;
|
|
1829
|
+
replaceArray(bindings.checkpointMarks, snapshot.checkpointMarks);
|
|
1830
|
+
bindings.clearedConversation = snapshot.clearedConversation;
|
|
1831
|
+
registeredHostSystemMessages = assertHostSystemMessages(
|
|
1832
|
+
snapshot.hostSystemMessages,
|
|
1833
|
+
);
|
|
1834
|
+
if (bindings.runtimeManager.current !== snapshot.mcpRuntime) {
|
|
1835
|
+
bindings.runtimeManager.commit(snapshot.mcpRuntime);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
const apply = (preparedState) => {
|
|
1840
|
+
const hostSystemMessages = assertHostSystemMessages(
|
|
1841
|
+
preparedState.hostSystemMessages,
|
|
1842
|
+
);
|
|
1843
|
+
const activeHostSystemMessages =
|
|
1844
|
+
materializeHostSystemMessages(hostSystemMessages);
|
|
1845
|
+
const canonicalSystemMessages = snapshotReplMessages(
|
|
1846
|
+
preparedState.canonicalSystemMessages,
|
|
1847
|
+
);
|
|
1848
|
+
const conversationMessages = snapshotReplMessages(
|
|
1849
|
+
preparedState.conversationMessages,
|
|
1850
|
+
);
|
|
1851
|
+
if (
|
|
1852
|
+
canonicalSystemMessages.some((message) => message.role !== "system") ||
|
|
1853
|
+
conversationMessages.some((message) => message.role === "system")
|
|
1854
|
+
) {
|
|
1855
|
+
throw invalidHostPrefix(
|
|
1856
|
+
"REPL prepared resume message partition is invalid",
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
bindings.sessionId = preparedState.sessionId;
|
|
1860
|
+
replaceArray(bindings.messages, activeHostSystemMessages);
|
|
1861
|
+
registeredHostSystemMessages = hostSystemMessages;
|
|
1862
|
+
for (const message of canonicalSystemMessages) {
|
|
1863
|
+
bindings.messages.push(message);
|
|
1864
|
+
}
|
|
1865
|
+
bindings.applyMcpRecoveryCommit(bindings.messages, preparedState.mcpCommit);
|
|
1866
|
+
for (const message of conversationMessages) {
|
|
1867
|
+
bindings.messages.push(message);
|
|
1868
|
+
}
|
|
1869
|
+
bindings.sanitizeRolesNextTurn = preparedState.sanitizeRolesNextTurn;
|
|
1870
|
+
bindings.turnBindingProducer = null;
|
|
1871
|
+
bindings.turnBindingCriticalError = null;
|
|
1872
|
+
bindings.checkpointMarks.length = 0;
|
|
1873
|
+
bindings.clearedConversation = null;
|
|
1874
|
+
bindings.runtimeManager.commit(preparedState.mcpRuntime);
|
|
1875
|
+
bindings.logMcpRecoveryCommit(preparedState.mcpCommit);
|
|
1876
|
+
bindings.logger.info(preparedState.logMessage);
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
return Object.freeze({
|
|
1880
|
+
capture,
|
|
1881
|
+
restore,
|
|
1882
|
+
apply,
|
|
1883
|
+
registerHostSystemMessages,
|
|
1884
|
+
refreshHostSystemMessages,
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Own the single recovery runtime for the active REPL session/client pair.
|
|
1890
|
+
* Session switches are prepared without mutating the active runtime, then
|
|
1891
|
+
* committed only after the transcript switch succeeds.
|
|
1892
|
+
*/
|
|
1893
|
+
export function createReplMcpHostRuntimeManager(dependencies = {}) {
|
|
1894
|
+
const createRuntime =
|
|
1895
|
+
dependencies.createMcpHostRecoveryRuntime || createMcpHostRecoveryRuntime;
|
|
1896
|
+
const createLedgerSink =
|
|
1897
|
+
dependencies.createSessionMcpLedgerSink || createSessionMcpLedgerSink;
|
|
1898
|
+
const appendLedgerEvent =
|
|
1899
|
+
dependencies.appendAuthorityEvent || appendAuthorityEvent;
|
|
1900
|
+
const candidates = new WeakSet();
|
|
1901
|
+
let current = null;
|
|
1902
|
+
|
|
1903
|
+
const prepare = ({
|
|
1904
|
+
adhocMcp = null,
|
|
1905
|
+
bundleMcpClient = null,
|
|
1906
|
+
sessionId = null,
|
|
1907
|
+
persistent = false,
|
|
1908
|
+
recovery = null,
|
|
1909
|
+
recoveryError = null,
|
|
1910
|
+
verifiedRecovery = false,
|
|
1911
|
+
} = {}) => {
|
|
1912
|
+
const bundle = adhocMcp?.mcpClient
|
|
1913
|
+
? adhocMcp
|
|
1914
|
+
: bundleMcpClient
|
|
1915
|
+
? { mcpClient: bundleMcpClient }
|
|
1916
|
+
: null;
|
|
1917
|
+
const rawClient = bundle?.mcpClient || null;
|
|
1918
|
+
const persistLedger = Boolean(persistent && sessionId);
|
|
1919
|
+
|
|
1920
|
+
if (
|
|
1921
|
+
!verifiedRecovery &&
|
|
1922
|
+
current &&
|
|
1923
|
+
current.rawClient === rawClient &&
|
|
1924
|
+
current.sessionId === sessionId &&
|
|
1925
|
+
current.persistent === persistLedger &&
|
|
1926
|
+
current.recovery === recovery &&
|
|
1927
|
+
current.recoveryError === recoveryError
|
|
1928
|
+
) {
|
|
1929
|
+
return current;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const sink = persistLedger
|
|
1933
|
+
? createLedgerSink(sessionId, { appendEvent: appendLedgerEvent })
|
|
1934
|
+
: null;
|
|
1935
|
+
const sharedController =
|
|
1936
|
+
!verifiedRecovery && current?.sessionId === sessionId
|
|
1937
|
+
? current.runtime.controller
|
|
1938
|
+
: null;
|
|
1939
|
+
const runtime = createRuntime({
|
|
1940
|
+
bundle,
|
|
1941
|
+
rawClient,
|
|
1942
|
+
sessionId,
|
|
1943
|
+
sink,
|
|
1944
|
+
recovery,
|
|
1945
|
+
recoveryError,
|
|
1946
|
+
...(sharedController ? { controller: sharedController } : {}),
|
|
1947
|
+
});
|
|
1948
|
+
const candidate = Object.freeze({
|
|
1949
|
+
runtime,
|
|
1950
|
+
rawClient,
|
|
1951
|
+
hostMcp: bundle
|
|
1952
|
+
? Object.freeze({
|
|
1953
|
+
...bundle,
|
|
1954
|
+
mcpClient: runtime.client,
|
|
1955
|
+
})
|
|
1956
|
+
: null,
|
|
1957
|
+
sessionId,
|
|
1958
|
+
persistent: persistLedger,
|
|
1959
|
+
recovery,
|
|
1960
|
+
recoveryError,
|
|
1961
|
+
verifiedRecovery: Boolean(verifiedRecovery),
|
|
1962
|
+
});
|
|
1963
|
+
candidates.add(candidate);
|
|
1964
|
+
return candidate;
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
const commit = (candidate) => {
|
|
1968
|
+
if (!candidate || !candidates.has(candidate)) {
|
|
1969
|
+
throw new TypeError("MCP host runtime candidate is invalid");
|
|
1970
|
+
}
|
|
1971
|
+
current = candidate;
|
|
1972
|
+
return current;
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
return Object.freeze({
|
|
1976
|
+
prepare,
|
|
1977
|
+
commit,
|
|
1978
|
+
activate(options) {
|
|
1979
|
+
return commit(prepare(options));
|
|
1980
|
+
},
|
|
1981
|
+
get current() {
|
|
1982
|
+
return current;
|
|
1983
|
+
},
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* Execute the startup boundary with explicit host-owned dependencies. Exported
|
|
1989
|
+
* for behavior tests; callers cannot obtain the private workspace starter, and
|
|
1990
|
+
* the public startAgentRepl() below always supplies the real fixed bindings.
|
|
1991
|
+
*/
|
|
1992
|
+
export async function runReplStartupBoundary(options, startupDependencies) {
|
|
1993
|
+
const prepareStartup = dependencyDataFunction(
|
|
1994
|
+
startupDependencies,
|
|
1995
|
+
"prepareReplStartupResume",
|
|
1996
|
+
null,
|
|
1997
|
+
);
|
|
1998
|
+
if (typeof prepareStartup !== "function") {
|
|
1999
|
+
throw invalidReplRecovery(
|
|
2000
|
+
"REPL startup admission dependency is missing",
|
|
2001
|
+
"CC_REPL_STARTUP_DEPENDENCY_INVALID",
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
const startupAdmission = assertReplStartupAdmission(
|
|
2005
|
+
synchronousReplRecoveryValue(
|
|
2006
|
+
prepareStartup(options.sessionId),
|
|
2007
|
+
"REPL startup admission",
|
|
2008
|
+
),
|
|
2009
|
+
);
|
|
2010
|
+
const startupCandidate = startupAdmission.candidate;
|
|
2011
|
+
if (startupCandidate && !startupCandidate.ok) {
|
|
2012
|
+
const refuseStartup = dependencyDataFunction(
|
|
2013
|
+
startupDependencies,
|
|
2014
|
+
"refuseReplStartupResume",
|
|
2015
|
+
null,
|
|
2016
|
+
);
|
|
2017
|
+
if (typeof refuseStartup !== "function") {
|
|
2018
|
+
throw invalidReplRecovery(
|
|
2019
|
+
"REPL startup refusal dependency is missing",
|
|
2020
|
+
"CC_REPL_STARTUP_DEPENDENCY_INVALID",
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
return refuseStartup(options, startupCandidate);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Only a frozen, branded admission capability may cross into workspace,
|
|
2027
|
+
// pipe, config, plugin, hook, MCP, model, or tool initialization.
|
|
2028
|
+
const readCwd = dependencyDataFunction(startupDependencies, "cwd", null);
|
|
2029
|
+
const enterWorkspace = dependencyDataFunction(
|
|
2030
|
+
startupDependencies,
|
|
2031
|
+
"runWithHostHooksV2Workspace",
|
|
2032
|
+
null,
|
|
2033
|
+
);
|
|
2034
|
+
const startWorkspace = dependencyDataFunction(
|
|
2035
|
+
startupDependencies,
|
|
2036
|
+
"startAgentReplInWorkspace",
|
|
2037
|
+
null,
|
|
2038
|
+
);
|
|
2039
|
+
if (
|
|
2040
|
+
typeof readCwd !== "function" ||
|
|
2041
|
+
typeof enterWorkspace !== "function" ||
|
|
2042
|
+
typeof startWorkspace !== "function"
|
|
2043
|
+
) {
|
|
2044
|
+
throw invalidReplRecovery(
|
|
2045
|
+
"REPL host startup dependency is missing",
|
|
2046
|
+
"CC_REPL_STARTUP_DEPENDENCY_INVALID",
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
const trustedWorkspaceRoot = readCwd();
|
|
2050
|
+
return enterWorkspace(trustedWorkspaceRoot, () =>
|
|
2051
|
+
startWorkspace(options, startupAdmission),
|
|
484
2052
|
);
|
|
485
2053
|
}
|
|
486
2054
|
|
|
487
|
-
|
|
2055
|
+
/** Start the agentic REPL with non-overridable production bindings. */
|
|
2056
|
+
export async function startAgentRepl(options = {}) {
|
|
2057
|
+
return runReplStartupBoundary(options, {
|
|
2058
|
+
prepareReplStartupResume,
|
|
2059
|
+
refuseReplStartupResume,
|
|
2060
|
+
cwd: () => process.cwd(),
|
|
2061
|
+
runWithHostHooksV2Workspace,
|
|
2062
|
+
startAgentReplInWorkspace,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
async function startAgentReplInWorkspace(options = {}, startupAdmission) {
|
|
488
2067
|
// EPIPE guard: if the REPL's stdout is piped and the consumer closes (e.g.
|
|
489
2068
|
// `cc agent | head`), the async stream `error` would otherwise crash the
|
|
490
2069
|
// process. Route a broken pipe into the REPL's own graceful shutdown (the
|
|
@@ -506,6 +2085,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
506
2085
|
}
|
|
507
2086
|
process.exit(0);
|
|
508
2087
|
});
|
|
2088
|
+
const { useJsonl, candidate: _startupJsonlResume } = startupAdmission;
|
|
509
2089
|
let model = options.model || "qwen2.5:7b";
|
|
510
2090
|
let provider = options.provider || "ollama";
|
|
511
2091
|
// Extended thinking (Anthropic; opt-in via --think/--ultrathink). Carried from
|
|
@@ -515,12 +2095,15 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
515
2095
|
// (--thinking-budget) is the companion legacy-model budget_tokens override.
|
|
516
2096
|
let thinking = options.thinking || null;
|
|
517
2097
|
const thinkingBudget = options.thinkingBudget || null;
|
|
518
|
-
// `/
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
// the injection after the turn (or as a backstop at the next submit on throw).
|
|
2098
|
+
// `/note-next` one-shot guidance rides ONLY the next main turn, then is
|
|
2099
|
+
// stripped so it never persists or carries forward. `/btw` itself is an
|
|
2100
|
+
// immediate independent side question handled below.
|
|
522
2101
|
let pendingBtw = [];
|
|
523
2102
|
let _btwRestore = null;
|
|
2103
|
+
// Provider-neutral, tool-free Advisor/Critic. Initialized lazily after the
|
|
2104
|
+
// durable session and resumed transcript are available; null means the
|
|
2105
|
+
// optional surface could not be initialized. Default is off via config.
|
|
2106
|
+
let _advisorRuntime = null;
|
|
524
2107
|
// Current permission mode (strict|trusted|autopilot|auto), mirrored here so
|
|
525
2108
|
// Shift+Tab can cycle it and `/permissions <tier>` can set it. Kept in sync
|
|
526
2109
|
// with _approvalGate.setSessionPolicy below. "auto" rides the trusted gate
|
|
@@ -588,6 +2171,13 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
588
2171
|
// Snapshot the work tree before each mutating tool (git engine) so the user
|
|
589
2172
|
// can `cc checkpoint restore` to just before any tool call.
|
|
590
2173
|
const autoCheckpoint = options.autoCheckpoint === true;
|
|
2174
|
+
const managedCheckpoint = options.managedCheckpoint === true;
|
|
2175
|
+
const managedCheckpointStateDir = options.managedCheckpointStateDir || null;
|
|
2176
|
+
const managedCheckpointExclusions = Array.isArray(
|
|
2177
|
+
options.managedCheckpointExclusions,
|
|
2178
|
+
)
|
|
2179
|
+
? options.managedCheckpointExclusions
|
|
2180
|
+
: [];
|
|
591
2181
|
|
|
592
2182
|
// --fallback-model: walk an ordered backup-model chain when a turn's LLM
|
|
593
2183
|
// call fails (transient error or model-not-found). Built once; passed into
|
|
@@ -616,7 +2206,10 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
616
2206
|
// Bootstrap runtime (best-effort, DB not required)
|
|
617
2207
|
let db = null;
|
|
618
2208
|
let contextEngine = null;
|
|
619
|
-
let sessionId = null;
|
|
2209
|
+
let sessionId = _startupJsonlResume?.sessionId || null;
|
|
2210
|
+
const _replCompactPersistence = createReplCompactPersistence(
|
|
2211
|
+
_startupJsonlResume?.ok ? _startupJsonlResume.rebuiltMessages : [],
|
|
2212
|
+
);
|
|
620
2213
|
|
|
621
2214
|
try {
|
|
622
2215
|
const ctx = await bootstrap({ verbose: false });
|
|
@@ -627,7 +2220,30 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
627
2220
|
|
|
628
2221
|
// Initialize prompt compressor (adaptive to model's context window)
|
|
629
2222
|
if (feature("PROMPT_COMPRESSOR")) {
|
|
630
|
-
_compressor = new PromptCompressor({
|
|
2223
|
+
_compressor = new PromptCompressor({
|
|
2224
|
+
model,
|
|
2225
|
+
provider,
|
|
2226
|
+
llmQuery: async (prompt) => {
|
|
2227
|
+
const response = await chatWithTools(
|
|
2228
|
+
[{ role: "user", content: prompt }],
|
|
2229
|
+
{
|
|
2230
|
+
provider,
|
|
2231
|
+
model,
|
|
2232
|
+
baseUrl,
|
|
2233
|
+
apiKey,
|
|
2234
|
+
enabledToolNames: [],
|
|
2235
|
+
extraToolDefinitions: [],
|
|
2236
|
+
maxOutputTokens: 2048,
|
|
2237
|
+
},
|
|
2238
|
+
);
|
|
2239
|
+
return {
|
|
2240
|
+
summary: response?.message?.content || "",
|
|
2241
|
+
usage: response?.usage || null,
|
|
2242
|
+
provider,
|
|
2243
|
+
model,
|
|
2244
|
+
};
|
|
2245
|
+
},
|
|
2246
|
+
});
|
|
631
2247
|
}
|
|
632
2248
|
const _autoPinOpt = resolveAutoPinOption({ config: _autoPinCfgValue });
|
|
633
2249
|
// Compaction options shared by /compact + auto-compact. Adds the
|
|
@@ -893,16 +2509,18 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
893
2509
|
// PostToolUse). The interactive _permissionConfirm above doubles as the
|
|
894
2510
|
// confirmer for a hook `ask` decision.
|
|
895
2511
|
try {
|
|
896
|
-
const { loadHooks, projectHookTrustNotice } =
|
|
2512
|
+
const { loadHooks, projectHookTrustNotice, attachAuthorityErrors } =
|
|
897
2513
|
await import("../lib/settings-hooks.cjs");
|
|
898
2514
|
const loaded = loadHooks({ cwd: process.cwd() });
|
|
899
2515
|
// Fold in installed plugins' hooks/hooks.json (Phase 3.3c).
|
|
900
2516
|
const { mergePluginHooks } = await import("../lib/plugin-runtime/hooks.js");
|
|
901
|
-
const effectiveHooks = mergePluginHooks(
|
|
902
|
-
|
|
903
|
-
|
|
2517
|
+
const effectiveHooks = mergePluginHooks(
|
|
2518
|
+
attachAuthorityErrors(loaded.hooks, loaded.authorityErrors),
|
|
2519
|
+
{ cwd: process.cwd() },
|
|
2520
|
+
);
|
|
904
2521
|
_settingsHooks =
|
|
905
|
-
|
|
2522
|
+
Object.keys(effectiveHooks).length > 0 ||
|
|
2523
|
+
effectiveHooks._authorityErrors.length > 0
|
|
906
2524
|
? effectiveHooks
|
|
907
2525
|
: null;
|
|
908
2526
|
// First-run trust notice for an untrusted/cloned repo's shell-running
|
|
@@ -914,7 +2532,16 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
914
2532
|
/* trust notice is best-effort */
|
|
915
2533
|
}
|
|
916
2534
|
} catch (_err) {
|
|
917
|
-
_settingsHooks =
|
|
2535
|
+
_settingsHooks = {};
|
|
2536
|
+
Object.defineProperty(_settingsHooks, "_authorityErrors", {
|
|
2537
|
+
value: Object.freeze([
|
|
2538
|
+
Object.freeze({
|
|
2539
|
+
sourceFile: null,
|
|
2540
|
+
code: _err?.code || "CC_HOOK_AUTHORITY_LOAD_FAILED",
|
|
2541
|
+
}),
|
|
2542
|
+
]),
|
|
2543
|
+
enumerable: false,
|
|
2544
|
+
});
|
|
918
2545
|
}
|
|
919
2546
|
|
|
920
2547
|
// Start installed plugins' background monitors (Phase 3.3i). Trust-gated
|
|
@@ -972,19 +2599,10 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
972
2599
|
_pluginSettingsRestore = null;
|
|
973
2600
|
}
|
|
974
2601
|
|
|
975
|
-
// Resume existing session or create new one
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (useJsonl && options.sessionId) {
|
|
979
|
-
// JSONL resume: check if session file exists
|
|
980
|
-
try {
|
|
981
|
-
if (sessionExists(options.sessionId)) {
|
|
982
|
-
sessionId = options.sessionId;
|
|
983
|
-
}
|
|
984
|
-
} catch (_err) {
|
|
985
|
-
// Non-critical
|
|
986
|
-
}
|
|
987
|
-
} else if (db && options.sessionId) {
|
|
2602
|
+
// Resume existing session or create new one. JSONL resumes were admitted
|
|
2603
|
+
// before bootstrap/config/plugin/hook initialization and use that exact
|
|
2604
|
+
// verified sample below; do not re-read the transcript here.
|
|
2605
|
+
if (!useJsonl && db && options.sessionId) {
|
|
988
2606
|
try {
|
|
989
2607
|
const existing = getSession(db, options.sessionId);
|
|
990
2608
|
if (existing && existing.messages) {
|
|
@@ -1144,6 +2762,56 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1144
2762
|
let _replBaseSystem = _buildReplBaseSystem();
|
|
1145
2763
|
let _activeOutputStyle = null; // { name, body }
|
|
1146
2764
|
const messages = [{ role: "system", content: _replBaseSystem }];
|
|
2765
|
+
let _mcpLedgerRecovery = null;
|
|
2766
|
+
let _mcpLedgerRecoveryError = null;
|
|
2767
|
+
const _prepareJsonlResumeCandidate = (targetSessionId) =>
|
|
2768
|
+
prepareReplJsonlResumeCandidate(targetSessionId);
|
|
2769
|
+
const _prepareMcpRecoveryCommit = (candidate) => {
|
|
2770
|
+
const authorized =
|
|
2771
|
+
candidate && REPL_MCP_RECOVERY_CANDIDATES.has(candidate)
|
|
2772
|
+
? candidate
|
|
2773
|
+
: failClosedReplMcpRecoveryCandidate(
|
|
2774
|
+
sessionId,
|
|
2775
|
+
invalidReplRecovery(
|
|
2776
|
+
"MCP recovery candidate capability is invalid",
|
|
2777
|
+
"CC_REPL_MCP_RECOVERY_CANDIDATE_INVALID",
|
|
2778
|
+
),
|
|
2779
|
+
"CC_REPL_MCP_RECOVERY_CANDIDATE_INVALID",
|
|
2780
|
+
);
|
|
2781
|
+
const recovery = authorized.recovery;
|
|
2782
|
+
const recoveryError = authorized.recoveryError;
|
|
2783
|
+
const noticeMessage = authorized.notice
|
|
2784
|
+
? Object.freeze({ role: "system", content: authorized.notice })
|
|
2785
|
+
: null;
|
|
2786
|
+
const warning = recoveryError
|
|
2787
|
+
? "⚠ MCP ledger recovery failed closed; inspect the transcript before using MCP tools."
|
|
2788
|
+
: authorized.notice
|
|
2789
|
+
? `⚠ ${recovery.unsettled.length} interrupted MCP call(s), ${recovery.incidents.length} ledger incident(s), and ${recovery.replayDenied.length} exact replay deny/denies require inspection before replay.`
|
|
2790
|
+
: null;
|
|
2791
|
+
return Object.freeze({
|
|
2792
|
+
recovery,
|
|
2793
|
+
recoveryError,
|
|
2794
|
+
noticeMessage,
|
|
2795
|
+
warning,
|
|
2796
|
+
});
|
|
2797
|
+
};
|
|
2798
|
+
const _applyMcpRecoveryCommit = (targetMessages, preparedCommit) => {
|
|
2799
|
+
_mcpLedgerRecovery = preparedCommit.recovery;
|
|
2800
|
+
_mcpLedgerRecoveryError = preparedCommit.recoveryError;
|
|
2801
|
+
if (preparedCommit.noticeMessage) {
|
|
2802
|
+
targetMessages.push(preparedCommit.noticeMessage);
|
|
2803
|
+
}
|
|
2804
|
+
return Boolean(preparedCommit.noticeMessage);
|
|
2805
|
+
};
|
|
2806
|
+
const _logMcpRecoveryCommit = (preparedCommit) => {
|
|
2807
|
+
if (preparedCommit.warning) logger.warn(preparedCommit.warning);
|
|
2808
|
+
};
|
|
2809
|
+
const _commitMcpRecoveryCandidate = (targetMessages, candidate) => {
|
|
2810
|
+
const preparedCommit = _prepareMcpRecoveryCommit(candidate);
|
|
2811
|
+
const addedNotice = _applyMcpRecoveryCommit(targetMessages, preparedCommit);
|
|
2812
|
+
_logMcpRecoveryCommit(preparedCommit);
|
|
2813
|
+
return addedNotice;
|
|
2814
|
+
};
|
|
1147
2815
|
// Resume-degenerate role sanitation (Claude Code 2.1.187 parity): a one-shot
|
|
1148
2816
|
// flag armed when a resumed transcript ends with a bare `user` turn (the prior
|
|
1149
2817
|
// run produced no assistant response). Consumed by the first model call so the
|
|
@@ -1420,6 +3088,89 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1420
3088
|
}
|
|
1421
3089
|
_persistReplContextSources();
|
|
1422
3090
|
|
|
3091
|
+
const _mcpHostRuntimeManager = createReplMcpHostRuntimeManager();
|
|
3092
|
+
const _mcpRuntimeInputs = ({
|
|
3093
|
+
targetSessionId = sessionId,
|
|
3094
|
+
recovery = _mcpLedgerRecovery,
|
|
3095
|
+
recoveryError = _mcpLedgerRecoveryError,
|
|
3096
|
+
} = {}) => ({
|
|
3097
|
+
adhocMcp: _adhocMcp,
|
|
3098
|
+
bundleMcpClient: _bundleMcpClient,
|
|
3099
|
+
sessionId: targetSessionId,
|
|
3100
|
+
persistent: useJsonl && Boolean(targetSessionId),
|
|
3101
|
+
recovery,
|
|
3102
|
+
recoveryError,
|
|
3103
|
+
});
|
|
3104
|
+
const _activateMcpHostRuntime = () =>
|
|
3105
|
+
_mcpHostRuntimeManager.activate(_mcpRuntimeInputs());
|
|
3106
|
+
const _prepareMcpHostRuntime = (targetSessionId, mcpCandidate = {}) =>
|
|
3107
|
+
_mcpHostRuntimeManager.prepare({
|
|
3108
|
+
..._mcpRuntimeInputs({
|
|
3109
|
+
targetSessionId,
|
|
3110
|
+
recovery: mcpCandidate.recovery ?? null,
|
|
3111
|
+
recoveryError: mcpCandidate.recoveryError ?? null,
|
|
3112
|
+
}),
|
|
3113
|
+
verifiedRecovery: true,
|
|
3114
|
+
});
|
|
3115
|
+
const _getReplHostMcp = () => _activateMcpHostRuntime().hostMcp;
|
|
3116
|
+
const _resumeStateController = createReplResumeStateController({
|
|
3117
|
+
get sessionId() {
|
|
3118
|
+
return sessionId;
|
|
3119
|
+
},
|
|
3120
|
+
set sessionId(value) {
|
|
3121
|
+
sessionId = value;
|
|
3122
|
+
},
|
|
3123
|
+
messages,
|
|
3124
|
+
get recovery() {
|
|
3125
|
+
return _mcpLedgerRecovery;
|
|
3126
|
+
},
|
|
3127
|
+
set recovery(value) {
|
|
3128
|
+
_mcpLedgerRecovery = value;
|
|
3129
|
+
},
|
|
3130
|
+
get recoveryError() {
|
|
3131
|
+
return _mcpLedgerRecoveryError;
|
|
3132
|
+
},
|
|
3133
|
+
set recoveryError(value) {
|
|
3134
|
+
_mcpLedgerRecoveryError = value;
|
|
3135
|
+
},
|
|
3136
|
+
get sanitizeRolesNextTurn() {
|
|
3137
|
+
return _sanitizeRolesNextTurn;
|
|
3138
|
+
},
|
|
3139
|
+
set sanitizeRolesNextTurn(value) {
|
|
3140
|
+
_sanitizeRolesNextTurn = value;
|
|
3141
|
+
},
|
|
3142
|
+
get turnBindingProducer() {
|
|
3143
|
+
return _turnBindingProducer;
|
|
3144
|
+
},
|
|
3145
|
+
set turnBindingProducer(value) {
|
|
3146
|
+
_turnBindingProducer = value;
|
|
3147
|
+
},
|
|
3148
|
+
get turnBindingCriticalError() {
|
|
3149
|
+
return _turnBindingCriticalError;
|
|
3150
|
+
},
|
|
3151
|
+
set turnBindingCriticalError(value) {
|
|
3152
|
+
_turnBindingCriticalError = value;
|
|
3153
|
+
},
|
|
3154
|
+
checkpointMarks: _checkpointMarks,
|
|
3155
|
+
get clearedConversation() {
|
|
3156
|
+
return _clearedConversation;
|
|
3157
|
+
},
|
|
3158
|
+
set clearedConversation(value) {
|
|
3159
|
+
_clearedConversation = value;
|
|
3160
|
+
},
|
|
3161
|
+
runtimeManager: _mcpHostRuntimeManager,
|
|
3162
|
+
applyMcpRecoveryCommit: _applyMcpRecoveryCommit,
|
|
3163
|
+
logMcpRecoveryCommit: _logMcpRecoveryCommit,
|
|
3164
|
+
logger,
|
|
3165
|
+
});
|
|
3166
|
+
const _captureResumeState = _resumeStateController.capture;
|
|
3167
|
+
const _restoreResumeState = _resumeStateController.restore;
|
|
3168
|
+
const _applyPreparedResumeState = _resumeStateController.apply;
|
|
3169
|
+
const _registerHostSystemMessages =
|
|
3170
|
+
_resumeStateController.registerHostSystemMessages;
|
|
3171
|
+
const _refreshHostSystemMessages =
|
|
3172
|
+
_resumeStateController.refreshHostSystemMessages;
|
|
3173
|
+
|
|
1423
3174
|
// Seed connected MCP servers with the startup workspace roots when the session
|
|
1424
3175
|
// began with extra `--add-dir` roots — otherwise `roots/list` would only ever
|
|
1425
3176
|
// return the cwd until the first mid-session /add-dir. No-op (byte-unchanged)
|
|
@@ -1500,35 +3251,37 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1500
3251
|
}
|
|
1501
3252
|
}
|
|
1502
3253
|
|
|
3254
|
+
// Seal the complete host-owned leading system prefix exactly once, after
|
|
3255
|
+
// startup hooks, bundle context, and optional memory injection, but before
|
|
3256
|
+
// any canonical/legacy replay is appended. Later live switches refresh only
|
|
3257
|
+
// entry 0 from the active base prompt; they never re-scan old session systems.
|
|
3258
|
+
_registerHostSystemMessages();
|
|
3259
|
+
|
|
3260
|
+
let _startupResumeCommitted = false;
|
|
1503
3261
|
// Load resumed session messages
|
|
1504
3262
|
if (options.sessionId && sessionId) {
|
|
1505
3263
|
try {
|
|
1506
3264
|
if (useJsonl) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (trust.status === "tampered") {
|
|
1515
|
-
logger.warn(
|
|
1516
|
-
`⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${sessionId}'.`,
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
} catch (_err) {
|
|
1520
|
-
// verification is best-effort in the interactive path
|
|
3265
|
+
const prepared = _startupJsonlResume;
|
|
3266
|
+
if (!prepared?.ok) {
|
|
3267
|
+
const error = new Error(
|
|
3268
|
+
"Startup JSONL resume lost its verified admission capability",
|
|
3269
|
+
);
|
|
3270
|
+
error.code = "CC_REPL_SESSION_RESUME_NOT_PREPARED";
|
|
3271
|
+
throw error;
|
|
1521
3272
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
3273
|
+
messages.push(...prepared.canonicalSystemMessages);
|
|
3274
|
+
_commitMcpRecoveryCandidate(messages, prepared.mcp);
|
|
3275
|
+
if (prepared.replayMessages.length > 0) {
|
|
3276
|
+
messages.push(...prepared.conversationMessages);
|
|
1525
3277
|
// Arm the resume role-merge if the prior run left a dangling user turn.
|
|
1526
3278
|
_sanitizeRolesNextTurn =
|
|
1527
|
-
|
|
3279
|
+
prepared.conversationMessages.at(-1)?.role === "user";
|
|
1528
3280
|
logger.info(
|
|
1529
|
-
`Resumed JSONL session ${sessionId} (${
|
|
3281
|
+
`Resumed JSONL session ${sessionId} (${prepared.rebuiltMessages.length} messages)`,
|
|
1530
3282
|
);
|
|
1531
3283
|
}
|
|
3284
|
+
_startupResumeCommitted = true;
|
|
1532
3285
|
} else if (db) {
|
|
1533
3286
|
const existing = getSession(db, sessionId);
|
|
1534
3287
|
if (existing && existing.messages) {
|
|
@@ -1542,15 +3295,26 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1542
3295
|
logger.info(
|
|
1543
3296
|
`Resumed session ${sessionId} (${parsed.length} messages)`,
|
|
1544
3297
|
);
|
|
3298
|
+
_startupResumeCommitted = true;
|
|
1545
3299
|
}
|
|
1546
3300
|
}
|
|
1547
|
-
} catch (
|
|
1548
|
-
|
|
3301
|
+
} catch (error) {
|
|
3302
|
+
if (useJsonl) {
|
|
3303
|
+
_commitMcpRecoveryCandidate(
|
|
3304
|
+
messages,
|
|
3305
|
+
failClosedReplMcpRecoveryCandidate(
|
|
3306
|
+
sessionId,
|
|
3307
|
+
error,
|
|
3308
|
+
"CC_REPL_SESSION_REBUILD_FAILED",
|
|
3309
|
+
"session transcript",
|
|
3310
|
+
),
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
1549
3313
|
}
|
|
1550
3314
|
// settings.json SessionResume hooks: a persisted transcript was just
|
|
1551
3315
|
// replayed into this interactive session (distinct from SessionStart).
|
|
1552
3316
|
// Observe-only, best-effort — never blocks entering the REPL.
|
|
1553
|
-
if (_settingsHooks &&
|
|
3317
|
+
if (_settingsHooks && _startupResumeCommitted) {
|
|
1554
3318
|
try {
|
|
1555
3319
|
const { runObserveHooks } =
|
|
1556
3320
|
await import("../lib/settings-hook-events.js");
|
|
@@ -1582,6 +3346,50 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1582
3346
|
}
|
|
1583
3347
|
}
|
|
1584
3348
|
|
|
3349
|
+
// Create the active session-scoped runtime only after startup recovery has
|
|
3350
|
+
// been loaded. Subsequent turns reuse this controller and ledger so a
|
|
3351
|
+
// settlement-failure latch cannot be cleared by starting another turn.
|
|
3352
|
+
_activateMcpHostRuntime();
|
|
3353
|
+
|
|
3354
|
+
// Advisor is independent from the main agent loop: it gets a redacted
|
|
3355
|
+
// transcript snapshot and ZERO tools. Compact call/outcome metadata shares
|
|
3356
|
+
// the session hash chain, while advice text itself is never persisted.
|
|
3357
|
+
try {
|
|
3358
|
+
const { createConfiguredAdvisorRuntime } =
|
|
3359
|
+
await import("../lib/advisor-runtime.js");
|
|
3360
|
+
const advisorOverrides = {};
|
|
3361
|
+
if (options.advisorEnabled !== undefined) {
|
|
3362
|
+
advisorOverrides.enabled = options.advisorEnabled === true;
|
|
3363
|
+
}
|
|
3364
|
+
_advisorRuntime = await createConfiguredAdvisorRuntime({
|
|
3365
|
+
cwd: process.cwd(),
|
|
3366
|
+
settingsFile: options.settingsFile || null,
|
|
3367
|
+
managedSettingsFile: options.managedSettingsFile || null,
|
|
3368
|
+
mainProvider: provider,
|
|
3369
|
+
mainModel: model,
|
|
3370
|
+
baseUrl,
|
|
3371
|
+
apiKey,
|
|
3372
|
+
overrides: advisorOverrides,
|
|
3373
|
+
onEvent: (event) => {
|
|
3374
|
+
if (!useJsonl || !sessionId) return;
|
|
3375
|
+
try {
|
|
3376
|
+
appendEvent(sessionId, event.type, event.data);
|
|
3377
|
+
} catch {
|
|
3378
|
+
// Advisor observability is best-effort and cannot fail a main turn.
|
|
3379
|
+
}
|
|
3380
|
+
},
|
|
3381
|
+
});
|
|
3382
|
+
// A resumed transcript may contain old tool results. Mark them observed so
|
|
3383
|
+
// the first new turn cannot trigger an Advisor call for historical work.
|
|
3384
|
+
_advisorRuntime.primeMessages(messages);
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
if (error?.code === "CC_MANAGED_SETTINGS_INVALID") throw error;
|
|
3387
|
+
_advisorRuntime = null;
|
|
3388
|
+
if (options.advisorEnabled === true) {
|
|
3389
|
+
logger.warn(`Advisor unavailable: ${error.message}`);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
|
|
1585
3393
|
// Vim mode (Claude-Code `/vim` parity): opt-in modal line editing. `_vim`
|
|
1586
3394
|
// holds the NORMAL-mode engine state while normal mode is active (readline's
|
|
1587
3395
|
// own key handling is suspended then); it is null in INSERT mode (readline
|
|
@@ -1751,6 +3559,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1751
3559
|
: [
|
|
1752
3560
|
"/add-dir",
|
|
1753
3561
|
"/agents",
|
|
3562
|
+
"/advisor",
|
|
1754
3563
|
"/auto",
|
|
1755
3564
|
"/btw",
|
|
1756
3565
|
"/cd",
|
|
@@ -1776,6 +3585,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1776
3585
|
"/memory",
|
|
1777
3586
|
"/microcompact",
|
|
1778
3587
|
"/model",
|
|
3588
|
+
"/note-next",
|
|
1779
3589
|
"/output-style",
|
|
1780
3590
|
"/permissions",
|
|
1781
3591
|
"/plan",
|
|
@@ -1821,12 +3631,13 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
1821
3631
|
}
|
|
1822
3632
|
},
|
|
1823
3633
|
getIdeOpenFiles: async () => {
|
|
1824
|
-
const
|
|
1825
|
-
|
|
3634
|
+
const hostMcp = _getReplHostMcp();
|
|
3635
|
+
const exec = hostMcp?.externalToolExecutors?.mcp__ide__getOpenEditors;
|
|
3636
|
+
if (!exec || exec.kind !== "mcp" || !hostMcp?.mcpClient?.callTool) {
|
|
1826
3637
|
return [];
|
|
1827
3638
|
}
|
|
1828
3639
|
const { parseToolResultJson } = await import("../lib/ide-context.js");
|
|
1829
|
-
const res = await
|
|
3640
|
+
const res = await hostMcp.mcpClient.callTool(
|
|
1830
3641
|
exec.serverName,
|
|
1831
3642
|
exec.toolName,
|
|
1832
3643
|
{},
|
|
@@ -2231,9 +4042,61 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
2231
4042
|
|
|
2232
4043
|
// Steering (Claude-Code parity): typing while a turn is running QUEUES the
|
|
2233
4044
|
// line instead of racing a second concurrent turn; the queue drains FIFO
|
|
2234
|
-
// when the current turn finishes.
|
|
4045
|
+
// when the current turn finishes. `/btw` is the deliberate exception: its
|
|
4046
|
+
// tool-free snapshot call may run concurrently without mutating main state.
|
|
2235
4047
|
let _processingLine = false;
|
|
2236
4048
|
const _pendingLines = [];
|
|
4049
|
+
|
|
4050
|
+
const runBtwSideQuestion = async (btw, { concurrent = false } = {}) => {
|
|
4051
|
+
if (!btw || btw.error) {
|
|
4052
|
+
const message = btw?.error || "usage: /btw [--fork] <question>";
|
|
4053
|
+
if (concurrent) process.stderr.write(`\n${message}\n`);
|
|
4054
|
+
else logger.info(message);
|
|
4055
|
+
return { ok: false, error: message };
|
|
4056
|
+
}
|
|
4057
|
+
try {
|
|
4058
|
+
const chatFn = createChatFn({ provider, model, baseUrl, apiKey });
|
|
4059
|
+
const { answer } = await runBtwQuestion({
|
|
4060
|
+
messages,
|
|
4061
|
+
question: btw.text,
|
|
4062
|
+
chatFn,
|
|
4063
|
+
model,
|
|
4064
|
+
});
|
|
4065
|
+
let forkedId = null;
|
|
4066
|
+
let forkNotice = "";
|
|
4067
|
+
if (btw.fork) {
|
|
4068
|
+
if (useJsonl && sessionId && sessionExists(sessionId)) {
|
|
4069
|
+
forkedId = forkSession(sessionId, {
|
|
4070
|
+
requestId: `btw-${randomUUID()}`,
|
|
4071
|
+
});
|
|
4072
|
+
if (forkedId) {
|
|
4073
|
+
appendUserMessage(forkedId, btw.text);
|
|
4074
|
+
appendAssistantMessage(forkedId, answer);
|
|
4075
|
+
forkNotice = `\nForked side thread: ${forkedId}`;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
if (!forkedId) {
|
|
4079
|
+
forkNotice =
|
|
4080
|
+
"\nSide answer was not persisted: the parent has no durable JSONL session.";
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
const block =
|
|
4084
|
+
`┌─ btw${btw.fork ? " --fork" : ""}\n` +
|
|
4085
|
+
`${answer || "(empty response)"}\n` +
|
|
4086
|
+
`└─ ephemeral · no tools · parent history unchanged${forkNotice}`;
|
|
4087
|
+
// A concurrent answer must not corrupt the main streamed stdout payload.
|
|
4088
|
+
// stderr also keeps headless/JSON consumers isolated from the overlay.
|
|
4089
|
+
if (concurrent) process.stderr.write(`\n${block}\n`);
|
|
4090
|
+
else logger.log(`\n${block}\n`);
|
|
4091
|
+
return { ok: true, answer, forkedId };
|
|
4092
|
+
} catch (error) {
|
|
4093
|
+
const message = `/btw failed: ${error.message}`;
|
|
4094
|
+
if (concurrent) process.stderr.write(`\n${message}\n`);
|
|
4095
|
+
else logger.error(message);
|
|
4096
|
+
return { ok: false, error: message };
|
|
4097
|
+
}
|
|
4098
|
+
};
|
|
4099
|
+
|
|
2237
4100
|
// Multiline input (Claude-Code parity): a physical line ending in a
|
|
2238
4101
|
// continuation backslash keeps the prompt open; `_mlBuffer` accumulates the
|
|
2239
4102
|
// pieces and the whole block submits when a line does not continue.
|
|
@@ -2368,7 +4231,13 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
2368
4231
|
` ${chalk.cyan("/effort")} Reasoning effort (/effort low|medium|high|xhigh; Anthropic)`,
|
|
2369
4232
|
);
|
|
2370
4233
|
logger.log(
|
|
2371
|
-
` ${chalk.cyan("/btw")}
|
|
4234
|
+
` ${chalk.cyan("/btw")} Ask an immediate tool-free side question (/btw [--fork] <question>)`,
|
|
4235
|
+
);
|
|
4236
|
+
logger.log(
|
|
4237
|
+
` ${chalk.cyan("/advisor")} Independent tool-free critic (/advisor on|off|once [focus]|status)`,
|
|
4238
|
+
);
|
|
4239
|
+
logger.log(
|
|
4240
|
+
` ${chalk.cyan("/note-next")} Apply ephemeral guidance to the next main turn`,
|
|
2372
4241
|
);
|
|
2373
4242
|
logger.log(` ${chalk.cyan("/clear")} Clear conversation`);
|
|
2374
4243
|
logger.log(
|
|
@@ -2907,22 +4776,52 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
2907
4776
|
}
|
|
2908
4777
|
}
|
|
2909
4778
|
|
|
2910
|
-
// `/btw
|
|
2911
|
-
//
|
|
4779
|
+
// `/btw` is an immediate, tool-free side question over a snapshot of the
|
|
4780
|
+
// current context. It never mutates or persists into the parent history.
|
|
2912
4781
|
{
|
|
2913
4782
|
const btw = parseBtwCommand(trimmed);
|
|
2914
4783
|
if (btw) {
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
4784
|
+
await runBtwSideQuestion(btw);
|
|
4785
|
+
prompt();
|
|
4786
|
+
return;
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
// `/advisor`: session-local enable/disable, one forced review, or status.
|
|
4791
|
+
// One-shot advice is queued as an ephemeral prepareCall suffix, requiring
|
|
4792
|
+
// the main agent to validate it locally on its next model/tool turn.
|
|
4793
|
+
{
|
|
4794
|
+
const { parseAdvisorCommand, executeAdvisorCommand } =
|
|
4795
|
+
await import("./advisor-command.js");
|
|
4796
|
+
const advisorCommand = parseAdvisorCommand(trimmed);
|
|
4797
|
+
if (advisorCommand) {
|
|
4798
|
+
const result = await executeAdvisorCommand(advisorCommand, {
|
|
4799
|
+
runtime: _advisorRuntime,
|
|
4800
|
+
messages,
|
|
4801
|
+
signal: _turnAbort?.signal || null,
|
|
4802
|
+
});
|
|
4803
|
+
if (result?.output) logger.log(result.output);
|
|
4804
|
+
if (result?.guidance) {
|
|
4805
|
+
_advisorRuntime?.queueGuidance(result.guidance);
|
|
4806
|
+
}
|
|
4807
|
+
prompt();
|
|
4808
|
+
return;
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
// Preserve the previous next-turn guidance behavior under an honest name.
|
|
4813
|
+
{
|
|
4814
|
+
const note = parseNoteNextCommand(trimmed);
|
|
4815
|
+
if (note) {
|
|
4816
|
+
if (note.error) logger.info(note.error);
|
|
4817
|
+
else {
|
|
4818
|
+
pendingBtw.push(note.text);
|
|
4819
|
+
logger.info(
|
|
4820
|
+
chalk.gray(
|
|
4821
|
+
`Next-turn note queued (${pendingBtw.length}); it will not be saved to history.`,
|
|
4822
|
+
),
|
|
4823
|
+
);
|
|
2919
4824
|
}
|
|
2920
|
-
pendingBtw.push(btw.text);
|
|
2921
|
-
logger.info(
|
|
2922
|
-
chalk.gray(
|
|
2923
|
-
`Aside noted (${pendingBtw.length}) — rides your next message only, not saved to history.`,
|
|
2924
|
-
),
|
|
2925
|
-
);
|
|
2926
4825
|
prompt();
|
|
2927
4826
|
return;
|
|
2928
4827
|
}
|
|
@@ -3665,6 +5564,11 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
3665
5564
|
logger.info(
|
|
3666
5565
|
`Compacted: ${stats.originalMessages} → ${stats.compressedMessages} messages, saved ${stats.saved} tokens (${stats.strategy})`,
|
|
3667
5566
|
);
|
|
5567
|
+
if (stats.degraded === true) {
|
|
5568
|
+
logger.warn(
|
|
5569
|
+
`Semantic compaction degraded to ${stats.summaryMode}: ${stats.degradedReason}`,
|
|
5570
|
+
);
|
|
5571
|
+
}
|
|
3668
5572
|
} else if (contextEngine && messages.length > 5) {
|
|
3669
5573
|
const compacted = contextEngine.smartCompact(messages);
|
|
3670
5574
|
messages.length = 0;
|
|
@@ -3732,39 +5636,40 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
3732
5636
|
if (sessionArg.startsWith("resume ")) {
|
|
3733
5637
|
const resumeId = sessionArg.slice(7).trim();
|
|
3734
5638
|
try {
|
|
3735
|
-
if (useJsonl
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
logger.warn(
|
|
3742
|
-
`⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${resumeId}'.`,
|
|
3743
|
-
);
|
|
3744
|
-
}
|
|
3745
|
-
} catch (_err) {
|
|
3746
|
-
// verification is best-effort in the interactive path
|
|
3747
|
-
}
|
|
3748
|
-
const rebuilt = rebuildMessages(resumeId);
|
|
3749
|
-
messages.length = 1; // keep system prompt
|
|
3750
|
-
messages.push(...rebuilt.filter((m) => m.role !== "system"));
|
|
3751
|
-
_sanitizeRolesNextTurn =
|
|
3752
|
-
messages[messages.length - 1]?.role === "user";
|
|
3753
|
-
sessionId = resumeId;
|
|
3754
|
-
// The producer owns one immutable session id. Recreate it lazily
|
|
3755
|
-
// on the next turn so bindings from the resumed conversation can
|
|
3756
|
-
// never be appended to the previously active session.
|
|
3757
|
-
_turnBindingProducer = null;
|
|
3758
|
-
_turnBindingCriticalError = null;
|
|
3759
|
-
// The prior session's checkpoint marks (turn-index → checkpoint id)
|
|
3760
|
-
// no longer map to this swapped-in history; keeping them would make
|
|
3761
|
-
// a later /rewind restore files from the WRONG session's snapshot.
|
|
3762
|
-
// Same reset /clear does on a context swap.
|
|
3763
|
-
_checkpointMarks.length = 0;
|
|
3764
|
-
_clearedConversation = null; // stash belongs to the swapped-out session
|
|
3765
|
-
logger.info(
|
|
3766
|
-
`Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
|
|
5639
|
+
if (useJsonl) {
|
|
5640
|
+
const prepared = _prepareJsonlResumeCandidate(resumeId);
|
|
5641
|
+
if (!prepared.ok) throw prepared.error;
|
|
5642
|
+
const preparedMcpRuntime = _prepareMcpHostRuntime(
|
|
5643
|
+
prepared.sessionId,
|
|
5644
|
+
prepared.mcp,
|
|
3767
5645
|
);
|
|
5646
|
+
const preparedMcpCommit = _prepareMcpRecoveryCommit(prepared.mcp);
|
|
5647
|
+
const hostSystemMessages = _refreshHostSystemMessages();
|
|
5648
|
+
const previousState = _captureResumeState();
|
|
5649
|
+
const preparedState = Object.freeze({
|
|
5650
|
+
sessionId: prepared.sessionId,
|
|
5651
|
+
hostSystemMessages,
|
|
5652
|
+
canonicalSystemMessages: prepared.canonicalSystemMessages,
|
|
5653
|
+
conversationMessages: prepared.conversationMessages,
|
|
5654
|
+
mcpCommit: preparedMcpCommit,
|
|
5655
|
+
mcpRuntime: preparedMcpRuntime,
|
|
5656
|
+
sanitizeRolesNextTurn:
|
|
5657
|
+
prepared.conversationMessages.at(-1)?.role === "user",
|
|
5658
|
+
logMessage: `Resumed JSONL session ${prepared.sessionId} (${prepared.rebuiltMessages.length} messages)`,
|
|
5659
|
+
});
|
|
5660
|
+
const committed = commitPreparedReplJsonlResume(
|
|
5661
|
+
prepared,
|
|
5662
|
+
() => _applyPreparedResumeState(preparedState),
|
|
5663
|
+
() => _restoreResumeState(previousState),
|
|
5664
|
+
);
|
|
5665
|
+
if (!committed) {
|
|
5666
|
+
const error = new Error(
|
|
5667
|
+
"Prepared JSONL session resume was not committed",
|
|
5668
|
+
);
|
|
5669
|
+
error.code = "CC_REPL_SESSION_RESUME_NOT_COMMITTED";
|
|
5670
|
+
throw error;
|
|
5671
|
+
}
|
|
5672
|
+
_replCompactPersistence.replace(prepared.rebuiltMessages);
|
|
3768
5673
|
} else if (db) {
|
|
3769
5674
|
const existing = getSession(db, resumeId);
|
|
3770
5675
|
if (existing && existing.messages) {
|
|
@@ -3772,18 +5677,35 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
3772
5677
|
typeof existing.messages === "string"
|
|
3773
5678
|
? JSON.parse(existing.messages)
|
|
3774
5679
|
: existing.messages;
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
5680
|
+
const rebuiltMessages = snapshotReplMessages(parsed);
|
|
5681
|
+
const replayMessages = Object.freeze(
|
|
5682
|
+
rebuiltMessages.filter((message) => message.role !== "system"),
|
|
5683
|
+
);
|
|
5684
|
+
const preparedMcpRuntime = _prepareMcpHostRuntime(existing.id, {
|
|
5685
|
+
recovery: null,
|
|
5686
|
+
recoveryError: null,
|
|
5687
|
+
});
|
|
5688
|
+
const hostSystemMessages = _refreshHostSystemMessages();
|
|
5689
|
+
const previousState = _captureResumeState();
|
|
5690
|
+
const preparedState = Object.freeze({
|
|
5691
|
+
sessionId: existing.id,
|
|
5692
|
+
hostSystemMessages,
|
|
5693
|
+
canonicalSystemMessages: Object.freeze([]),
|
|
5694
|
+
conversationMessages: replayMessages,
|
|
5695
|
+
mcpCommit: Object.freeze({
|
|
5696
|
+
recovery: null,
|
|
5697
|
+
recoveryError: null,
|
|
5698
|
+
noticeMessage: null,
|
|
5699
|
+
warning: null,
|
|
5700
|
+
}),
|
|
5701
|
+
mcpRuntime: preparedMcpRuntime,
|
|
5702
|
+
sanitizeRolesNextTurn: replayMessages.at(-1)?.role === "user",
|
|
5703
|
+
logMessage: `Resumed session ${existing.id} (${rebuiltMessages.length} messages)`,
|
|
5704
|
+
});
|
|
5705
|
+
commitPreparedReplDbResume(
|
|
5706
|
+
preparedState,
|
|
5707
|
+
() => _applyPreparedResumeState(preparedState),
|
|
5708
|
+
() => _restoreResumeState(previousState),
|
|
3787
5709
|
);
|
|
3788
5710
|
} else {
|
|
3789
5711
|
logger.info(`Session not found: ${resumeId}`);
|
|
@@ -3792,7 +5714,14 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
3792
5714
|
logger.info("No session store available");
|
|
3793
5715
|
}
|
|
3794
5716
|
} catch (err) {
|
|
3795
|
-
logger.error(
|
|
5717
|
+
logger.error(
|
|
5718
|
+
JSON.stringify({
|
|
5719
|
+
type: "session.resume.refused",
|
|
5720
|
+
code: err?.code || "CC_REPL_SESSION_RESUME_FAILED",
|
|
5721
|
+
sessionId: resumeId,
|
|
5722
|
+
message: err?.message || "Session resume failed",
|
|
5723
|
+
}),
|
|
5724
|
+
);
|
|
3796
5725
|
}
|
|
3797
5726
|
} else {
|
|
3798
5727
|
logger.info(`Session ID: ${sessionId || "none"}`);
|
|
@@ -3841,15 +5770,30 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
3841
5770
|
|
|
3842
5771
|
// Re-merge plugin hooks onto the user's settings hooks (live session).
|
|
3843
5772
|
try {
|
|
3844
|
-
const { loadHooks } =
|
|
5773
|
+
const { loadHooks, attachAuthorityErrors } =
|
|
5774
|
+
await import("../lib/settings-hooks.cjs");
|
|
3845
5775
|
const { mergePluginHooks } =
|
|
3846
5776
|
await import("../lib/plugin-runtime/hooks.js");
|
|
3847
|
-
const
|
|
3848
|
-
const merged = mergePluginHooks(
|
|
5777
|
+
const loaded = loadHooks({ cwd });
|
|
5778
|
+
const merged = mergePluginHooks(
|
|
5779
|
+
attachAuthorityErrors(loaded.hooks, loaded.authorityErrors),
|
|
5780
|
+
{ cwd },
|
|
5781
|
+
);
|
|
3849
5782
|
_settingsHooks =
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
5783
|
+
Object.keys(merged).length > 0 || merged._authorityErrors.length > 0
|
|
5784
|
+
? merged
|
|
5785
|
+
: null;
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
_settingsHooks = {};
|
|
5788
|
+
Object.defineProperty(_settingsHooks, "_authorityErrors", {
|
|
5789
|
+
value: Object.freeze([
|
|
5790
|
+
Object.freeze({
|
|
5791
|
+
sourceFile: null,
|
|
5792
|
+
code: error?.code || "CC_HOOK_AUTHORITY_LOAD_FAILED",
|
|
5793
|
+
}),
|
|
5794
|
+
]),
|
|
5795
|
+
enumerable: false,
|
|
5796
|
+
});
|
|
3853
5797
|
}
|
|
3854
5798
|
|
|
3855
5799
|
// settings.json ConfigChange hooks (Claude-Code parity): the live
|
|
@@ -4497,6 +6441,31 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
4497
6441
|
"Plan has no items yet. Let the AI analyze the task first.",
|
|
4498
6442
|
);
|
|
4499
6443
|
} else {
|
|
6444
|
+
// Review this immutable plan version before approval. Advisor output
|
|
6445
|
+
// cannot block or approve the plan and grants no extra authority; its
|
|
6446
|
+
// local-verification checklist is queued for the next main turn.
|
|
6447
|
+
if (_advisorRuntime?.status().enabled) {
|
|
6448
|
+
try {
|
|
6449
|
+
const result = await _advisorRuntime.reviewPlan(
|
|
6450
|
+
{
|
|
6451
|
+
id: planManager.currentPlan.id,
|
|
6452
|
+
title: planManager.currentPlan.title,
|
|
6453
|
+
description: planManager.currentPlan.description,
|
|
6454
|
+
summary: planManager.generatePlanSummary(),
|
|
6455
|
+
},
|
|
6456
|
+
{ messages, signal: _turnAbort?.signal || null },
|
|
6457
|
+
);
|
|
6458
|
+
if (result.ok) {
|
|
6459
|
+
const { renderAdvisorAdvice } =
|
|
6460
|
+
await import("./advisor-command.js");
|
|
6461
|
+
logger.log(renderAdvisorAdvice(result));
|
|
6462
|
+
} else if (result.effect !== "disabled") {
|
|
6463
|
+
logger.warn(result.error);
|
|
6464
|
+
}
|
|
6465
|
+
} catch (error) {
|
|
6466
|
+
logger.warn(`Advisor plan review skipped: ${error.message}`);
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
4500
6469
|
planManager.approvePlan();
|
|
4501
6470
|
logger.success(
|
|
4502
6471
|
`Plan approved! ${planManager.currentPlan.items.length} items ready for execution.`,
|
|
@@ -4673,7 +6642,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
4673
6642
|
|
|
4674
6643
|
// `/mcp` — overview of connected MCP servers' resources + prompts.
|
|
4675
6644
|
if (trimmed === "/mcp" || trimmed.startsWith("/mcp ")) {
|
|
4676
|
-
const mcpClient =
|
|
6645
|
+
const mcpClient = _getReplHostMcp()?.mcpClient;
|
|
4677
6646
|
logger.log(renderMcpSurface(mcpClient));
|
|
4678
6647
|
prompt();
|
|
4679
6648
|
return;
|
|
@@ -4700,7 +6669,12 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
4700
6669
|
} else if (cmd.action === "help") {
|
|
4701
6670
|
logger.log(renderConfigHelp());
|
|
4702
6671
|
} else if (cmd.action === "get") {
|
|
4703
|
-
logger.log(
|
|
6672
|
+
logger.log(
|
|
6673
|
+
renderConfigGet(
|
|
6674
|
+
cmd.key,
|
|
6675
|
+
cm.getConfigValue(cmd.key, { resolveSecrets: false }),
|
|
6676
|
+
),
|
|
6677
|
+
);
|
|
4704
6678
|
} else if (cmd.action === "set") {
|
|
4705
6679
|
cm.setConfigValue(cmd.key, cmd.value);
|
|
4706
6680
|
logger.log(
|
|
@@ -5112,7 +7086,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5112
7086
|
try {
|
|
5113
7087
|
const expanded = await expandMcpPrompt(
|
|
5114
7088
|
promptText,
|
|
5115
|
-
|
|
7089
|
+
_getReplHostMcp()?.mcpClient,
|
|
5116
7090
|
);
|
|
5117
7091
|
if (expanded != null) {
|
|
5118
7092
|
promptText = expanded;
|
|
@@ -5185,7 +7159,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5185
7159
|
}
|
|
5186
7160
|
|
|
5187
7161
|
// Backstop: if the previous turn's agentLoop threw before the success-path
|
|
5188
|
-
// restore ran, undo its /
|
|
7162
|
+
// restore ran, undo its /note-next guidance so it never leaks into this turn's
|
|
5189
7163
|
// history/model call. (Normal turns clear _btwRestore right after agentLoop.)
|
|
5190
7164
|
if (_btwRestore) {
|
|
5191
7165
|
_btwRestore.msg.content = _btwRestore.content;
|
|
@@ -5335,11 +7309,12 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5335
7309
|
try {
|
|
5336
7310
|
const { buildIdePromptContext, expandIdeMentions } =
|
|
5337
7311
|
await import("../lib/ide-context.js");
|
|
5338
|
-
const
|
|
7312
|
+
const hostMcp = _getReplHostMcp();
|
|
7313
|
+
const ideCtx = await buildIdePromptContext(hostMcp);
|
|
5339
7314
|
if (ideCtx) userContent += `\n\n${ideCtx}`;
|
|
5340
7315
|
// Explicit @selection / @diagnostics mentions (Claude-Code parity);
|
|
5341
7316
|
// scan the user's original prompt, append the expansion ephemerally.
|
|
5342
|
-
const mentioned = await expandIdeMentions(effectivePrompt,
|
|
7317
|
+
const mentioned = await expandIdeMentions(effectivePrompt, hostMcp);
|
|
5343
7318
|
for (const w of mentioned.warnings) {
|
|
5344
7319
|
logger.info(chalk.yellow(`[@ide] ${w}`));
|
|
5345
7320
|
}
|
|
@@ -5389,7 +7364,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5389
7364
|
return;
|
|
5390
7365
|
}
|
|
5391
7366
|
|
|
5392
|
-
// Add user message (keep the object ref so
|
|
7367
|
+
// Add user message (keep the object ref so /note-next can be injected for
|
|
5393
7368
|
// this turn's model call and then stripped before persistence).
|
|
5394
7369
|
const _userMsg = { role: "user", content: _userMessageContent };
|
|
5395
7370
|
messages.push(_userMsg);
|
|
@@ -5529,6 +7504,28 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5529
7504
|
/* goal binding is best-effort — fall back to defaultPrepareCall */
|
|
5530
7505
|
}
|
|
5531
7506
|
_turnAbort = new AbortController();
|
|
7507
|
+
if (_advisorRuntime) {
|
|
7508
|
+
_advisorRuntime.beginTurn(
|
|
7509
|
+
`${sessionId || "ephemeral"}:${_turnCount + 1}`,
|
|
7510
|
+
);
|
|
7511
|
+
prepareCall = _advisorRuntime.createPrepareCall({
|
|
7512
|
+
messages,
|
|
7513
|
+
basePrepareCall: prepareCall,
|
|
7514
|
+
subject: promptText,
|
|
7515
|
+
signal: _turnAbort.signal,
|
|
7516
|
+
onAdvice: async (result) => {
|
|
7517
|
+
if (!result.ok) {
|
|
7518
|
+
if (result.effect !== "disabled") {
|
|
7519
|
+
process.stderr.write(` Advisor: ${result.error}\n`);
|
|
7520
|
+
}
|
|
7521
|
+
return;
|
|
7522
|
+
}
|
|
7523
|
+
const { renderAdvisorAdvice } =
|
|
7524
|
+
await import("./advisor-command.js");
|
|
7525
|
+
process.stderr.write(`\n${renderAdvisorAdvice(result)}\n`);
|
|
7526
|
+
},
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
5532
7529
|
// Live streaming hooks: write the answer token-by-token, and stream the
|
|
5533
7530
|
// reasoning dimmed before it. Skipped (left undefined) in replay mode.
|
|
5534
7531
|
let _liveStreamed = false;
|
|
@@ -5553,7 +7550,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5553
7550
|
}
|
|
5554
7551
|
: {};
|
|
5555
7552
|
if (_streamLive) process.stdout.write("\n");
|
|
5556
|
-
// Inject
|
|
7553
|
+
// Inject queued /note-next guidance into THIS turn just before
|
|
5557
7554
|
// the model call. We remember the pre-aside content so it's restored right
|
|
5558
7555
|
// after agentLoop returns — the aside steers this answer but is never
|
|
5559
7556
|
// persisted (saveMessages/JSONL) or carried into later turns. Consumed on
|
|
@@ -5565,7 +7562,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5565
7562
|
_btwRestore = { msg: _userMsg, content: _userMsg.content };
|
|
5566
7563
|
_userMsg.content = applyAside(_userMsg.content, block);
|
|
5567
7564
|
logger.verbose(
|
|
5568
|
-
`[
|
|
7565
|
+
`[note-next] applied ${pendingBtw.length} note(s) to this turn`,
|
|
5569
7566
|
);
|
|
5570
7567
|
}
|
|
5571
7568
|
pendingBtw = [];
|
|
@@ -5582,6 +7579,8 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5582
7579
|
await import("../lib/async-hook-supervisor.js");
|
|
5583
7580
|
_asyncHookSupervisor = new AsyncHookSupervisor({ persistStats: true });
|
|
5584
7581
|
}
|
|
7582
|
+
const activeMcpRuntime = _activateMcpHostRuntime();
|
|
7583
|
+
const activeRawMcpClient = activeMcpRuntime.rawClient || undefined;
|
|
5585
7584
|
const {
|
|
5586
7585
|
content: response,
|
|
5587
7586
|
usageEvents,
|
|
@@ -5653,6 +7652,9 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5653
7652
|
sandbox: _sandbox,
|
|
5654
7653
|
autoCheckpoint,
|
|
5655
7654
|
checkpointSession: sessionId,
|
|
7655
|
+
managedCheckpoint,
|
|
7656
|
+
managedCheckpointStateDir,
|
|
7657
|
+
managedCheckpointExclusions,
|
|
5656
7658
|
checkpointMarks: _checkpointMarks,
|
|
5657
7659
|
// Explicit turn-binding producer (null unless a JSONL session): the
|
|
5658
7660
|
// wrapper folds every loop event into the live table.
|
|
@@ -5683,16 +7685,21 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5683
7685
|
interactiveApproval: true,
|
|
5684
7686
|
// MCP: --mcp-config (ad-hoc) wins; bundle MCP is the fallback. The 3
|
|
5685
7687
|
// tool channels expose --mcp-config servers' tools to the LLM directly.
|
|
5686
|
-
mcpClient:
|
|
7688
|
+
mcpClient: activeRawMcpClient,
|
|
7689
|
+
mcpHostClient: activeMcpRuntime.runtime.client,
|
|
5687
7690
|
extraToolDefinitions: _adhocMcp?.extraToolDefinitions,
|
|
5688
7691
|
externalToolExecutors: _adhocMcp?.externalToolExecutors,
|
|
5689
7692
|
externalToolDescriptors: _adhocMcp?.externalToolDescriptors,
|
|
7693
|
+
// The recovery controller is required even when this legacy/ephemeral
|
|
7694
|
+
// session has no durable sink. Otherwise an outcome-unknown call could
|
|
7695
|
+
// be retried in the same process after agent-core creates a plain ledger.
|
|
7696
|
+
mcpCallLedger: activeMcpRuntime.runtime.ledger,
|
|
5690
7697
|
chatFn: _fallbackChatFn,
|
|
5691
7698
|
});
|
|
5692
7699
|
_persistReplContextSources();
|
|
5693
7700
|
_turnAbort = null;
|
|
5694
7701
|
|
|
5695
|
-
// Strip
|
|
7702
|
+
// Strip one-shot /note-next guidance now the model has seen it so it is never
|
|
5696
7703
|
// persisted (DB saveMessages below) or carried into the next turn.
|
|
5697
7704
|
if (_btwRestore) {
|
|
5698
7705
|
_btwRestore.msg.content = _btwRestore.content;
|
|
@@ -5867,8 +7874,16 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5867
7874
|
if (useJsonl) {
|
|
5868
7875
|
// Append incremental events (user + assistant)
|
|
5869
7876
|
appendUserMessage(sessionId, effectivePrompt);
|
|
7877
|
+
_replCompactPersistence.record({
|
|
7878
|
+
role: "user",
|
|
7879
|
+
content: effectivePrompt,
|
|
7880
|
+
});
|
|
5870
7881
|
if (effectiveResponse) {
|
|
5871
7882
|
appendAssistantMessage(sessionId, effectiveResponse);
|
|
7883
|
+
_replCompactPersistence.record({
|
|
7884
|
+
role: "assistant",
|
|
7885
|
+
content: effectiveResponse,
|
|
7886
|
+
});
|
|
5872
7887
|
}
|
|
5873
7888
|
} else if (db) {
|
|
5874
7889
|
saveMessages(db, sessionId, messages);
|
|
@@ -5903,16 +7918,21 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5903
7918
|
logger.verbose(
|
|
5904
7919
|
`Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
|
|
5905
7920
|
);
|
|
7921
|
+
if (stats.degraded === true) {
|
|
7922
|
+
logger.warn(
|
|
7923
|
+
`Auto-compaction degraded to ${stats.summaryMode}: ${stats.degradedReason}`,
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
5906
7926
|
// Write compact checkpoint to JSONL for crash recovery
|
|
5907
7927
|
if (useJsonl && sessionId) {
|
|
5908
|
-
|
|
7928
|
+
_replCompactPersistence.persist(sessionId, {
|
|
5909
7929
|
...stats,
|
|
5910
7930
|
messages: compacted,
|
|
5911
7931
|
});
|
|
5912
7932
|
}
|
|
5913
7933
|
}
|
|
5914
7934
|
} catch (_e) {
|
|
5915
|
-
|
|
7935
|
+
logger.warn(`Auto-compaction failed: ${_e.message}`);
|
|
5916
7936
|
}
|
|
5917
7937
|
}
|
|
5918
7938
|
|
|
@@ -5993,6 +8013,15 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
5993
8013
|
|
|
5994
8014
|
rl.on("line", async (input) => {
|
|
5995
8015
|
if (_processingLine) {
|
|
8016
|
+
const concurrentBtw = _slashCommandsDisabled
|
|
8017
|
+
? null
|
|
8018
|
+
: parseBtwCommand(input.trim());
|
|
8019
|
+
if (concurrentBtw) {
|
|
8020
|
+
// Intentionally do not await: the main turn keeps streaming while this
|
|
8021
|
+
// independent, tool-free snapshot call runs alongside it.
|
|
8022
|
+
void runBtwSideQuestion(concurrentBtw, { concurrent: true });
|
|
8023
|
+
return;
|
|
8024
|
+
}
|
|
5996
8025
|
if (input.trim()) {
|
|
5997
8026
|
_pendingLines.push(input);
|
|
5998
8027
|
logger.log(
|
|
@@ -6057,7 +8086,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
6057
8086
|
if (useJsonl) {
|
|
6058
8087
|
_persistReplContextSources();
|
|
6059
8088
|
// JSONL: write final compact snapshot for fast rebuild
|
|
6060
|
-
|
|
8089
|
+
_replCompactPersistence.persist(sessionId, {
|
|
6061
8090
|
strategy: "session-end",
|
|
6062
8091
|
messages,
|
|
6063
8092
|
});
|
|
@@ -6065,7 +8094,7 @@ async function startAgentReplInWorkspace(options = {}) {
|
|
|
6065
8094
|
saveMessages(db, sessionId, messages);
|
|
6066
8095
|
}
|
|
6067
8096
|
} catch (_e) {
|
|
6068
|
-
|
|
8097
|
+
logger.warn(`Session-end compact not persisted: ${_e.message}`);
|
|
6069
8098
|
}
|
|
6070
8099
|
}
|
|
6071
8100
|
// Auto-summarize session into permanent memory
|