@overdeck/desktop 0.45.15 → 0.45.18
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/dist-electron/main.js +149 -158
- package/dist-electron/main.js.map +1 -1
- package/dist-electron/preload.js +17 -2
- package/dist-electron/preload.js.map +1 -1
- package/package.json +5 -4
- package/server/acceptance-criteria-aQFKQf-J.js +62 -0
- package/server/{advancing-selfheal-CSuZ31bB.js → advancing-selfheal-DgwQgm4D.js} +6 -5
- package/server/{agent-gc-D_DxfBHR.js → agent-gc-kpzxqT7P.js} +3 -3
- package/server/agent-state-C_6TG-Ad.js +3 -0
- package/server/{agent-state-service-C_2ZAQRP.js → agent-state-service-BUfovFfB.js} +1 -1
- package/server/{agent-state-service-BZkvgQf4.js → agent-state-service-nUD3qfob.js} +9 -52
- package/server/agents-BA15AQqo.js +3 -0
- package/server/{briefing-freshness-Kh5V8RTh.js → agents-BHBxr8c9.js} +5 -131
- package/server/{archive-planning-DkivCk1A.js → archive-planning-CzCW9Ifl.js} +5 -10
- package/server/{archive-planning-CTILQRnV.js → archive-planning-D-QsBufn.js} +1 -1
- package/server/{artifact-index-jobs-CmsvQ1ey.js → artifact-index-jobs-qqa51B2Z.js} +2 -2
- package/server/{backlog-auto-trigger-ueRaT8Dy.js → backlog-auto-trigger-B7Bi0bvY.js} +3 -3
- package/server/behavior-CWqNMqE5.js +10 -0
- package/server/briefing-freshness-DuxIE1yr.js +131 -0
- package/server/{checkpoint-manager-fu9oVsR-.js → checkpoint-manager-DF6hb9Jw.js} +3 -3
- package/server/{checkpoint-manager-CAFos8e1.js → checkpoint-manager-DixkLus7.js} +1 -1
- package/server/checkpoint-worker.js +1 -1
- package/server/{checkpoints-DvhGi8P_.js → checkpoints-B1Z42tJE.js} +1 -1
- package/server/{checkpoints-1BqOlS4q.js → checkpoints-BiviJqei.js} +2 -2
- package/server/claude-hooks-provision-BeMGi4ty.js +3 -0
- package/server/{claude-hooks-provision-s_wdpeVj.js → claude-hooks-provision-Du_PkMsp.js} +132 -114
- package/server/{claude-permissions-CaWqysff.js → claude-permissions-BB6agcOZ.js} +2 -2
- package/server/{claude-settings-overlay-CcTf0xzG.js → claude-settings-overlay-CAq2oFSK.js} +2 -2
- package/server/{claude-settings-overlay-BbA4FbVI.js → claude-settings-overlay-DUWWLd6M.js} +1 -1
- package/server/cli-BFgrSWKV.js +3 -0
- package/server/{cli-BwCvCUBK.js → cli-C33xPZNm.js} +9 -9
- package/server/{cliproxy-C2xC-3vH.js → cliproxy-BQX1TL6N.js} +1 -1
- package/server/{cliproxy-BsoNNDAj.js → cliproxy-CGBA1GzA.js} +2 -2
- package/server/{close-issue-BleIwP3f.js → close-issue-DNv8npc1.js} +5 -5
- package/server/{closed-issue-reaper-BwUUvjEW.js → closed-issue-reaper-DrqgBJlr.js} +1 -1
- package/server/{closed-issue-reaper-mC5pujeb.js → closed-issue-reaper-LoMnnjtJ.js} +9 -9
- package/server/codex-DxLivxri.js +3 -0
- package/server/{behavior-Cpj03N4v.js → codex-parser-DboQnEXN.js} +3 -9
- package/server/{codex-DW60_5j2.js → codex-sgWVCGUd.js} +43 -17
- package/server/{config-BRNQJXhA.js → config-6MCHOZ2N.js} +1 -1
- package/server/{config-ZNnMpIlh.js → config-DfaUXr6r.js} +2 -2
- package/server/{config-yaml-Dcz-8uit.js → config-yaml-CqaAyn8y.js} +149 -80
- package/server/{conflict-gate-DTje2Ll7.js → conflict-gate-BF_zhcmb.js} +3 -3
- package/server/{conflict-gate-DGT8jCLp.js → conflict-gate-BubJprtD.js} +1 -1
- package/server/{context-layers-BdUCQ-cl.js → context-layers-BFrxfl0l.js} +17 -3
- package/server/{context-layers-ByOWUjUg.js → context-layers-DWL_8HhY.js} +2 -2
- package/server/{control-settings-DwkAeooY.js → control-settings-BguNQ1F_.js} +4 -4
- package/server/{conversation-compaction-WYQZUVDm.js → conversation-compaction-CRKAf2qP.js} +1 -1
- package/server/{conversation-compaction-OTYgd6eg.js → conversation-compaction-DxSXl0pT.js} +8 -8
- package/server/{conversation-service-C_kNZPLx.js → conversation-service-DzuY9qoH.js} +1 -1
- package/server/{conversation-service-BR1OgvxO.js → conversation-service-mFVFdDQK.js} +6 -5
- package/server/{conversations-DG2M8WnT.js → conversations-BX5aD-6F.js} +4 -4
- package/server/{conversations-izHvSoAR.js → conversations-BajHQ60c.js} +1 -1
- package/server/{cost-DQ2ajr_m.js → cost-BoqD1Pxz.js} +2 -2
- package/server/{cost-B2mj17iG.js → cost-DuZ5vlv-.js} +3 -3
- package/server/{cost-sync-gTayEuWP.js → cost-sync-BYnmnMkY.js} +3 -3
- package/server/{dag-CqTehWpY.js → dag-C6kYyF17.js} +7 -243
- package/server/{dangerous-git-ops-DDWQlPC3.js → dangerous-git-ops-q5OPtXMd.js} +1 -1
- package/server/dashboard-db-worker.js +6 -6
- package/server/{deacon-BqW9HMew.js → deacon-CwCiNuEZ.js} +3 -3
- package/server/{deacon-DrKDBeJL.js → deacon-Dru5onpk.js} +258 -167
- package/server/{deacon-nudge-log-D1wEK6zF.js → deacon-nudge-log-ilDfxrhK.js} +1 -1
- package/server/{deacon-swarm-CO_A7xyd.js → deacon-swarm-BPmBmSoU.js} +298 -72
- package/server/deacon-swarm-CKyGJng1.js +2 -0
- package/server/deacon.js +14 -7
- package/server/{devcontainer-renderer-BQolp_qZ.js → devcontainer-renderer-BqGNCzvN.js} +3 -3
- package/server/{dist-Bc32rWJB.js → dist-BwlEiLCU.js} +1 -1
- package/server/dist-Bx4KkgYL.js +3 -0
- package/server/{dist-src-Byxt1ifL.js → dist-src-BE0Denfs.js} +1 -1
- package/server/{docker-s-esqZ__.js → docker-CNjXS72x.js} +2 -2
- package/server/{ensure-devcontainer-DA4-jF6R.js → ensure-devcontainer-5e9xOve4.js} +4 -4
- package/server/{ensure-devcontainer-pUVaT0h3.js → ensure-devcontainer-C6gyUQdE.js} +1 -1
- package/server/{esm-shims-CJHy8CZY.js → esm-shims-Cso7IGBC.js} +1 -1
- package/server/{event-store-CcJAhKCl.js → event-store-D9UhQyMS.js} +1 -1
- package/server/{event-store-DRte2Rwt.js → event-store-KZnWUKWh.js} +3 -3
- package/server/{events-Bej1w_k8.js → events-2eFUPdqz.js} +3 -3
- package/server/{events-CtsLScn8.js → events-BYbvqZky.js} +1 -1
- package/server/{factory-FN727tWr.js → factory-BtaRsHFC.js} +20 -17
- package/server/{feature-registry-population-ie-yBcQC.js → feature-registry-population-Ca2UaV0U.js} +4 -4
- package/server/{feature-registry-storage-CkZTFSsn.js → feature-registry-storage-DaP-bQxO.js} +2 -2
- package/server/{features-Dtm9ABuz.js → features-DqvaoQoa.js} +2 -2
- package/server/{feedback-target-BYtpyt1i.js → feedback-target-BXWdOiYI.js} +9 -7
- package/server/{feedback-target-DVJQDiE4.js → feedback-target-BblmMe9P.js} +1 -1
- package/server/{feedback-writer-DOUhyIW2.js → feedback-writer-CULeOOY4.js} +1 -1
- package/server/{feedback-writer-fdE1BzeL.js → feedback-writer-Dn8o9ZyE.js} +4 -4
- package/server/{flywheel-Bi82iiNl.js → flywheel-BUWdcfhq.js} +1 -1
- package/server/{flywheel-Bv3wv2S6.js → flywheel-CqkO3cDj.js} +15 -28
- package/server/{flywheel-CUErLAyN.js → flywheel-D9r1mw2U.js} +13 -13
- package/server/{flywheel-merge-order-DXjeBnJ1.js → flywheel-merge-order-LDzQDwSg.js} +1 -1
- package/server/{flywheel-merge-order-BaEN3iyN.js → flywheel-merge-order-VtjUweEb.js} +7 -7
- package/server/forge-DJy3Zx4s.js +2 -0
- package/server/{forge-ButEdJ31.js → forge-wEollMCi.js} +22 -12
- package/server/{from-CbtTxftl.js → from-B2tOYhYV.js} +1 -1
- package/server/{fts-db-CBgBLZcL.js → fts-db-Bgzn2RO7.js} +2 -2
- package/server/{fts-operations-CI9Xuo_1.js → fts-operations-Bj8yutnR.js} +2 -2
- package/server/{git-utils-ClfpP47N.js → git-utils-GxlL0tmt.js} +2 -2
- package/server/{git-utils-BCLRWQ-y.js → git-utils-q8jNoBts.js} +1 -1
- package/server/{github-app-DRD2woO0.js → github-app-BrYwK_Gb.js} +1 -1
- package/server/{github-app-DscZ0_B6.js → github-app-tUbb8Ldr.js} +8 -3
- package/server/{github-client-DpiZnxGH.js → github-client-BwBU41jK.js} +3 -3
- package/server/{github-client-iFRw8f7e.js → github-client-D6CNsftm.js} +1 -1
- package/server/{github-pr-lookup-D-EBexxC.js → github-pr-lookup-BS5PqkJb.js} +2 -2
- package/server/{harness-resolve-iMjyNUJ2.js → harness-resolve-Dx8hUsvC.js} +3 -3
- package/server/{health-events-CRdxDFN2.js → health-events-2DLp-5Xp.js} +2 -2
- package/server/{health-events-B_WsQyYm.js → health-events-B0mj6Iae.js} +1 -1
- package/server/{hooks-C1ScyVxP.js → hooks-BDcQJDUe.js} +1 -1
- package/server/{hooks-C9b7Vqpw.js → hooks-BS_JrvYP.js} +3 -3
- package/server/hume-B4seN95Y.js +3 -0
- package/server/{hume-TQ5pvOZ_.js → hume-BRhNKyXQ.js} +2 -2
- package/server/{idle-stack-reaper-B1kG7B_X.js → idle-stack-reaper-BCITXNW5.js} +2 -2
- package/server/{idle-stack-reaper-iFVOAoJy.js → idle-stack-reaper-C1ymihno.js} +1 -1
- package/server/{infra-bpq1MHKU.js → infra-_Kx1m9pm.js} +9 -4
- package/server/{injection-BLWfwTVZ.js → injection-Bvhyg_MV.js} +1 -1
- package/server/{injection-DJqWld1v.js → injection-CCaYkofq.js} +7 -7
- package/server/{inspect-agent-DytCsltn.js → inspect-agent-BqyZ4pCo.js} +35 -67
- package/server/inspect-agent-DCJQm9Wu.js +2 -0
- package/server/{internal-token-CjlhdufS.js → internal-token-Cl-1IxaE.js} +2 -2
- package/server/{internal-token-CCaWaTwE.js → internal-token-D-t28de5.js} +1 -1
- package/server/{io-BiQ2Cs3k.js → io-DkC4sFSy.js} +42 -56
- package/server/{issue-closed-FApNiOOs.js → issue-closed-Cpl4VDUM.js} +41 -6
- package/server/{issue-lifecycle-C-K53ASY.js → issue-lifecycle-CFR8TJ22.js} +22 -37
- package/server/{issue-lifecycle-BkFWNjlD.js → issue-lifecycle-YNTy3OT3.js} +1 -1
- package/server/{issue-service-singleton-bG1WdSdQ.js → issue-service-singleton-D81mCts_.js} +1 -1
- package/server/{issue-service-singleton-CuHGpJJP.js → issue-service-singleton-ldvUUXi3.js} +29 -29
- package/server/{issues-CwQMphDH.js → issues-DIEHt9sq.js} +2 -2
- package/server/{jsonl-parser-dNIjHgym.js → jsonl-parser-Cd5jIqh7.js} +3 -3
- package/server/{label-cleanup-DYvAZknd.js → label-cleanup-Btny6Gyo.js} +3 -3
- package/server/{state-home-CRowbXqU.js → launcher-generator-BXLM9f4b.js} +7 -127
- package/server/{layers-CD6FysRU.js → layers-gCJlK62m.js} +2 -2
- package/server/lifecycle-D7aQUnTO.js +12 -0
- package/server/{lifecycle-io-C_Wu0_i-.js → lifecycle-io-DQBF-GHO.js} +1 -1
- package/server/{lifecycle-io-CeYxP3cE.js → lifecycle-io-FfPUtz-y.js} +36 -47
- package/server/{linear-client-EtVyblVz.js → linear-client-CDH2D6Qy.js} +3 -3
- package/server/{linear-client-4qoKnuQu.js → linear-client-WrYrUIAH.js} +1 -1
- package/server/memory-fts-worker.js +1 -1
- package/server/{merge-7rqkTUm2.js → merge-BsDgUxFz.js} +3 -3
- package/server/{merge-brm721Es.js → merge-Dervs4BF.js} +1 -1
- package/server/{merge-agent-4Zhlgvqp.js → merge-agent-D6DNhe9j.js} +64 -51
- package/server/{merge-agent-36wfYm2u.js → merge-agent-DMQTJMP0.js} +1 -1
- package/server/{merge-batch-itfnwdsT.js → merge-batch-Bv6Z5K2H.js} +1 -1
- package/server/{merge-blockers-Bto_Oe_o.js → merge-blockers-WWWi6Ur2.js} +2 -2
- package/server/merge-ops-h9OCnIVz.js +2 -0
- package/server/{merge-rebase-Czfqfe4q.js → merge-rebase-Czth9ggm.js} +2 -2
- package/server/{merge-set-BIkGQL9j.js → merge-set-DNc4zBqd.js} +3 -3
- package/server/{merge-set-G1RHcu3D.js → merge-set-vuE3MEVt.js} +1 -1
- package/server/{merge-sync-B43Dybea.js → merge-sync-B2KSk54U.js} +2 -2
- package/server/{merge-sync-DIrmlZUr.js → merge-sync-B9nHdUGx.js} +1 -1
- package/server/{merge-train-DGqVuNZV.js → merge-train-DJgr81tF.js} +3 -3
- package/server/{merge-train-deps-q15XLnAS.js → merge-train-deps-B2OWS0k9.js} +4 -4
- package/server/{merge-verification-Cqfxx6bE.js → merge-verification-BBIXUEqa.js} +4 -4
- package/server/{multipart-parser-CiPxxfUH.js → multipart-parser-9DhuAkPd.js} +2 -2
- package/server/{no-resume-mode-yrPEQ_pC.js → no-resume-mode-Gh5R_YLF.js} +1 -1
- package/server/node_modules/.package-lock.json +11 -11
- package/server/node_modules/@effect/platform-node/node_modules/@effect/platform-node-shared/package.json +3 -3
- package/server/node_modules/toml/README.md +54 -0
- package/server/node_modules/toml/index.d.ts +42 -1
- package/server/node_modules/toml/index.js +3 -3
- package/server/node_modules/toml/lib/compiler.js +35 -3
- package/server/node_modules/toml/lib/parser.js +224 -143
- package/server/node_modules/toml/package.json +3 -1
- package/server/node_modules/ws/lib/receiver.js +15 -32
- package/server/node_modules/ws/lib/websocket-server.js +4 -4
- package/server/node_modules/ws/lib/websocket.js +4 -4
- package/server/node_modules/ws/package.json +5 -1
- package/server/{orphan-proposed-reconciler-BWWHn4Lr.js → orphan-proposed-reconciler-B8H41C4a.js} +14 -24
- package/server/{orphan-proposed-reconciler-DAD9FJtQ.js → orphan-proposed-reconciler-DbTmT7qy.js} +1 -1
- package/server/package-lock.json +12 -12
- package/server/package.json +1 -1
- package/server/{pan-dir-Bj7Hy8iu.js → pan-dir-BTw4MdSe.js} +4 -4
- package/server/{paths-C9DkhIOT.js → paths-Btkc8mzp.js} +2 -2
- package/server/{pickup-CKRuGDsB.js → pickup-BtvRBbvI.js} +1 -1
- package/server/{pipeline-notifier-BkKbQ8ji.js → pipeline-notifier-3frIvSi3.js} +1 -1
- package/server/{pipeline-notifier-CAurUste.js → pipeline-notifier-DTd19f18.js} +2 -2
- package/server/{project-pipelines-BRWaGmI4.js → project-pipelines-B95fcAek.js} +2 -2
- package/server/projects-Dc9aoVoG.js +3 -0
- package/server/{prompts-DfI5qbTW.js → prompts-CIVmjHxn.js} +2 -2
- package/server/{providers-D-5txBRc.js → providers-BLLk5IIb.js} +1 -1
- package/server/{providers-CWDXoaMA.js → providers-C-yWibCk.js} +7 -7
- package/server/public/assets/chunk-BO2N2NFS-DzKZyoKY.js +103 -0
- package/server/public/assets/{diagram-OG6HWLK6-Avtvq2qI.js → diagram-OG6HWLK6-Cx8mWUb8.js} +1 -1
- package/server/public/assets/{dist-BEHQuasD.js → dist-BqccLhaw.js} +3 -3
- package/server/public/assets/{dist-BvmY-qb1.js → dist-rC3nAs_O.js} +1 -1
- package/server/public/assets/{ganttDiagram-6RSMTGT7-7KhCd0fH.js → ganttDiagram-6RSMTGT7-DLYPLf5T.js} +1 -1
- package/server/public/assets/{highlighted-body-OFNGDK62-iQoBMTf0.js → highlighted-body-OFNGDK62-D4DqixbV.js} +1 -1
- package/server/public/assets/index-CPcysUHk.js +796 -0
- package/server/public/assets/index-H5M0yhM4.css +1 -0
- package/server/public/assets/{linear-CiJHW9g5.js → linear-yqcXkDfj.js} +1 -1
- package/server/public/assets/mermaid-GHXKKRXX-CWdZ_heB.js +1 -0
- package/server/public/assets/{pieDiagram-4H26LBE5-BOhmvUhW.js → pieDiagram-4H26LBE5-DhraIOaV.js} +1 -1
- package/server/public/assets/{prod-DmLBBqat.js → prod-DKUunUdN.js} +4 -4
- package/server/public/assets/{quadrantDiagram-W4KKPZXB-Cjku660G.js → quadrantDiagram-W4KKPZXB-DPiRcToa.js} +1 -1
- package/server/public/assets/{sankeyDiagram-5OEKKPKP-D52w5oPh.js → sankeyDiagram-5OEKKPKP-3An5HNkZ.js} +1 -1
- package/server/public/assets/{xychartDiagram-2RQKCTM6-BTO07_Iv.js → xychartDiagram-2RQKCTM6-lbEOjfqQ.js} +1 -1
- package/server/public/index.html +2 -2
- package/server/{pull-requests-CKZPWhjD.js → pull-requests-D7AL5woD.js} +1 -1
- package/server/{pull-requests-Bmre-Xlz.js → pull-requests-NDDNtqNq.js} +3 -3
- package/server/{agents-CsOzAmRg.js → queries-C0LCGG21.js} +743 -399
- package/server/{rally-BBd9tQ__.js → rally-4oKvnxLG.js} +3 -3
- package/server/rally-DF5a3ioP.js +3 -0
- package/server/{rally-api-DxZFObI_.js → rally-api-41tT6ikZ.js} +2 -2
- package/server/{rally-api-DDc85HIz.js → rally-api-BsaQ8Vnz.js} +1 -1
- package/server/{rally-client-DvvLiAQP.js → rally-client-Bo4hNcCZ.js} +5 -5
- package/server/{rally-client-BADHn0m_.js → rally-client-b5PT1x5L.js} +1 -1
- package/server/{reap-terminal-sessions-D_xvy-Mk.js → reap-terminal-sessions-BR39qflC.js} +1 -1
- package/server/{reap-terminal-sessions-D6JDJsqF.js → reap-terminal-sessions-CFkuquQj.js} +1 -1
- package/server/{rebuild-stack-GOsWqXi3.js → rebuild-stack-C7JPUom6.js} +1 -1
- package/server/{rebuild-stack-D19i080j.js → rebuild-stack-Ddv6W017.js} +6 -6
- package/server/{reconstruct-cache-lQrG4xGg.js → reconstruct-cache-Gqhm---K.js} +6 -6
- package/server/record-DLOqfbkI.js +3 -0
- package/server/record-update-BltEmPge.js +168 -0
- package/server/record-update-CDp7aTaT.js +3 -0
- package/server/recovery-trip-Cn7KJVkS.js +44 -0
- package/server/{release-engine-DxcMWRuK.js → release-engine-BYzmIBf9.js} +5 -5
- package/server/{release-set-BYsgX7o6.js → release-set-7pnxFOWR.js} +3 -3
- package/server/{remote-DmeL21dF.js → remote-Cvu4CSDk.js} +3 -3
- package/server/{remote-agents-BfQBE6MN.js → remote-agents-C2C6z9tj.js} +6 -33
- package/server/{remote-agents-DlvbzO7m.js → remote-agents-DYY5fFpd.js} +1 -1
- package/server/{remote-completion-BgkiSQmx.js → remote-completion-2Kb4IDg_.js} +13 -13
- package/server/{remote-8W2Yhygw.js → remote-rN0ecjHE.js} +3 -3
- package/server/resume-BFLl-yLn.js +3 -0
- package/server/{review-agent-D025Wt7w.js → review-agent-BZvk0N5G.js} +31 -23
- package/server/{review-agent-Rub0OpZc.js → review-agent-CpSHLzed.js} +1 -1
- package/server/{review-artifacts-CX951jTU.js → review-artifacts-PCo53YMn.js} +5 -19
- package/server/{review-context-DGGGR_w5.js → review-context-M7QtJlJq.js} +8 -8
- package/server/{review-monitor-0_qqMcnS.js → review-monitor-BLC5mTTg.js} +2 -2
- package/server/{review-status-Br6lAFGO.js → review-status-77N3PoZr.js} +1 -1
- package/server/{review-status-4qqLkRMu.js → review-status-DANr0t-v.js} +33 -59
- package/server/{review-status-enrichment-Cr8m3uVL.js → review-status-enrichment-DwOg4UfL.js} +3 -3
- package/server/{review-status-sync-D3KUhsdA.js → review-status-sync-Bd08OOFA.js} +1 -1
- package/server/{review-status-sync-OC-l1lNt.js → review-status-sync-BjTUJ5fx.js} +2 -2
- package/server/{review-verdict-feedback-YOzmsq9G.js → review-verdict-feedback-BsFLjN_6.js} +1 -1
- package/server/{review-verdict-feedback-DrWMuRaj.js → review-verdict-feedback-CBf-J_wS.js} +8 -8
- package/server/{rollup-CnxbwaLH.js → rollup-Bu0f2EyN.js} +8 -8
- package/server/{rollup-DmazRY0j.js → rollup-DTCCdEy4.js} +1 -1
- package/server/{scanner-DE1MqAaF.js → scanner-BlZ2vB7j.js} +7 -7
- package/server/{sequence-io-C0uoqfGx.js → sequence-io-ylyDtz2J.js} +4 -4
- package/server/{sequencer-agent-i-b2u4SJ.js → sequencer-agent-DsWQ-87H.js} +6 -17
- package/server/server.js +5670 -6392
- package/server/{service-D0_gh2tl.js → service-CLVBC_1Y.js} +116 -90
- package/server/{session-fork-DZ12vipL.js → session-fork-DlEIAhRy.js} +2 -2
- package/server/session-history-bnOFKMwf.js +168 -0
- package/server/settings-CP3GGon2.js +2 -0
- package/server/{settings-ByqjPtFO.js → settings-Cqa-8nyj.js} +2 -2
- package/server/{settings-DCC4BAqj.js → settings-gJrdLZy2.js} +3 -3
- package/server/{shadow-engineering-BhnmtW5m.js → shadow-engineering-fq1YEKV3.js} +3 -3
- package/server/{shadow-state-ByXZehsy.js → shadow-state-BqeFAygK.js} +1 -1
- package/server/{shadow-state-CKm8xWX9.js → shadow-state-RptE9Cau.js} +2 -2
- package/server/shadow-utils-BMWr5tRT.js +39 -0
- package/server/{ship-log-DE_0Cv_i.js → ship-log-BRQQi2lU.js} +1 -1
- package/server/{smart-compaction-D5Zen3AA.js → smart-compaction-CmoXpjeU.js} +7 -7
- package/server/{smart-compaction-h4_ca6Uk.js → smart-compaction-DldUel4B.js} +1 -1
- package/server/{spawn-planning-session-Dueit4WX.js → spawn-planning-session-CoS6LTc6.js} +16 -16
- package/server/{spawn-planning-session-Bgi7Ub02.js → spawn-planning-session-DT2tSMxo.js} +1 -1
- package/server/{specialist-completion-BZ8M-WZ0.js → specialist-completion-pJtESo7m.js} +1 -1
- package/server/{specialist-context-BMq83C58.js → specialist-context-CspI4Eog.js} +5 -5
- package/server/{specialist-handoff-logger-BxX1YefH.js → specialist-handoff-logger-Duz9RGvk.js} +2 -2
- package/server/{specialist-logs-BWZYcJ6P.js → specialist-logs-DtSkPJZK.js} +3 -3
- package/server/{specialists-ZkW4Mb7U.js → specialists-CywEMwxk.js} +1 -1
- package/server/{specialists-8ZAguXef.js → specialists-D9-dMUtI.js} +1 -1
- package/server/{specialists-BudKrjOu.js → specialists-Dax7tqXC.js} +1817 -560
- package/server/{specialists-DrPrC9Y9.js → specialists-GF42OOqd.js} +18 -18
- package/server/specs-VwcFtEDi.js +3 -0
- package/server/{src-DHn4Rd2F.js → src-By3D85Ry.js} +77 -1
- package/server/{src-Dv6L03vW.js → src-CqyFg52C.js} +5 -5
- package/server/{src-DNUUqkec.js → src-DhxM-eCb.js} +6 -6
- package/server/{stack-health-Dclm7_tu.js → stack-health-BsEVrJZ6.js} +1 -1
- package/server/{stack-health-DxsTx7II.js → stack-health-C-SnpRHh.js} +2 -2
- package/server/stale-task-claims-vTxmRzLi.js +69 -0
- package/server/state-home-Bur1y4oH.js +213 -0
- package/server/{state-mapping-BvAgFK8n.js → state-mapping-Ci0pkac4.js} +1 -1
- package/server/state-migrate-D3VveYfO.js +362 -0
- package/server/{stuck-remediation-CFWUP8_I.js → stuck-remediation-B699-p8v.js} +63 -46
- package/server/{stuck-remediation-CLHd-lrF.js → stuck-remediation-CaFYYTQW.js} +1 -1
- package/server/{summary-fork-CKc01Our.js → summary-fork-BxnF8Xh0.js} +2 -2
- package/server/{summary-fork-CtCwBJ2Z.js → summary-fork-COqYpVa3.js} +8 -8
- package/server/supervisor/{paths-3R6b989k.js → paths-Bk6hJRH9.js} +4 -3
- package/server/supervisor/pty-supervisor.js +2 -2
- package/server/supervisor/{pty-token-CScQ32Rj.js → pty-token-BZS5GYdG.js} +2 -2
- package/server/supervisor-eWAAmXKu.js +2 -0
- package/server/{supervisor-BA5D72Qa.js → supervisor-liMgIPDF.js} +2 -2
- package/server/{swarm-readiness-CL5sM9B1.js → swarm-readiness-sXB1vhzo.js} +3 -3
- package/server/{system-prerequisites-DmFSXIE5.js → system-prerequisites-Bz6KdkFJ.js} +1 -13
- package/server/{teardown-workspace-DWmFJzTw.js → teardown-workspace-DvzqnmUF.js} +14 -71
- package/server/{test-agent-queue-DjsQoV5X.js → test-agent-queue-hAU1Ah08.js} +6 -6
- package/server/{test-verdict-BSqsQp3G.js → test-verdict-CRsPpByy.js} +1 -1
- package/server/{test-verdict-D1AmZ2c0.js → test-verdict-CXMLATGc.js} +1 -1
- package/server/{tldr-daemon-C-MGomL8.js → tldr-daemon-Bam7Xhoo.js} +2 -2
- package/server/{tldr-daemon-DD9rI5gA.js → tldr-daemon-DhGwd88s.js} +1 -1
- package/server/tmux-C-rgxRDA.js +3 -0
- package/server/{tmux-C_d-lSIq.js → tmux-U55cNGFG.js} +92 -4
- package/server/{token-DfBf1bsd.js → token-Dsa-i36o.js} +2 -2
- package/server/{token-util-CROoF5ic.js → token-util-BWNpH96m.js} +1 -1
- package/server/token-util-DRt0j2nQ.js +3 -0
- package/server/{tracker-config-CNODpd11.js → tracker-config-OkdbaUCm.js} +2 -2
- package/server/{tracker-utils-Ch2_5GGf.js → tracker-utils-CghXMF3q.js} +2 -2
- package/server/{tracker-utils-B9ZoVA9b.js → tracker-utils-DdzUiPNU.js} +1 -1
- package/server/{traefik-connect-BF4x_LUQ.js → traefik-connect-CZYB3dlm.js} +1 -1
- package/server/{tunnel-r_5nHAI6.js → tunnel-D1YxesBL.js} +3 -3
- package/server/{tunnel-YD80_IHr.js → tunnel-DjlzgTl3.js} +1 -1
- package/server/typed-errors-CH4roFIj.js +3 -0
- package/server/{typed-errors-BEJhbjJg.js → typed-errors-n_Tnb4Fh.js} +3 -4
- package/server/{types-CE1m_daD.js → types-DOjWNv3Z.js} +1 -1
- package/server/{types-33JEP4LZ.js → types-DXBMm4mX.js} +1 -1
- package/server/{uat-train-DDI00aqD.js → uat-train-CaaWs9Rp.js} +1 -1
- package/server/{uat-train-DjkFWXge.js → uat-train-CqN34zFx.js} +24 -25
- package/server/{validation-DUTiJnyz.js → validation-4-z2Cw4z.js} +70 -23
- package/server/validation-DpTNakM6.js +3 -0
- package/server/verification-artifact-DK1xb1zG.js +56 -0
- package/server/verification-artifact-JD_edpZ9.js +2 -0
- package/server/{verification-runner-inLgeQux.js → verification-runner-BjHN80pJ.js} +338 -58
- package/server/verification-runner-ShYu5Ph3.js +2 -0
- package/server/{work-agent-prompt-fZljjA1_.js → work-agent-prompt-BiJ43Der.js} +12 -88
- package/server/workflows-B4C3lY9E.js +2 -0
- package/server/{workflows-oBnJ7Tm0.js → workflows-DkTotz_m.js} +17 -24
- package/server/{workspace-CNuW2j41.js → workspace-CuN0ev4Z.js} +3 -3
- package/server/{workspace-B2WOW9Hw.js → workspace-DQ4vVivd.js} +1 -1
- package/server/{workspace-config-CBgMGhWs.js → workspace-config-C77NKnsV.js} +1 -1
- package/server/{workspace-manager-DjUAJCSs.js → workspace-manager-B1mYn0lN.js} +1 -1
- package/server/{workspace-manager-CB-R07R3.js → workspace-manager-Beeo2aKF.js} +23 -48
- package/server/{workspace-metadata-9Nn5kgRl.js → workspace-metadata-BZC5BmoP.js} +2 -2
- package/server/{workspace-metadata-Fd58uUQG.js → workspace-metadata-Cy7_ZACL.js} +1 -1
- package/server/{workspaces-BjWXntty.js → workspaces-CGuzW1Ia.js} +191 -153
- package/sync-sources/hooks/ask-user-question-hook +7 -1
- package/sync-sources/hooks/codex-notify-hook +64 -8
- package/sync-sources/hooks/record-cost-event.js +1109 -36
- package/sync-sources/hooks/record-cost-event.js.map +1 -1
- package/sync-sources/hooks/statusline.sh +2 -2
- package/sync-sources/hooks/work-agent-stop-hook +1 -2
- package/server/acceptance-criteria-DHZk2ceV.js +0 -33
- package/server/agent-runtime-mirror-DrWxopNA.js +0 -37
- package/server/agent-state-EP2QFV9n.js +0 -3
- package/server/agents-DCvyTXNU.js +0 -3
- package/server/auto-commit-QAfsu3Sb.js +0 -3
- package/server/bd-process-lock-D3S4T4o-.js +0 -390
- package/server/beads-BGGxrkZ5.js +0 -649
- package/server/beads-CYsLiyIM.js +0 -2
- package/server/beads-query-B1DlJVp9.js +0 -95
- package/server/beads-restore-CyygSmmn.js +0 -23
- package/server/cli-ClIQwgMI.js +0 -3
- package/server/codex-B7DTpwEw.js +0 -3
- package/server/compact-beads-CK_hHEnB.js +0 -3
- package/server/compact-beads-DKL54y6j.js +0 -94
- package/server/deacon-swarm-BY5hKNaU.js +0 -2
- package/server/dist-DGTwcZhI.js +0 -3
- package/server/export-Dx_qJ3h8.js +0 -87
- package/server/forge-XlDgPQWs.js +0 -2
- package/server/hume-K1pi8jC4.js +0 -3
- package/server/inspect-agent-477VxQim.js +0 -2
- package/server/lifecycle-C_73gi-C.js +0 -14
- package/server/merge-ops-PzqsW69V.js +0 -2
- package/server/projects-KJ94e50I.js +0 -3
- package/server/public/assets/chunk-BO2N2NFS-ClukPsZQ.js +0 -103
- package/server/public/assets/index-DUFDmvmH.css +0 -1
- package/server/public/assets/index-DnTZ2QeN.js +0 -796
- package/server/public/assets/mermaid-GHXKKRXX-DZJOwgBG.js +0 -1
- package/server/rally-Dc2Wnxw2.js +0 -3
- package/server/record-pyB6VA32.js +0 -3
- package/server/recovery-trip-Dfh1tNHn.js +0 -98
- package/server/resolver-dTpSSzaH.js +0 -158
- package/server/resume-DhcIqsTm.js +0 -3
- package/server/settings-yydgaqhv.js +0 -2
- package/server/specs-DwoAEGrW.js +0 -3
- package/server/supervisor-DHjumXlL.js +0 -2
- package/server/tmux-x291Gjjb.js +0 -3
- package/server/token-util-xNaV-00f.js +0 -3
- package/server/tracker-config-DItsdelv.js +0 -3
- package/server/typed-errors-DSPn_xWP.js +0 -3
- package/server/validation-ict8hnXk.js +0 -3
- package/server/verification-runner-ATaLbpr5.js +0 -2
- package/server/workflows-tU7KKxQi.js +0 -2
- package/server/writer-CQMGzEkw.js +0 -3
- package/server/writer-fe1-8iGc.js +0 -251
|
@@ -1,46 +1,53 @@
|
|
|
1
1
|
import { a as __toCommonJS } from "./rolldown-runtime-DNCzzKnI.js";
|
|
2
|
-
import { C as init_tier_table, D as canUseHarnessSync,
|
|
2
|
+
import { B as piProviderForModel, C as init_tier_table, D as canUseHarnessSync, Fn as init_projects, Jr as getOverdeckHome, N as UnknownModelError, Qr as resolveOhmypiExtensionPath, R as getProviderForModelSync, Sr as init_errors, T as resolveTieredExecutionEnabledForIssue, Vr as encodeClaudeProjectDir, Yr as init_paths, Z as resolveModel, ar as parseIssueIdSync, c as isClaudeCodeChannelsEnabled, f as loadConfigSync, fr as FsError, hn as shouldCommitLegacyWorkspaceArtifacts, k as init_harness_policy, kn as getProjectSync, nr as extractPrefixSync, pn as init_state_read_home, qn as resolveProjectFromIssueSync, rr as init_issue_id, t as init_config_yaml, w as resolveTieredExecutionEnabled, z as init_providers, zn as listProjectsSync } from "./config-yaml-CqaAyn8y.js";
|
|
3
3
|
import { a as init_activity_logger, n as emitActivityEntrySync } from "./activity-logger-pcbf-e30.js";
|
|
4
|
-
import { d as getOverdeckDatabaseSync, f as init_infra } from "./infra-
|
|
5
|
-
import {
|
|
6
|
-
import { a as init_event_store, n as getEventStore } from "./event-store-
|
|
7
|
-
import { C as normalizeChannelReplyPayload, a as getHarnessBehavior, b as DomainEvent, t as init_src } from "./src-
|
|
8
|
-
import {
|
|
9
|
-
import { a as EventStoreService, c as ReadModelService, d as init_read_model, i as init_issue_lifecycle, s as init_domain_services, t as IssueLifecycle, u as getClosedIssueIdsForReadSource } from "./issue-lifecycle-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { t as
|
|
13
|
-
import {
|
|
14
|
-
import { a as
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { a as
|
|
31
|
-
import { c as
|
|
32
|
-
import {
|
|
4
|
+
import { d as getOverdeckDatabaseSync, f as init_infra } from "./infra-_Kx1m9pm.js";
|
|
5
|
+
import { A as listSessions, B as setOption, C as isHarnessProcessAlive, G as BLANKED_PROVIDER_ENV, I as sendKeysAsync, J as init_child_env, L as sendRawKeystroke, R as sessionExists, S as init_tmux, T as killSession, f as exactPaneTarget, i as capturePane, k as listSessionNames, m as findManagedServerPidSync, o as createSession, q as buildChildEnvWithoutTmuxSync, z as sessionExistsSync } from "./tmux-U55cNGFG.js";
|
|
6
|
+
import { a as init_event_store, n as getEventStore } from "./event-store-KZnWUKWh.js";
|
|
7
|
+
import { C as normalizeChannelReplyPayload, a as getHarnessBehavior, b as DomainEvent, t as init_src } from "./src-By3D85Ry.js";
|
|
8
|
+
import { A as stateToOverdeckParamsForDb, D as init_agent_state_sync, T as AGENT_COLUMNS_FOR_DB, c as init_review_status, s as getReviewStatusSync, u as loadReviewStatuses, y as setReviewStatusSync } from "./review-status-DANr0t-v.js";
|
|
9
|
+
import { a as EventStoreService, c as ReadModelService, d as init_read_model, i as init_issue_lifecycle, s as init_domain_services, t as IssueLifecycle, u as getClosedIssueIdsForReadSource } from "./issue-lifecycle-CFR8TJ22.js";
|
|
10
|
+
import { d as isTerminalIssueStage, o as init_agents, u as getIssueStageSync } from "./agents-BHBxr8c9.js";
|
|
11
|
+
import { n as ensureSessionContextBriefingFile, r as init_briefing_freshness } from "./briefing-freshness-DuxIE1yr.js";
|
|
12
|
+
import { o as getPricingSync, s as init_cost, t as calculateCostSync } from "./cost-BoqD1Pxz.js";
|
|
13
|
+
import { $n as wipeAgentStateDirs, At as init_bridge_token, Ct as deliverAgentPermissionDecision, Dn as clearAgentTroubled, Et as setAgentDeliveryMethod, Fn as getAgentStateSync, G as saveSessionId, H as getLatestSessionId, Hn as markAgentStoppedState, Jn as saveAgentState, Kt as getAgentRuntimeBaseCommand, L as messageAgent, Lt as transitionIssueToInProgress, M as resumeAgent, Nn as getAgentState, O as recoverAgent, Q as stopAgent, R as init_operator_interventions, S as init_tier_escalation, St as deliverAgentMessage, Tn as clearAgentPaused, U as getLatestSessionIdSync, V as getActivity, Vt as resolveStaffing, W as getSessionId, Xn as setAgentPaused, Yn as saveAgentStateSync, Yt as getProviderAuthMode, _t as init_config, ar as init_pty_supervisor_locate, bn as dismissDevChannelsDialog, br as checkCodexAuthStatus, dn as getProviderExportsForModel, er as writeAgentStateJsonSync, fr as waitForReadySignal, gr as saveAgentRuntimeState, i as listRunningAgents, in as ohmypiFifoPaths, ir as writePtyToken, jn as getAgentDir, jt as writeBridgeTokenSync, k as restartAgent, ln as determineModel, lr as normalizeAgentId, mn as validateProviderHealth, mr as getAgentRuntimeStateSync, n as init_queries, nn as piFifoPaths, or as resolvePtySupervisorScriptPath, pn as init_provider_health, pr as getAgentRuntimeState, r as listAgentStates, rn as init_ohmypi_fifo, rr as init_pty_token, s as init_agents$1, sr as clearReadySignal, tn as init_pi_fifo, un as getProviderEnvForModel, x as decideEscalation, xn as writeChannelsBridgeMcpConfig, xr as init_codex_auth, yt as loadCloisterConfigSync, z as operatorInterventionEvent, zt as init_staffing } from "./queries-C0LCGG21.js";
|
|
14
|
+
import { a as performHandoff, i as checkAllTriggers, s as getAgentHealth, t as getCloisterService } from "./service-CLVBC_1Y.js";
|
|
15
|
+
import { o as init_persistent_logger, s as logAgentLifecycleSync } from "./session-history-bnOFKMwf.js";
|
|
16
|
+
import { t as init_behavior } from "./behavior-CWqNMqE5.js";
|
|
17
|
+
import { a as init_tracker_config, t as getGitHubConfig } from "./tracker-config-OkdbaUCm.js";
|
|
18
|
+
import { n as init_issue_service_singleton, r as issue_service_singleton_exports } from "./issue-service-singleton-ldvUUXi3.js";
|
|
19
|
+
import { a as PAN_DIRNAME, i as PAN_CONTINUE_FILENAME, p as init_types } from "./types-DOjWNv3Z.js";
|
|
20
|
+
import { F as init_lifecycle, P as generateVBriefFilename, f as readTierOverrides, g as serializeVBriefDocument, h as recordTierPromotion, m as readWorkspacePlanSync, o as init_io, r as findPlan, u as readPlan } from "./io-DkC4sFSy.js";
|
|
21
|
+
import { B as removeFavorite, E as hasOtherActiveConversationOnTmuxSession, F as markConversationEnded, G as setConversationHarness, K as setConversationModel, M as listConversations, P as markConversationActive, Q as updateConversationDeliveryMethod, S as getConversationByName, W as setConversationEffort, g as backfillConversationModel, h as archiveConversation, k as init_conversations, nt as updateLastAttached, rt as updateSpawnError, x as getConversationById, y as createConversation } from "./conversations-BX5aD-6F.js";
|
|
22
|
+
import { n as recordFeatureRegistryLifecycle, t as init_feature_registry_population } from "./feature-registry-population-Ca2UaV0U.js";
|
|
23
|
+
import { a as requireModelOverrideSync, i as normalizeModelOverrideSync, n as init_launcher_generator, r as init_model_validation, t as generateLauncherScriptSync } from "./launcher-generator-BXLM9f4b.js";
|
|
24
|
+
import { a as init_claude_permissions, o as resolvePermissionModeSync, r as getClaudePermissionFlagsStringSync, t as BYPASS_PERMISSION_MODE } from "./claude-permissions-BB6agcOZ.js";
|
|
25
|
+
import { h as workspaceContextFile, l as piGlobalContextFile, o as init_layers } from "./layers-gCJlK62m.js";
|
|
26
|
+
import { a as init_stack_health, i as getWorkspaceStackHealth } from "./stack-health-C-SnpRHh.js";
|
|
27
|
+
import { l as init_dag, o as getDispatchableItems } from "./dag-C6kYyF17.js";
|
|
28
|
+
import { n as resolveHarness, t as init_harness_resolve } from "./harness-resolve-Dx8hUsvC.js";
|
|
29
|
+
import { c as isStateMigrated, o as init_state_home } from "./state-home-Bur1y4oH.js";
|
|
30
|
+
import { a as init_lifecycle_io, c as updatePlanStatus, s as transitionVBriefOnMain } from "./lifecycle-io-FfPUtz-y.js";
|
|
31
|
+
import { _ as init_codex_conversation_parser, c as parseEntireConversation, d as init_pi_conversation_parser, g as parseOhmypiConversationMessages, m as init_ohmypi_conversation_parser, p as parsePiConversationMessages, t as init_conversation_service, v as parseCodexConversationMessages } from "./conversation-service-mFVFdDQK.js";
|
|
32
|
+
import { _ as parseCodexApprovalPrompt, d as getAgentPendingQuestions$1, f as getAgentWorkspace$1, g as normalizeAwaitingInputPrompt, m as detectAwaitingInputForAgent, o as getRuntimeForAgent, p as scanPendingInputsPromise, s as computeAgentEnrichment, u as getAgentJsonlPath$1 } from "./stuck-remediation-B699-p8v.js";
|
|
33
|
+
import { Q as createInFlightGuard, Rt as classifyMemoryPressure, Ut as getResourceConfig, Wt as getSystemHealthSnapshot } from "./deacon-Dru5onpk.js";
|
|
34
|
+
import { o as stopWorkspaceDocker, r as init_workspace_manager } from "./workspace-manager-Beeo2aKF.js";
|
|
35
|
+
import { a as init_jsonl_parser, o as normalizeModelName } from "./jsonl-parser-Cd5jIqh7.js";
|
|
36
|
+
import { a as loadWorkspaceMetadataSync } from "./workspace-metadata-BZC5BmoP.js";
|
|
37
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
33
38
|
import { Cause, Data, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
34
39
|
import { join as join$1 } from "path";
|
|
35
40
|
import { homedir } from "node:os";
|
|
36
|
-
import { existsSync } from "node:fs";
|
|
41
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
37
42
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
38
|
-
import { access, appendFile,
|
|
43
|
+
import { access, appendFile, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat as stat$1, writeFile } from "node:fs/promises";
|
|
39
44
|
import { randomUUID } from "crypto";
|
|
40
|
-
import { mkdir as mkdir$1,
|
|
45
|
+
import { mkdir as mkdir$1, writeFile as writeFile$1 } from "fs/promises";
|
|
41
46
|
import { exec, execFile, spawn } from "node:child_process";
|
|
42
47
|
import { promisify } from "node:util";
|
|
43
|
-
import { timingSafeEqual } from "node:crypto";
|
|
48
|
+
import { randomUUID as randomUUID$1, timingSafeEqual } from "node:crypto";
|
|
49
|
+
import { pathToFileURL } from "node:url";
|
|
50
|
+
import { createInterface } from "node:readline";
|
|
44
51
|
import * as Result$1 from "effect/Result";
|
|
45
52
|
//#region http-helpers.ts
|
|
46
53
|
init_paths();
|
|
@@ -65,10 +72,11 @@ function jsonStringResponse(json, options) {
|
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
74
|
//#endregion
|
|
68
|
-
//#region
|
|
75
|
+
//#region ../../lib/default-cwd.ts
|
|
69
76
|
init_activity_logger();
|
|
70
77
|
init_issue_id();
|
|
71
78
|
init_projects();
|
|
79
|
+
init_providers();
|
|
72
80
|
init_harness_policy();
|
|
73
81
|
init_tier_table();
|
|
74
82
|
init_config_yaml();
|
|
@@ -78,13 +86,36 @@ init_infra();
|
|
|
78
86
|
init_event_store();
|
|
79
87
|
init_src();
|
|
80
88
|
init_agent_state_sync();
|
|
89
|
+
init_agents();
|
|
81
90
|
init_read_model();
|
|
91
|
+
init_briefing_freshness();
|
|
82
92
|
init_domain_services();
|
|
93
|
+
init_cost();
|
|
94
|
+
init_behavior();
|
|
95
|
+
init_persistent_logger();
|
|
96
|
+
init_tracker_config();
|
|
83
97
|
init_types();
|
|
84
98
|
init_review_status();
|
|
85
99
|
init_lifecycle();
|
|
86
100
|
init_io();
|
|
87
101
|
/**
|
|
102
|
+
* Resolve the default cwd used when no project is selected — the "devroot"
|
|
103
|
+
* fallback. Prefers `~/Projects` if it exists, otherwise the user's home
|
|
104
|
+
* directory.
|
|
105
|
+
*
|
|
106
|
+
* Replaces the deprecated `sync.devroot` config (PAN-1201) for the runtime
|
|
107
|
+
* code paths that still need a sensible default working directory:
|
|
108
|
+
*
|
|
109
|
+
* - New conversations created without a `projectKey` (conversations.ts)
|
|
110
|
+
* - The global editor-launcher in the dashboard top bar (PanOpenInPicker)
|
|
111
|
+
*/
|
|
112
|
+
function getDefaultCwd() {
|
|
113
|
+
const candidate = join(homedir(), "Projects");
|
|
114
|
+
return existsSync(candidate) ? candidate : homedir();
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region routes/http-handler.ts
|
|
118
|
+
/**
|
|
88
119
|
* httpHandler — typed error-to-HTTP mapping wrapper (PAN-470)
|
|
89
120
|
*
|
|
90
121
|
* Wraps an Effect route body and catches all typed TaggedErrors,
|
|
@@ -110,7 +141,7 @@ let opaqueLastSummaryAt = 0;
|
|
|
110
141
|
* TrackerNotConfigured → 503 Service Unavailable
|
|
111
142
|
* RateLimited → 429 Too Many Requests
|
|
112
143
|
* AgentAlreadyRunning → 409 Conflict
|
|
113
|
-
*
|
|
144
|
+
* PlanEmpty → 422 Unprocessable Entity
|
|
114
145
|
* TrackerApiError → 502 Bad Gateway
|
|
115
146
|
* WorkspaceCreateError, AgentStartError → 500 Internal Server Error
|
|
116
147
|
* Unknown errors → 500 Internal Server Error
|
|
@@ -120,7 +151,7 @@ function httpHandler(effect) {
|
|
|
120
151
|
return effect.pipe(Effect.catchTag("IssueNotFound", (err) => Effect.succeed(jsonResponse({ error: `Issue not found: ${err.id}` }, { status: 404 }))), Effect.catchTag("WorkspaceNotFound", (err) => Effect.succeed(jsonResponse({ error: `Workspace not found: ${err.id}` }, { status: 404 }))), Effect.catchTag("TrackerNotConfigured", (err) => Effect.succeed(jsonResponse({ error: `Tracker not configured: ${err.tracker}` }, { status: 503 }))), Effect.catchTag("RateLimited", (err) => Effect.succeed(jsonResponse({
|
|
121
152
|
error: "Rate limited",
|
|
122
153
|
retryAfter: err.retryAfter
|
|
123
|
-
}, { status: 429 }))), Effect.catchTag("AgentAlreadyRunning", (err) => Effect.succeed(jsonResponse({ error: `Agent already running for issue: ${err.id}` }, { status: 409 }))), Effect.catchTag("
|
|
154
|
+
}, { status: 429 }))), Effect.catchTag("AgentAlreadyRunning", (err) => Effect.succeed(jsonResponse({ error: `Agent already running for issue: ${err.id}` }, { status: 409 }))), Effect.catchTag("PlanEmpty", (err) => Effect.succeed(jsonResponse({ error: `Plan is empty for issue: ${err.id}` }, { status: 422 }))), Effect.catchTag("TrackerApiError", (err) => Effect.succeed(jsonResponse({ error: `Tracker API error (${err.tracker}): ${err.message}` }, { status: 502 }))), Effect.catchTag("WorkspaceCreateError", (err) => Effect.succeed(jsonResponse({ error: `Workspace creation failed for ${err.id}: ${err.message}` }, { status: 500 }))), Effect.catchTag("AgentStartError", (err) => Effect.succeed(jsonResponse({ error: `Agent start failed for ${err.id}: ${err.message}` }, { status: 500 }))), Effect.catchCause((cause) => Effect.gen(function* () {
|
|
124
155
|
if (Cause.hasInterruptsOnly(cause)) return jsonResponse({ error: "Request cancelled" }, { status: 499 });
|
|
125
156
|
const error = Cause.squash(cause);
|
|
126
157
|
const message = error instanceof Error ? error.message : "Internal server error";
|
|
@@ -152,171 +183,27 @@ function httpHandler(effect) {
|
|
|
152
183
|
})));
|
|
153
184
|
}
|
|
154
185
|
//#endregion
|
|
155
|
-
//#region ../../lib/codex-auth.ts
|
|
156
|
-
init_cost();
|
|
157
|
-
init_tracker_config();
|
|
158
|
-
init_cliproxy();
|
|
159
|
-
/** Wrapper error for Codex auth probing — preserves the underlying cause. */
|
|
160
|
-
var CodexAuthCheckError = class extends Data.TaggedError("CodexAuthCheckError") {};
|
|
161
|
-
async function checkCodexAuthStatusPromise(options = {}) {
|
|
162
|
-
const credPath = join$1(getCliproxyAuthDir(), "codex-primary.json");
|
|
163
|
-
let raw;
|
|
164
|
-
let credMtimeMs = null;
|
|
165
|
-
try {
|
|
166
|
-
raw = await readFile$1(credPath, "utf8");
|
|
167
|
-
credMtimeMs = (await stat$2(credPath)).mtimeMs;
|
|
168
|
-
} catch {
|
|
169
|
-
return { status: "missing" };
|
|
170
|
-
}
|
|
171
|
-
let creds;
|
|
172
|
-
try {
|
|
173
|
-
creds = JSON.parse(raw);
|
|
174
|
-
} catch {
|
|
175
|
-
return {
|
|
176
|
-
status: "unknown",
|
|
177
|
-
message: "Malformed credential file"
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const accessToken = typeof creds.access_token === "string" ? creds.access_token : null;
|
|
181
|
-
if (!accessToken) return {
|
|
182
|
-
status: "unknown",
|
|
183
|
-
message: "Missing access_token in credential file"
|
|
184
|
-
};
|
|
185
|
-
const claims = decodeJwtPayload(accessToken);
|
|
186
|
-
if (!claims) return {
|
|
187
|
-
status: "unknown",
|
|
188
|
-
message: "Unable to decode access_token"
|
|
189
|
-
};
|
|
190
|
-
const expSec = typeof claims.exp === "number" ? claims.exp : null;
|
|
191
|
-
if (expSec === null) return {
|
|
192
|
-
status: "unknown",
|
|
193
|
-
message: "Missing exp claim in access_token"
|
|
194
|
-
};
|
|
195
|
-
const email = typeof creds.email === "string" ? creds.email : "";
|
|
196
|
-
const expiresAt = (/* @__PURE__ */ new Date(expSec * 1e3)).toISOString();
|
|
197
|
-
if (expSec * 1e3 <= Date.now()) return {
|
|
198
|
-
status: "expired",
|
|
199
|
-
email,
|
|
200
|
-
expiresAt
|
|
201
|
-
};
|
|
202
|
-
return await applyBurnedTokenOverride({
|
|
203
|
-
status: "valid",
|
|
204
|
-
email,
|
|
205
|
-
expiresAt
|
|
206
|
-
}, email, expiresAt, { ignoreBurnBefore: options.ignoreBurnBefore ?? credMtimeMs ?? void 0 });
|
|
207
|
-
}
|
|
208
|
-
/** Read the trailing window of the cliproxy log for burn/failure analysis. */
|
|
209
|
-
async function readLogTail(path) {
|
|
210
|
-
const TAIL_BYTES = 128 * 1024;
|
|
211
|
-
const file = await open$1(path, "r");
|
|
212
|
-
try {
|
|
213
|
-
const stat = await file.stat();
|
|
214
|
-
const length = Math.min(stat.size, TAIL_BYTES);
|
|
215
|
-
const buffer = Buffer.alloc(length);
|
|
216
|
-
await file.read(buffer, 0, length, stat.size - length);
|
|
217
|
-
return buffer.toString("utf8");
|
|
218
|
-
} finally {
|
|
219
|
-
await file.close();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async function applyBurnedTokenOverride(baseStatus, email, expiresAt, options) {
|
|
223
|
-
let logRaw;
|
|
224
|
-
try {
|
|
225
|
-
logRaw = await readLogTail(getCliproxyLogPath());
|
|
226
|
-
} catch {
|
|
227
|
-
return baseStatus;
|
|
228
|
-
}
|
|
229
|
-
return evaluateBurnedFromLog(logRaw, baseStatus, email, expiresAt, options);
|
|
230
|
-
}
|
|
231
|
-
const BURN_STALENESS_MS = 3600 * 1e3;
|
|
232
|
-
/**
|
|
233
|
-
* Pure decision: given the cliproxy log tail, decide whether a JWT-valid token
|
|
234
|
-
* is actually burned. Exported for regression testing (PAN-1584).
|
|
235
|
-
*
|
|
236
|
-
* "Auth failure" evidence = either a refresh-token burn line
|
|
237
|
-
* (`refresh token has already been used`) OR a `503` on `/v1/messages` /
|
|
238
|
-
* `/v1/chat/completions` (the live symptom the agent hits once cliproxy disables
|
|
239
|
-
* the provider — and which keeps appearing after the burn line stops, since a
|
|
240
|
-
* disabled provider no longer attempts refreshes). "Success" = a `200` on those
|
|
241
|
-
* same paths (proves the auth path works right now).
|
|
242
|
-
*
|
|
243
|
-
* Decision:
|
|
244
|
-
* - No failure evidence → trust base status.
|
|
245
|
-
* - Success AFTER the last failure → trust base status (recovered).
|
|
246
|
-
* - Failure AFTER the credential's last write (ignoreBurnBefore) → BURNED,
|
|
247
|
-
* regardless of age. The credential write time is when you last logged in;
|
|
248
|
-
* a failure after it means the current token is dead. This is authoritative
|
|
249
|
-
* and is the fix for the bug where a >1h-stale burn line was dismissed as
|
|
250
|
-
* "valid" during a quiet period even though the token was dead (PAN-1584).
|
|
251
|
-
* - Failure BEFORE that cutoff → trust base status (re-authed since).
|
|
252
|
-
* - No cutoff available → fall back to the staleness backstop:
|
|
253
|
-
* burned only if the last failure is within BURN_STALENESS_MS.
|
|
254
|
-
*
|
|
255
|
-
* We intentionally don't probe cliproxy with HTTP — `GET /v1/models` always
|
|
256
|
-
* 401s (it needs real OAuth, not the local key) and would generate spurious log
|
|
257
|
-
* lines on every dashboard load.
|
|
258
|
-
*/
|
|
259
|
-
function evaluateBurnedFromLog(logRaw, baseStatus, email, expiresAt, options = {}) {
|
|
260
|
-
const now = options.now ?? Date.now();
|
|
261
|
-
const lines = logRaw.split("\n").slice(-500);
|
|
262
|
-
const isBurnLine = (l) => l.includes("refresh token has already been used") || l.includes("refresh_token_reused");
|
|
263
|
-
const isAuthFailure503 = (l) => /\b503 \|/.test(l) && /POST\s+"\/v1\/(messages|chat\/completions)/.test(l);
|
|
264
|
-
const isSuccess = (l) => /\b200 \|/.test(l) && /POST\s+"\/v1\/(messages|chat\/completions)/.test(l);
|
|
265
|
-
const timestampAt = (idx) => {
|
|
266
|
-
for (let j = idx; j >= Math.max(0, idx - 10); j--) {
|
|
267
|
-
const m = lines[j]?.match(/^\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\]/);
|
|
268
|
-
if (m) {
|
|
269
|
-
const t = Date.parse(`${m[1]}T${m[2]}Z`);
|
|
270
|
-
if (Number.isFinite(t)) return t;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return null;
|
|
274
|
-
};
|
|
275
|
-
let lastFailureIdx = -1;
|
|
276
|
-
let lastFailureTimestamp = null;
|
|
277
|
-
let lastSuccessIdx = -1;
|
|
278
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
279
|
-
const line = lines[i] ?? "";
|
|
280
|
-
if (lastFailureIdx < 0 && (isBurnLine(line) || isAuthFailure503(line))) {
|
|
281
|
-
lastFailureIdx = i;
|
|
282
|
-
lastFailureTimestamp = timestampAt(i);
|
|
283
|
-
}
|
|
284
|
-
if (lastSuccessIdx < 0 && isSuccess(line)) lastSuccessIdx = i;
|
|
285
|
-
if (lastFailureIdx >= 0 && lastSuccessIdx >= 0) break;
|
|
286
|
-
}
|
|
287
|
-
if (lastFailureIdx < 0) return baseStatus;
|
|
288
|
-
if (lastSuccessIdx > lastFailureIdx) return baseStatus;
|
|
289
|
-
if (options.ignoreBurnBefore !== void 0) {
|
|
290
|
-
if (lastFailureTimestamp !== null && lastFailureTimestamp + 1e3 <= options.ignoreBurnBefore) return baseStatus;
|
|
291
|
-
return {
|
|
292
|
-
status: "burned",
|
|
293
|
-
email,
|
|
294
|
-
expiresAt
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
if (lastFailureTimestamp !== null && now - lastFailureTimestamp > BURN_STALENESS_MS) return baseStatus;
|
|
298
|
-
return {
|
|
299
|
-
status: "burned",
|
|
300
|
-
email,
|
|
301
|
-
expiresAt
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Effect-native checkCodexAuthStatus. The Promise version is designed to
|
|
306
|
-
* swallow all I/O errors and report the auth state through the typed status
|
|
307
|
-
* union. The Effect variant wraps that to make it composable; it only fails
|
|
308
|
-
* with CodexAuthCheckError if the underlying call itself throws unexpectedly
|
|
309
|
-
* (i.e., not from the documented "missing/unknown" branches).
|
|
310
|
-
*/
|
|
311
|
-
const checkCodexAuthStatus = (options = {}) => Effect.tryPromise({
|
|
312
|
-
try: () => checkCodexAuthStatusPromise(options),
|
|
313
|
-
catch: (cause) => new CodexAuthCheckError({
|
|
314
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
315
|
-
cause
|
|
316
|
-
})
|
|
317
|
-
});
|
|
318
|
-
//#endregion
|
|
319
186
|
//#region routes/origin-validation.ts
|
|
187
|
+
init_conversations();
|
|
188
|
+
init_pty_supervisor_locate();
|
|
189
|
+
init_pty_token();
|
|
190
|
+
init_feature_registry_population();
|
|
191
|
+
init_model_validation();
|
|
192
|
+
init_provider_health();
|
|
193
|
+
init_claude_permissions();
|
|
194
|
+
init_layers();
|
|
195
|
+
init_ohmypi_fifo();
|
|
196
|
+
init_pi_fifo();
|
|
197
|
+
init_stack_health();
|
|
198
|
+
init_launcher_generator();
|
|
199
|
+
init_dag();
|
|
200
|
+
init_staffing();
|
|
201
|
+
init_harness_resolve();
|
|
202
|
+
init_bridge_token();
|
|
203
|
+
init_operator_interventions();
|
|
204
|
+
init_lifecycle_io();
|
|
205
|
+
init_agents$1();
|
|
206
|
+
init_queries();
|
|
320
207
|
let cachedTrustedOrigins;
|
|
321
208
|
function addTrustedOrigin(origins, raw) {
|
|
322
209
|
if (!raw) return;
|
|
@@ -408,21 +295,75 @@ function isTrustedOriginForHost(origin, _host) {
|
|
|
408
295
|
return getTrustedOrigins().includes(normalized);
|
|
409
296
|
}
|
|
410
297
|
//#endregion
|
|
298
|
+
//#region services/pending-respawn.ts
|
|
299
|
+
/**
|
|
300
|
+
* Tracks tmux sessions that are mid-respawn so that `/ws/terminal`
|
|
301
|
+
* reconnects landing in the kill→spawn gap don't get a fatal 4404.
|
|
302
|
+
*
|
|
303
|
+
* Background: switch-model, resume, and restart-all all do
|
|
304
|
+
*
|
|
305
|
+
* await killSessionAsync(name) // old tmux dies, PTY exits,
|
|
306
|
+
* // ws-terminal closes clients with 1000
|
|
307
|
+
* await spawnConversationSession(...) // new tmux session with same name
|
|
308
|
+
* await waitForTmuxSession(name)
|
|
309
|
+
* await waitForReadySignal(name, 30) // up to 30s
|
|
310
|
+
*
|
|
311
|
+
* The terminal frontend (`XTerminal.tsx`) reconnects on close 1000 with
|
|
312
|
+
* exponential backoff (1s, 2s, 4s, 8s, 16s). Every reconnect re-enters
|
|
313
|
+
* `ws-terminal.ts`, which checks the tmux session list upfront and
|
|
314
|
+
* `close(4404, 'session-not-found')` if the name isn't there yet.
|
|
315
|
+
* 4404 is treated as fatal on the client (no retry — the session is
|
|
316
|
+
* presumed gone), so the panel sticks on "Could not reconnect" even
|
|
317
|
+
* after the respawn completes a moment later.
|
|
318
|
+
*
|
|
319
|
+
* This registry lets `ws-terminal.ts` distinguish "transient absence
|
|
320
|
+
* during respawn" from "session is actually gone". Respawn sites mark
|
|
321
|
+
* the name with `markRespawnPending()` for the duration of their kill+
|
|
322
|
+
* spawn block; ws-terminal calls `waitForSessionRespawn()` instead of
|
|
323
|
+
* 4404-ing immediately when the session is missing AND
|
|
324
|
+
* `isRespawnPending()` is true.
|
|
325
|
+
*/
|
|
326
|
+
const pendingRespawns = /* @__PURE__ */ new Set();
|
|
327
|
+
/**
|
|
328
|
+
* Mark a tmux session as mid-respawn. The returned `done()` must run in
|
|
329
|
+
* a `finally` so the marker is cleared even if the respawn throws.
|
|
330
|
+
*/
|
|
331
|
+
function markRespawnPending(sessionName) {
|
|
332
|
+
pendingRespawns.add(sessionName);
|
|
333
|
+
return { done: () => {
|
|
334
|
+
pendingRespawns.delete(sessionName);
|
|
335
|
+
} };
|
|
336
|
+
}
|
|
337
|
+
/** True if a respawn is currently in progress for this session name. */
|
|
338
|
+
function isRespawnPending(sessionName) {
|
|
339
|
+
return pendingRespawns.has(sessionName);
|
|
340
|
+
}
|
|
341
|
+
const POLL_INTERVAL_MS = 200;
|
|
342
|
+
/**
|
|
343
|
+
* Poll for the session to come back, up to `timeoutMs`. Returns whether
|
|
344
|
+
* it did. Bails early once `pendingRespawns` no longer contains the name
|
|
345
|
+
* (the wrapping respawn block finished, success or failure) and the
|
|
346
|
+
* session still isn't there — at that point the absence is real.
|
|
347
|
+
*
|
|
348
|
+
* `timeoutMs` should comfortably exceed the longest respawn window. The
|
|
349
|
+
* dominant cost is `waitForReadySignal`'s 30s ceiling in the conversation
|
|
350
|
+
* routes, so 35s is the right default for those callers.
|
|
351
|
+
*/
|
|
352
|
+
async function waitForSessionRespawn(sessionName, timeoutMs) {
|
|
353
|
+
const deadline = Date.now() + timeoutMs;
|
|
354
|
+
while (Date.now() < deadline) {
|
|
355
|
+
if (await Effect.runPromise(sessionExists(sessionName))) return true;
|
|
356
|
+
if (!pendingRespawns.has(sessionName)) return Effect.runPromise(sessionExists(sessionName));
|
|
357
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
358
|
+
}
|
|
359
|
+
return Effect.runPromise(sessionExists(sessionName));
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
411
362
|
//#region routes/jsonl-resolver.ts
|
|
412
363
|
init_codex_conversation_parser();
|
|
413
364
|
init_ohmypi_conversation_parser();
|
|
414
365
|
init_pi_conversation_parser();
|
|
415
366
|
init_conversation_service();
|
|
416
|
-
init_persistent_logger();
|
|
417
|
-
init_feature_registry_population();
|
|
418
|
-
init_model_validation();
|
|
419
|
-
init_provider_health();
|
|
420
|
-
init_stack_health();
|
|
421
|
-
init_dag();
|
|
422
|
-
init_staffing();
|
|
423
|
-
init_beads_query();
|
|
424
|
-
init_operator_interventions();
|
|
425
|
-
init_lifecycle_io();
|
|
426
367
|
/**
|
|
427
368
|
* JSONL transcript resolver for the Command Deck (PAN-830).
|
|
428
369
|
*
|
|
@@ -435,7 +376,8 @@ init_lifecycle_io();
|
|
|
435
376
|
*
|
|
436
377
|
* For claude-code agents the JSONL filename is the *Claude session ID* (a UUID
|
|
437
378
|
* written by Claude Code itself), NOT the agent/tmux name. Lookup order:
|
|
438
|
-
* 1. session.id —
|
|
379
|
+
* 1. session.id — UUID pinned before fresh work-agent launch and updated
|
|
380
|
+
* by auto-suspend/resume flows
|
|
439
381
|
* 2. sessions.json — array of UUIDs the agent has used; the heartbeat hook
|
|
440
382
|
* APPENDS new IDs and dedupes, so once a session has been
|
|
441
383
|
* seen its array index never moves. After a `pan resume`
|
|
@@ -447,8 +389,13 @@ init_lifecycle_io();
|
|
|
447
389
|
* Async-only (fs/promises) because this code path runs inside the dashboard
|
|
448
390
|
* server's event loop.
|
|
449
391
|
*/
|
|
450
|
-
|
|
451
|
-
|
|
392
|
+
const transcriptResolutionSignatures = /* @__PURE__ */ new Map();
|
|
393
|
+
function logTranscriptResolution(agentId, signature, message, opts) {
|
|
394
|
+
const signatureKey = `${agentId}:${opts.agentsDirOverride ?? "live"}`;
|
|
395
|
+
if (transcriptResolutionSignatures.get(signatureKey) === signature) return;
|
|
396
|
+
transcriptResolutionSignatures.set(signatureKey, signature);
|
|
397
|
+
(opts.logDiagnostic ?? (opts.agentsDirOverride ? void 0 : logAgentLifecycleSync))?.(agentId, `transcript resolution: ${message}`);
|
|
398
|
+
}
|
|
452
399
|
async function pathExists$1(p) {
|
|
453
400
|
return access(p).then(() => true, () => false);
|
|
454
401
|
}
|
|
@@ -629,7 +576,7 @@ async function resolveCodexRolloutPath(agentId, opts = {}) {
|
|
|
629
576
|
const agentDir = join(opts.agentsDirOverride ?? join(getOverdeckHome(), "agents"), agentId);
|
|
630
577
|
const codexHome = join(agentDir, "codex-home");
|
|
631
578
|
if (!await pathExists$1(codexHome)) return null;
|
|
632
|
-
const { findRolloutPath, findLatestRollout } = await import("./codex-
|
|
579
|
+
const { findRolloutPath, findLatestRollout } = await import("./codex-DxLivxri.js");
|
|
633
580
|
const threadId = (await readOptional(join(agentDir, "codex-thread-id")))?.trim();
|
|
634
581
|
if (threadId) {
|
|
635
582
|
const rollout = findRolloutPath(codexHome, threadId);
|
|
@@ -681,18 +628,27 @@ async function resolvePiSessionPath(agentId, opts = {}) {
|
|
|
681
628
|
* the corresponding JSONL file exists; otherwise null.
|
|
682
629
|
*/
|
|
683
630
|
async function resolveJsonlPath(agentId, workspacePath, opts = {}) {
|
|
684
|
-
const
|
|
631
|
+
const harness = await resolveAgentHarness(agentId, opts);
|
|
632
|
+
const behavior = behaviorForHarness(harness);
|
|
685
633
|
if (behavior.transcriptKind === "codex-rollout-jsonl") return resolveCodexRolloutPath(agentId, opts);
|
|
686
634
|
if (behavior.transcriptKind === "ohmypi-jsonl") return resolvePiSessionPath(agentId, opts);
|
|
687
635
|
const claudeSessionId = await resolveClaudeSessionId(agentId, opts);
|
|
688
|
-
if (!claudeSessionId)
|
|
636
|
+
if (!claudeSessionId) {
|
|
637
|
+
logTranscriptResolution(agentId, `missing-session-id:${harness ?? "unknown"}`, `failed harness=${harness ?? "unknown"} reason=no-session-id checked=session.id,sessions.json,runtime-state`, opts);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
689
640
|
const jsonlPath = join(opts.claudeProjectsDirOverride ?? join(homedir(), ".claude", "projects"), encodeClaudeProjectDir(workspacePath), `${claudeSessionId}.jsonl`);
|
|
690
|
-
if (await pathExists$1(jsonlPath))
|
|
641
|
+
if (await pathExists$1(jsonlPath)) {
|
|
642
|
+
logTranscriptResolution(agentId, `resolved:${jsonlPath}`, `resolved harness=${harness ?? "claude-code"} sessionId=${claudeSessionId} path=${jsonlPath}`, opts);
|
|
643
|
+
return jsonlPath;
|
|
644
|
+
}
|
|
645
|
+
logTranscriptResolution(agentId, `missing-jsonl:${jsonlPath}`, `failed harness=${harness ?? "claude-code"} reason=jsonl-missing sessionId=${claudeSessionId} workspace=${workspacePath} expectedPath=${jsonlPath}`, opts);
|
|
691
646
|
return null;
|
|
692
647
|
}
|
|
693
648
|
//#endregion
|
|
694
649
|
//#region ../../lib/work-agent-lifecycle.ts
|
|
695
|
-
init_agents();
|
|
650
|
+
init_agents$1();
|
|
651
|
+
init_tmux();
|
|
696
652
|
async function pathExists(path) {
|
|
697
653
|
try {
|
|
698
654
|
await access(path);
|
|
@@ -861,7 +817,7 @@ const getWorkAgentLifecycleState = (agentOrIssueId) => Effect.promise(() => getW
|
|
|
861
817
|
*
|
|
862
818
|
* All I/O is async (execFile / fs.promises). No execSync, no readFileSync.
|
|
863
819
|
*/
|
|
864
|
-
const execFileAsync$
|
|
820
|
+
const execFileAsync$3 = promisify(execFile);
|
|
865
821
|
const GIT_INFO_TTL_MS = 3e4;
|
|
866
822
|
const GIT_INFO_TIMEOUT_MS = 2e3;
|
|
867
823
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -902,7 +858,7 @@ async function readHeadMtimeMs(absPath) {
|
|
|
902
858
|
}
|
|
903
859
|
}
|
|
904
860
|
async function readGitInfoFresh(absPath) {
|
|
905
|
-
const [branchResult, worktreeResult] = await Promise.all([execFileAsync$
|
|
861
|
+
const [branchResult, worktreeResult] = await Promise.all([execFileAsync$3("git", [
|
|
906
862
|
"-C",
|
|
907
863
|
absPath,
|
|
908
864
|
"rev-parse",
|
|
@@ -911,7 +867,7 @@ async function readGitInfoFresh(absPath) {
|
|
|
911
867
|
], {
|
|
912
868
|
encoding: "utf-8",
|
|
913
869
|
timeout: GIT_INFO_TIMEOUT_MS
|
|
914
|
-
}).catch(() => null), execFileAsync$
|
|
870
|
+
}).catch(() => null), execFileAsync$3("git", [
|
|
915
871
|
"-C",
|
|
916
872
|
absPath,
|
|
917
873
|
"rev-parse",
|
|
@@ -984,10 +940,10 @@ async function resolveAgentGitInfo(workspacePath, expectedBranch) {
|
|
|
984
940
|
//#endregion
|
|
985
941
|
//#region routes/agents/shared.ts
|
|
986
942
|
init_paths();
|
|
987
|
-
init_agents();
|
|
943
|
+
init_agents$1();
|
|
988
944
|
init_tmux();
|
|
989
|
-
const execAsync$
|
|
990
|
-
const execFileAsync$
|
|
945
|
+
const execAsync$2 = promisify(exec);
|
|
946
|
+
const execFileAsync$2 = promisify(execFile);
|
|
991
947
|
function buildPanStartArgs(input) {
|
|
992
948
|
return [
|
|
993
949
|
"start",
|
|
@@ -1228,7 +1184,7 @@ function buildStoppedAgentLifecycle(agentOrIssueId, state, runtimeData) {
|
|
|
1228
1184
|
const agentId = normalizeAgentId(agentOrIssueId);
|
|
1229
1185
|
const hasAgentState = true;
|
|
1230
1186
|
const hasLiveTmuxSession = false;
|
|
1231
|
-
const hasSavedSession = !!runtimeData.claudeSessionId;
|
|
1187
|
+
const hasSavedSession = !!runtimeData.claudeSessionId || !!getLatestSessionIdSync(agentId);
|
|
1232
1188
|
const hasWorkspace = typeof state.workspace === "string" && state.workspace.length > 0;
|
|
1233
1189
|
const agentStatus = state.status || "unknown";
|
|
1234
1190
|
const runtime = runtimeData.state || "uninitialized";
|
|
@@ -1335,9 +1291,9 @@ async function getGitStatusAsync(workspacePath) {
|
|
|
1335
1291
|
try {
|
|
1336
1292
|
if (!existsSync(workspacePath)) return null;
|
|
1337
1293
|
const [branchResult, uncommittedResult, commitResult] = await Promise.all([
|
|
1338
|
-
execAsync$
|
|
1339
|
-
execAsync$
|
|
1340
|
-
execAsync$
|
|
1294
|
+
execAsync$2("git rev-parse --abbrev-ref HEAD 2>/dev/null || echo \"\"", { cwd: workspacePath }),
|
|
1295
|
+
execAsync$2("git status --porcelain 2>/dev/null | wc -l", { cwd: workspacePath }),
|
|
1296
|
+
execAsync$2("git log -1 --pretty=format:\"%s\" 2>/dev/null || echo \"\"", { cwd: workspacePath })
|
|
1341
1297
|
]);
|
|
1342
1298
|
const branch = branchResult.stdout.trim();
|
|
1343
1299
|
const uncommitted = uncommittedResult.stdout.trim();
|
|
@@ -1467,7 +1423,7 @@ const getAgentJsonlPath = getAgentJsonlPath$1;
|
|
|
1467
1423
|
const getAgentPendingQuestions = getAgentPendingQuestions$1;
|
|
1468
1424
|
//#endregion
|
|
1469
1425
|
//#region routes/agents/listing.ts
|
|
1470
|
-
init_agents();
|
|
1426
|
+
init_agents$1();
|
|
1471
1427
|
init_tmux();
|
|
1472
1428
|
const getAgentsRoute = HttpRouter.add("GET", "/api/agents", httpHandler(Effect.gen(function* () {
|
|
1473
1429
|
const now = Date.now();
|
|
@@ -1698,7 +1654,7 @@ const getAgentHasSessionRoute = HttpRouter.add("GET", "/api/agents/:id/has-sessi
|
|
|
1698
1654
|
//#endregion
|
|
1699
1655
|
//#region routes/agents/conversation.ts
|
|
1700
1656
|
init_paths();
|
|
1701
|
-
init_agents();
|
|
1657
|
+
init_agents$1();
|
|
1702
1658
|
init_tmux();
|
|
1703
1659
|
const getAgentOutputRoute = HttpRouter.add("GET", "/api/agents/:id/output", httpHandler(Effect.gen(function* () {
|
|
1704
1660
|
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
@@ -1720,7 +1676,7 @@ const getAgentOutputRoute = HttpRouter.add("GET", "/api/agents/:id/output", http
|
|
|
1720
1676
|
} catch {}
|
|
1721
1677
|
let stdout;
|
|
1722
1678
|
if (isRemote && vmName) {
|
|
1723
|
-
const { getRemoteAgentOutput } = await import("./remote-agents-
|
|
1679
|
+
const { getRemoteAgentOutput } = await import("./remote-agents-DYY5fFpd.js");
|
|
1724
1680
|
stdout = await getRemoteAgentOutput(id, vmName, parseInt(String(lines), 10) || 100);
|
|
1725
1681
|
} else stdout = await Effect.runPromise(capturePane(id, parseInt(String(lines), 10) || 100));
|
|
1726
1682
|
if (!stdout || stdout.trim() === "" || stdout.trim() === "Session not found") {
|
|
@@ -1794,119 +1750,1516 @@ async function buildConversationResponse(id) {
|
|
|
1794
1750
|
if (existsSync(candidate)) jsonlPath = candidate;
|
|
1795
1751
|
}
|
|
1796
1752
|
}
|
|
1797
|
-
if (!jsonlPath) jsonlPath = await Effect.runPromise(getAgentJsonlPath(id));
|
|
1798
|
-
if (!jsonlPath || !existsSync(jsonlPath)) return EMPTY_CONVERSATION;
|
|
1799
|
-
return {
|
|
1800
|
-
...await parseEntireConversation(jsonlPath),
|
|
1801
|
-
streaming: false
|
|
1802
|
-
};
|
|
1803
|
-
} catch (err) {
|
|
1804
|
-
console.error("[conversation] failed for", id, err);
|
|
1805
|
-
return EMPTY_CONVERSATION;
|
|
1753
|
+
if (!jsonlPath) jsonlPath = await Effect.runPromise(getAgentJsonlPath(id));
|
|
1754
|
+
if (!jsonlPath || !existsSync(jsonlPath)) return EMPTY_CONVERSATION;
|
|
1755
|
+
return {
|
|
1756
|
+
...await parseEntireConversation(jsonlPath),
|
|
1757
|
+
streaming: false
|
|
1758
|
+
};
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
console.error("[conversation] failed for", id, err);
|
|
1761
|
+
return EMPTY_CONVERSATION;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const getAgentConversationRoute = HttpRouter.add("GET", "/api/agents/:id/conversation", httpHandler(Effect.gen(function* () {
|
|
1765
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1766
|
+
return yield* Effect.promise(async () => jsonResponse(await buildConversationResponse(id)));
|
|
1767
|
+
})));
|
|
1768
|
+
const getAgentActivityRoute = HttpRouter.add("GET", "/api/agents/:id/activity", httpHandler(Effect.gen(function* () {
|
|
1769
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1770
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
1771
|
+
const urlOpt = HttpServerRequest.toURL(request);
|
|
1772
|
+
const limitStr = Option.isSome(urlOpt) ? urlOpt.value.searchParams.get("limit") ?? "100" : "100";
|
|
1773
|
+
return jsonResponse({ activity: getActivity(id, parseInt(limitStr) || 100) });
|
|
1774
|
+
})));
|
|
1775
|
+
const getAgentFilesRoute = HttpRouter.add("GET", "/api/agents/:id/files", httpHandler(Effect.gen(function* () {
|
|
1776
|
+
const agentState = yield* getAgentState((yield* HttpRouter.params)["id"] ?? "");
|
|
1777
|
+
if (!agentState?.workspace) return jsonResponse({ files: [] });
|
|
1778
|
+
const workspacePath = agentState.workspace;
|
|
1779
|
+
if (!existsSync(workspacePath)) return jsonResponse({ files: [] });
|
|
1780
|
+
const { stdout } = yield* Effect.promise(() => execAsync$2("git diff --name-status HEAD 2>/dev/null || git status --porcelain 2>/dev/null || echo \"\"", {
|
|
1781
|
+
cwd: workspacePath,
|
|
1782
|
+
encoding: "utf-8"
|
|
1783
|
+
}));
|
|
1784
|
+
return jsonResponse({ files: stdout.split("\n").filter((l) => l.trim()).map((l) => {
|
|
1785
|
+
const parts = l.trim().split(/\s+/);
|
|
1786
|
+
if (parts.length >= 2) return {
|
|
1787
|
+
status: parts[0],
|
|
1788
|
+
path: parts[parts.length - 1]
|
|
1789
|
+
};
|
|
1790
|
+
return {
|
|
1791
|
+
status: "?",
|
|
1792
|
+
path: l.trim()
|
|
1793
|
+
};
|
|
1794
|
+
}).filter((f) => f.path) });
|
|
1795
|
+
})));
|
|
1796
|
+
const getAgentTimelineRoute = HttpRouter.add("GET", "/api/agents/:id/timeline", httpHandler(Effect.gen(function* () {
|
|
1797
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1798
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
1799
|
+
const urlOpt = HttpServerRequest.toURL(request);
|
|
1800
|
+
const limitStr = Option.isSome(urlOpt) ? urlOpt.value.searchParams.get("limit") ?? "50" : "50";
|
|
1801
|
+
const limit = parseInt(limitStr) || 50;
|
|
1802
|
+
const activity = getActivity(id, limit);
|
|
1803
|
+
const agentState = yield* getAgentState(id);
|
|
1804
|
+
const events = activity.map((a) => ({
|
|
1805
|
+
timestamp: a.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1806
|
+
type: a.type || "activity",
|
|
1807
|
+
message: a.message || a.content || ""
|
|
1808
|
+
}));
|
|
1809
|
+
if (agentState?.startedAt) events.unshift({
|
|
1810
|
+
timestamp: agentState.startedAt,
|
|
1811
|
+
type: "started",
|
|
1812
|
+
message: "Agent started"
|
|
1813
|
+
});
|
|
1814
|
+
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1815
|
+
return jsonResponse({ timeline: events.slice(0, limit) });
|
|
1816
|
+
})));
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region routes/agents/messaging.ts
|
|
1819
|
+
init_agents$1();
|
|
1820
|
+
async function sendAgentMessage(id, message) {
|
|
1821
|
+
const remoteStateFile = join(join(homedir(), ".overdeck", "agents", id), "remote-state.json");
|
|
1822
|
+
let isRemote = false;
|
|
1823
|
+
if (existsSync(remoteStateFile)) try {
|
|
1824
|
+
const state = JSON.parse(await readFile(remoteStateFile, "utf-8"));
|
|
1825
|
+
isRemote = state.location === "remote" && Boolean(state.vmName);
|
|
1826
|
+
} catch {}
|
|
1827
|
+
await messageAgent(id, message, "dashboard:user-message");
|
|
1828
|
+
return isRemote ? {
|
|
1829
|
+
success: true,
|
|
1830
|
+
remote: true
|
|
1831
|
+
} : { success: true };
|
|
1832
|
+
}
|
|
1833
|
+
function validateAgentMessageOrigin(request) {
|
|
1834
|
+
const originCheck = validateOrigin(request);
|
|
1835
|
+
if (!originCheck.ok) return {
|
|
1836
|
+
ok: false,
|
|
1837
|
+
response: jsonResponse({
|
|
1838
|
+
ok: false,
|
|
1839
|
+
error: originCheck.error
|
|
1840
|
+
}, { status: 403 })
|
|
1841
|
+
};
|
|
1842
|
+
return { ok: true };
|
|
1843
|
+
}
|
|
1844
|
+
function postAgentMessageLikeRoute(path) {
|
|
1845
|
+
return HttpRouter.add("POST", path, httpHandler(Effect.gen(function* () {
|
|
1846
|
+
const originCheck = validateAgentMessageOrigin(yield* HttpServerRequest.HttpServerRequest);
|
|
1847
|
+
if (!originCheck.ok) return originCheck.response;
|
|
1848
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1849
|
+
const { message } = yield* readJsonBody$1;
|
|
1850
|
+
if (!message) return jsonResponse({ error: "Message required" }, { status: 400 });
|
|
1851
|
+
return yield* Effect.promise(() => sendAgentMessage(id, message)).pipe(Effect.map((result) => jsonResponse(result)));
|
|
1852
|
+
})));
|
|
1853
|
+
}
|
|
1854
|
+
const postAgentMessageRoute = postAgentMessageLikeRoute("/api/agents/:id/message");
|
|
1855
|
+
const postAgentTellRoute = postAgentMessageLikeRoute("/api/agents/:id/tell");
|
|
1856
|
+
const postAgentPokeRoute = HttpRouter.add("POST", "/api/agents/:id/poke", httpHandler(Effect.gen(function* () {
|
|
1857
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1858
|
+
const { message } = yield* readJsonBody$1;
|
|
1859
|
+
const pokeMsg = message || "You seem to have been inactive for a while. If you're stuck:\n1. Check your current task in continue.vbrief.json\n2. Try an alternative approach if blocked\n3. Ask for help if needed\n\nWhat's your current status?";
|
|
1860
|
+
yield* Effect.promise(() => messageAgent(id, pokeMsg));
|
|
1861
|
+
return jsonResponse({
|
|
1862
|
+
success: true,
|
|
1863
|
+
message: "Agent poked successfully"
|
|
1864
|
+
});
|
|
1865
|
+
})));
|
|
1866
|
+
//#endregion
|
|
1867
|
+
//#region ../../lib/runtimes/conversation-control.ts
|
|
1868
|
+
function conversationControlPaths(agentId, home) {
|
|
1869
|
+
const { agentDir } = ohmypiFifoPaths(agentId, home);
|
|
1870
|
+
return {
|
|
1871
|
+
agentDir,
|
|
1872
|
+
controlDir: join(agentDir, "control")
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
async function writeConversationControlCommand(agentId, command, home) {
|
|
1876
|
+
const paths = conversationControlPaths(agentId, home);
|
|
1877
|
+
await mkdir(paths.controlDir, {
|
|
1878
|
+
recursive: true,
|
|
1879
|
+
mode: 448
|
|
1880
|
+
});
|
|
1881
|
+
await chmod(paths.controlDir, 448);
|
|
1882
|
+
const finalPath = join(paths.controlDir, `${Date.now()}-${command.id}-${randomUUID$1()}.json`);
|
|
1883
|
+
const tempPath = `${finalPath}.tmp`;
|
|
1884
|
+
await writeFile(tempPath, `${JSON.stringify(command)}\n`, {
|
|
1885
|
+
encoding: "utf8",
|
|
1886
|
+
mode: 384
|
|
1887
|
+
});
|
|
1888
|
+
await chmod(tempPath, 384);
|
|
1889
|
+
await rename(tempPath, finalPath);
|
|
1890
|
+
await chmod(finalPath, 384);
|
|
1891
|
+
return finalPath;
|
|
1892
|
+
}
|
|
1893
|
+
//#endregion
|
|
1894
|
+
//#region ../../lib/ohmypi-codex-auth.ts
|
|
1895
|
+
/**
|
|
1896
|
+
* oh-my-pi (omp) OAuth helpers for the openai-codex (ChatGPT subscription) provider.
|
|
1897
|
+
* Forked from pi-codex-auth.ts (PAN-1520) for the omp migration (PAN-1989).
|
|
1898
|
+
*
|
|
1899
|
+
* omp stores its general credentials in ~/.omp/agent/agent.db (SQLite). The
|
|
1900
|
+
* openai-codex OAuth credential is persisted separately at ~/.omp/agent/auth.json
|
|
1901
|
+
* (same JSON format as Pi's ~/.pi/agent/auth.json) so that this helper's
|
|
1902
|
+
* read/write logic is unchanged from the Pi side.
|
|
1903
|
+
*
|
|
1904
|
+
* This module:
|
|
1905
|
+
* - reads/writes the openai-codex credential under ~/.omp/agent/auth.json,
|
|
1906
|
+
* - reports auth status (for `pan doctor` and the conversation spawn gate),
|
|
1907
|
+
* - refreshes an expired-but-recoverable token, and
|
|
1908
|
+
* - drives omp's headless device-code login to re-authenticate.
|
|
1909
|
+
*
|
|
1910
|
+
* The OAuth module is loaded from the installed `omp` package dynamically.
|
|
1911
|
+
* A missing or relocated omp OAuth module degrades to 'unavailable' — never throws.
|
|
1912
|
+
*/
|
|
1913
|
+
const PI_CODEX_PROVIDER = "openai-codex";
|
|
1914
|
+
function getOhmypiAuthPath() {
|
|
1915
|
+
return join(homedir(), ".omp", "agent", "auth.json");
|
|
1916
|
+
}
|
|
1917
|
+
function readAuthFile() {
|
|
1918
|
+
const path = getOhmypiAuthPath();
|
|
1919
|
+
if (!existsSync(path)) return {};
|
|
1920
|
+
try {
|
|
1921
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
1922
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1923
|
+
} catch {
|
|
1924
|
+
return {};
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function readOhmypiCodexCredential() {
|
|
1928
|
+
const cred = readAuthFile()[PI_CODEX_PROVIDER];
|
|
1929
|
+
if (cred && typeof cred === "object" && typeof cred["access"] === "string" && typeof cred["refresh"] === "string") return cred;
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
/** Atomically merge a fresh codex credential into auth.json (mode 0600). */
|
|
1933
|
+
function writeOhmypiCodexCredential(tokens) {
|
|
1934
|
+
const path = getOhmypiAuthPath();
|
|
1935
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1936
|
+
const auth = readAuthFile();
|
|
1937
|
+
const stored = {
|
|
1938
|
+
type: "oauth",
|
|
1939
|
+
access: tokens.access,
|
|
1940
|
+
refresh: tokens.refresh,
|
|
1941
|
+
expires: tokens.expires,
|
|
1942
|
+
accountId: tokens.accountId
|
|
1943
|
+
};
|
|
1944
|
+
auth[PI_CODEX_PROVIDER] = stored;
|
|
1945
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
1946
|
+
writeFileSync(tmp, JSON.stringify(auth, null, 2), { mode: 384 });
|
|
1947
|
+
renameSync(tmp, path);
|
|
1948
|
+
return stored;
|
|
1949
|
+
}
|
|
1950
|
+
/** Locate the `omp` executable on PATH (no subprocess — event-loop safe). */
|
|
1951
|
+
function findOmpBinary() {
|
|
1952
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
1953
|
+
if (!dir) continue;
|
|
1954
|
+
const candidate = join(dir, "omp");
|
|
1955
|
+
if (existsSync(candidate)) return candidate;
|
|
1956
|
+
}
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
/** omp is @oh-my-pi/pi-coding-agent; the oauth submodule path mirrors pi-ai's layout. */
|
|
1960
|
+
const OMP_AI_OAUTH_SUBPATH = join("@oh-my-pi", "pi-ai", "dist", "utils", "oauth", "openai-codex.js");
|
|
1961
|
+
/**
|
|
1962
|
+
* Resolve omp's openai-codex OAuth submodule by walking node_modules upward
|
|
1963
|
+
* from the real omp binary. Returns null when not found — the caller degrades
|
|
1964
|
+
* gracefully to 'unavailable' status.
|
|
1965
|
+
*/
|
|
1966
|
+
function resolveOmpCodexOAuthFile(ompBin) {
|
|
1967
|
+
let dir = dirname(realpathSync(ompBin));
|
|
1968
|
+
for (let i = 0; i < 10; i++) {
|
|
1969
|
+
const candidate = join(dir, "node_modules", OMP_AI_OAUTH_SUBPATH);
|
|
1970
|
+
if (existsSync(candidate)) return candidate;
|
|
1971
|
+
const parent = dirname(dir);
|
|
1972
|
+
if (parent === dir) break;
|
|
1973
|
+
dir = parent;
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
let cachedModule = null;
|
|
1978
|
+
/**
|
|
1979
|
+
* Dynamically load omp's openai-codex OAuth submodule from the installed `omp`
|
|
1980
|
+
* package. Returns null on any failure so callers can degrade gracefully.
|
|
1981
|
+
*/
|
|
1982
|
+
async function loadOhmypiCodexOAuth() {
|
|
1983
|
+
if (cachedModule) return cachedModule;
|
|
1984
|
+
cachedModule = (async () => {
|
|
1985
|
+
try {
|
|
1986
|
+
const ompBin = findOmpBinary();
|
|
1987
|
+
if (!ompBin) return null;
|
|
1988
|
+
const oauthFile = resolveOmpCodexOAuthFile(ompBin);
|
|
1989
|
+
if (!oauthFile) return null;
|
|
1990
|
+
const mod = await import(pathToFileURL(oauthFile).href);
|
|
1991
|
+
if (typeof mod.refreshOpenAICodexToken !== "function" || typeof mod.loginOpenAICodexDeviceCode !== "function") return null;
|
|
1992
|
+
return mod;
|
|
1993
|
+
} catch {
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
})();
|
|
1997
|
+
return cachedModule;
|
|
1998
|
+
}
|
|
1999
|
+
const EXPIRY_MARGIN_MS = 6e4;
|
|
2000
|
+
async function getOhmypiCodexAuthStatus(opts) {
|
|
2001
|
+
const cred = readOhmypiCodexCredential();
|
|
2002
|
+
if (!cred) return { status: "missing" };
|
|
2003
|
+
if (cred.expires > Date.now() + EXPIRY_MARGIN_MS) return {
|
|
2004
|
+
status: "ok",
|
|
2005
|
+
expiresAt: cred.expires
|
|
2006
|
+
};
|
|
2007
|
+
if (opts?.refreshIfExpired) {
|
|
2008
|
+
const refreshed = await refreshOhmypiCodexAuth();
|
|
2009
|
+
if (refreshed) return {
|
|
2010
|
+
status: "ok",
|
|
2011
|
+
expiresAt: refreshed.expires
|
|
2012
|
+
};
|
|
2013
|
+
return {
|
|
2014
|
+
status: "expired",
|
|
2015
|
+
expiresAt: cred.expires,
|
|
2016
|
+
refreshFailed: true
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
status: "expired",
|
|
2021
|
+
expiresAt: cred.expires,
|
|
2022
|
+
refreshFailed: false
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
async function refreshOhmypiCodexAuth() {
|
|
2026
|
+
const cred = readOhmypiCodexCredential();
|
|
2027
|
+
if (!cred?.refresh) return null;
|
|
2028
|
+
const mod = await loadOhmypiCodexOAuth();
|
|
2029
|
+
if (!mod) return null;
|
|
2030
|
+
try {
|
|
2031
|
+
return writeOhmypiCodexCredential(await mod.refreshOpenAICodexToken(cred.refresh));
|
|
2032
|
+
} catch {
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region services/conversation-attachments.ts
|
|
2038
|
+
init_paths();
|
|
2039
|
+
const CONVERSATION_ATTACHMENTS_DIR = "conversation-attachments";
|
|
2040
|
+
/** Run async tasks in bounded batches to avoid unbounded Promise.all
|
|
2041
|
+
* that can exhaust file descriptors or memory under heavy load. */
|
|
2042
|
+
async function runInBatches(items, batchSize, fn) {
|
|
2043
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
2044
|
+
const batch = items.slice(i, i + batchSize);
|
|
2045
|
+
await Promise.all(batch.map(fn));
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
/** Max JSONL line length to process. Lines larger than this are skipped to
|
|
2049
|
+
* prevent readline from stalling permanently on oversized input (e.g. a
|
|
2050
|
+
* single-line base64 payload >15 MB). */
|
|
2051
|
+
const MAX_JSONL_LINE_LENGTH = 15 * 1024 * 1024;
|
|
2052
|
+
/** Bounded LRU cache for readSessionAttachmentBasenames to avoid full JSONL
|
|
2053
|
+
* rescans when the session file has not changed. Key = sessionFile:mtimeMs. */
|
|
2054
|
+
const SESSION_ATTACHMENT_CACHE_MAX = 100;
|
|
2055
|
+
const sessionAttachmentCache = /* @__PURE__ */ new Map();
|
|
2056
|
+
function getAttachmentCacheKey(sessionFile, mtimeMs) {
|
|
2057
|
+
return `${sessionFile}:${mtimeMs}`;
|
|
2058
|
+
}
|
|
2059
|
+
function setAttachmentCache(sessionFile, mtimeMs, basenames) {
|
|
2060
|
+
const key = getAttachmentCacheKey(sessionFile, mtimeMs);
|
|
2061
|
+
sessionAttachmentCache.set(key, basenames);
|
|
2062
|
+
if (sessionAttachmentCache.size > SESSION_ATTACHMENT_CACHE_MAX) {
|
|
2063
|
+
const firstKey = sessionAttachmentCache.keys().next().value;
|
|
2064
|
+
if (firstKey !== void 0) sessionAttachmentCache.delete(firstKey);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function getAttachmentCache(sessionFile, mtimeMs) {
|
|
2068
|
+
const key = getAttachmentCacheKey(sessionFile, mtimeMs);
|
|
2069
|
+
return sessionAttachmentCache.get(key);
|
|
2070
|
+
}
|
|
2071
|
+
/** Conversation names are sanitized to [a-zA-Z0-9_-]{1,64} on creation.
|
|
2072
|
+
* Re-validate here for defense-in-depth against path traversal. */
|
|
2073
|
+
function assertSafeName(name) {
|
|
2074
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name)) throw new Error("Invalid conversation name");
|
|
2075
|
+
}
|
|
2076
|
+
function getConversationAttachmentsRoot() {
|
|
2077
|
+
return join(getOverdeckHome(), CONVERSATION_ATTACHMENTS_DIR);
|
|
2078
|
+
}
|
|
2079
|
+
function getConversationAttachmentDir(name) {
|
|
2080
|
+
assertSafeName(name);
|
|
2081
|
+
return join(getConversationAttachmentsRoot(), name);
|
|
2082
|
+
}
|
|
2083
|
+
async function ensureConversationAttachmentDir(name) {
|
|
2084
|
+
const dir = getConversationAttachmentDir(name);
|
|
2085
|
+
await mkdir(dir, { recursive: true });
|
|
2086
|
+
return dir;
|
|
2087
|
+
}
|
|
2088
|
+
async function cleanupConversationAttachments(name) {
|
|
2089
|
+
assertSafeName(name);
|
|
2090
|
+
await rm(getConversationAttachmentDir(name), {
|
|
2091
|
+
recursive: true,
|
|
2092
|
+
force: true
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
/** Safety-net cleanup: remove attachment directories for conversations that no
|
|
2096
|
+
* longer exist in the database. Catches orphaned files from abandoned/crashed
|
|
2097
|
+
* sessions that the lifecycle poller missed. */
|
|
2098
|
+
async function cleanupOrphanedConversationAttachments() {
|
|
2099
|
+
try {
|
|
2100
|
+
const { listConversations } = await import("./conversations-BajHQ60c.js");
|
|
2101
|
+
const convNames = new Set(listConversations().map((c) => c.name));
|
|
2102
|
+
const root = getConversationAttachmentsRoot();
|
|
2103
|
+
await runInBatches((await readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !convNames.has(entry.name)).map((entry) => join(root, entry.name)), 10, (path) => rm(path, {
|
|
2104
|
+
recursive: true,
|
|
2105
|
+
force: true
|
|
2106
|
+
}).catch(() => {}));
|
|
2107
|
+
} catch {}
|
|
2108
|
+
}
|
|
2109
|
+
async function listConversationAttachmentPaths(name) {
|
|
2110
|
+
const dir = getConversationAttachmentDir(name);
|
|
2111
|
+
try {
|
|
2112
|
+
return (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => join(dir, entry.name));
|
|
2113
|
+
} catch {
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
/** Synchronous containment check: does the normalized path start with the
|
|
2118
|
+
* pre-resolved attachment directory prefix? Uses resolve() (not realpath)
|
|
2119
|
+
* so the check is I/O-free — safe because we are only cataloguing references
|
|
2120
|
+
* in the JSONL, not accessing the filesystem. */
|
|
2121
|
+
function isUnderAttachmentDir(resolvedAttachmentDir, attachmentPath) {
|
|
2122
|
+
try {
|
|
2123
|
+
return resolve(attachmentPath).startsWith(`${resolvedAttachmentDir}/`);
|
|
2124
|
+
} catch {
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function readSessionAttachmentBasenames(sessionFile, name) {
|
|
2129
|
+
try {
|
|
2130
|
+
let mtimeMs;
|
|
2131
|
+
try {
|
|
2132
|
+
mtimeMs = (await stat$1(sessionFile)).mtimeMs;
|
|
2133
|
+
} catch {
|
|
2134
|
+
return /* @__PURE__ */ new Set();
|
|
2135
|
+
}
|
|
2136
|
+
const cached = getAttachmentCache(sessionFile, mtimeMs);
|
|
2137
|
+
if (cached) return cached;
|
|
2138
|
+
let resolvedAttachmentDir;
|
|
2139
|
+
try {
|
|
2140
|
+
resolvedAttachmentDir = await realpath(getConversationAttachmentDir(name));
|
|
2141
|
+
} catch {
|
|
2142
|
+
resolvedAttachmentDir = resolve(getConversationAttachmentDir(name));
|
|
2143
|
+
}
|
|
2144
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2145
|
+
const stream = createReadStream(sessionFile, { encoding: "utf-8" });
|
|
2146
|
+
const rl = createInterface({
|
|
2147
|
+
input: stream,
|
|
2148
|
+
crlfDelay: Infinity
|
|
2149
|
+
});
|
|
2150
|
+
try {
|
|
2151
|
+
for await (const line of rl) {
|
|
2152
|
+
if (!line.trim()) continue;
|
|
2153
|
+
if (line.length > MAX_JSONL_LINE_LENGTH) continue;
|
|
2154
|
+
let text;
|
|
2155
|
+
let structuredFoundPaths = false;
|
|
2156
|
+
try {
|
|
2157
|
+
const entry = JSON.parse(line);
|
|
2158
|
+
if (typeof entry?.message?.content === "string") text = entry.message.content;
|
|
2159
|
+
else if (Array.isArray(entry?.message?.content)) text = entry.message.content.map((c) => typeof c === "string" ? c : c?.text ?? "").join("");
|
|
2160
|
+
else if (typeof entry?.message?.text === "string") text = entry.message.text;
|
|
2161
|
+
else if (typeof entry?.text === "string") text = entry.text;
|
|
2162
|
+
} catch {
|
|
2163
|
+
text = line;
|
|
2164
|
+
}
|
|
2165
|
+
if (text) {
|
|
2166
|
+
for (const attachmentPath of extractConversationAttachmentPaths(text)) if (isUnderAttachmentDir(resolvedAttachmentDir, attachmentPath)) {
|
|
2167
|
+
referenced.add(basename(attachmentPath));
|
|
2168
|
+
structuredFoundPaths = true;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (!structuredFoundPaths) for (const match of line.matchAll(/"@([^"]+)"/g)) {
|
|
2172
|
+
const rawValue = match[1];
|
|
2173
|
+
if (!rawValue.startsWith("/") && !rawValue.startsWith("\\/")) continue;
|
|
2174
|
+
let attachmentPath;
|
|
2175
|
+
try {
|
|
2176
|
+
attachmentPath = JSON.parse(`"${rawValue}"`);
|
|
2177
|
+
} catch {
|
|
2178
|
+
attachmentPath = rawValue;
|
|
2179
|
+
}
|
|
2180
|
+
if (typeof attachmentPath === "string" && attachmentPath.startsWith("/")) {
|
|
2181
|
+
if (isUnderAttachmentDir(resolvedAttachmentDir, attachmentPath)) referenced.add(basename(attachmentPath));
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
} finally {
|
|
2186
|
+
rl.close();
|
|
2187
|
+
stream.destroy();
|
|
2188
|
+
}
|
|
2189
|
+
setAttachmentCache(sessionFile, mtimeMs, referenced);
|
|
2190
|
+
return referenced;
|
|
2191
|
+
} catch {
|
|
2192
|
+
return /* @__PURE__ */ new Set();
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
async function cleanupUnreferencedConversationAttachments(conversation) {
|
|
2196
|
+
const attachmentPaths = await listConversationAttachmentPaths(conversation.name);
|
|
2197
|
+
if (attachmentPaths.length === 0) return;
|
|
2198
|
+
if (!conversation.sessionFile) {
|
|
2199
|
+
await runInBatches(attachmentPaths, 10, async (attachmentPath) => {
|
|
2200
|
+
await rm(attachmentPath, { force: true });
|
|
2201
|
+
});
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
let sessionMtimeMs;
|
|
2205
|
+
try {
|
|
2206
|
+
sessionMtimeMs = (await stat$1(conversation.sessionFile)).mtimeMs;
|
|
2207
|
+
} catch {
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const referencedBasenames = await readSessionAttachmentBasenames(conversation.sessionFile, conversation.name);
|
|
2211
|
+
try {
|
|
2212
|
+
const freshStats = await stat$1(conversation.sessionFile);
|
|
2213
|
+
if (freshStats.mtimeMs > sessionMtimeMs) sessionMtimeMs = freshStats.mtimeMs;
|
|
2214
|
+
} catch {}
|
|
2215
|
+
await runInBatches(attachmentPaths, 10, async (attachmentPath) => {
|
|
2216
|
+
if (referencedBasenames.has(basename(attachmentPath))) return;
|
|
2217
|
+
try {
|
|
2218
|
+
if ((await stat$1(attachmentPath)).mtimeMs >= sessionMtimeMs) return;
|
|
2219
|
+
} catch {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
await rm(attachmentPath, { force: true });
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
function extractConversationAttachmentPaths(message) {
|
|
2226
|
+
return Array.from(message.matchAll(/(?<!\S)@(\/[^\s]+)/g), ([, attachmentPath]) => {
|
|
2227
|
+
let path = attachmentPath;
|
|
2228
|
+
while (path.length > 1 && /[.,;:!?)\]}+"']$/.test(path)) path = path.slice(0, -1);
|
|
2229
|
+
return path;
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
/** Resolve a path for containment checking, walking parent directories with
|
|
2233
|
+
* realpath to preserve symlink detection even when the target file is missing. */
|
|
2234
|
+
async function resolveForContainment(attachmentPath) {
|
|
2235
|
+
try {
|
|
2236
|
+
return await realpath(attachmentPath);
|
|
2237
|
+
} catch {
|
|
2238
|
+
let dir = dirname(attachmentPath);
|
|
2239
|
+
const remaining = [basename(attachmentPath)];
|
|
2240
|
+
while (dir !== "/" && dir !== ".") try {
|
|
2241
|
+
return join(await realpath(dir), ...remaining);
|
|
2242
|
+
} catch {
|
|
2243
|
+
remaining.unshift(basename(dir));
|
|
2244
|
+
dir = dirname(dir);
|
|
2245
|
+
}
|
|
2246
|
+
return resolve(attachmentPath);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
async function isManagedConversationAttachmentPath(attachmentPath) {
|
|
2250
|
+
try {
|
|
2251
|
+
const attachmentsRoot = await realpath(getConversationAttachmentsRoot());
|
|
2252
|
+
return (await resolveForContainment(attachmentPath)).startsWith(`${attachmentsRoot}/`);
|
|
2253
|
+
} catch {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
async function isConversationAttachmentPath(name, attachmentPath) {
|
|
2258
|
+
try {
|
|
2259
|
+
const attachmentDir = resolve(getConversationAttachmentDir(name));
|
|
2260
|
+
return (await resolveForContainment(attachmentPath)).startsWith(`${attachmentDir}/`);
|
|
2261
|
+
} catch {
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
async function hasConversationAttachment(name, attachmentPath) {
|
|
2266
|
+
try {
|
|
2267
|
+
const attachmentDir = await realpath(getConversationAttachmentDir(name));
|
|
2268
|
+
const candidate = await resolveForContainment(attachmentPath);
|
|
2269
|
+
if (!candidate.startsWith(`${attachmentDir}/`)) return false;
|
|
2270
|
+
return existsSync(candidate);
|
|
2271
|
+
} catch {
|
|
2272
|
+
return false;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
async function removeConversationAttachment(name, attachmentPath) {
|
|
2276
|
+
if (!await isConversationAttachmentPath(name, attachmentPath)) return false;
|
|
2277
|
+
await rm(attachmentPath, { force: true });
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region ../../lib/overdeck/conversation-runtime.ts
|
|
2282
|
+
init_projects();
|
|
2283
|
+
init_tmux();
|
|
2284
|
+
init_agents$1();
|
|
2285
|
+
init_behavior();
|
|
2286
|
+
init_paths();
|
|
2287
|
+
const execAsync$1 = promisify(exec);
|
|
2288
|
+
const execFileAsync$1 = promisify(execFile);
|
|
2289
|
+
const PROCESS_CLEANUP_GRACE_MS = 750;
|
|
2290
|
+
const PTY_SUPERVISOR_SOCKET_WAIT_MS = 3e4;
|
|
2291
|
+
function parseProcessTable(output) {
|
|
2292
|
+
return output.split("\n").map((line) => {
|
|
2293
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
2294
|
+
if (!match) return null;
|
|
2295
|
+
return {
|
|
2296
|
+
pid: Number(match[1]),
|
|
2297
|
+
ppid: Number(match[2]),
|
|
2298
|
+
args: match[3] ?? ""
|
|
2299
|
+
};
|
|
2300
|
+
}).filter((row) => row !== null && Number.isFinite(row.pid) && Number.isFinite(row.ppid));
|
|
2301
|
+
}
|
|
2302
|
+
async function readProcessTable() {
|
|
2303
|
+
const { stdout } = await execFileAsync$1("ps", ["-eo", "pid=,ppid=,args="], {
|
|
2304
|
+
encoding: "utf-8",
|
|
2305
|
+
maxBuffer: 10 * 1024 * 1024
|
|
2306
|
+
});
|
|
2307
|
+
return parseProcessTable(stdout);
|
|
2308
|
+
}
|
|
2309
|
+
function collectProcessTree(rootPids, rows) {
|
|
2310
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const row of rows) {
|
|
2312
|
+
const children = childrenByParent.get(row.ppid) ?? [];
|
|
2313
|
+
children.push(row.pid);
|
|
2314
|
+
childrenByParent.set(row.ppid, children);
|
|
2315
|
+
}
|
|
2316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2317
|
+
const ordered = [];
|
|
2318
|
+
const visit = (pid) => {
|
|
2319
|
+
if (seen.has(pid) || pid === process.pid) return;
|
|
2320
|
+
seen.add(pid);
|
|
2321
|
+
for (const child of childrenByParent.get(pid) ?? []) visit(child);
|
|
2322
|
+
ordered.push(pid);
|
|
2323
|
+
};
|
|
2324
|
+
for (const pid of rootPids) visit(pid);
|
|
2325
|
+
return ordered;
|
|
2326
|
+
}
|
|
2327
|
+
function isProcessAlive(pid) {
|
|
2328
|
+
try {
|
|
2329
|
+
process.kill(pid, 0);
|
|
2330
|
+
return true;
|
|
2331
|
+
} catch {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
async function terminatePids(pids) {
|
|
2336
|
+
if (pids.length === 0) return;
|
|
2337
|
+
for (const pid of pids) try {
|
|
2338
|
+
process.kill(pid, "SIGTERM");
|
|
2339
|
+
} catch {}
|
|
2340
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESS_CLEANUP_GRACE_MS));
|
|
2341
|
+
for (const pid of pids) {
|
|
2342
|
+
if (!isProcessAlive(pid)) continue;
|
|
2343
|
+
try {
|
|
2344
|
+
process.kill(pid, "SIGKILL");
|
|
2345
|
+
} catch {}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function conversationRuntimeRootPids(conv, rows) {
|
|
2349
|
+
const launcherScript = join(getOverdeckHome(), "conversations", conv.tmuxSession, "launcher.sh");
|
|
2350
|
+
const sessionId = conv.claudeSessionId?.trim();
|
|
2351
|
+
const sessionNeedles = sessionId ? [`--resume ${sessionId}`, `--session-id ${sessionId}`] : [];
|
|
2352
|
+
const serverPid = findManagedServerPidSync();
|
|
2353
|
+
return rows.filter((row) => {
|
|
2354
|
+
if (row.pid === process.pid) return false;
|
|
2355
|
+
if (serverPid !== void 0 && row.pid === serverPid) return false;
|
|
2356
|
+
if (row.args.includes(launcherScript)) return true;
|
|
2357
|
+
return sessionNeedles.some((needle) => row.args.includes(needle));
|
|
2358
|
+
}).map((row) => row.pid);
|
|
2359
|
+
}
|
|
2360
|
+
async function killConversationRuntimeProcesses(conv) {
|
|
2361
|
+
const rows = await readProcessTable();
|
|
2362
|
+
await terminatePids(collectProcessTree(conversationRuntimeRootPids(conv, rows), rows));
|
|
2363
|
+
}
|
|
2364
|
+
async function stopConversationRuntime(conv, name) {
|
|
2365
|
+
if (hasOtherActiveConversationOnTmuxSession(conv.tmuxSession, name)) return;
|
|
2366
|
+
await Effect.runPromise(killSession(conv.tmuxSession).pipe(Effect.catch(() => Effect.succeed(void 0))));
|
|
2367
|
+
try {
|
|
2368
|
+
await killConversationRuntimeProcesses(conv);
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2371
|
+
console.warn(`[conversations] failed to cleanup runtime processes for ${name}: ${msg}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
/** Quote a string for safe use in a bash script using single-quote wrapping. */
|
|
2375
|
+
function shellQuote(str) {
|
|
2376
|
+
return "'" + str.replace(/'/g, "'\"'\"'") + "'";
|
|
2377
|
+
}
|
|
2378
|
+
const SAFE_MODEL_PATTERN = /^[a-zA-Z0-9_.:\/-]+$/;
|
|
2379
|
+
const SAFE_EFFORT_PATTERN = /^(low|medium|high)$/;
|
|
2380
|
+
const SAFE_ISSUE_ID_PATTERN = /^[A-Z0-9]+-[0-9]+$/;
|
|
2381
|
+
const PI_CONVERSATION_SOURCE_CONTRACT = [
|
|
2382
|
+
"Pi conversation source contract:",
|
|
2383
|
+
"A message marked source:'extension' was injected by the Overdeck orchestrator or another agent, not typed by the human operator.",
|
|
2384
|
+
"Treat it as coordination guidance; do not attribute it to the human operator."
|
|
2385
|
+
].join(" ");
|
|
2386
|
+
async function resolveAllowedHarness(requested, model) {
|
|
2387
|
+
if (!model) return "claude-code";
|
|
2388
|
+
return resolveHarness({
|
|
2389
|
+
model,
|
|
2390
|
+
explicit: requested === "ohmypi" || requested === "claude-code" || requested === "codex" ? requested : void 0
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
async function isInsideGitWorkTree(dir) {
|
|
2394
|
+
try {
|
|
2395
|
+
const { stdout } = await execAsync$1("git rev-parse --is-inside-work-tree", {
|
|
2396
|
+
cwd: dir,
|
|
2397
|
+
encoding: "utf-8"
|
|
2398
|
+
});
|
|
2399
|
+
return stdout.trim() === "true";
|
|
2400
|
+
} catch {
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
async function validateCwdContainment(cwd) {
|
|
2405
|
+
if (!cwd.startsWith("/")) return false;
|
|
2406
|
+
if (cwd.split("/").filter(Boolean).includes("..")) return false;
|
|
2407
|
+
try {
|
|
2408
|
+
const resolved = await realpath(cwd);
|
|
2409
|
+
if (!(await stat$1(resolved)).isDirectory()) return false;
|
|
2410
|
+
const home = homedir();
|
|
2411
|
+
return resolved.startsWith(`${home}/`) || resolved === home;
|
|
2412
|
+
} catch {
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
async function waitForClaudeReady(tmuxSession) {
|
|
2417
|
+
const deadline = Date.now() + 3e4;
|
|
2418
|
+
while (Date.now() < deadline) {
|
|
2419
|
+
if ((await Effect.runPromise(capturePane(tmuxSession, 200))).includes("❯")) {
|
|
2420
|
+
console.log(`[conversations] Claude Code ready in ${tmuxSession}`);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2424
|
+
}
|
|
2425
|
+
console.warn(`[conversations] Timed out waiting for Claude Code prompt in ${tmuxSession}`);
|
|
2426
|
+
}
|
|
2427
|
+
function isPiTuiInputReady(snapshot) {
|
|
2428
|
+
return /^\s*[❯›>]\s/m.test(snapshot) || /(?:^|\s)0(?:\.\d+)?%\s+context\s+used\b/i.test(snapshot) || /⬢[^\n]*◕/.test(snapshot);
|
|
2429
|
+
}
|
|
2430
|
+
async function waitForPiTuiReady(tmuxSession, timeoutMs = 6e4) {
|
|
2431
|
+
const deadline = Date.now() + timeoutMs;
|
|
2432
|
+
while (Date.now() < deadline) {
|
|
2433
|
+
if (isPiTuiInputReady(await Effect.runPromise(capturePane(tmuxSession, 40).pipe(Effect.catch(() => Effect.succeed("")))))) {
|
|
2434
|
+
console.log(`[conversations] Pi TUI ready for ${tmuxSession}`);
|
|
2435
|
+
return true;
|
|
2436
|
+
}
|
|
2437
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2438
|
+
}
|
|
2439
|
+
console.warn(`[conversations] Timed out waiting for Pi TUI to render in ${tmuxSession}`);
|
|
2440
|
+
return false;
|
|
2441
|
+
}
|
|
2442
|
+
function shouldReportUnresolvedLiveSession(conv) {
|
|
2443
|
+
if (!conv || conv.status !== "active") return false;
|
|
2444
|
+
return getHarnessBehavior(conv.harness).transcriptKind === "claude-jsonl";
|
|
2445
|
+
}
|
|
2446
|
+
function conversationSessionAliveFromState(conv, tmuxSessionAlive) {
|
|
2447
|
+
return conv.status === "active" && !conv.forkStatus && tmuxSessionAlive;
|
|
2448
|
+
}
|
|
2449
|
+
function conversationNeedsRunningRepair(conv, tmuxSessionAlive, harnessProcessAlive) {
|
|
2450
|
+
return conv.status === "ended" && !conv.forkStatus && tmuxSessionAlive && harnessProcessAlive;
|
|
2451
|
+
}
|
|
2452
|
+
/** Generate a default conversation name, e.g. 20260404-1234 */
|
|
2453
|
+
function generateConversationName() {
|
|
2454
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}-${Math.floor(Math.random() * 9e3 + 1e3)}`;
|
|
2455
|
+
}
|
|
2456
|
+
async function tmuxSessionExists(sessionName) {
|
|
2457
|
+
return Effect.runPromise(sessionExists(sessionName));
|
|
2458
|
+
}
|
|
2459
|
+
async function waitForTmuxSession(sessionName, timeoutMs = 3e4) {
|
|
2460
|
+
const start = Date.now();
|
|
2461
|
+
while (Date.now() - start < timeoutMs) {
|
|
2462
|
+
if (await Effect.runPromise(sessionExists(sessionName))) return;
|
|
2463
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2464
|
+
}
|
|
2465
|
+
throw new Error(`Timed out waiting for tmux session ${sessionName}`);
|
|
2466
|
+
}
|
|
2467
|
+
function shouldUseSupervisorForConversation(harness) {
|
|
2468
|
+
return getHarnessBehavior(harness).supportsPtySupervisor && process.env.OVERDECK_DOCKER_WORKSPACE !== "1" && process.env.PAN_DOCKER !== "1";
|
|
2469
|
+
}
|
|
2470
|
+
async function waitForConversationRuntimeReady(tmuxSession, harness, mode) {
|
|
2471
|
+
const transcriptKind = getHarnessBehavior(harness).transcriptKind;
|
|
2472
|
+
if (transcriptKind === "ohmypi-jsonl") {
|
|
2473
|
+
if (!await waitForPiTuiReady(tmuxSession)) throw new Error(`Pi (ohmypi) did not become interactive in ${tmuxSession} within the startup window — MCP server connections are likely still in progress. Retry once MCP servers finish connecting, or check ~/.claude/mcp.json for a slow/hanging server.`);
|
|
2474
|
+
} else if (transcriptKind !== "codex-rollout-jsonl" && mode === "spawn") {
|
|
2475
|
+
await waitForClaudeReady(tmuxSession);
|
|
2476
|
+
console.log(`[conversations] Claude ready in ${tmuxSession}`);
|
|
2477
|
+
} else if (transcriptKind !== "codex-rollout-jsonl") await waitForReadySignal(tmuxSession, 30);
|
|
2478
|
+
}
|
|
2479
|
+
async function handleConversationSwitchModel(name, body) {
|
|
2480
|
+
const conv = getConversationByName(name);
|
|
2481
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
2482
|
+
const model = typeof body["model"] === "string" && body["model"].trim() ? body["model"].trim() : conv.model ?? void 0;
|
|
2483
|
+
const currentHarness = conv.harness ?? "claude-code";
|
|
2484
|
+
const requestedHarness = body["harness"];
|
|
2485
|
+
let harness = currentHarness;
|
|
2486
|
+
if (requestedHarness === "ohmypi" || requestedHarness === "pi" || requestedHarness === "claude-code" || requestedHarness === "codex") {
|
|
2487
|
+
const requestedRuntime = requestedHarness === "pi" ? "ohmypi" : requestedHarness;
|
|
2488
|
+
if (requestedRuntime !== currentHarness) {
|
|
2489
|
+
const policyModel = model ?? conv.model ?? "";
|
|
2490
|
+
const decision = canUseHarnessSync(requestedRuntime, policyModel, await getProviderAuthMode(policyModel));
|
|
2491
|
+
if (!decision.allowed) return jsonResponse({ error: decision.reason ?? "Harness not allowed for this model" }, { status: 400 });
|
|
2492
|
+
}
|
|
2493
|
+
harness = requestedRuntime;
|
|
2494
|
+
}
|
|
2495
|
+
const harnessChanged = harness !== currentHarness;
|
|
2496
|
+
if (!await validateCwdContainment(conv.cwd)) return jsonResponse({ error: "Invalid cwd" }, { status: 400 });
|
|
2497
|
+
if (model && !SAFE_MODEL_PATTERN.test(model)) return jsonResponse({ error: "Invalid model" }, { status: 400 });
|
|
2498
|
+
const livePiSwitch = isPiControlChannelHarness(currentHarness) && conv.status !== "ended";
|
|
2499
|
+
if (conv.claudeSessionId && harnessChanged) return jsonResponse({ error: "Conversation harness is locked once a conversation has started" }, { status: 409 });
|
|
2500
|
+
if (conv.claudeSessionId && !livePiSwitch) return jsonResponse({ error: "Conversation model is locked once a conversation has started" }, { status: 409 });
|
|
2501
|
+
if (model) {
|
|
2502
|
+
if (livePiSwitch) await sendConversationControlCommand(conv, {
|
|
2503
|
+
type: "set_model",
|
|
2504
|
+
model
|
|
2505
|
+
});
|
|
2506
|
+
setConversationModel(name, model);
|
|
2507
|
+
}
|
|
2508
|
+
if (harnessChanged) setConversationHarness(name, harness);
|
|
2509
|
+
const updated = getConversationByName(name) ?? conv;
|
|
2510
|
+
return jsonResponse({
|
|
2511
|
+
...updated,
|
|
2512
|
+
model: model ?? updated.model,
|
|
2513
|
+
harness,
|
|
2514
|
+
sessionAlive: false
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
function getPtySupervisorSocketPath(agentId) {
|
|
2518
|
+
return join(getOverdeckHome(), "sockets", `pty-${agentId}.sock`);
|
|
2519
|
+
}
|
|
2520
|
+
async function waitForPtySupervisorSocket(agentId, timeoutMs = PTY_SUPERVISOR_SOCKET_WAIT_MS) {
|
|
2521
|
+
const socketPath = getPtySupervisorSocketPath(agentId);
|
|
2522
|
+
const start = Date.now();
|
|
2523
|
+
while (Date.now() - start < timeoutMs) {
|
|
2524
|
+
try {
|
|
2525
|
+
if (((await stat$1(socketPath)).mode & 511) === 384) return;
|
|
2526
|
+
} catch {}
|
|
2527
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2528
|
+
}
|
|
2529
|
+
throw new Error(`Timed out waiting for PTY supervisor socket ${socketPath}`);
|
|
2530
|
+
}
|
|
2531
|
+
async function extractModelFromSessionFile(sessionFile) {
|
|
2532
|
+
try {
|
|
2533
|
+
if (!existsSync(sessionFile)) return null;
|
|
2534
|
+
const stream = createReadStream(sessionFile, { encoding: "utf-8" });
|
|
2535
|
+
const rl = createInterface({
|
|
2536
|
+
input: stream,
|
|
2537
|
+
crlfDelay: Infinity
|
|
2538
|
+
});
|
|
2539
|
+
try {
|
|
2540
|
+
for await (const line of rl) {
|
|
2541
|
+
if (!line.trim()) continue;
|
|
2542
|
+
const entry = JSON.parse(line);
|
|
2543
|
+
if (entry.type === "assistant" && entry.message?.model) return entry.message.model;
|
|
2544
|
+
}
|
|
2545
|
+
} finally {
|
|
2546
|
+
rl.close();
|
|
2547
|
+
stream.destroy();
|
|
2548
|
+
}
|
|
2549
|
+
} catch {}
|
|
2550
|
+
return null;
|
|
2551
|
+
}
|
|
2552
|
+
let backfillRunning = false;
|
|
2553
|
+
async function backfillConversationModels(resolveSessionFile) {
|
|
2554
|
+
if (backfillRunning) return;
|
|
2555
|
+
backfillRunning = true;
|
|
2556
|
+
try {
|
|
2557
|
+
const convs = listConversations();
|
|
2558
|
+
const probe = await Promise.all(convs.map(async (conv) => !conv.model ? await resolveSessionFile(conv) : null));
|
|
2559
|
+
const candidates = convs.filter((_, i) => probe[i] !== null);
|
|
2560
|
+
const BATCH_SIZE = 10;
|
|
2561
|
+
let backfilled = 0;
|
|
2562
|
+
for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
|
|
2563
|
+
const batch = candidates.slice(i, i + BATCH_SIZE);
|
|
2564
|
+
const results = await Promise.all(batch.map(async (conv) => {
|
|
2565
|
+
const sessionFile = await resolveSessionFile(conv);
|
|
2566
|
+
if (!sessionFile) return false;
|
|
2567
|
+
const model = await extractModelFromSessionFile(sessionFile);
|
|
2568
|
+
if (model && SAFE_MODEL_PATTERN.test(model)) {
|
|
2569
|
+
backfillConversationModel(conv.name, model);
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
return false;
|
|
2573
|
+
}));
|
|
2574
|
+
backfilled += results.filter(Boolean).length;
|
|
2575
|
+
}
|
|
2576
|
+
if (backfilled > 0) console.log(`[conversations] Backfilled model for ${backfilled} conversation(s)`);
|
|
2577
|
+
} finally {
|
|
2578
|
+
backfillRunning = false;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function startConversationModelBackfill(resolveSessionFile) {
|
|
2582
|
+
backfillConversationModels(resolveSessionFile).catch((err) => {
|
|
2583
|
+
console.error("[conversations] Model backfill failed:", err);
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
async function claudeConversationSystemPromptFiles(cwd) {
|
|
2587
|
+
const files = [];
|
|
2588
|
+
const contextFile = workspaceContextFile(cwd);
|
|
2589
|
+
try {
|
|
2590
|
+
await stat$1(contextFile);
|
|
2591
|
+
files.push(contextFile);
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
if (!isNotFound(error)) throw error;
|
|
2594
|
+
}
|
|
2595
|
+
files.push(await ensureSessionContextBriefingFile());
|
|
2596
|
+
return files;
|
|
2597
|
+
}
|
|
2598
|
+
async function ensurePiConversationSourceContractFile() {
|
|
2599
|
+
const contextDir = join(getOverdeckHome(), "context");
|
|
2600
|
+
await mkdir(contextDir, { recursive: true });
|
|
2601
|
+
const path = join(contextDir, "pi-conversation-source-contract.md");
|
|
2602
|
+
await writeFile(path, `${PI_CONVERSATION_SOURCE_CONTRACT}\n`, "utf-8");
|
|
2603
|
+
return path;
|
|
2604
|
+
}
|
|
2605
|
+
async function piConversationSystemPromptFiles(cwd) {
|
|
2606
|
+
const files = [];
|
|
2607
|
+
const globalFile = piGlobalContextFile();
|
|
2608
|
+
try {
|
|
2609
|
+
await stat$1(globalFile);
|
|
2610
|
+
files.push(globalFile);
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
if (!isNotFound(error)) throw error;
|
|
2613
|
+
}
|
|
2614
|
+
files.push(await ensurePiConversationSourceContractFile());
|
|
2615
|
+
const contextFile = workspaceContextFile(cwd);
|
|
2616
|
+
try {
|
|
2617
|
+
await stat$1(contextFile);
|
|
2618
|
+
files.push(contextFile);
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
if (!isNotFound(error)) throw error;
|
|
2621
|
+
}
|
|
2622
|
+
files.push(await ensureSessionContextBriefingFile());
|
|
2623
|
+
return files;
|
|
2624
|
+
}
|
|
2625
|
+
function isNotFound(error) {
|
|
2626
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2627
|
+
}
|
|
2628
|
+
async function spawnConversationSession(tmuxSession, cwd, claudeSessionId, model, effort, issueId, resume = false, harness = "claude-code", plainFork = false) {
|
|
2629
|
+
const behavior = getHarnessBehavior(harness);
|
|
2630
|
+
const stateDir = join(getOverdeckHome(), "conversations", tmuxSession);
|
|
2631
|
+
await mkdir(stateDir, { recursive: true });
|
|
2632
|
+
clearReadySignal(tmuxSession);
|
|
2633
|
+
const launcherScript = join(stateDir, "launcher.sh");
|
|
2634
|
+
let runtimeCommand = `claude ${getClaudePermissionFlagsStringSync()}`;
|
|
2635
|
+
let providerExportsStr = "";
|
|
2636
|
+
let piFields;
|
|
2637
|
+
let codexFields;
|
|
2638
|
+
if (model) {
|
|
2639
|
+
if (!SAFE_MODEL_PATTERN.test(model)) throw new Error("Invalid model name");
|
|
2640
|
+
runtimeCommand = await getAgentRuntimeBaseCommand(model, void 0, void 0, harness);
|
|
2641
|
+
const mode = resolvePermissionModeSync();
|
|
2642
|
+
if (!runtimeCommand.includes("--permission-mode")) runtimeCommand = `${runtimeCommand} --permission-mode ${mode === "auto" ? "auto" : BYPASS_PERMISSION_MODE}`;
|
|
2643
|
+
providerExportsStr = (await getProviderExportsForModel(model)).trim();
|
|
2644
|
+
await getProviderEnvForModel(model);
|
|
2645
|
+
if (behavior.transcriptKind === "ohmypi-jsonl") {
|
|
2646
|
+
if (getProviderForModelSync(model).name === "openai") {
|
|
2647
|
+
const auth = await getOhmypiCodexAuthStatus({ refreshIfExpired: true });
|
|
2648
|
+
if (auth.status === "missing" || auth.status === "expired") throw new Error("ohmypi ChatGPT/Codex login (openai-codex) has expired and could not be refreshed. Re-authenticate with `pan pi-auth login`, then retry.");
|
|
2649
|
+
}
|
|
2650
|
+
const paths = piFifoPaths(tmuxSession);
|
|
2651
|
+
const piSessionDir = join(paths.agentDir, "sessions");
|
|
2652
|
+
await mkdir(paths.agentDir, {
|
|
2653
|
+
recursive: true,
|
|
2654
|
+
mode: 448
|
|
2655
|
+
});
|
|
2656
|
+
await mkdir(piSessionDir, {
|
|
2657
|
+
recursive: true,
|
|
2658
|
+
mode: 448
|
|
2659
|
+
});
|
|
2660
|
+
const storedPiSessionId = resume ? await readFile(join(paths.agentDir, "session.id"), "utf-8").then((s) => s.trim()).catch(() => void 0) : void 0;
|
|
2661
|
+
piFields = {
|
|
2662
|
+
harness: "ohmypi",
|
|
2663
|
+
piMode: "tui",
|
|
2664
|
+
piExtensionPath: resolveOhmypiExtensionPath() ?? resolve(process.cwd(), "packages/ohmypi-extension/dist/index.js"),
|
|
2665
|
+
piSessionDir,
|
|
2666
|
+
resumeSessionId: storedPiSessionId || void 0
|
|
2667
|
+
};
|
|
2668
|
+
} else if (behavior.usesCodexHome) {
|
|
2669
|
+
const codexHome = join(getOverdeckHome(), "agents", tmuxSession, "codex-home");
|
|
2670
|
+
const codexPermMode = loadConfigSync().config.codex?.permissionMode ?? "workspace";
|
|
2671
|
+
const codexApprovalPolicy = codexPermMode === "full-access" ? "never" : "on-request";
|
|
2672
|
+
const codexSandboxMode = codexPermMode === "full-access" ? "danger-full-access" : codexPermMode === "read-only" ? "read-only" : "workspace-write";
|
|
2673
|
+
const codexApprovalsReviewer = codexPermMode === "auto-review" ? "auto_review" : void 0;
|
|
2674
|
+
const { initCodexHome, extractThreadIdFromRollout } = await import("./codex-DxLivxri.js");
|
|
2675
|
+
initCodexHome(codexHome, {
|
|
2676
|
+
trustedDir: cwd,
|
|
2677
|
+
approvalPolicy: codexApprovalPolicy,
|
|
2678
|
+
sandboxMode: codexSandboxMode,
|
|
2679
|
+
approvalsReviewer: codexApprovalsReviewer
|
|
2680
|
+
});
|
|
2681
|
+
const resumeSessionId = resume ? await resolveCodexRolloutPath(tmuxSession, { agentsDirOverride: join(getOverdeckHome(), "agents") }).then((rollout) => rollout ? extractThreadIdFromRollout(rollout) ?? void 0 : void 0) : void 0;
|
|
2682
|
+
codexFields = {
|
|
2683
|
+
harness: "codex",
|
|
2684
|
+
codexMode: "tui",
|
|
2685
|
+
codexHome,
|
|
2686
|
+
codexSessionDir: join(codexHome, "sessions"),
|
|
2687
|
+
resumeSessionId
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
let launcherModel = model;
|
|
2692
|
+
if (behavior.contextLayerKind === "pi" && model) {
|
|
2693
|
+
const piProvider = piProviderForModel(model);
|
|
2694
|
+
if (piProvider) launcherModel = `${piProvider}/${model}`;
|
|
2695
|
+
}
|
|
2696
|
+
if (effort && !SAFE_EFFORT_PATTERN.test(effort)) throw new Error("Invalid effort level");
|
|
2697
|
+
const useSupervisor = shouldUseSupervisorForConversation(harness);
|
|
2698
|
+
let supervisorScriptPath;
|
|
2699
|
+
if (useSupervisor) {
|
|
2700
|
+
supervisorScriptPath = resolvePtySupervisorScriptPath();
|
|
2701
|
+
await writePtyToken(tmuxSession);
|
|
2702
|
+
}
|
|
2703
|
+
let channelsBridgeMcpConfig;
|
|
2704
|
+
if (!piFields && !codexFields && !plainFork && isClaudeCodeChannelsEnabled() && (!model || getProviderForModelSync(model).name === "anthropic") && process.env.CLAUDE_CODE_USE_BEDROCK !== "1" && process.env.CLAUDE_CODE_USE_VERTEX !== "1" && process.env.CLAUDE_CODE_USE_FOUNDRY !== "1" && process.env.OVERDECK_DOCKER_WORKSPACE !== "1" && process.env.PAN_DOCKER !== "1") {
|
|
2705
|
+
channelsBridgeMcpConfig = join(stateDir, "agent-mcp.json");
|
|
2706
|
+
writeBridgeTokenSync(tmuxSession);
|
|
2707
|
+
await writeChannelsBridgeMcpConfig(channelsBridgeMcpConfig, tmuxSession);
|
|
2708
|
+
}
|
|
2709
|
+
const launcherTmp = `${launcherScript}.${randomUUID$1()}.tmp`;
|
|
2710
|
+
await writeFile(launcherTmp, generateLauncherScriptSync({
|
|
2711
|
+
role: "work",
|
|
2712
|
+
spawnMode: "conversation",
|
|
2713
|
+
workingDir: cwd,
|
|
2714
|
+
setTerminalEnv: true,
|
|
2715
|
+
unsetProviderEnv: true,
|
|
2716
|
+
overdeckEnv: {
|
|
2717
|
+
...issueId ? { issueId } : {},
|
|
2718
|
+
...piFields || codexFields || useSupervisor ? { agentId: tmuxSession } : {}
|
|
2719
|
+
},
|
|
2720
|
+
extraEnvExports: [`export OVERDECK_DASHBOARD_URL="http://127.0.0.1:${process.env["API_PORT"] ?? process.env["PORT"] ?? "3011"}"`],
|
|
2721
|
+
providerExports: providerExportsStr || void 0,
|
|
2722
|
+
trapHup: true,
|
|
2723
|
+
baseCommand: runtimeCommand,
|
|
2724
|
+
appendSystemPromptFiles: piFields ? await piConversationSystemPromptFiles(cwd) : codexFields ? [] : await claudeConversationSystemPromptFiles(cwd),
|
|
2725
|
+
model: launcherModel,
|
|
2726
|
+
...piFields ?? codexFields ?? {
|
|
2727
|
+
resumeSessionId: resume ? claudeSessionId : void 0,
|
|
2728
|
+
sessionId: resume ? void 0 : claudeSessionId
|
|
2729
|
+
},
|
|
2730
|
+
extraArgs: !piFields && effort ? `--effort "${effort}"` : void 0,
|
|
2731
|
+
keepAlive: true,
|
|
2732
|
+
fileMode: 448,
|
|
2733
|
+
channelsBridgeMcpConfig,
|
|
2734
|
+
useSupervisor,
|
|
2735
|
+
supervisorScriptPath
|
|
2736
|
+
}), { mode: 448 });
|
|
2737
|
+
await rename(launcherTmp, launcherScript);
|
|
2738
|
+
try {
|
|
2739
|
+
await Effect.runPromise(killSession(tmuxSession));
|
|
2740
|
+
} catch {}
|
|
2741
|
+
console.log(`[claude-invoke] purpose=conversation-session | model=${model || "default"} | source=conversations.ts:spawnConversationSession | session=${tmuxSession} | resume=${resume} | command="${runtimeCommand}"`);
|
|
2742
|
+
try {
|
|
2743
|
+
const { preTrustDirectory } = await import("./workspace-manager-B1mYn0lN.js");
|
|
2744
|
+
preTrustDirectory(cwd);
|
|
2745
|
+
} catch {}
|
|
2746
|
+
try {
|
|
2747
|
+
await Effect.runPromise(createSession(tmuxSession, cwd, `bash ${shellQuote(launcherScript)}`, { env: {
|
|
2748
|
+
...BLANKED_PROVIDER_ENV,
|
|
2749
|
+
TERM: "xterm-256color"
|
|
2750
|
+
} }));
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
if (err?.code === "ENOENT") throw new Error("tmux is not installed. Install it with: brew install tmux (macOS) or sudo apt-get install tmux (Linux)");
|
|
2753
|
+
throw err;
|
|
2754
|
+
}
|
|
2755
|
+
if (useSupervisor) await waitForPtySupervisorSocket(tmuxSession);
|
|
2756
|
+
if (behavior.usesCodexHome && codexFields?.codexHome) {
|
|
2757
|
+
const codexHomeDir = codexFields.codexHome;
|
|
2758
|
+
(async () => {
|
|
2759
|
+
try {
|
|
2760
|
+
const { waitForCodexRollout, extractThreadIdFromRollout, writeThreadId } = await import("./codex-DxLivxri.js");
|
|
2761
|
+
const rollout = await waitForCodexRollout(codexHomeDir, 12e4);
|
|
2762
|
+
if (rollout) {
|
|
2763
|
+
const threadId = extractThreadIdFromRollout(rollout);
|
|
2764
|
+
if (threadId) writeThreadId(tmuxSession, threadId);
|
|
2765
|
+
}
|
|
2766
|
+
} catch {}
|
|
2767
|
+
})();
|
|
2768
|
+
}
|
|
2769
|
+
if (channelsBridgeMcpConfig) dismissDevChannelsDialog(tmuxSession).catch((err) => {
|
|
2770
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2771
|
+
console.error(`[conversations] dismissDevChannelsDialog failed for ${tmuxSession}: ${msg}`);
|
|
2772
|
+
});
|
|
2773
|
+
await Effect.runPromise(setOption(tmuxSession, "destroy-unattached", "off"));
|
|
2774
|
+
await Effect.runPromise(setOption(exactPaneTarget(tmuxSession), "remain-on-exit", "on"));
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Resolve a conversation's cwd from a project identifier.
|
|
2778
|
+
*
|
|
2779
|
+
* The Command Deck identifies projects by display name, not yaml key
|
|
2780
|
+
* (PAN-2590) — accept either, like GET /api/session-trees does.
|
|
2781
|
+
*/
|
|
2782
|
+
function resolveProjectCwd(projectKey) {
|
|
2783
|
+
const projectConfig = getProjectSync(projectKey) ?? listProjectsSync().find((p) => p.config.name === projectKey)?.config ?? null;
|
|
2784
|
+
if (!projectConfig) return { error: `Unknown project: ${projectKey}` };
|
|
2785
|
+
if (!projectConfig.path || !existsSync(projectConfig.path)) return { error: `Project path does not exist: ${projectConfig.path || "(unset)"} (project: ${projectKey})` };
|
|
2786
|
+
return { cwd: projectConfig.path };
|
|
2787
|
+
}
|
|
2788
|
+
async function handleConversationCreate(body, deps) {
|
|
2789
|
+
try {
|
|
2790
|
+
const message = typeof body["message"] === "string" ? body["message"].trim() : "";
|
|
2791
|
+
const model = typeof body["model"] === "string" ? body["model"].trim() : void 0;
|
|
2792
|
+
const effort = typeof body["effort"] === "string" ? body["effort"].trim() : void 0;
|
|
2793
|
+
const harness = await resolveAllowedHarness(body["harness"], model);
|
|
2794
|
+
const issueId = typeof body["issueId"] === "string" ? body["issueId"] : void 0;
|
|
2795
|
+
const projectKey = typeof body["projectKey"] === "string" ? body["projectKey"].trim() : void 0;
|
|
2796
|
+
if (issueId && !SAFE_ISSUE_ID_PATTERN.test(issueId)) return jsonResponse({ error: "Invalid issueId" }, { status: 400 });
|
|
2797
|
+
if (model && !SAFE_MODEL_PATTERN.test(model)) return jsonResponse({ error: "Invalid model" }, { status: 400 });
|
|
2798
|
+
if (effort && !SAFE_EFFORT_PATTERN.test(effort)) return jsonResponse({ error: "Invalid effort" }, { status: 400 });
|
|
2799
|
+
let cwd = getDefaultCwd();
|
|
2800
|
+
if (projectKey) {
|
|
2801
|
+
const resolved = resolveProjectCwd(projectKey);
|
|
2802
|
+
if ("error" in resolved) return jsonResponse({ error: resolved.error }, { status: 400 });
|
|
2803
|
+
cwd = resolved.cwd;
|
|
2804
|
+
}
|
|
2805
|
+
if (message && message.length > 5e4) return jsonResponse({ error: "message exceeds maximum length of 50000 characters" }, { status: 400 });
|
|
2806
|
+
let name = generateConversationName();
|
|
2807
|
+
for (let i = 0; i < 5 && getConversationByName(name); i++) name = generateConversationName();
|
|
2808
|
+
const tmuxSession = `conv-${name}`;
|
|
2809
|
+
const claudeSessionId = randomUUID$1();
|
|
2810
|
+
console.log(`[conversations] Creating conversation "${name}" with model=${model ?? "default"} effort=${effort ?? "default"} cwd=${cwd}`);
|
|
2811
|
+
const MAX_TITLE_LEN = 60;
|
|
2812
|
+
const title = message ? message.slice(0, MAX_TITLE_LEN) + (message.length > MAX_TITLE_LEN ? "…" : "") : "New conversation";
|
|
2813
|
+
const conv = createConversation({
|
|
2814
|
+
name,
|
|
2815
|
+
tmuxSession,
|
|
2816
|
+
cwd,
|
|
2817
|
+
issueId,
|
|
2818
|
+
claudeSessionId,
|
|
2819
|
+
title,
|
|
2820
|
+
titleSource: message ? "auto" : "default",
|
|
2821
|
+
titleSeed: title,
|
|
2822
|
+
model,
|
|
2823
|
+
effort,
|
|
2824
|
+
harness
|
|
2825
|
+
});
|
|
2826
|
+
getEventStore().emitOnly({
|
|
2827
|
+
type: "conversation.created",
|
|
2828
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2829
|
+
payload: { conversationName: name }
|
|
2830
|
+
});
|
|
2831
|
+
(async () => {
|
|
2832
|
+
try {
|
|
2833
|
+
await spawnConversationSession(tmuxSession, cwd, claudeSessionId, model, effort, issueId, false, harness);
|
|
2834
|
+
console.log(`[conversations] tmux session ${tmuxSession} spawned, sessionId: ${claudeSessionId}`);
|
|
2835
|
+
await waitForConversationRuntimeReady(tmuxSession, harness, "spawn");
|
|
2836
|
+
if (message) await deliverAgentMessage(tmuxSession, message, "conversation-message", resolveConversationDeliveryMethod(conv));
|
|
2837
|
+
} catch (spawnErr) {
|
|
2838
|
+
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
2839
|
+
console.error(`[conversations] background spawn failed for ${tmuxSession}: ${msg}`);
|
|
2840
|
+
updateSpawnError(name, msg);
|
|
2841
|
+
getEventStore().emitOnly({
|
|
2842
|
+
type: "conversation.created",
|
|
2843
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2844
|
+
payload: { conversationName: name }
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
})();
|
|
2848
|
+
if (message) deps.generateAiTitle(name, message).catch((err) => {
|
|
2849
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2850
|
+
console.error(`[TITLE-GEN-FAILED] AI title generation FAILED for "${name}" — NO RETRY, NO FALLBACK:`, msg);
|
|
2851
|
+
});
|
|
2852
|
+
return jsonResponse({
|
|
2853
|
+
...conv,
|
|
2854
|
+
sessionAlive: false
|
|
2855
|
+
}, { status: 201 });
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2858
|
+
console.error("[conversations] create conversation failed:", msg);
|
|
2859
|
+
return jsonResponse({ error: msg || "Internal server error" }, { status: error instanceof UnknownModelError ? 400 : 500 });
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
async function handleConversationStop(name, deps) {
|
|
2863
|
+
try {
|
|
2864
|
+
const conv = getConversationByName(name);
|
|
2865
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
2866
|
+
await stopConversationRuntime(conv, name);
|
|
2867
|
+
markConversationEnded(name);
|
|
2868
|
+
(async () => {
|
|
2869
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2870
|
+
await cleanupUnreferencedConversationAttachments({
|
|
2871
|
+
name: conv.name,
|
|
2872
|
+
sessionFile: deps.resolveSessionFileForCleanup?.(conv) ?? null
|
|
2873
|
+
});
|
|
2874
|
+
})();
|
|
2875
|
+
return jsonResponse({ success: true });
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2878
|
+
console.error("[conversations] stop conversation failed:", msg);
|
|
2879
|
+
return jsonResponse({ error: "Internal server error" }, { status: 500 });
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
async function handleConversationResume(name, body, deps) {
|
|
2883
|
+
try {
|
|
2884
|
+
const conv = getConversationByName(name);
|
|
2885
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
2886
|
+
const model = typeof body["model"] === "string" && body["model"].trim() ? body["model"].trim() : conv.model ?? void 0;
|
|
2887
|
+
const effort = typeof body["effort"] === "string" && body["effort"].trim() ? body["effort"].trim() : conv.effort ?? void 0;
|
|
2888
|
+
if (await isHarnessProcessAlive(conv.tmuxSession)) {
|
|
2889
|
+
updateLastAttached(name);
|
|
2890
|
+
markConversationActive(name);
|
|
2891
|
+
return jsonResponse({
|
|
2892
|
+
...conv,
|
|
2893
|
+
status: "active",
|
|
2894
|
+
reattached: true
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
const oldSessionId = conv.claudeSessionId;
|
|
2898
|
+
const harness = conv.harness ?? "claude-code";
|
|
2899
|
+
const modelChanged = !!model && model !== conv.model;
|
|
2900
|
+
if (!await validateCwdContainment(conv.cwd)) return jsonResponse({ error: "Invalid cwd" }, { status: 400 });
|
|
2901
|
+
if (model && modelChanged && !SAFE_MODEL_PATTERN.test(model)) return jsonResponse({ error: "Invalid model" }, { status: 400 });
|
|
2902
|
+
if (model && modelChanged) setConversationModel(name, model);
|
|
2903
|
+
let canResume = !!oldSessionId;
|
|
2904
|
+
if (oldSessionId) {
|
|
2905
|
+
const resumeFile = await deps.resolveSessionFile(conv);
|
|
2906
|
+
if (!resumeFile || !existsSync(resumeFile)) {
|
|
2907
|
+
canResume = false;
|
|
2908
|
+
console.error(`[conversations] SESSION-LOST ${name} harness=${harness} claudeSessionId=${oldSessionId} resolved=${resumeFile ?? "null"} — resuming with a fresh session`);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const respawn = markRespawnPending(conv.tmuxSession);
|
|
2912
|
+
try {
|
|
2913
|
+
await spawnConversationSession(conv.tmuxSession, conv.cwd, oldSessionId ?? randomUUID$1(), model, effort, conv.issueId ?? void 0, canResume, harness);
|
|
2914
|
+
await waitForTmuxSession(conv.tmuxSession);
|
|
2915
|
+
await waitForConversationRuntimeReady(conv.tmuxSession, harness, "respawn");
|
|
2916
|
+
markConversationActive(name);
|
|
2917
|
+
return jsonResponse({
|
|
2918
|
+
...conv,
|
|
2919
|
+
status: "active",
|
|
2920
|
+
model: model ?? conv.model,
|
|
2921
|
+
harness,
|
|
2922
|
+
reattached: false,
|
|
2923
|
+
sessionAlive: true
|
|
2924
|
+
});
|
|
2925
|
+
} finally {
|
|
2926
|
+
respawn.done();
|
|
2927
|
+
}
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2930
|
+
console.error("[conversations] resume conversation failed:", msg);
|
|
2931
|
+
return jsonResponse({ error: msg || "Internal server error" }, { status: 500 });
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
async function handleConversationDelete(name, deps) {
|
|
2935
|
+
try {
|
|
2936
|
+
const conv = getConversationByName(name);
|
|
2937
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
2938
|
+
await stopConversationRuntime(conv, name);
|
|
2939
|
+
markConversationEnded(name);
|
|
2940
|
+
archiveConversation(name);
|
|
2941
|
+
removeFavorite("conversation", name);
|
|
2942
|
+
deps.invalidateFavoritesCache();
|
|
2943
|
+
await cleanupConversationAttachments(name);
|
|
2944
|
+
return jsonResponse({ success: true });
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2947
|
+
console.error("[conversations] delete conversation failed:", msg);
|
|
2948
|
+
return jsonResponse({ error: "Internal server error" }, { status: 500 });
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
async function handleConversationRestartAll(deps) {
|
|
2952
|
+
try {
|
|
2953
|
+
const allConvs = listConversations();
|
|
2954
|
+
const liveSessionNames = new Set(await Effect.runPromise(listSessionNames()));
|
|
2955
|
+
const convs = allConvs.filter((c) => liveSessionNames.has(c.tmuxSession));
|
|
2956
|
+
const results = [];
|
|
2957
|
+
for (const conv of convs) {
|
|
2958
|
+
const respawn = markRespawnPending(conv.tmuxSession);
|
|
2959
|
+
try {
|
|
2960
|
+
await Effect.runPromise(killSession(conv.tmuxSession).pipe(Effect.catch(() => Effect.succeed(void 0))));
|
|
2961
|
+
const oldSessionId = conv.claudeSessionId;
|
|
2962
|
+
const sessionFileForResume = await deps.resolveSessionFile(conv);
|
|
2963
|
+
const canResume = !!oldSessionId && !!sessionFileForResume && existsSync(sessionFileForResume);
|
|
2964
|
+
const harness = await resolveAllowedHarness(conv.harness, conv.model);
|
|
2965
|
+
await spawnConversationSession(conv.tmuxSession, conv.cwd, oldSessionId ?? randomUUID$1(), conv.model ?? void 0, conv.effort ?? void 0, conv.issueId ?? void 0, canResume, harness);
|
|
2966
|
+
setConversationHarness(conv.name, harness);
|
|
2967
|
+
markConversationActive(conv.name);
|
|
2968
|
+
results.push({
|
|
2969
|
+
name: conv.name,
|
|
2970
|
+
model: conv.model,
|
|
2971
|
+
status: "restarted"
|
|
2972
|
+
});
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2975
|
+
console.error(`[conversations] Failed to restart ${conv.name}:`, msg);
|
|
2976
|
+
results.push({
|
|
2977
|
+
name: conv.name,
|
|
2978
|
+
model: conv.model,
|
|
2979
|
+
status: "failed"
|
|
2980
|
+
});
|
|
2981
|
+
} finally {
|
|
2982
|
+
respawn.done();
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
console.log(`[conversations] Restarted ${results.filter((r) => r.status === "restarted").length}/${convs.length} conversations`);
|
|
2986
|
+
return jsonResponse({
|
|
2987
|
+
restarted: results.length,
|
|
2988
|
+
results
|
|
2989
|
+
});
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2992
|
+
console.error("[conversations] restart conversations failed:", msg);
|
|
2993
|
+
return jsonResponse({ error: "Internal server error" }, { status: 500 });
|
|
1806
2994
|
}
|
|
1807
2995
|
}
|
|
1808
|
-
const getAgentConversationRoute = HttpRouter.add("GET", "/api/agents/:id/conversation", httpHandler(Effect.gen(function* () {
|
|
1809
|
-
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1810
|
-
return yield* Effect.promise(async () => jsonResponse(await buildConversationResponse(id)));
|
|
1811
|
-
})));
|
|
1812
|
-
const getAgentActivityRoute = HttpRouter.add("GET", "/api/agents/:id/activity", httpHandler(Effect.gen(function* () {
|
|
1813
|
-
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1814
|
-
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
1815
|
-
const urlOpt = HttpServerRequest.toURL(request);
|
|
1816
|
-
const limitStr = Option.isSome(urlOpt) ? urlOpt.value.searchParams.get("limit") ?? "100" : "100";
|
|
1817
|
-
return jsonResponse({ activity: getActivity(id, parseInt(limitStr) || 100) });
|
|
1818
|
-
})));
|
|
1819
|
-
const getAgentFilesRoute = HttpRouter.add("GET", "/api/agents/:id/files", httpHandler(Effect.gen(function* () {
|
|
1820
|
-
const agentState = yield* getAgentState((yield* HttpRouter.params)["id"] ?? "");
|
|
1821
|
-
if (!agentState?.workspace) return jsonResponse({ files: [] });
|
|
1822
|
-
const workspacePath = agentState.workspace;
|
|
1823
|
-
if (!existsSync(workspacePath)) return jsonResponse({ files: [] });
|
|
1824
|
-
const { stdout } = yield* Effect.promise(() => execAsync$1("git diff --name-status HEAD 2>/dev/null || git status --porcelain 2>/dev/null || echo \"\"", {
|
|
1825
|
-
cwd: workspacePath,
|
|
1826
|
-
encoding: "utf-8"
|
|
1827
|
-
}));
|
|
1828
|
-
return jsonResponse({ files: stdout.split("\n").filter((l) => l.trim()).map((l) => {
|
|
1829
|
-
const parts = l.trim().split(/\s+/);
|
|
1830
|
-
if (parts.length >= 2) return {
|
|
1831
|
-
status: parts[0],
|
|
1832
|
-
path: parts[parts.length - 1]
|
|
1833
|
-
};
|
|
1834
|
-
return {
|
|
1835
|
-
status: "?",
|
|
1836
|
-
path: l.trim()
|
|
1837
|
-
};
|
|
1838
|
-
}).filter((f) => f.path) });
|
|
1839
|
-
})));
|
|
1840
|
-
const getAgentTimelineRoute = HttpRouter.add("GET", "/api/agents/:id/timeline", httpHandler(Effect.gen(function* () {
|
|
1841
|
-
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
1842
|
-
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
1843
|
-
const urlOpt = HttpServerRequest.toURL(request);
|
|
1844
|
-
const limitStr = Option.isSome(urlOpt) ? urlOpt.value.searchParams.get("limit") ?? "50" : "50";
|
|
1845
|
-
const limit = parseInt(limitStr) || 50;
|
|
1846
|
-
const activity = getActivity(id, limit);
|
|
1847
|
-
const agentState = yield* getAgentState(id);
|
|
1848
|
-
const events = activity.map((a) => ({
|
|
1849
|
-
timestamp: a.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1850
|
-
type: a.type || "activity",
|
|
1851
|
-
message: a.message || a.content || ""
|
|
1852
|
-
}));
|
|
1853
|
-
if (agentState?.startedAt) events.unshift({
|
|
1854
|
-
timestamp: agentState.startedAt,
|
|
1855
|
-
type: "started",
|
|
1856
|
-
message: "Agent started"
|
|
1857
|
-
});
|
|
1858
|
-
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1859
|
-
return jsonResponse({ timeline: events.slice(0, limit) });
|
|
1860
|
-
})));
|
|
1861
2996
|
//#endregion
|
|
1862
|
-
//#region
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2997
|
+
//#region ../../lib/overdeck/conversation-delivery.ts
|
|
2998
|
+
init_conversations();
|
|
2999
|
+
init_behavior();
|
|
3000
|
+
init_tmux();
|
|
3001
|
+
init_agents$1();
|
|
3002
|
+
const CONTROL_ACK_TIMEOUT_MS = 1e4;
|
|
3003
|
+
const pendingConversationControlAcks = /* @__PURE__ */ new Map();
|
|
3004
|
+
const PLAN_ACTION_KEYSTROKES = {
|
|
3005
|
+
"approve-auto": "1",
|
|
3006
|
+
"approve-manual": "2",
|
|
3007
|
+
"reject-ultraplan": "3"
|
|
3008
|
+
};
|
|
3009
|
+
/**
|
|
3010
|
+
* PAN-1520 (FR-4) — deliver a native plan-menu action to any tmux session
|
|
3011
|
+
* (conversation or agent). The plan menu is answered with raw keystrokes
|
|
3012
|
+
* (1/2/3/4), not a text message; `reject-feedback` follows the '4' keystroke
|
|
3013
|
+
* with the operator's feedback through the normal message pipeline.
|
|
3014
|
+
* Returns null on success, or an error string for an invalid action.
|
|
3015
|
+
*/
|
|
3016
|
+
async function deliverPlanActionToSession(sessionName, action, feedback, deliveryMethod = "auto") {
|
|
3017
|
+
if (action === "reject-feedback") {
|
|
3018
|
+
await Effect.runPromise(sendRawKeystroke(sessionName, "4", "plan-action-reject"));
|
|
3019
|
+
if (feedback) {
|
|
3020
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3021
|
+
await deliverAgentMessage(sessionName, feedback, "plan-action-feedback", deliveryMethod);
|
|
3022
|
+
}
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
const keystroke = PLAN_ACTION_KEYSTROKES[action];
|
|
3026
|
+
if (!keystroke) return `Invalid action: ${action}`;
|
|
3027
|
+
await Effect.runPromise(sendRawKeystroke(sessionName, keystroke, `plan-action-${action}`));
|
|
3028
|
+
return null;
|
|
1876
3029
|
}
|
|
1877
|
-
function
|
|
1878
|
-
const
|
|
1879
|
-
if (
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
3030
|
+
function registerConversationControlAck(commandId, timeoutMs = CONTROL_ACK_TIMEOUT_MS) {
|
|
3031
|
+
const existing = pendingConversationControlAcks.get(commandId);
|
|
3032
|
+
if (existing) {
|
|
3033
|
+
clearTimeout(existing.timer);
|
|
3034
|
+
existing.reject(/* @__PURE__ */ new Error(`Replaced pending conversation control ack ${commandId}`));
|
|
3035
|
+
pendingConversationControlAcks.delete(commandId);
|
|
3036
|
+
}
|
|
3037
|
+
return new Promise((resolve, reject) => {
|
|
3038
|
+
const timer = setTimeout(() => {
|
|
3039
|
+
pendingConversationControlAcks.delete(commandId);
|
|
3040
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for conversation control ack ${commandId}`));
|
|
3041
|
+
}, timeoutMs);
|
|
3042
|
+
pendingConversationControlAcks.set(commandId, {
|
|
3043
|
+
resolve: () => {
|
|
3044
|
+
clearTimeout(timer);
|
|
3045
|
+
pendingConversationControlAcks.delete(commandId);
|
|
3046
|
+
resolve();
|
|
3047
|
+
},
|
|
3048
|
+
reject: (error) => {
|
|
3049
|
+
clearTimeout(timer);
|
|
3050
|
+
pendingConversationControlAcks.delete(commandId);
|
|
3051
|
+
reject(error);
|
|
3052
|
+
},
|
|
3053
|
+
timer
|
|
3054
|
+
});
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
function resolveConversationControlAck(ack) {
|
|
3058
|
+
const pending = pendingConversationControlAcks.get(ack.id);
|
|
3059
|
+
if (!pending) return "unknown";
|
|
3060
|
+
if (ack.ok) {
|
|
3061
|
+
pending.resolve();
|
|
3062
|
+
return "resolved";
|
|
3063
|
+
}
|
|
3064
|
+
pending.reject(new Error(ack.error || `Conversation control command ${ack.id} failed`));
|
|
3065
|
+
return "rejected";
|
|
3066
|
+
}
|
|
3067
|
+
function handleConversationControlAck(body) {
|
|
3068
|
+
const id = typeof body["id"] === "string" ? body["id"].trim() : "";
|
|
3069
|
+
if (!id) return {
|
|
3070
|
+
status: 400,
|
|
3071
|
+
body: { error: "id is required" }
|
|
3072
|
+
};
|
|
3073
|
+
const ok = body["ok"] === true;
|
|
3074
|
+
const error = typeof body["error"] === "string" ? body["error"] : void 0;
|
|
3075
|
+
return {
|
|
3076
|
+
status: 200,
|
|
3077
|
+
body: {
|
|
3078
|
+
ok: true,
|
|
3079
|
+
outcome: resolveConversationControlAck({
|
|
3080
|
+
id,
|
|
3081
|
+
ok,
|
|
3082
|
+
...error !== void 0 ? { error } : {}
|
|
3083
|
+
})
|
|
3084
|
+
}
|
|
1885
3085
|
};
|
|
1886
|
-
return { ok: true };
|
|
1887
3086
|
}
|
|
1888
|
-
function
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const { message } = yield* readJsonBody$1;
|
|
1894
|
-
if (!message) return jsonResponse({ error: "Message required" }, { status: 400 });
|
|
1895
|
-
return yield* Effect.promise(() => sendAgentMessage(id, message)).pipe(Effect.map((result) => jsonResponse(result)));
|
|
1896
|
-
})));
|
|
3087
|
+
function validateConversationControlAckOrigin(headers, method = "POST") {
|
|
3088
|
+
const origin = getHeaderFromMap(headers, "origin");
|
|
3089
|
+
const referer = getHeaderFromMap(headers, "referer");
|
|
3090
|
+
if (!origin && !referer) return { ok: true };
|
|
3091
|
+
return validateOriginHeaders(headers, method);
|
|
1897
3092
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
3093
|
+
async function handleConversationCodexApproval(rawId, body) {
|
|
3094
|
+
try {
|
|
3095
|
+
const optionNumber = Number(body.optionNumber);
|
|
3096
|
+
if (!Number.isInteger(optionNumber) || optionNumber < 1 || optionNumber > 9) return jsonResponse({ error: "optionNumber must be an integer 1-9" }, { status: 400 });
|
|
3097
|
+
const numericId = Number(rawId);
|
|
3098
|
+
const conv = !Number.isNaN(numericId) && /^\d+$/.test(rawId) ? getConversationById(numericId) : getConversationByName(rawId);
|
|
3099
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3100
|
+
if (getHarnessBehavior(conv.harness).transcriptKind !== "codex-rollout-jsonl") return jsonResponse({ error: "Not a Codex conversation" }, { status: 400 });
|
|
3101
|
+
if (!await tmuxSessionExists(conv.tmuxSession)) return jsonResponse({ error: "Conversation session is not running" }, { status: 409 });
|
|
3102
|
+
const detection = await Effect.runPromise(detectAwaitingInputForAgent(conv.tmuxSession, {
|
|
3103
|
+
isPlanning: false,
|
|
3104
|
+
cache: false
|
|
3105
|
+
}));
|
|
3106
|
+
const parsed = detection ? parseCodexApprovalPrompt(detection.prompt) : null;
|
|
3107
|
+
if (!parsed) return jsonResponse({ error: "No Codex approval prompt is currently pending" }, { status: 409 });
|
|
3108
|
+
if (optionNumber > parsed.options.length) return jsonResponse({ error: `optionNumber out of range (1-${parsed.options.length})` }, { status: 400 });
|
|
3109
|
+
await deliverCodexApprovalChoice(conv.tmuxSession, optionNumber);
|
|
3110
|
+
return jsonResponse({
|
|
3111
|
+
ok: true,
|
|
3112
|
+
optionNumber
|
|
3113
|
+
});
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3116
|
+
console.error("[conversations] codex approval failed:", msg);
|
|
3117
|
+
return jsonResponse({ error: "Internal server error" }, { status: 500 });
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async function handleConversationPlanAction(name, body) {
|
|
3121
|
+
try {
|
|
3122
|
+
const conv = getConversationByName(name);
|
|
3123
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3124
|
+
const action = typeof body["action"] === "string" ? body["action"] : "";
|
|
3125
|
+
const feedback = typeof body["feedback"] === "string" ? body["feedback"].trim() : "";
|
|
3126
|
+
const error = await deliverPlanActionToSession(conv.tmuxSession, action, feedback, resolveConversationDeliveryMethod(conv));
|
|
3127
|
+
if (error) return jsonResponse({ error }, { status: 400 });
|
|
3128
|
+
return jsonResponse({ ok: true });
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3131
|
+
console.error("[conversations] plan action failed:", msg);
|
|
3132
|
+
return jsonResponse({ error: "Internal server error" }, { status: 500 });
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const THINKING_LEVELS = [
|
|
3136
|
+
"off",
|
|
3137
|
+
"minimal",
|
|
3138
|
+
"low",
|
|
3139
|
+
"medium",
|
|
3140
|
+
"high",
|
|
3141
|
+
"xhigh"
|
|
3142
|
+
];
|
|
3143
|
+
const PI_CONVERSATION_ABORT_KEY = "Escape";
|
|
3144
|
+
function isPiControlChannelHarness(harness) {
|
|
3145
|
+
return harness === "ohmypi" || harness === "pi";
|
|
3146
|
+
}
|
|
3147
|
+
function parseThinkingLevel(value) {
|
|
3148
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value) ? value : null;
|
|
3149
|
+
}
|
|
3150
|
+
function pickDeliverAs(bodyDeliverAs) {
|
|
3151
|
+
if (bodyDeliverAs === "follow_up") return "follow_up";
|
|
3152
|
+
return "steer";
|
|
3153
|
+
}
|
|
3154
|
+
function resolveConversationDeliveryMethod(conv) {
|
|
3155
|
+
const harness = conv.harness ?? "claude-code";
|
|
3156
|
+
if (isPiControlChannelHarness(harness)) return "auto";
|
|
3157
|
+
return conv.deliveryMethod ?? (getHarnessBehavior(harness).deliveryKind === "rpc-fifo" ? "tmux" : "auto");
|
|
3158
|
+
}
|
|
3159
|
+
async function sendConversationControlCommand(conv, commandInput) {
|
|
3160
|
+
const id = randomUUID$1();
|
|
3161
|
+
const ackPromise = registerConversationControlAck(id);
|
|
3162
|
+
const command = {
|
|
3163
|
+
id,
|
|
3164
|
+
...commandInput
|
|
3165
|
+
};
|
|
3166
|
+
try {
|
|
3167
|
+
await writeConversationControlCommand(conv.tmuxSession, command);
|
|
3168
|
+
} catch (err) {
|
|
3169
|
+
resolveConversationControlAck({
|
|
3170
|
+
id,
|
|
3171
|
+
ok: false,
|
|
3172
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
await ackPromise;
|
|
3176
|
+
}
|
|
3177
|
+
async function deliverConversationViaControlChannel(conv, message, options) {
|
|
3178
|
+
await sendConversationControlCommand(conv, {
|
|
3179
|
+
type: options.deliverAs,
|
|
3180
|
+
message,
|
|
3181
|
+
source: options.source
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
async function handleConversationThinkingLevel(name, body) {
|
|
3185
|
+
const conv = getConversationByName(name);
|
|
3186
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3187
|
+
if (!isPiControlChannelHarness(conv.harness ?? "claude-code")) return jsonResponse({ error: "Thinking level control is only supported for Pi conversations" }, { status: 400 });
|
|
3188
|
+
if (conv.status === "ended") return jsonResponse({ error: "Session has ended — start a new run to interact" }, { status: 422 });
|
|
3189
|
+
const level = parseThinkingLevel(body["level"]);
|
|
3190
|
+
if (!level) return jsonResponse({ error: "Invalid thinking level" }, { status: 400 });
|
|
3191
|
+
await sendConversationControlCommand(conv, {
|
|
3192
|
+
type: "set_thinking_level",
|
|
3193
|
+
level
|
|
3194
|
+
});
|
|
3195
|
+
setConversationEffort(name, level);
|
|
1905
3196
|
return jsonResponse({
|
|
1906
|
-
|
|
1907
|
-
|
|
3197
|
+
ok: true,
|
|
3198
|
+
effort: (getConversationByName(name) ?? conv).effort ?? level
|
|
1908
3199
|
});
|
|
1909
|
-
}
|
|
3200
|
+
}
|
|
3201
|
+
async function handleConversationCompact(name) {
|
|
3202
|
+
const conv = getConversationByName(name);
|
|
3203
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3204
|
+
if (!isPiControlChannelHarness(conv.harness ?? "claude-code")) return jsonResponse({ error: "Compact control endpoint is only supported for Pi conversations" }, { status: 400 });
|
|
3205
|
+
if (conv.status === "ended") return jsonResponse({ error: "Session has ended — start a new run to interact" }, { status: 422 });
|
|
3206
|
+
await sendConversationControlCommand(conv, { type: "compact" });
|
|
3207
|
+
return jsonResponse({ ok: true });
|
|
3208
|
+
}
|
|
3209
|
+
async function handleConversationAbort(name) {
|
|
3210
|
+
const conv = getConversationByName(name);
|
|
3211
|
+
if (!conv) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3212
|
+
if (!isPiControlChannelHarness(conv.harness ?? "claude-code")) return jsonResponse({ error: "Abort control endpoint is only supported for Pi conversations" }, { status: 400 });
|
|
3213
|
+
if (conv.status === "ended") return jsonResponse({ error: "Session has ended — start a new run to interact" }, { status: 422 });
|
|
3214
|
+
await sendKeysAsync(conv.tmuxSession, PI_CONVERSATION_ABORT_KEY, "conversation-abort");
|
|
3215
|
+
return jsonResponse({
|
|
3216
|
+
ok: true,
|
|
3217
|
+
key: PI_CONVERSATION_ABORT_KEY
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
async function handleConversationDeliveryMethod(name, body) {
|
|
3221
|
+
if (!getConversationByName(name)) return jsonResponse({ error: "Conversation not found" }, { status: 404 });
|
|
3222
|
+
const deliveryMethod = body["deliveryMethod"] ?? body["method"];
|
|
3223
|
+
if (deliveryMethod !== "auto" && deliveryMethod !== "channels" && deliveryMethod !== "tmux" && deliveryMethod !== null) return jsonResponse({ error: "deliveryMethod must be 'auto', 'channels', 'tmux', or null" }, { status: 400 });
|
|
3224
|
+
updateConversationDeliveryMethod(name, deliveryMethod);
|
|
3225
|
+
return jsonResponse({
|
|
3226
|
+
ok: true,
|
|
3227
|
+
deliveryMethod
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
const CODEX_APPROVAL_TOOL_PREFIX = "codex-approval:";
|
|
3231
|
+
async function codexConversationPendingInput(conv, sessionAlive, askedAt) {
|
|
3232
|
+
if (!sessionAlive || getHarnessBehavior(conv.harness).transcriptKind !== "codex-rollout-jsonl") return { kinds: [] };
|
|
3233
|
+
try {
|
|
3234
|
+
const detection = await Effect.runPromise(detectAwaitingInputForAgent(conv.tmuxSession, { isPlanning: false }));
|
|
3235
|
+
if (!detection) return { kinds: [] };
|
|
3236
|
+
if (detection.reason === "session_resume") return { kinds: ["sessionResume"] };
|
|
3237
|
+
const parsed = parseCodexApprovalPrompt(detection.prompt);
|
|
3238
|
+
if (parsed) return {
|
|
3239
|
+
kinds: ["permissionRequest"],
|
|
3240
|
+
approval: {
|
|
3241
|
+
toolUseId: `${CODEX_APPROVAL_TOOL_PREFIX}${conv.tmuxSession}`,
|
|
3242
|
+
askedAt,
|
|
3243
|
+
questions: [{
|
|
3244
|
+
question: parsed.detail ? `${parsed.header}\n\n${parsed.detail}` : parsed.header,
|
|
3245
|
+
header: "Codex approval",
|
|
3246
|
+
multiSelect: false,
|
|
3247
|
+
options: parsed.options.map((o) => ({ label: `${o.number}. ${o.label}` }))
|
|
3248
|
+
}]
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
return { kinds: ["permissionRequest"] };
|
|
3252
|
+
} catch {
|
|
3253
|
+
return { kinds: [] };
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
async function deliverCodexApprovalChoice(tmuxSession, optionNumber) {
|
|
3257
|
+
for (let i = 1; i < optionNumber; i += 1) {
|
|
3258
|
+
await Effect.runPromise(sendRawKeystroke(tmuxSession, "Down", "codex-approval"));
|
|
3259
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
3260
|
+
}
|
|
3261
|
+
await Effect.runPromise(sendRawKeystroke(tmuxSession, "Enter", "codex-approval"));
|
|
3262
|
+
}
|
|
1910
3263
|
//#endregion
|
|
1911
3264
|
//#region ../../lib/channels/permission-payload.ts
|
|
1912
3265
|
const CHANNEL_PERMISSION_LIMITS = {
|
|
@@ -2095,7 +3448,7 @@ async function processPermissionResponse(deps, args) {
|
|
|
2095
3448
|
}
|
|
2096
3449
|
//#endregion
|
|
2097
3450
|
//#region routes/agents/permissions.ts
|
|
2098
|
-
init_agents();
|
|
3451
|
+
init_agents$1();
|
|
2099
3452
|
init_activity_logger();
|
|
2100
3453
|
init_read_model();
|
|
2101
3454
|
const getAgentPendingQuestionsRoute = HttpRouter.add("GET", "/api/agents/:id/pending-questions", httpHandler(Effect.gen(function* () {
|
|
@@ -2127,6 +3480,30 @@ const postAgentAnswerQuestionRoute = HttpRouter.add("POST", "/api/agents/:id/ans
|
|
|
2127
3480
|
delivered: answers.length
|
|
2128
3481
|
});
|
|
2129
3482
|
})));
|
|
3483
|
+
const postAgentPlanActionRoute = HttpRouter.add("POST", "/api/agents/:id/plan-action", httpHandler(Effect.gen(function* () {
|
|
3484
|
+
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
3485
|
+
if (!id.trim()) return jsonResponse({ error: "missing agent id" }, { status: 400 });
|
|
3486
|
+
const body = yield* readJsonBody$1;
|
|
3487
|
+
const action = typeof body["action"] === "string" ? body["action"] : "";
|
|
3488
|
+
const feedback = typeof body["feedback"] === "string" ? body["feedback"].trim() : "";
|
|
3489
|
+
const agentState = yield* getAgentState(id).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
3490
|
+
if (!agentState) return jsonResponse({ error: "Agent not found" }, { status: 404 });
|
|
3491
|
+
const jsonlPath = yield* getAgentJsonlPath$1(id).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
3492
|
+
if (!(jsonlPath ? yield* Effect.promise(() => scanPendingInputsPromise(jsonlPath)) : null)?.exitPlanModePending) return jsonResponse({ error: "No pending plan approval for this agent" }, { status: 409 });
|
|
3493
|
+
const error = yield* Effect.promise(() => deliverPlanActionToSession(id, action, feedback));
|
|
3494
|
+
if (error) return jsonResponse({ error }, { status: 400 });
|
|
3495
|
+
emitActivityEntrySync({
|
|
3496
|
+
source: "dashboard",
|
|
3497
|
+
level: "info",
|
|
3498
|
+
issueId: agentState.issueId,
|
|
3499
|
+
message: `Operator ${action.startsWith("approve") ? "approved" : "requested changes to"} ${id}'s plan from the dashboard`
|
|
3500
|
+
});
|
|
3501
|
+
return jsonResponse({
|
|
3502
|
+
success: true,
|
|
3503
|
+
agentId: id,
|
|
3504
|
+
action
|
|
3505
|
+
});
|
|
3506
|
+
})));
|
|
2130
3507
|
const postInternalAgentPermissionRequestRoute = HttpRouter.add("POST", "/api/internal/agents/:id/permissions/request", httpHandler(Effect.gen(function* () {
|
|
2131
3508
|
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
2132
3509
|
if (!id.trim()) return jsonResponse({
|
|
@@ -2134,7 +3511,7 @@ const postInternalAgentPermissionRequestRoute = HttpRouter.add("POST", "/api/int
|
|
|
2134
3511
|
error: "missing agent id"
|
|
2135
3512
|
}, { status: 400 });
|
|
2136
3513
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
2137
|
-
const { INTERNAL_TOKEN_HEADER, getInternalTokenSync } = yield* Effect.promise(() => import("./internal-token-
|
|
3514
|
+
const { INTERNAL_TOKEN_HEADER, getInternalTokenSync } = yield* Effect.promise(() => import("./internal-token-D-t28de5.js"));
|
|
2138
3515
|
const expected = getInternalTokenSync();
|
|
2139
3516
|
if (!expected) return jsonResponse({
|
|
2140
3517
|
ok: false,
|
|
@@ -2401,7 +3778,7 @@ const getAgentHealthHistoryRoute = HttpRouter.add("GET", "/api/agents/:id/health
|
|
|
2401
3778
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
2402
3779
|
const urlOpt = HttpServerRequest.toURL(request);
|
|
2403
3780
|
const hours = Option.isSome(urlOpt) ? urlOpt.value.searchParams.get("hours") ?? "24" : "24";
|
|
2404
|
-
const { getHealthHistory } = yield* Effect.promise(() => import("./health-events-
|
|
3781
|
+
const { getHealthHistory } = yield* Effect.promise(() => import("./health-events-B0mj6Iae.js"));
|
|
2405
3782
|
const endTime = /* @__PURE__ */ new Date();
|
|
2406
3783
|
const startTime = /* @__PURE__ */ new Date(endTime.getTime() - parseInt(hours) * 60 * 60 * 1e3);
|
|
2407
3784
|
const events = getHealthHistory(id, startTime.toISOString(), endTime.toISOString());
|
|
@@ -2442,7 +3819,7 @@ function emitAgentRuntimeEvent(id, body, timestamp) {
|
|
|
2442
3819
|
detail: String(decoded.failure)
|
|
2443
3820
|
}, { status: 400 })
|
|
2444
3821
|
};
|
|
2445
|
-
const { AgentStateService } = yield* Effect.promise(() => import("./agent-state-service-
|
|
3822
|
+
const { AgentStateService } = yield* Effect.promise(() => import("./agent-state-service-BUfovFfB.js"));
|
|
2446
3823
|
yield* (yield* AgentStateService).emit(decoded.success);
|
|
2447
3824
|
return {
|
|
2448
3825
|
ok: true,
|
|
@@ -2508,14 +3885,14 @@ const postAgentStuckRoute = HttpRouter.add("POST", "/api/agents/:id/stuck", http
|
|
|
2508
3885
|
});
|
|
2509
3886
|
})));
|
|
2510
3887
|
function hasNegatedCompletionOutput(output) {
|
|
2511
|
-
return /\b(not|never|no|cannot|can't|blocked|waiting|needs input|not yet|isn't|aren't)\b.{0,48}\b(implementation complete|all
|
|
3888
|
+
return /\b(not|never|no|cannot|can't|blocked|waiting|needs input|not yet|isn't|aren't)\b.{0,48}\b(implementation complete|all tasks closed|ready for review|work complete)\b/i.test(output);
|
|
2512
3889
|
}
|
|
2513
3890
|
const postAgentClassifyCompletionRoute = HttpRouter.add("POST", "/api/agents/:id/classify-completion", httpHandler(Effect.gen(function* () {
|
|
2514
3891
|
const body = yield* readJsonBody$1;
|
|
2515
3892
|
const output = typeof body["output"] === "string" ? body["output"] : "";
|
|
2516
3893
|
return jsonResponse({
|
|
2517
3894
|
success: true,
|
|
2518
|
-
verdict: /blocked|needs input|waiting for|not ready for review/i.test(output) || hasNegatedCompletionOutput(output) ? "STOPPED_FOR_INPUT" : /Implementation complete|all
|
|
3895
|
+
verdict: /blocked|needs input|waiting for|not ready for review/i.test(output) || hasNegatedCompletionOutput(output) ? "STOPPED_FOR_INPUT" : /Implementation complete|all tasks closed|ready for review|work complete/i.test(output) ? "FORGOT_COMPLETION" : "UNCLEAR"
|
|
2519
3896
|
});
|
|
2520
3897
|
})));
|
|
2521
3898
|
const getAgentRuntimeRoute = HttpRouter.add("GET", "/api/agents/:id/runtime", httpHandler(Effect.gen(function* () {
|
|
@@ -2524,7 +3901,7 @@ const getAgentRuntimeRoute = HttpRouter.add("GET", "/api/agents/:id/runtime", ht
|
|
|
2524
3901
|
success: false,
|
|
2525
3902
|
error: "missing agent id"
|
|
2526
3903
|
}, { status: 400 });
|
|
2527
|
-
const { AgentStateService } = yield* Effect.promise(() => import("./agent-state-service-
|
|
3904
|
+
const { AgentStateService } = yield* Effect.promise(() => import("./agent-state-service-BUfovFfB.js"));
|
|
2528
3905
|
const snapshot = yield* (yield* AgentStateService).get(id);
|
|
2529
3906
|
if (!snapshot) return jsonResponse({
|
|
2530
3907
|
success: false,
|
|
@@ -2550,7 +3927,9 @@ init_workspace_manager();
|
|
|
2550
3927
|
* commit, the persisted event is emitted to the event store's subscribers so
|
|
2551
3928
|
* the in-memory read model stays current.
|
|
2552
3929
|
*/
|
|
2553
|
-
|
|
3930
|
+
init_event_store();
|
|
3931
|
+
init_agents$1();
|
|
3932
|
+
init_persistent_logger();
|
|
2554
3933
|
function prepareAgentStateForSave(state) {
|
|
2555
3934
|
if (state.status === "running" || state.status === "starting") delete state.stoppedAt;
|
|
2556
3935
|
else if (state.status === "stopped" && !state.stoppedAt) state.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2617,7 +3996,7 @@ function saveAgentStateAndEmitEventWithDeps(db, eventStore, state, event) {
|
|
|
2617
3996
|
}
|
|
2618
3997
|
//#endregion
|
|
2619
3998
|
//#region routes/agents/lifecycle-stop.ts
|
|
2620
|
-
init_agents();
|
|
3999
|
+
init_agents$1();
|
|
2621
4000
|
init_activity_logger();
|
|
2622
4001
|
init_tmux();
|
|
2623
4002
|
init_domain_services();
|
|
@@ -2635,8 +4014,8 @@ function createAgentStopHandler(lifecycleEvent) {
|
|
|
2635
4014
|
yield* stopAgent(id);
|
|
2636
4015
|
if (stateBeforeStop?.issueId) yield* Effect.promise(async () => {
|
|
2637
4016
|
try {
|
|
2638
|
-
const { resolveProjectFromIssueSync } = await import("./projects-
|
|
2639
|
-
const { findWorkspacePath } = await import("./archive-planning-
|
|
4017
|
+
const { resolveProjectFromIssueSync } = await import("./projects-Dc9aoVoG.js");
|
|
4018
|
+
const { findWorkspacePath } = await import("./archive-planning-D-QsBufn.js");
|
|
2640
4019
|
const issueLower = stateBeforeStop.issueId.toLowerCase();
|
|
2641
4020
|
const workspacePath = findWorkspacePath(resolveProjectFromIssueSync(stateBeforeStop.issueId)?.projectPath ?? process.cwd(), issueLower);
|
|
2642
4021
|
if (workspacePath) {
|
|
@@ -2806,7 +4185,9 @@ const postAgentUntroubledRoute = HttpRouter.add("POST", "/api/agents/:id/untroub
|
|
|
2806
4185
|
})));
|
|
2807
4186
|
//#endregion
|
|
2808
4187
|
//#region routes/agents/lifecycle-restart.ts
|
|
2809
|
-
|
|
4188
|
+
init_config_yaml();
|
|
4189
|
+
init_agents$1();
|
|
4190
|
+
init_harness_policy();
|
|
2810
4191
|
init_operator_interventions();
|
|
2811
4192
|
init_projects();
|
|
2812
4193
|
init_tmux();
|
|
@@ -3092,9 +4473,14 @@ const postAgentRestartFreshRoute = HttpRouter.add("POST", "/api/agents/:id/resta
|
|
|
3092
4473
|
const agentState = yield* getAgentState(id);
|
|
3093
4474
|
if (!agentState) return jsonResponse({ error: `Agent ${id} not found` }, { status: 404 });
|
|
3094
4475
|
const issueId = agentState.issueId ?? id.replace(/^agent-/, "").toUpperCase();
|
|
4476
|
+
const issueStage = getIssueStageSync(issueId);
|
|
4477
|
+
if (wantsSpawn && isTerminalIssueStage(issueStage)) return jsonResponse({
|
|
4478
|
+
error: `${issueId} is already ${issueStage?.replaceAll("_", " ")}. Reopen the issue before starting fresh work.`,
|
|
4479
|
+
issueStage
|
|
4480
|
+
}, { status: 409 });
|
|
3095
4481
|
const lifecycle = yield* getWorkAgentLifecycleState(id);
|
|
3096
4482
|
if (lifecycle.hasLiveTmuxSession) return jsonResponse({
|
|
3097
|
-
error: `Agent ${id} has a live tmux session. Run 'pan kill ${
|
|
4483
|
+
error: `Agent ${id} has a live tmux session. Run 'pan kill ${id}' to stop only this work agent, then retry.`,
|
|
3098
4484
|
lifecycle
|
|
3099
4485
|
}, { status: 409 });
|
|
3100
4486
|
yield* Effect.promise(() => appendAgentLifecycleLog(id, "agent.restart_fresh_requested", {
|
|
@@ -3157,9 +4543,21 @@ const postAgentRestartFreshRoute = HttpRouter.add("POST", "/api/agents/:id/resta
|
|
|
3157
4543
|
cwd: workspacePath
|
|
3158
4544
|
}));
|
|
3159
4545
|
} catch (err) {
|
|
4546
|
+
saveAgentStateSync({
|
|
4547
|
+
id: agentSessionName,
|
|
4548
|
+
issueId,
|
|
4549
|
+
workspace: workspacePath,
|
|
4550
|
+
harness: effectiveHarness ?? agentState.harness ?? "claude-code",
|
|
4551
|
+
role: "work",
|
|
4552
|
+
model: spawnModel,
|
|
4553
|
+
status: "stopped",
|
|
4554
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4555
|
+
stoppedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4556
|
+
});
|
|
3160
4557
|
return jsonResponse({
|
|
3161
4558
|
success: false,
|
|
3162
|
-
error:
|
|
4559
|
+
error: "The old agent state was cleared, but the fresh agent could not start. Resolve the startup blocker, then try again.",
|
|
4560
|
+
details: err?.message ?? String(err),
|
|
3163
4561
|
wiped: wipeResult.removed
|
|
3164
4562
|
}, { status: 500 });
|
|
3165
4563
|
}
|
|
@@ -3345,6 +4743,7 @@ async function resolveCurrentStaffing(agentId, agentState, issueId) {
|
|
|
3345
4743
|
const staffing = resolveStaffing(workItem, {
|
|
3346
4744
|
planMetadata: plan.plan.metadata,
|
|
3347
4745
|
spawnKey: `work:${issueId.toLowerCase()}`,
|
|
4746
|
+
issueId,
|
|
3348
4747
|
config: {
|
|
3349
4748
|
...config,
|
|
3350
4749
|
tieredExecution: {
|
|
@@ -3441,7 +4840,7 @@ init_config();
|
|
|
3441
4840
|
init_jsonl_parser();
|
|
3442
4841
|
init_model_validation();
|
|
3443
4842
|
init_paths();
|
|
3444
|
-
init_agents();
|
|
4843
|
+
init_agents$1();
|
|
3445
4844
|
const getAgentCloisterHealthRoute = HttpRouter.add("GET", "/api/agents/:id/cloister-health", httpHandler(Effect.gen(function* () {
|
|
3446
4845
|
const id = (yield* HttpRouter.params)["id"] ?? "";
|
|
3447
4846
|
const health = getCloisterService().getAgentHealth(id);
|
|
@@ -3593,6 +4992,7 @@ const postAgentSwitchModelRoute = HttpRouter.add("POST", "/api/agents/:id/switch
|
|
|
3593
4992
|
//#endregion
|
|
3594
4993
|
//#region ../../lib/vbrief/auto-synthesize.ts
|
|
3595
4994
|
init_state_read_home();
|
|
4995
|
+
init_codex_auth();
|
|
3596
4996
|
init_state_home();
|
|
3597
4997
|
init_io();
|
|
3598
4998
|
function cleanMarkdownLine(line) {
|
|
@@ -3711,19 +5111,20 @@ const writeAutoStartVBrief = (projectRoot, workspacePath, issue) => Effect.tryPr
|
|
|
3711
5111
|
//#endregion
|
|
3712
5112
|
//#region routes/agents/spawn-helpers.ts
|
|
3713
5113
|
init_issue_lifecycle();
|
|
3714
|
-
|
|
5114
|
+
init_child_env();
|
|
5115
|
+
init_agents$1();
|
|
3715
5116
|
function handleRemoteAgentSpawn(input) {
|
|
3716
5117
|
return Effect.gen(function* () {
|
|
3717
5118
|
const { issueId, workspacePath, workspaceMetadata, spawnModel, projectPath, spawnGuardrails, lifecycle } = input;
|
|
3718
|
-
const { spawnRemoteAgent, checkRemoteSpendCap } = yield* Effect.promise(() => import("./remote-agents-
|
|
3719
|
-
const { createFlyProviderFromConfig } = yield* Effect.promise(() => import("./remote-
|
|
3720
|
-
const { loadConfigSync: loadPanConfig } = yield* Effect.promise(() => import("./config-
|
|
5119
|
+
const { spawnRemoteAgent, checkRemoteSpendCap } = yield* Effect.promise(() => import("./remote-agents-DYY5fFpd.js"));
|
|
5120
|
+
const { createFlyProviderFromConfig } = yield* Effect.promise(() => import("./remote-Cvu4CSDk.js"));
|
|
5121
|
+
const { loadConfigSync: loadPanConfig } = yield* Effect.promise(() => import("./config-6MCHOZ2N.js"));
|
|
3721
5122
|
const panConfig = loadPanConfig();
|
|
3722
5123
|
const spendCap = checkRemoteSpendCap(panConfig);
|
|
3723
5124
|
if (!spendCap.allowed) return jsonResponse({ error: spendCap.message }, { status: 429 });
|
|
3724
5125
|
const fly = createFlyProviderFromConfig(panConfig.remote);
|
|
3725
5126
|
yield* Effect.promise(() => fly.syncAllCredentials(workspaceMetadata.vmName));
|
|
3726
|
-
const { buildWorkAgentPrompt, getTrackerContext } = yield* Effect.promise(() => import("./work-agent-prompt-
|
|
5127
|
+
const { buildWorkAgentPrompt, getTrackerContext } = yield* Effect.promise(() => import("./work-agent-prompt-BiJ43Der.js"));
|
|
3727
5128
|
const trackerContext = yield* Effect.promise(() => getTrackerContext(issueId, workspacePath));
|
|
3728
5129
|
const agentPrompt = yield* Effect.promise(() => buildWorkAgentPrompt({
|
|
3729
5130
|
issueId,
|
|
@@ -3809,7 +5210,7 @@ function handleContainerOrchestration(input) {
|
|
|
3809
5210
|
if (existsSync(workspacePath) && existsSync(devScript)) {
|
|
3810
5211
|
let dockerRunning = false;
|
|
3811
5212
|
try {
|
|
3812
|
-
yield* Effect.promise(() => execAsync$
|
|
5213
|
+
yield* Effect.promise(() => execAsync$2("docker info >/dev/null 2>&1", { encoding: "utf-8" }));
|
|
3813
5214
|
dockerRunning = true;
|
|
3814
5215
|
} catch {}
|
|
3815
5216
|
if (dockerRunning) {
|
|
@@ -3852,7 +5253,7 @@ function handleContainerOrchestration(input) {
|
|
|
3852
5253
|
}));
|
|
3853
5254
|
let containersReady = false;
|
|
3854
5255
|
try {
|
|
3855
|
-
const { stdout: existing } = yield* Effect.promise(() => execFileAsync$
|
|
5256
|
+
const { stdout: existing } = yield* Effect.promise(() => execFileAsync$2("docker", [
|
|
3856
5257
|
"ps",
|
|
3857
5258
|
"--filter",
|
|
3858
5259
|
`name=${featureName}`,
|
|
@@ -3918,7 +5319,7 @@ function handleContainerOrchestration(input) {
|
|
|
3918
5319
|
let healthy = false;
|
|
3919
5320
|
while (Date.now() - startTime < maxWaitMs) {
|
|
3920
5321
|
try {
|
|
3921
|
-
const { stdout } = await execFileAsync$
|
|
5322
|
+
const { stdout } = await execFileAsync$2("docker", [
|
|
3922
5323
|
"ps",
|
|
3923
5324
|
"--filter",
|
|
3924
5325
|
`name=${featureName}`,
|
|
@@ -4038,7 +5439,7 @@ function handleContainerOrchestration(input) {
|
|
|
4038
5439
|
}
|
|
4039
5440
|
//#endregion
|
|
4040
5441
|
//#region routes/agents/spawn.ts
|
|
4041
|
-
init_agents();
|
|
5442
|
+
init_agents$1();
|
|
4042
5443
|
init_child_env();
|
|
4043
5444
|
init_harness_policy();
|
|
4044
5445
|
init_activity_logger();
|
|
@@ -4070,45 +5471,6 @@ function spawnGuardrailResourcesHint(hint) {
|
|
|
4070
5471
|
const resourcesHint = "Open /resources to inspect Machine Room pressure before retrying.";
|
|
4071
5472
|
return hint ? `${hint} ${resourcesHint}` : resourcesHint;
|
|
4072
5473
|
}
|
|
4073
|
-
/**
|
|
4074
|
-
* Count materialized beads for an issue by shelling out to `bd list`.
|
|
4075
|
-
*
|
|
4076
|
-
* Returns 0 on ANY bd failure — non-zero exit (e.g. bd refusing with "Linear
|
|
4077
|
-
* data has never been pulled"), an unparseable JSON body, or the 10s timeout.
|
|
4078
|
-
* The caller treats 0 as "no beads yet" and falls through to auto-recovery
|
|
4079
|
-
* (createBeadsFromVBrief) and, failing that, a clean 422 `beads_required`.
|
|
4080
|
-
*
|
|
4081
|
-
* The failure is folded into the success channel with `Effect.matchCause`. A
|
|
4082
|
-
* bare `try/catch` around `yield* withBdMutex(...)` does NOT work: withBdMutex
|
|
4083
|
-
* converts the child-process rejection into an Effect error-channel failure
|
|
4084
|
-
* (via its internal `Effect.tryPromise`), which short-circuits the fiber and is
|
|
4085
|
-
* invisible to a JS `try/catch`. Before this helper, that escaped as an
|
|
4086
|
-
* unhandled 500 on POST /api/agents instead of the designed 422.
|
|
4087
|
-
*/
|
|
4088
|
-
const countBeadsForIssue = (workspacePath, issueLower) => withBdMutex(() => Effect.promise(() => execFileAsync$1("bd", [
|
|
4089
|
-
"list",
|
|
4090
|
-
"--json",
|
|
4091
|
-
"-l",
|
|
4092
|
-
issueLower,
|
|
4093
|
-
"--status",
|
|
4094
|
-
"all",
|
|
4095
|
-
"--limit",
|
|
4096
|
-
"0"
|
|
4097
|
-
], {
|
|
4098
|
-
cwd: workspacePath,
|
|
4099
|
-
encoding: "utf-8",
|
|
4100
|
-
timeout: 1e4
|
|
4101
|
-
}))).pipe(Effect.matchCause({
|
|
4102
|
-
onFailure: () => 0,
|
|
4103
|
-
onSuccess: ({ stdout }) => {
|
|
4104
|
-
try {
|
|
4105
|
-
const bdTasks = JSON.parse(stdout.trim() || "[]");
|
|
4106
|
-
return Array.isArray(bdTasks) ? bdTasks.length : 0;
|
|
4107
|
-
} catch {
|
|
4108
|
-
return 0;
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
}));
|
|
4112
5474
|
const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect.gen(function* () {
|
|
4113
5475
|
const originCheck = validateOrigin(yield* HttpServerRequest.HttpServerRequest);
|
|
4114
5476
|
if (!originCheck.ok) return jsonResponse({
|
|
@@ -4176,7 +5538,7 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4176
5538
|
const workspacePath = join(projectPath, "workspaces", `feature-${issueLower}`);
|
|
4177
5539
|
if (!existsSync(workspacePath)) try {
|
|
4178
5540
|
const nodeDir = dirname(process.execPath);
|
|
4179
|
-
yield* Effect.promise(() => execAsync$
|
|
5541
|
+
yield* Effect.promise(() => execAsync$2(`pan workspace create ${issueId} --local`, {
|
|
4180
5542
|
cwd: projectPath,
|
|
4181
5543
|
encoding: "utf-8",
|
|
4182
5544
|
timeout: 6e4,
|
|
@@ -4190,13 +5552,6 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4190
5552
|
}
|
|
4191
5553
|
const workspacePanDir = join(workspacePath, PAN_DIRNAME);
|
|
4192
5554
|
const workspacePanContinuePath = join(workspacePanDir, PAN_CONTINUE_FILENAME);
|
|
4193
|
-
const workspaceBeadsDir = join(workspacePath, ".beads");
|
|
4194
|
-
if (!existsSync(workspaceBeadsDir)) {
|
|
4195
|
-
const projectRootBeadsDir = join(projectPath, ".beads");
|
|
4196
|
-
if (existsSync(projectRootBeadsDir)) try {
|
|
4197
|
-
yield* Effect.promise(() => cp(projectRootBeadsDir, workspaceBeadsDir, { recursive: true }));
|
|
4198
|
-
} catch {}
|
|
4199
|
-
}
|
|
4200
5555
|
let planPath = yield* findPlan(workspacePath);
|
|
4201
5556
|
if (autoStart && !planPath) {
|
|
4202
5557
|
yield* writeAutoStartVBrief(projectPath, workspacePath, {
|
|
@@ -4208,7 +5563,7 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4208
5563
|
planPath = yield* findPlan(workspacePath);
|
|
4209
5564
|
}
|
|
4210
5565
|
if (!planPath) return jsonResponse({
|
|
4211
|
-
error: `No workspace vBRIEF found for ${issueId}. Work agents require a finalized plan
|
|
5566
|
+
error: `No workspace vBRIEF found for ${issueId}. Work agents require a finalized plan.`,
|
|
4212
5567
|
hint: "Run planning first, or use auto-start to synthesize a plan before starting the work agent.",
|
|
4213
5568
|
issueId
|
|
4214
5569
|
}, { status: 422 });
|
|
@@ -4239,44 +5594,11 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4239
5594
|
expectedIssue: issueId,
|
|
4240
5595
|
actualIssue: planIssueId.toUpperCase()
|
|
4241
5596
|
}, { status: 422 });
|
|
4242
|
-
|
|
4243
|
-
if (planItemCount === 0) return jsonResponse({
|
|
5597
|
+
if ((planDoc?.plan?.items?.length ?? 0) === 0) return jsonResponse({
|
|
4244
5598
|
error: "Plan exists but contains no items. Planning may have failed or produced an empty plan.",
|
|
4245
5599
|
hint: "Re-run planning to produce a plan with tasks and acceptance criteria.",
|
|
4246
5600
|
issueId
|
|
4247
5601
|
}, { status: 422 });
|
|
4248
|
-
let beadCount = yield* countBeadsForIssue(workspacePath, issueLower);
|
|
4249
|
-
let hasBeads = beadCount > 0;
|
|
4250
|
-
if (planItemCount !== null && planItemCount > 0 && beadCount !== planItemCount) hasBeads = false;
|
|
4251
|
-
let recoveryError = null;
|
|
4252
|
-
if (!hasBeads) {
|
|
4253
|
-
console.log(`[agents] No beads for ${issueId} — attempting auto-recovery via createBeadsFromVBrief`);
|
|
4254
|
-
try {
|
|
4255
|
-
const { createBeadsFromVBrief } = yield* Effect.promise(() => import("./beads-CYsLiyIM.js"));
|
|
4256
|
-
const recovery = yield* createBeadsFromVBrief(workspacePath);
|
|
4257
|
-
beadCount = recovery.created.length;
|
|
4258
|
-
hasBeads = recovery.created.length > 0 && (planItemCount === null || recovery.created.length === planItemCount);
|
|
4259
|
-
if (hasBeads) console.log(`[agents] Auto-recovery created ${recovery.created.length} beads for ${issueId}`);
|
|
4260
|
-
else if (recovery.created.length > 0 && planItemCount !== null) recoveryError = `created ${recovery.created.length} beads, but vBRIEF has ${planItemCount} plan items`;
|
|
4261
|
-
else if (recovery.errors.length > 0) {
|
|
4262
|
-
recoveryError = recovery.errors[0] ?? "Unknown error during beads creation";
|
|
4263
|
-
console.warn(`[agents] Auto-recovery errors: ${recovery.errors.join(", ")}`);
|
|
4264
|
-
} else recoveryError = "createBeadsFromVBrief returned no beads and no errors";
|
|
4265
|
-
} catch (recoveryErr) {
|
|
4266
|
-
recoveryError = recoveryErr.message;
|
|
4267
|
-
console.warn(`[agents] Auto-recovery failed: ${recoveryErr.message}`);
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
if (!hasBeads) {
|
|
4271
|
-
const detail = recoveryError ? ` Beads recovery failed: ${recoveryError}.` : " No beads exist for this issue.";
|
|
4272
|
-
console.warn(`[agents] Refusing to start work agent for ${issueId} without beads.${detail}`);
|
|
4273
|
-
return jsonResponse({
|
|
4274
|
-
success: false,
|
|
4275
|
-
error: "beads_required",
|
|
4276
|
-
message: `Cannot start work agent for ${issueId} without beads.${detail} Re-run planning so beads can be materialized from the vBRIEF plan, then retry.`,
|
|
4277
|
-
...recoveryError ? { recoveryError } : {}
|
|
4278
|
-
}, { status: 422 });
|
|
4279
|
-
}
|
|
4280
5602
|
const health = yield* readModel.getSnapshot.pipe(Effect.flatMap((snapshot) => Effect.promise(() => getSystemHealthSnapshot(snapshot))));
|
|
4281
5603
|
emitStartAgentPhase(issueId, "guardrails", "start", "evaluating spawn guardrails");
|
|
4282
5604
|
const spawnGuardrails = evaluateSpawnGuardrails(health);
|
|
@@ -4320,7 +5642,7 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4320
5642
|
return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
|
|
4321
5643
|
}
|
|
4322
5644
|
if ((yield* Effect.promise(() => getProviderAuthMode(spawnModel))) === "subscription") {
|
|
4323
|
-
const codexAuth = yield* checkCodexAuthStatus();
|
|
5645
|
+
const codexAuth = yield* checkCodexAuthStatus({ agentStates: listAgentStates() });
|
|
4324
5646
|
if (codexAuth.status === "expired" || codexAuth.status === "burned") return jsonResponse({
|
|
4325
5647
|
success: false,
|
|
4326
5648
|
blocked: true,
|
|
@@ -4398,24 +5720,20 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4398
5720
|
if (shouldCommitLegacyWorkspaceArtifacts(projectConfig ? yield* Effect.promise(() => isStateMigrated(projectConfig)) : false) && (existsSync(workspacePanContinuePath) || existsSync(workspacePanDir))) yield* Effect.gen(function* () {
|
|
4399
5721
|
const gitRoot = workspacePath;
|
|
4400
5722
|
if (existsSync(join(gitRoot, ".pan"))) {
|
|
4401
|
-
yield* Effect.promise(() => execAsync$
|
|
5723
|
+
yield* Effect.promise(() => execAsync$2(`git add .pan/`, {
|
|
4402
5724
|
cwd: gitRoot,
|
|
4403
5725
|
encoding: "utf-8"
|
|
4404
5726
|
}));
|
|
4405
|
-
yield* Effect.promise(() => execAsync$
|
|
5727
|
+
yield* Effect.promise(() => execAsync$2(`git reset HEAD -- .pan/kickoff.md .pan/continue.json .pan/handoff-*.md .pan/spec.vbrief.json`, {
|
|
4406
5728
|
cwd: gitRoot,
|
|
4407
5729
|
encoding: "utf-8"
|
|
4408
5730
|
}));
|
|
4409
5731
|
}
|
|
4410
|
-
if (
|
|
4411
|
-
cwd: gitRoot,
|
|
4412
|
-
encoding: "utf-8"
|
|
4413
|
-
}));
|
|
4414
|
-
if (yield* Effect.promise(() => execAsync$1(`git diff --cached --quiet`, {
|
|
5732
|
+
if (yield* Effect.promise(() => execAsync$2(`git diff --cached --quiet`, {
|
|
4415
5733
|
cwd: gitRoot,
|
|
4416
5734
|
encoding: "utf-8"
|
|
4417
5735
|
}).then(() => false).catch(() => true))) {
|
|
4418
|
-
yield* Effect.promise(() => execAsync$
|
|
5736
|
+
yield* Effect.promise(() => execAsync$2(`git commit -m "chore: planning artifacts for ${issueId} before agent start"`, {
|
|
4419
5737
|
cwd: gitRoot,
|
|
4420
5738
|
encoding: "utf-8"
|
|
4421
5739
|
}));
|
|
@@ -4442,7 +5760,7 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4442
5760
|
console.warn(`[start-agent] Failed to set plan.status=running (non-fatal): ${planStatusErr?.message ?? planStatusErr}`);
|
|
4443
5761
|
}
|
|
4444
5762
|
try {
|
|
4445
|
-
const { appendSessionEntry, getProjectConfigFromWorkspacePath, resolveProjectForIssue } = yield* Effect.promise(() => import("./record-
|
|
5763
|
+
const { appendSessionEntry, getProjectConfigFromWorkspacePath, resolveProjectForIssue } = yield* Effect.promise(() => import("./record-DLOqfbkI.js"));
|
|
4446
5764
|
const recordProject = resolveProjectForIssue(issueId) ?? getProjectConfigFromWorkspacePath(workspacePath);
|
|
4447
5765
|
yield* Effect.promise(() => appendSessionEntry(recordProject, issueId, {
|
|
4448
5766
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4491,12 +5809,12 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4491
5809
|
console.log(`[start-agent] Killed stale tmux session ${agentSessionName}`);
|
|
4492
5810
|
}).pipe(Effect.catch(() => Effect.void));
|
|
4493
5811
|
if (!(body.acknowledgeDirtyWorkspace === true)) try {
|
|
4494
|
-
const { stdout: statusOut } = yield* Effect.promise(() => execAsync$
|
|
5812
|
+
const { stdout: statusOut } = yield* Effect.promise(() => execAsync$2("git status --porcelain", {
|
|
4495
5813
|
cwd: workspacePath,
|
|
4496
5814
|
encoding: "utf-8"
|
|
4497
5815
|
}));
|
|
4498
5816
|
if (statusOut.trim()) {
|
|
4499
|
-
const { stdout: diffOut } = yield* Effect.promise(() => execAsync$
|
|
5817
|
+
const { stdout: diffOut } = yield* Effect.promise(() => execAsync$2("git diff HEAD --stat", {
|
|
4500
5818
|
cwd: workspacePath,
|
|
4501
5819
|
encoding: "utf-8"
|
|
4502
5820
|
}).catch(() => ({ stdout: "" })));
|
|
@@ -4688,39 +6006,7 @@ const postAgentsRoute = HttpRouter.add("POST", "/api/agents", httpHandler(Effect
|
|
|
4688
6006
|
* Composes all /api/agents/* route layers and preserves the public helper
|
|
4689
6007
|
* exports that external callers historically imported from routes/agents.js.
|
|
4690
6008
|
*/
|
|
4691
|
-
const agentsRouteLayer = Layer.mergeAll(getAgentsRoute, getAgentOutputRoute, getAgentConversationRoute, postAgentMessageRoute, postAgentTellRoute, deleteAgentRoute, postAgentStopRoute, getAgentHealthHistoryRoute, postAgentPokeRoute, getAgentPendingQuestionsRoute, postAgentAnswerQuestionRoute, postAgentHeartbeatRoute, postAgentWorkCompleteRoute, postAgentStuckRoute, postAgentClassifyCompletionRoute, postInternalAgentPermissionRequestRoute, postAgentPermissionResponseRoute, getAgentRuntimeRoute, getAgentGitInfoRoute, getAgentActivityRoute, getAgentFilesRoute, getAgentTimelineRoute, postAgentSuspendRoute, postAgentPauseRoute, postAgentUnpauseRoute, postAgentUntroubledRoute, postAgentResumeRoute, postAgentRecoverRoute, postAgentRestartRoute, getAgentCloisterHealthRoute, getAgentHandoffSuggestionRoute, postAgentHandoffRoute, getAgentCostRoute, postAgentsRoute, postAgentsRestartAllRoute, getAgentTmuxAliveRoute, getAgentHasSessionRoute, postAgentResetSessionRoute, postAgentSwitchModelRoute, postAgentRestartFreshRoute, postAgentDeliveryMethodRoute, getAgentsRestartConfigRoute, postAgentsRestartWithConfigRoute);
|
|
4692
|
-
//#endregion
|
|
4693
|
-
//#region ../../lib/cloister/task-readiness.ts
|
|
4694
|
-
init_io();
|
|
4695
|
-
const TERMINAL_STATUSES = ["completed", "cancelled"];
|
|
4696
|
-
/**
|
|
4697
|
-
* Returns the list of item IDs that are newly unblocked after `justCompletedId`
|
|
4698
|
-
* reaches a terminal status.
|
|
4699
|
-
*
|
|
4700
|
-
* "Newly unblocked" means: the item was not ready before (had at least one
|
|
4701
|
-
* non-terminal blocker) and is now ready after this completion.
|
|
4702
|
-
*
|
|
4703
|
-
* Returns [] when no plan exists.
|
|
4704
|
-
*/
|
|
4705
|
-
function getUnblockedItemsSync(workspacePath, justCompletedId) {
|
|
4706
|
-
const doc = readWorkspacePlanSync(workspacePath);
|
|
4707
|
-
if (!doc) return [];
|
|
4708
|
-
const itemById = new Map(doc.plan.items.map((i) => [i.id, i]));
|
|
4709
|
-
const directlyUnblocking = doc.plan.edges.filter((e) => e.type === "blocks" && e.from === justCompletedId).map((e) => e.to);
|
|
4710
|
-
const unblocked = [];
|
|
4711
|
-
for (const candidateId of directlyUnblocking) {
|
|
4712
|
-
const candidate = itemById.get(candidateId);
|
|
4713
|
-
if (!candidate) continue;
|
|
4714
|
-
if (TERMINAL_STATUSES.includes(candidate.status)) continue;
|
|
4715
|
-
if (doc.plan.edges.filter((e) => e.type === "blocks" && e.to === candidateId).map((e) => e.from).every((blockerId) => {
|
|
4716
|
-
const blocker = itemById.get(blockerId);
|
|
4717
|
-
if (!blocker) return true;
|
|
4718
|
-
if (blockerId === justCompletedId) return true;
|
|
4719
|
-
return TERMINAL_STATUSES.includes(blocker.status);
|
|
4720
|
-
})) unblocked.push(candidateId);
|
|
4721
|
-
}
|
|
4722
|
-
return unblocked;
|
|
4723
|
-
}
|
|
6009
|
+
const agentsRouteLayer = Layer.mergeAll(getAgentsRoute, getAgentOutputRoute, getAgentConversationRoute, postAgentMessageRoute, postAgentTellRoute, deleteAgentRoute, postAgentStopRoute, getAgentHealthHistoryRoute, postAgentPokeRoute, getAgentPendingQuestionsRoute, postAgentAnswerQuestionRoute, postAgentPlanActionRoute, postAgentHeartbeatRoute, postAgentWorkCompleteRoute, postAgentStuckRoute, postAgentClassifyCompletionRoute, postInternalAgentPermissionRequestRoute, postAgentPermissionResponseRoute, getAgentRuntimeRoute, getAgentGitInfoRoute, getAgentActivityRoute, getAgentFilesRoute, getAgentTimelineRoute, postAgentSuspendRoute, postAgentPauseRoute, postAgentUnpauseRoute, postAgentUntroubledRoute, postAgentResumeRoute, postAgentRecoverRoute, postAgentRestartRoute, getAgentCloisterHealthRoute, getAgentHandoffSuggestionRoute, postAgentHandoffRoute, getAgentCostRoute, postAgentsRoute, postAgentsRestartAllRoute, getAgentTmuxAliveRoute, getAgentHasSessionRoute, postAgentResetSessionRoute, postAgentSwitchModelRoute, postAgentRestartFreshRoute, postAgentDeliveryMethodRoute, getAgentsRestartConfigRoute, postAgentsRestartWithConfigRoute);
|
|
4724
6010
|
//#endregion
|
|
4725
6011
|
//#region routes/tiered-inspect-escalation.ts
|
|
4726
6012
|
init_tier_escalation();
|
|
@@ -4745,18 +6031,18 @@ function handleTieredInspectFailureEscalation(issueId, notes, deps = {}) {
|
|
|
4745
6031
|
if (!exists(workspacePath)) return null;
|
|
4746
6032
|
const doc = readPlan(workspacePath);
|
|
4747
6033
|
if (!resolveTieredExecutionEnabled(tiered, doc?.plan.metadata)) return null;
|
|
4748
|
-
const
|
|
4749
|
-
if (!doc || !
|
|
4750
|
-
const item = doc.plan.items.find((candidate) => candidate.id ===
|
|
6034
|
+
const taskId = notes?.match(/[Bb]ead\s+(\S+)/)?.[1];
|
|
6035
|
+
if (!doc || !taskId) return null;
|
|
6036
|
+
const item = doc.plan.items.find((candidate) => candidate.id === taskId);
|
|
4751
6037
|
if (!item) return null;
|
|
4752
6038
|
const overrides = readOverrides(workspacePath);
|
|
4753
6039
|
const decision = decide({
|
|
4754
6040
|
kind: "supervisor-blocked",
|
|
4755
|
-
|
|
6041
|
+
taskId,
|
|
4756
6042
|
sha: getReviewStatusSync(issueId)?.reviewedAtCommit ?? "unknown",
|
|
4757
6043
|
attemptsAtCurrentTier: tiered.escalation.retries_at_tier
|
|
4758
6044
|
}, item, tiered.escalation, overrides);
|
|
4759
|
-
if (decision.action === "promote") recordPromotion(workspacePath,
|
|
6045
|
+
if (decision.action === "promote") recordPromotion(workspacePath, taskId, decision.from, decision.to, decision.reason);
|
|
4760
6046
|
return decision;
|
|
4761
6047
|
}
|
|
4762
6048
|
async function reportTieredInspectFailureEscalation(issueId, notes, deps = {}) {
|
|
@@ -4860,10 +6146,10 @@ const _serverManagedMerges = /* @__PURE__ */ new Set();
|
|
|
4860
6146
|
function firePostMergeLifecycle(issueId, options) {
|
|
4861
6147
|
const started = postMergeGuard.run(issueId, async () => {
|
|
4862
6148
|
const projectPath = getProjectPathForIssue(extractPrefixSync(issueId) ?? issueId.split("-")[0]);
|
|
4863
|
-
const { postMergeLifecycle } = await import("./merge-agent-
|
|
6149
|
+
const { postMergeLifecycle } = await import("./merge-agent-DMQTJMP0.js");
|
|
4864
6150
|
await postMergeLifecycle(issueId, projectPath, options?.sourceBranch, options?.verifiedMergedRef ? { verifiedMergedRef: options.verifiedMergedRef } : void 0);
|
|
4865
6151
|
console.log(`[merge] post-merge lifecycle completed for ${issueId}`);
|
|
4866
|
-
const { runMergeTrainReconcile } = await import("./merge-train-
|
|
6152
|
+
const { runMergeTrainReconcile } = await import("./merge-train-DJgr81tF.js");
|
|
4867
6153
|
const outcomes = await runMergeTrainReconcile(issueId);
|
|
4868
6154
|
if (outcomes.length > 0) console.log(`[merge-train] reconciled ${outcomes.length} sibling(s) after ${issueId}: ` + outcomes.map((o) => `${o.issueId}=${o.result}`).join(", "));
|
|
4869
6155
|
}, (err) => console.error(`[merge] post-merge lifecycle failed for ${issueId}:`, err));
|
|
@@ -4877,22 +6163,21 @@ function getProjectPathForIssue(issuePrefix) {
|
|
|
4877
6163
|
}
|
|
4878
6164
|
//#endregion
|
|
4879
6165
|
//#region routes/specialists/legacy-routes.ts
|
|
4880
|
-
init_agents();
|
|
6166
|
+
init_agents$1();
|
|
4881
6167
|
init_projects();
|
|
4882
6168
|
init_review_status();
|
|
4883
|
-
init_io();
|
|
4884
6169
|
init_domain_services();
|
|
4885
6170
|
init_tmux();
|
|
4886
6171
|
const getSpecialistsRoute = HttpRouter.add("GET", "/api/specialists", httpHandler(Effect.gen(function* () {
|
|
4887
|
-
const { getAllSpecialistStatus, getAllProjectSpecialistStatuses } = yield* Effect.promise(() => import("./specialists-
|
|
6172
|
+
const { getAllSpecialistStatus, getAllProjectSpecialistStatuses } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
4888
6173
|
return jsonResponse({
|
|
4889
6174
|
specialists: yield* Effect.promise(() => getAllSpecialistStatus()),
|
|
4890
6175
|
projects: yield* Effect.promise(() => getAllProjectSpecialistStatuses())
|
|
4891
6176
|
});
|
|
4892
6177
|
})));
|
|
4893
6178
|
const postSpecialistsResetAllRoute = HttpRouter.add("POST", "/api/specialists/reset-all", httpHandler(Effect.gen(function* () {
|
|
4894
|
-
const { getAllSpecialists, isRunning, getTmuxSessionName } = yield* Effect.promise(() => import("./specialists-
|
|
4895
|
-
const { clearHookSync } = yield* Effect.promise(() => import("./hooks-
|
|
6179
|
+
const { getAllSpecialists, isRunning, getTmuxSessionName } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
6180
|
+
const { clearHookSync } = yield* Effect.promise(() => import("./hooks-BDcQJDUe.js"));
|
|
4896
6181
|
const specialists = getAllSpecialists();
|
|
4897
6182
|
const results = [];
|
|
4898
6183
|
for (const specialist of specialists) {
|
|
@@ -4942,7 +6227,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
4942
6227
|
const normalizedIssueId = issueId.toUpperCase();
|
|
4943
6228
|
console.log(`[specialists/done] ${specialist} signaling ${status} for ${normalizedIssueId}`);
|
|
4944
6229
|
if (specialist === "merge") {
|
|
4945
|
-
const { reportSpecialistCompletion } = yield* Effect.promise(() => import("./specialist-completion-
|
|
6230
|
+
const { reportSpecialistCompletion } = yield* Effect.promise(() => import("./specialist-completion-pJtESo7m.js"));
|
|
4946
6231
|
if (yield* reportSpecialistCompletion(normalizedIssueId, {
|
|
4947
6232
|
status,
|
|
4948
6233
|
notes
|
|
@@ -4989,7 +6274,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
4989
6274
|
if (specialist === "inspect" && status === "failed") yield* Effect.promise(() => reportTieredInspectFailureEscalation(normalizedIssueId, notes));
|
|
4990
6275
|
yield* Effect.promise(async () => {
|
|
4991
6276
|
try {
|
|
4992
|
-
const { getTmuxSessionName, updateRunMetadata, makeSpecialistRegistryKey } = await import("./specialists-
|
|
6277
|
+
const { getTmuxSessionName, updateRunMetadata, makeSpecialistRegistryKey } = await import("./specialists-D9-dMUtI.js");
|
|
4993
6278
|
const projectKey = resolveProjectFromIssueSync(normalizedIssueId)?.projectKey;
|
|
4994
6279
|
const tmuxSession = projectKey ? getTmuxSessionName(`${specialist}-agent`, projectKey, normalizedIssueId) : getTmuxSessionName(`${specialist}-agent`);
|
|
4995
6280
|
saveAgentRuntimeState(tmuxSession, {
|
|
@@ -5008,7 +6293,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5008
6293
|
});
|
|
5009
6294
|
console.log(`[specialists/done] Cleared registry lock for ${registryKey} (${projectKey})`);
|
|
5010
6295
|
}
|
|
5011
|
-
const { updateSpecialistHandoffStatus } = await import("./specialist-handoff-logger-
|
|
6296
|
+
const { updateSpecialistHandoffStatus } = await import("./specialist-handoff-logger-Duz9RGvk.js");
|
|
5012
6297
|
if (await Effect.runPromise(updateSpecialistHandoffStatus(normalizedIssueId, `${specialist}-agent`, status === "passed" ? "completed" : "failed", status === "passed" ? "success" : "failure"))) console.log(`[specialists/done] Updated handoff log: ${specialist}-agent ${normalizedIssueId} → ${status}`);
|
|
5013
6298
|
} catch (err) {
|
|
5014
6299
|
console.error(`[specialists/done] Error managing specialist state:`, err);
|
|
@@ -5020,7 +6305,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5020
6305
|
if (project) {
|
|
5021
6306
|
const workspacePath = join(project.projectPath, "workspaces", `feature-${normalizedIssueId.toLowerCase()}`);
|
|
5022
6307
|
if (existsSync(workspacePath)) {
|
|
5023
|
-
const { getWorkspaceGitInfo } = await import("./git-utils-
|
|
6308
|
+
const { getWorkspaceGitInfo } = await import("./git-utils-q8jNoBts.js");
|
|
5024
6309
|
const { HEAD } = await Effect.runPromise(getWorkspaceGitInfo(workspacePath));
|
|
5025
6310
|
setReviewStatusSync(normalizedIssueId, { reviewedAtCommit: HEAD });
|
|
5026
6311
|
console.log(`[specialists/done] Snapshotted reviewedAtCommit=${HEAD.substring(0, 8)} for ${normalizedIssueId}`);
|
|
@@ -5032,40 +6317,12 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5032
6317
|
});
|
|
5033
6318
|
if (specialist === "inspect" && status === "passed") yield* Effect.promise(async () => {
|
|
5034
6319
|
try {
|
|
5035
|
-
const { onInspectComplete } = await import("./inspect-agent-
|
|
5036
|
-
const
|
|
6320
|
+
const { onInspectComplete } = await import("./inspect-agent-DCJQm9Wu.js");
|
|
6321
|
+
const taskId = (notes?.match(/[Bb]ead\s+(\S+)/))?.[1] || "unknown";
|
|
5037
6322
|
const project = resolveProjectFromIssueSync(normalizedIssueId);
|
|
5038
6323
|
if (project) {
|
|
5039
6324
|
const workspacePath = join(project.projectPath, "workspaces", `feature-${normalizedIssueId.toLowerCase()}`);
|
|
5040
|
-
if (existsSync(workspacePath))
|
|
5041
|
-
await Effect.runPromise(onInspectComplete(project.projectKey, normalizedIssueId, beadId, "passed", workspacePath));
|
|
5042
|
-
try {
|
|
5043
|
-
const beadData = await Effect.runPromise(queryBeadById(workspacePath, beadId));
|
|
5044
|
-
const updatedItemId = await Effect.runPromise(syncBeadStatusToVBrief(beadId, workspacePath, "completed", beadData?.title));
|
|
5045
|
-
if (updatedItemId) try {
|
|
5046
|
-
const unblockedItems = getUnblockedItemsSync(workspacePath, updatedItemId);
|
|
5047
|
-
if (unblockedItems.length > 0) {
|
|
5048
|
-
console.log(`[auto-wake] ${normalizedIssueId}: items unblocked after "${updatedItemId}": ${unblockedItems.join(", ")}`);
|
|
5049
|
-
const workAgentId = `agent-${normalizedIssueId.toLowerCase()}`;
|
|
5050
|
-
const doc = readWorkspacePlanSync(workspacePath);
|
|
5051
|
-
const unblockedTitles = unblockedItems.map((id) => {
|
|
5052
|
-
const it = doc?.plan.items.find((i) => i.id === id);
|
|
5053
|
-
return it ? `"${it.title}"` : `"${id}"`;
|
|
5054
|
-
}).join(", ");
|
|
5055
|
-
await messageAgent(workAgentId, `DAG SCHEDULER: Task${unblockedItems.length > 1 ? "s" : ""} now unblocked after completing "${updatedItemId}": ${unblockedTitles}. Pick up the next available task.`).catch((err) => {
|
|
5056
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5057
|
-
console.log(`[auto-wake] Could not wake ${workAgentId} (may not be running): ${errMsg}`);
|
|
5058
|
-
});
|
|
5059
|
-
}
|
|
5060
|
-
} catch (wakeErr) {
|
|
5061
|
-
const errMsg = wakeErr instanceof Error ? wakeErr.message : String(wakeErr);
|
|
5062
|
-
console.warn(`[auto-wake] Failed to check unblocked items: ${errMsg}`);
|
|
5063
|
-
}
|
|
5064
|
-
} catch (syncErr) {
|
|
5065
|
-
const errMsg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
5066
|
-
console.warn(`[specialists/done] vBRIEF sync failed: ${errMsg}`);
|
|
5067
|
-
}
|
|
5068
|
-
}
|
|
6325
|
+
if (existsSync(workspacePath)) await Effect.runPromise(onInspectComplete(project.projectKey, normalizedIssueId, taskId, "passed", workspacePath));
|
|
5069
6326
|
}
|
|
5070
6327
|
} catch (err) {
|
|
5071
6328
|
console.error(`[specialists/done] Error saving inspect checkpoint:`, err);
|
|
@@ -5077,7 +6334,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5077
6334
|
if (project) {
|
|
5078
6335
|
if (existsSync(join(project.projectPath, "workspaces", `feature-${normalizedIssueId.toLowerCase()}`))) {
|
|
5079
6336
|
setReviewStatusSync(normalizedIssueId, { testStatus: "passed" });
|
|
5080
|
-
const { initEventStore } = await import("./event-store-
|
|
6337
|
+
const { initEventStore } = await import("./event-store-D9UhQyMS.js");
|
|
5081
6338
|
await (await initEventStore()).appendAsync({
|
|
5082
6339
|
type: "test.passed",
|
|
5083
6340
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5130,8 +6387,8 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5130
6387
|
if (notes?.toLowerCase().includes("conflict")) yield* Effect.promise(async () => {
|
|
5131
6388
|
try {
|
|
5132
6389
|
const workAgentId = `agent-${normalizedIssueId.toLowerCase()}`;
|
|
5133
|
-
const { sessionExists } = await import("./tmux-
|
|
5134
|
-
const { messageAgent, spawnAgent, getAgentStateSync } = await import("./agents-
|
|
6390
|
+
const { sessionExists } = await import("./tmux-C-rgxRDA.js");
|
|
6391
|
+
const { messageAgent, spawnAgent, getAgentStateSync } = await import("./agents-BA15AQqo.js");
|
|
5135
6392
|
if (await Effect.runPromise(sessionExists(workAgentId))) {
|
|
5136
6393
|
await messageAgent(workAgentId, `MERGE CONFLICT: The merge-agent could not rebase your branch onto main due to conflicts. Please fix this now:\n\n1. git fetch origin main\n2. git rebase origin/main\n3. Resolve any conflicts (git add <file> && git rebase --continue)\n4. git push --force-with-lease\n5. Resubmit: curl -s -X POST http://localhost:3011/api/review/${normalizedIssueId}/request -H "Content-Type: application/json" -d "{}"\n\nConflict details: ${notes}`);
|
|
5137
6394
|
console.log(`[specialists/done] Sent rebase instructions to ${workAgentId}`);
|
|
@@ -5145,7 +6402,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5145
6402
|
try {
|
|
5146
6403
|
const project = resolveProjectFromIssueSync(normalizedIssueId);
|
|
5147
6404
|
const workspacePath = project ? join(project.projectPath, "workspaces", `feature-${normalizedIssueId.toLowerCase()}`) : void 0;
|
|
5148
|
-
const { deliverReviewVerdictFeedback } = await import("./review-verdict-feedback-
|
|
6405
|
+
const { deliverReviewVerdictFeedback } = await import("./review-verdict-feedback-BsFLjN_6.js");
|
|
5149
6406
|
const result = await Effect.runPromise(deliverReviewVerdictFeedback({
|
|
5150
6407
|
issueId: normalizedIssueId,
|
|
5151
6408
|
verdict: status === "failed" ? "failed" : "blocked",
|
|
@@ -5186,7 +6443,7 @@ const postSpecialistsDoneRoute = HttpRouter.add("POST", "/api/specialists/done",
|
|
|
5186
6443
|
});
|
|
5187
6444
|
})));
|
|
5188
6445
|
const postSpecialistsLogsCleanupAllRoute = HttpRouter.add("POST", "/api/specialists/logs/cleanup-all", httpHandler(Effect.gen(function* () {
|
|
5189
|
-
const { cleanupAllLogsSync } = yield* Effect.promise(() => import("./specialist-logs-
|
|
6446
|
+
const { cleanupAllLogsSync } = yield* Effect.promise(() => import("./specialist-logs-DtSkPJZK.js"));
|
|
5190
6447
|
const results = cleanupAllLogsSync();
|
|
5191
6448
|
return jsonResponse({
|
|
5192
6449
|
success: true,
|
|
@@ -5196,7 +6453,7 @@ const postSpecialistsLogsCleanupAllRoute = HttpRouter.add("POST", "/api/speciali
|
|
|
5196
6453
|
});
|
|
5197
6454
|
})));
|
|
5198
6455
|
const getSpecialistsProjectsRoute = HttpRouter.add("GET", "/api/specialists/projects", httpHandler(Effect.gen(function* () {
|
|
5199
|
-
const { getAllProjectSpecialistStatuses } = yield* Effect.promise(() => import("./specialists-
|
|
6456
|
+
const { getAllProjectSpecialistStatuses } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5200
6457
|
return jsonResponse(yield* Effect.promise(() => getAllProjectSpecialistStatuses()));
|
|
5201
6458
|
})));
|
|
5202
6459
|
const postSpecialistWakeRoute = HttpRouter.add("POST", "/api/specialists/:name/wake", httpHandler(Effect.gen(function* () {
|
|
@@ -5240,7 +6497,7 @@ const postSpecialistReportStatusRoute = HttpRouter.add("POST", "/api/specialists
|
|
|
5240
6497
|
"blocked",
|
|
5241
6498
|
"failed"
|
|
5242
6499
|
].includes(status)) {
|
|
5243
|
-
const { getTmuxSessionName } = yield* Effect.promise(() => import("./specialists-
|
|
6500
|
+
const { getTmuxSessionName } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5244
6501
|
saveAgentRuntimeState(getTmuxSessionName(name), {
|
|
5245
6502
|
state: "idle",
|
|
5246
6503
|
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5342,7 +6599,7 @@ init_config_yaml();
|
|
|
5342
6599
|
init_projects();
|
|
5343
6600
|
init_review_status();
|
|
5344
6601
|
init_tmux();
|
|
5345
|
-
init_agents();
|
|
6602
|
+
init_agents$1();
|
|
5346
6603
|
const getProjectSpecialistStatusRoute = HttpRouter.add("GET", "/api/specialists/:project/:issueId/:type/status", httpHandler(Effect.gen(function* () {
|
|
5347
6604
|
const params = yield* HttpRouter.params;
|
|
5348
6605
|
return jsonResponse({
|
|
@@ -5361,7 +6618,7 @@ const postProjectSpecialistKillRoute = HttpRouter.add("POST", "/api/specialists/
|
|
|
5361
6618
|
const issueId = params["issueId"];
|
|
5362
6619
|
const type = params["type"];
|
|
5363
6620
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5364
|
-
const { getTmuxSessionName, makeSpecialistRegistryKey, getRunMetadata } = yield* Effect.promise(() => import("./specialists-
|
|
6621
|
+
const { getTmuxSessionName, makeSpecialistRegistryKey, getRunMetadata } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5365
6622
|
const tmuxSession = getRunMetadata(project, makeSpecialistRegistryKey(type, issueId)).tmuxSession ?? getTmuxSessionName(type, project, issueId);
|
|
5366
6623
|
yield* Effect.promise(() => Effect.runPromise(killSession(tmuxSession)).catch(() => {}));
|
|
5367
6624
|
saveAgentRuntimeState(tmuxSession, {
|
|
@@ -5388,7 +6645,7 @@ const getProjectSpecialistRunsRoute = HttpRouter.add("GET", "/api/specialists/:p
|
|
|
5388
6645
|
if (limitParam) limit = parseInt(limitParam, 10);
|
|
5389
6646
|
if (offsetParam) offset = parseInt(offsetParam, 10);
|
|
5390
6647
|
}
|
|
5391
|
-
const { listRunLogsSync } = yield* Effect.promise(() => import("./specialist-logs-
|
|
6648
|
+
const { listRunLogsSync } = yield* Effect.promise(() => import("./specialist-logs-DtSkPJZK.js"));
|
|
5392
6649
|
return jsonResponse(listRunLogsSync(project, type, {
|
|
5393
6650
|
limit,
|
|
5394
6651
|
offset
|
|
@@ -5399,7 +6656,7 @@ const getProjectSpecialistRunStreamRoute = HttpRouter.add("GET", "/api/specialis
|
|
|
5399
6656
|
const project = params["project"];
|
|
5400
6657
|
const type = params["type"];
|
|
5401
6658
|
const runId = params["runId"];
|
|
5402
|
-
const { getRunLogPath, isRunLogActive } = yield* Effect.promise(() => import("./specialist-logs-
|
|
6659
|
+
const { getRunLogPath, isRunLogActive } = yield* Effect.promise(() => import("./specialist-logs-DtSkPJZK.js"));
|
|
5403
6660
|
const logPath = getRunLogPath(project, type, runId);
|
|
5404
6661
|
if (!existsSync(logPath)) return jsonResponse({ error: "Run log not found" }, { status: 404 });
|
|
5405
6662
|
const encoder = new TextEncoder();
|
|
@@ -5458,7 +6715,7 @@ const getProjectSpecialistRunRoute = HttpRouter.add("GET", "/api/specialists/:pr
|
|
|
5458
6715
|
const project = params["project"];
|
|
5459
6716
|
const type = params["type"];
|
|
5460
6717
|
const runId = params["runId"];
|
|
5461
|
-
const { getRunLogSync, parseLogMetadata } = yield* Effect.promise(() => import("./specialist-logs-
|
|
6718
|
+
const { getRunLogSync, parseLogMetadata } = yield* Effect.promise(() => import("./specialist-logs-DtSkPJZK.js"));
|
|
5462
6719
|
const content = getRunLogSync(project, type, runId);
|
|
5463
6720
|
if (!content) return jsonResponse({ error: "Run log not found" }, { status: 404 });
|
|
5464
6721
|
return jsonResponse({
|
|
@@ -5472,7 +6729,7 @@ const postProjectSpecialistRunTerminateRoute = HttpRouter.add("POST", "/api/spec
|
|
|
5472
6729
|
const project = params["project"];
|
|
5473
6730
|
const type = params["type"];
|
|
5474
6731
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5475
|
-
const { terminateSpecialist } = yield* Effect.promise(() => import("./specialists-
|
|
6732
|
+
const { terminateSpecialist } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5476
6733
|
yield* Effect.promise(() => terminateSpecialist(project, type));
|
|
5477
6734
|
return jsonResponse({
|
|
5478
6735
|
success: true,
|
|
@@ -5484,7 +6741,7 @@ const postProjectSpecialistGracePauseRoute = HttpRouter.add("POST", "/api/specia
|
|
|
5484
6741
|
const project = params["project"];
|
|
5485
6742
|
const type = params["type"];
|
|
5486
6743
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5487
|
-
const { pauseGracePeriod } = yield* Effect.promise(() => import("./specialists-
|
|
6744
|
+
const { pauseGracePeriod } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5488
6745
|
if (pauseGracePeriod(project, type)) return jsonResponse({
|
|
5489
6746
|
success: true,
|
|
5490
6747
|
message: "Grace period paused"
|
|
@@ -5496,7 +6753,7 @@ const postProjectSpecialistGraceResumeRoute = HttpRouter.add("POST", "/api/speci
|
|
|
5496
6753
|
const project = params["project"];
|
|
5497
6754
|
const type = params["type"];
|
|
5498
6755
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5499
|
-
const { resumeGracePeriod } = yield* Effect.promise(() => import("./specialists-
|
|
6756
|
+
const { resumeGracePeriod } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5500
6757
|
if (resumeGracePeriod(project, type)) return jsonResponse({
|
|
5501
6758
|
success: true,
|
|
5502
6759
|
message: "Grace period resumed"
|
|
@@ -5508,7 +6765,7 @@ const postProjectSpecialistGraceExitRoute = HttpRouter.add("POST", "/api/special
|
|
|
5508
6765
|
const project = params["project"];
|
|
5509
6766
|
const type = params["type"];
|
|
5510
6767
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5511
|
-
const { exitGracePeriod } = yield* Effect.promise(() => import("./specialists-
|
|
6768
|
+
const { exitGracePeriod } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5512
6769
|
exitGracePeriod(project, type);
|
|
5513
6770
|
return jsonResponse({
|
|
5514
6771
|
success: true,
|
|
@@ -5520,7 +6777,7 @@ const getProjectSpecialistGraceRoute = HttpRouter.add("GET", "/api/specialists/:
|
|
|
5520
6777
|
const project = params["project"];
|
|
5521
6778
|
const type = params["type"];
|
|
5522
6779
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5523
|
-
const { getGracePeriodState } = yield* Effect.promise(() => import("./specialists-
|
|
6780
|
+
const { getGracePeriodState } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5524
6781
|
const state = getGracePeriodState(project, type);
|
|
5525
6782
|
if (state) return jsonResponse(state);
|
|
5526
6783
|
else return jsonResponse({ error: "No active grace period" }, { status: 404 });
|
|
@@ -5529,7 +6786,7 @@ const getProjectSpecialistContextRoute = HttpRouter.add("GET", "/api/specialists
|
|
|
5529
6786
|
const params = yield* HttpRouter.params;
|
|
5530
6787
|
const project = params["project"];
|
|
5531
6788
|
const type = params["type"];
|
|
5532
|
-
const { loadContextDigest } = yield* Effect.promise(() => import("./specialist-context-
|
|
6789
|
+
const { loadContextDigest } = yield* Effect.promise(() => import("./specialist-context-CspI4Eog.js"));
|
|
5533
6790
|
const digest = loadContextDigest(project, type);
|
|
5534
6791
|
if (digest) return jsonResponse({ digest });
|
|
5535
6792
|
else return jsonResponse({ error: "No context digest found" }, { status: 404 });
|
|
@@ -5538,7 +6795,7 @@ const postProjectSpecialistContextRegenerateRoute = HttpRouter.add("POST", "/api
|
|
|
5538
6795
|
const params = yield* HttpRouter.params;
|
|
5539
6796
|
const project = params["project"];
|
|
5540
6797
|
const type = params["type"];
|
|
5541
|
-
const { regenerateContextDigest } = yield* Effect.promise(() => import("./specialist-context-
|
|
6798
|
+
const { regenerateContextDigest } = yield* Effect.promise(() => import("./specialist-context-CspI4Eog.js"));
|
|
5542
6799
|
const digest = yield* Effect.promise(() => regenerateContextDigest(project, type));
|
|
5543
6800
|
if (digest) return jsonResponse({
|
|
5544
6801
|
digest,
|
|
@@ -5557,7 +6814,7 @@ const postProjectSpecialistCompleteRoute = HttpRouter.add("POST", "/api/speciali
|
|
|
5557
6814
|
"blocked"
|
|
5558
6815
|
].includes(status)) return jsonResponse({ error: "Valid status (passed/failed/blocked) is required" }, { status: 400 });
|
|
5559
6816
|
if (!validateSpecialistAgentName(type)) return jsonResponse({ error: "Invalid specialist type. Must be review-agent, test-agent, or merge-agent" }, { status: 400 });
|
|
5560
|
-
const { signalSpecialistCompletion } = yield* Effect.promise(() => import("./specialists-
|
|
6817
|
+
const { signalSpecialistCompletion } = yield* Effect.promise(() => import("./specialists-D9-dMUtI.js"));
|
|
5561
6818
|
signalSpecialistCompletion(project, type, {
|
|
5562
6819
|
status,
|
|
5563
6820
|
notes
|
|
@@ -5591,8 +6848,8 @@ const postProjectSpecialistLogsCleanupRoute = HttpRouter.add("POST", "/api/speci
|
|
|
5591
6848
|
const params = yield* HttpRouter.params;
|
|
5592
6849
|
const project = params["project"];
|
|
5593
6850
|
const type = params["type"];
|
|
5594
|
-
const { cleanupOldLogsSync } = yield* Effect.promise(() => import("./specialist-logs-
|
|
5595
|
-
const { getSpecialistRetention } = yield* Effect.promise(() => import("./projects-
|
|
6851
|
+
const { cleanupOldLogsSync } = yield* Effect.promise(() => import("./specialist-logs-DtSkPJZK.js"));
|
|
6852
|
+
const { getSpecialistRetention } = yield* Effect.promise(() => import("./projects-Dc9aoVoG.js"));
|
|
5596
6853
|
const retention = getSpecialistRetention(project);
|
|
5597
6854
|
const deleted = cleanupOldLogsSync(project, type, {
|
|
5598
6855
|
maxDays: retention.max_days,
|
|
@@ -5613,7 +6870,7 @@ const postProjectReviewRestartRoute = HttpRouter.add("POST", "/api/specialists/:
|
|
|
5613
6870
|
const project = params["project"];
|
|
5614
6871
|
const issueId = params["issueId"];
|
|
5615
6872
|
const { model, harness } = yield* readJsonBody;
|
|
5616
|
-
const { killAllReviewerSessions } = yield* Effect.promise(() => import("./review-agent-
|
|
6873
|
+
const { killAllReviewerSessions } = yield* Effect.promise(() => import("./review-agent-CpSHLzed.js"));
|
|
5617
6874
|
const killResult = yield* killAllReviewerSessions(project, issueId);
|
|
5618
6875
|
const projectConfig = resolveProjectFromIssueSync(issueId);
|
|
5619
6876
|
if (!projectConfig) return jsonResponse({ error: `Cannot resolve project for ${issueId}` }, { status: 404 });
|
|
@@ -5627,7 +6884,7 @@ const postProjectReviewRestartRoute = HttpRouter.add("POST", "/api/specialists/:
|
|
|
5627
6884
|
}));
|
|
5628
6885
|
branch = stdout.trim() || "unknown";
|
|
5629
6886
|
} catch {}
|
|
5630
|
-
const { spawnReviewRoleForIssue } = yield* Effect.promise(() => import("./review-agent-
|
|
6887
|
+
const { spawnReviewRoleForIssue } = yield* Effect.promise(() => import("./review-agent-CpSHLzed.js"));
|
|
5631
6888
|
const prUrl = getReviewStatusSync(issueId)?.prUrl;
|
|
5632
6889
|
const result = yield* spawnReviewRoleForIssue({
|
|
5633
6890
|
issueId,
|
|
@@ -5776,6 +7033,6 @@ const specialistsProjectRouteLayer = Layer.mergeAll(getProjectSpecialistStatusRo
|
|
|
5776
7033
|
*/
|
|
5777
7034
|
const specialistsRouteLayer = Layer.mergeAll(specialistsLegacyRouteLayer, specialistsProjectRouteLayer);
|
|
5778
7035
|
//#endregion
|
|
5779
|
-
export {
|
|
7036
|
+
export { readLauncherPinnedSessionId as $, resolveAllowedHarness as A, cleanupUnreferencedConversationAttachments as B, handleConversationCreate as C, handleConversationStop as D, handleConversationResume as E, tmuxSessionExists as F, isManagedConversationAttachmentPath as G, extractConversationAttachmentPaths as H, waitForPiTuiReady as I, evaluateSpawnGuardrails as J, removeConversationAttachment as K, waitForTmuxSession as L, spawnConversationSession as M, startConversationModelBackfill as N, handleConversationSwitchModel as O, stopConversationRuntime as P, getWorkAgentLifecycleStateSync as Q, cleanupConversationAttachments as R, conversationSessionAliveFromState as S, handleConversationRestartAll as T, getConversationAttachmentsRoot as U, ensureConversationAttachmentDir as V, hasConversationAttachment as W, resolveAgentGitInfo as X, invalidateAgentsCache as Y, resolveConversationGitInfo as Z, isPiControlChannelHarness as _, agentsRouteLayer as a, isRespawnPending as at, validateConversationControlAckOrigin as b, codexConversationPendingInput as c, getTrustedOrigins as ct, handleConversationCodexApproval as d, validateOrigin as dt, resolveAgentHarness as et, handleConversationCompact as f, validateOriginHeaders as ft, handleConversationThinkingLevel as g, jsonStringResponse as gt, handleConversationPlanAction as h, jsonResponse as ht, validateSpecialistAutoCompleteMetadata as i, resolvePiSessionPath as it, shouldReportUnresolvedLiveSession as j, isInsideGitWorkTree as k, deliverConversationViaControlChannel as l, isTrustedOriginForHost as lt, handleConversationDeliveryMethod as m, getDefaultCwd as mt, _serverManagedMerges as n, resolveCodexRolloutPath as nt, saveAgentStateAndEmitEvent as o, waitForSessionRespawn as ot, handleConversationControlAck as p, httpHandler as pt, runInBatches as q, firePostMergeLifecycle as r, resolveJsonlPath as rt, saveAgentStateAndEmitEventProgram as s, getHeaderFromMap as st, specialistsRouteLayer as t, resolveClaudeSessionId as tt, handleConversationAbort as u, normalizeOrigin as ut, pickDeliverAs as v, handleConversationDelete as w, conversationNeedsRunningRepair as x, resolveConversationDeliveryMethod as y, cleanupOrphanedConversationAttachments as z };
|
|
5780
7037
|
|
|
5781
|
-
//# sourceMappingURL=specialists-
|
|
7038
|
+
//# sourceMappingURL=specialists-Dax7tqXC.js.map
|