@overdeck/core 0.50.3 → 0.50.4
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/agents-DNMRPC8n.js.map +1 -1
- package/dist/{build-info-BJuEVWOh.js → build-info-ioRdOmqT.js} +3 -3
- package/dist/{build-info-BJuEVWOh.js.map → build-info-ioRdOmqT.js.map} +1 -1
- package/dist/build-info-kMMitDOP.js +2 -0
- package/dist/cli/index.js +4 -4
- package/dist/conversation-compaction-GFfO5UZL.js.map +1 -1
- package/dist/dashboard/{advancing-selfheal-Bq8V0QUa.js → advancing-selfheal-Dp5_Gai3.js} +3 -3
- package/dist/dashboard/{advancing-selfheal-Bq8V0QUa.js.map → advancing-selfheal-Dp5_Gai3.js.map} +1 -1
- package/dist/dashboard/{agent-gc-Coz8zQt_.js → agent-gc-JzoSs5Pi.js} +3 -3
- package/dist/dashboard/{agent-gc-Coz8zQt_.js.map → agent-gc-JzoSs5Pi.js.map} +1 -1
- package/dist/dashboard/{agent-idle-CAwB-K6M.js → agent-idle-D-Qy5BN_.js} +2 -2
- package/dist/dashboard/{agent-idle-CAwB-K6M.js.map → agent-idle-D-Qy5BN_.js.map} +1 -1
- package/dist/dashboard/{agent-state-Ci6PhjOE.js → agent-state-B196fj1h.js} +38 -16
- package/dist/dashboard/agent-state-B196fj1h.js.map +1 -0
- package/dist/dashboard/{agent-state-CbUEwUEQ.js → agent-state-IpWnw8lv.js} +1 -1
- package/dist/dashboard/{agent-state-service-D6IVrX3B.js → agent-state-service-CCo5-9DR.js} +3 -3
- package/dist/dashboard/{agent-state-service-D6IVrX3B.js.map → agent-state-service-CCo5-9DR.js.map} +1 -1
- package/dist/dashboard/{agent-state-service-DcN1j6eK.js → agent-state-service-D_F3VZ23.js} +1 -1
- package/dist/dashboard/{agents-BVvL4o_4.js → agents-CkGhnAmu.js} +1 -1
- package/dist/dashboard/{backlog-auto-trigger-BuSBqG2V.js → backlog-auto-trigger-B5Z2zBtC.js} +2 -2
- package/dist/dashboard/{backlog-auto-trigger-BuSBqG2V.js.map → backlog-auto-trigger-B5Z2zBtC.js.map} +1 -1
- package/dist/dashboard/build-info-BTjOB0Xr.js +2 -0
- package/dist/dashboard/{build-info-Cj3Y0K87.js → build-info-D0EJNj6C.js} +3 -3
- package/dist/dashboard/{build-info-Cj3Y0K87.js.map → build-info-D0EJNj6C.js.map} +1 -1
- package/dist/dashboard/cli-CIznFrcE.js +2 -0
- package/dist/dashboard/{cli-DIhQieif.js → cli-DCbVrSsg.js} +2 -2
- package/dist/dashboard/{cli-DIhQieif.js.map → cli-DCbVrSsg.js.map} +1 -1
- package/dist/dashboard/{close-issue-CRlmrE5g.js → close-issue-Bmu9aOIx.js} +2 -2
- package/dist/dashboard/{close-issue-CRlmrE5g.js.map → close-issue-Bmu9aOIx.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-DrHyYgJR.js → closed-issue-reaper-COxfTtLz.js} +5 -5
- package/dist/dashboard/{closed-issue-reaper-DrHyYgJR.js.map → closed-issue-reaper-COxfTtLz.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-DWlw2EtL.js → closed-issue-reaper-DwMwG_Pz.js} +1 -1
- package/dist/dashboard/{compaction-continuation-DhYgsXmF.js → compaction-continuation-Bqc212Zm.js} +2 -2
- package/dist/dashboard/{compaction-continuation-DhYgsXmF.js.map → compaction-continuation-Bqc212Zm.js.map} +1 -1
- package/dist/dashboard/{conflict-gate-vczgdVTa.js → conflict-gate-9mPVg7-5.js} +1 -1
- package/dist/dashboard/{conflict-gate-Bt-lLgYf.js → conflict-gate-CGQTcR0I.js} +3 -3
- package/dist/dashboard/{conflict-gate-Bt-lLgYf.js.map → conflict-gate-CGQTcR0I.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-CsdCbcQB.js → conversation-compaction-C7P6V8Iw.js} +5 -5
- package/dist/dashboard/{conversation-compaction-CsdCbcQB.js.map → conversation-compaction-C7P6V8Iw.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-D0zgWM1f.js → conversation-compaction-Cck5oi-V.js} +1 -1
- package/dist/dashboard/{conversation-service-6LaymUV-.js → conversation-service-CsOpwI9Y.js} +37 -5
- package/dist/dashboard/{conversation-service-6LaymUV-.js.map → conversation-service-CsOpwI9Y.js.map} +1 -1
- package/dist/dashboard/conversation-service-plOFkoMo.js +3 -0
- package/dist/dashboard/dashboard-db-worker.js +1 -1
- package/dist/dashboard/{deacon-CU47dU7U.js → deacon-B8oMMIgN.js} +62 -62
- package/dist/dashboard/{deacon-CU47dU7U.js.map → deacon-B8oMMIgN.js.map} +1 -1
- package/dist/dashboard/{deacon-DYsOa_ss.js → deacon-b7J57Svt.js} +2 -2
- package/dist/dashboard/{deacon-pending-lifecycle-ijGX8wLb.js → deacon-pending-lifecycle-3NIJ7ew5.js} +2 -2
- package/dist/dashboard/{deacon-pending-lifecycle-ijGX8wLb.js.map → deacon-pending-lifecycle-3NIJ7ew5.js.map} +1 -1
- package/dist/dashboard/deacon.js +5 -5
- package/dist/dashboard/{delivery-DMYxIE0J.js → delivery-lnI-gQKo.js} +1 -1
- package/dist/dashboard/{deploy-patrol-C4eXco1S.js → deploy-patrol-u3v2krg9.js} +2 -2
- package/dist/dashboard/{deploy-patrol-C4eXco1S.js.map → deploy-patrol-u3v2krg9.js.map} +1 -1
- package/dist/dashboard/{feedback-stuck-retirement-Dd5XNrMr.js → feedback-stuck-retirement-CoFNqjCc.js} +2 -2
- package/dist/dashboard/{feedback-stuck-retirement-Dd5XNrMr.js.map → feedback-stuck-retirement-CoFNqjCc.js.map} +1 -1
- package/dist/dashboard/{feedback-target-BzdJ2wIR.js → feedback-target-BBCh3RRq.js} +1 -1
- package/dist/dashboard/{feedback-target-BcsA_GRV.js → feedback-target-CeCq6EhW.js} +5 -5
- package/dist/dashboard/{feedback-target-BcsA_GRV.js.map → feedback-target-CeCq6EhW.js.map} +1 -1
- package/dist/dashboard/{flywheel-Is3w2lcT.js → flywheel-DKldI63J.js} +7 -7
- package/dist/dashboard/{flywheel-Is3w2lcT.js.map → flywheel-DKldI63J.js.map} +1 -1
- package/dist/dashboard/{flywheel-D0guNqkh.js → flywheel-DpRPLErh.js} +7 -7
- package/dist/dashboard/{flywheel-D0guNqkh.js.map → flywheel-DpRPLErh.js.map} +1 -1
- package/dist/dashboard/{flywheel-B34cdfbd.js → flywheel-jeZFYvm-.js} +1 -1
- package/dist/dashboard/{flywheel-merge-order-6HiPR2WG.js → flywheel-merge-order-0OnoosaO.js} +1 -1
- package/dist/dashboard/{flywheel-merge-order-BsNPMu0S.js → flywheel-merge-order-PG5op4j7.js} +2 -2
- package/dist/dashboard/{flywheel-merge-order-BsNPMu0S.js.map → flywheel-merge-order-PG5op4j7.js.map} +1 -1
- package/dist/dashboard/{harness-resolve-CzTFVZ5l.js → harness-resolve-C0HWj3H9.js} +2 -2
- package/dist/dashboard/{harness-resolve-CzTFVZ5l.js.map → harness-resolve-C0HWj3H9.js.map} +1 -1
- package/dist/dashboard/{identity-Dc7rQLln.js → identity-Cot1Hg4G.js} +2 -2
- package/dist/dashboard/{identity-Dc7rQLln.js.map → identity-Cot1Hg4G.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-CP7Bu47O.js → inspect-agent-BDmLWbGx.js} +3 -3
- package/dist/dashboard/{inspect-agent-CP7Bu47O.js.map → inspect-agent-BDmLWbGx.js.map} +1 -1
- package/dist/dashboard/inspect-agent-BcOkA-hC.js +2 -0
- package/dist/dashboard/{issue-lifecycle-BPbczKyy.js → issue-lifecycle-1B_v8Rf5.js} +6 -6
- package/dist/dashboard/{issue-lifecycle-BPbczKyy.js.map → issue-lifecycle-1B_v8Rf5.js.map} +1 -1
- package/dist/dashboard/{issue-lifecycle-FaugTi39.js → issue-lifecycle-CIrfbIbj.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-Dsad9NTt.js → issue-service-singleton-BVwPzjwF.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-BrZQJLRI.js → issue-service-singleton-KcRrOiFD.js} +2 -2
- package/dist/dashboard/{issue-service-singleton-BrZQJLRI.js.map → issue-service-singleton-KcRrOiFD.js.map} +1 -1
- package/dist/dashboard/{jsonl-resolver-DQEe0DHM.js → jsonl-resolver-jxbif7MD.js} +3 -3
- package/dist/dashboard/{jsonl-resolver-DQEe0DHM.js.map → jsonl-resolver-jxbif7MD.js.map} +1 -1
- package/dist/dashboard/{kimi-code-Cx5YUJpS.js → kimi-code-BZh3NgcN.js} +1 -1
- package/dist/dashboard/{lifecycle-DZFHmpGI.js → lifecycle-CvcZ4_fA.js} +4 -4
- package/dist/dashboard/{lifecycle-DZFHmpGI.js.map → lifecycle-CvcZ4_fA.js.map} +1 -1
- package/dist/dashboard/{linear-mcp-auth-DylPJABZ.js → linear-mcp-auth-CZoj_-d7.js} +1 -1
- package/dist/dashboard/{linear-mcp-auth-BFlpMvWh.js → linear-mcp-auth-Dx74yCAY.js} +2 -2
- package/dist/dashboard/{linear-mcp-auth-BFlpMvWh.js.map → linear-mcp-auth-Dx74yCAY.js.map} +1 -1
- package/dist/dashboard/{memory-governor-B7uDWRo9.js → memory-governor-Bv3LAqk3.js} +5 -5
- package/dist/dashboard/{memory-governor-B7uDWRo9.js.map → memory-governor-Bv3LAqk3.js.map} +1 -1
- package/dist/dashboard/{merge-agent-ddEcZDZD.js → merge-agent-1zVLl6k_.js} +1 -1
- package/dist/dashboard/{merge-agent-ttRY053V.js → merge-agent-8zrQu48t.js} +8 -8
- package/dist/dashboard/{merge-agent-ttRY053V.js.map → merge-agent-8zrQu48t.js.map} +1 -1
- package/dist/dashboard/merge-ops-CFJaMPLu.js +2 -0
- package/dist/dashboard/{merge-train-f_un38UJ.js → merge-train-BYH_cF7G.js} +2 -2
- package/dist/dashboard/{merge-train-f_un38UJ.js.map → merge-train-BYH_cF7G.js.map} +1 -1
- package/dist/dashboard/{merge-train-deps-GPlHOjK6.js → merge-train-deps-BTfXM5AR.js} +2 -2
- package/dist/dashboard/{merge-train-deps-GPlHOjK6.js.map → merge-train-deps-BTfXM5AR.js.map} +1 -1
- package/dist/dashboard/{messaging-BasbftDu.js → messaging-Bk6vGsMw.js} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-DFHdQSEg.js → orphan-proposed-reconciler-B898fk26.js} +6 -6
- package/dist/dashboard/{orphan-proposed-reconciler-DFHdQSEg.js.map → orphan-proposed-reconciler-B898fk26.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-DzAxrM99.js → orphan-proposed-reconciler-Bx_aWS4B.js} +1 -1
- package/dist/dashboard/{parked-residue-DMtbxJml.js → parked-residue-q2kzrYSm.js} +9 -9
- package/dist/dashboard/{parked-residue-DMtbxJml.js.map → parked-residue-q2kzrYSm.js.map} +1 -1
- package/dist/dashboard/{post-merge-lifecycle-worker-jzN2g_EY.js → post-merge-lifecycle-worker-Cl7-uf4g.js} +2 -2
- package/dist/dashboard/{post-merge-lifecycle-worker-jzN2g_EY.js.map → post-merge-lifecycle-worker-Cl7-uf4g.js.map} +1 -1
- package/dist/dashboard/{project-resource-refresh-queue-cqOeJ0fB.js → project-resource-refresh-queue-O_dRfPHn.js} +1 -1
- package/dist/dashboard/{project-resource-refresh-queue-DebwjPPA.js → project-resource-refresh-queue-se3T9IYP.js} +4 -4
- package/dist/dashboard/{project-resource-refresh-queue-DebwjPPA.js.map → project-resource-refresh-queue-se3T9IYP.js.map} +1 -1
- package/dist/dashboard/{reconstruct-cache-Bb2Jg2DZ.js → reconstruct-cache-XpWC9L0F.js} +2 -2
- package/dist/dashboard/{reconstruct-cache-Bb2Jg2DZ.js.map → reconstruct-cache-XpWC9L0F.js.map} +1 -1
- package/dist/dashboard/{release-engine-78iCajIc.js → release-engine-BuzxOamf.js} +2 -2
- package/dist/dashboard/{release-engine-78iCajIc.js.map → release-engine-BuzxOamf.js.map} +1 -1
- package/dist/dashboard/{remote-completion-D5wzZCte.js → remote-completion-BCZgeKPS.js} +3 -3
- package/dist/dashboard/{remote-completion-D5wzZCte.js.map → remote-completion-BCZgeKPS.js.map} +1 -1
- package/dist/dashboard/{resolver-BGZh3lXA.js → resolver-BeNrqNou.js} +5 -5
- package/dist/dashboard/{resolver-BGZh3lXA.js.map → resolver-BeNrqNou.js.map} +1 -1
- package/dist/dashboard/{resource-pressure-patrol-Bg23rPV-.js → resource-pressure-patrol-Ccg_aCgO.js} +3 -3
- package/dist/dashboard/{resource-pressure-patrol-Bg23rPV-.js.map → resource-pressure-patrol-Ccg_aCgO.js.map} +1 -1
- package/dist/dashboard/{resume-BKyB4hA1.js → resume-KGCrn0DV.js} +1 -1
- package/dist/dashboard/{review-agent-ZdP-HmwY.js → review-agent-BmM6YMZW.js} +1 -1
- package/dist/dashboard/{review-agent-DHs-JngN.js → review-agent-DRWVU8fP.js} +13 -13
- package/dist/dashboard/{review-agent-DHs-JngN.js.map → review-agent-DRWVU8fP.js.map} +1 -1
- package/dist/dashboard/{review-convoy-liveness-BTMvU4CA.js → review-convoy-liveness-EvCDv8Q_.js} +3 -3
- package/dist/dashboard/{review-convoy-liveness-BTMvU4CA.js.map → review-convoy-liveness-EvCDv8Q_.js.map} +1 -1
- package/dist/dashboard/{review-run-recovery-BYalQzbh.js → review-run-recovery-Ctm48Snt.js} +1 -1
- package/dist/dashboard/{review-run-recovery-BvVG3oUr.js → review-run-recovery-DNDI58cM.js} +2 -2
- package/dist/dashboard/{review-run-recovery-BvVG3oUr.js.map → review-run-recovery-DNDI58cM.js.map} +1 -1
- package/dist/dashboard/{review-status-DXTFzCmR.js → review-status-FnlvQuPI.js} +1 -1
- package/dist/dashboard/{review-status-emit-C9YW6SQL.js → review-status-emit-CeMS31DX.js} +2 -2
- package/dist/dashboard/{review-status-emit-C9YW6SQL.js.map → review-status-emit-CeMS31DX.js.map} +1 -1
- package/dist/dashboard/{review-status-BegAr86G.js → review-status-yY5qt7Rl.js} +5 -5
- package/dist/dashboard/{review-status-BegAr86G.js.map → review-status-yY5qt7Rl.js.map} +1 -1
- package/dist/dashboard/{review-verdict-feedback-qlVBi19-.js → review-verdict-feedback-BNgRj1e3.js} +4 -4
- package/dist/dashboard/{review-verdict-feedback-qlVBi19-.js.map → review-verdict-feedback-BNgRj1e3.js.map} +1 -1
- package/dist/dashboard/{review-verdict-feedback-Dp139Lmv.js → review-verdict-feedback-D39cuSV1.js} +1 -1
- package/dist/dashboard/{review-verdict-writer-CJr_Ob0Y.js → review-verdict-writer-DvFfKAn-.js} +2 -2
- package/dist/dashboard/{review-verdict-writer-CJr_Ob0Y.js.map → review-verdict-writer-DvFfKAn-.js.map} +1 -1
- package/dist/dashboard/{review-verdict-writer-BVco3WoI.js → review-verdict-writer-PcGBkvJ4.js} +1 -1
- package/dist/dashboard/{sequence-io-iljWlslY.js → sequence-io-DZyschsf.js} +2 -2
- package/dist/dashboard/{sequence-io-iljWlslY.js.map → sequence-io-DZyschsf.js.map} +1 -1
- package/dist/dashboard/{sequencer-agent-BGGVKxtw.js → sequencer-agent-BXDw5cNx.js} +5 -5
- package/dist/dashboard/{sequencer-agent-BGGVKxtw.js.map → sequencer-agent-BXDw5cNx.js.map} +1 -1
- package/dist/dashboard/server.js +79 -72
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{service-kzhN0DLg.js → service-DP50Omvn.js} +33 -33
- package/dist/dashboard/{service-kzhN0DLg.js.map → service-DP50Omvn.js.map} +1 -1
- package/dist/dashboard/{smart-compaction-owKHJDbL.js → smart-compaction-944GL8PN.js} +1 -1
- package/dist/dashboard/{smart-compaction-C96F2aFQ.js → smart-compaction-BzDfF6yb.js} +2 -2
- package/dist/dashboard/{smart-compaction-C96F2aFQ.js.map → smart-compaction-BzDfF6yb.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-CiJdOHTD.js → spawn-planning-session-BcVnHJjm.js} +1 -1
- package/dist/dashboard/{spawn-planning-session-FmJa6PCS.js → spawn-planning-session-CH8IYli7.js} +3 -3
- package/dist/dashboard/{spawn-planning-session-FmJa6PCS.js.map → spawn-planning-session-CH8IYli7.js.map} +1 -1
- package/dist/dashboard/{specialist-context-_ootmzn2.js → specialist-context-DhDTXVht.js} +3 -3
- package/dist/dashboard/{specialist-context-_ootmzn2.js.map → specialist-context-DhDTXVht.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-BbAX221-.js → specialist-logs-C1bcnZ-G.js} +2 -2
- package/dist/dashboard/{specialist-logs-BbAX221-.js.map → specialist-logs-C1bcnZ-G.js.map} +1 -1
- package/dist/dashboard/{specialists-COZoYaiz.js → specialists-BKmReEWT.js} +53 -53
- package/dist/dashboard/{specialists-COZoYaiz.js.map → specialists-BKmReEWT.js.map} +1 -1
- package/dist/dashboard/{specialists-RPLJnYc1.js → specialists-Bvrvt0br.js} +12 -12
- package/dist/dashboard/{specialists-RPLJnYc1.js.map → specialists-Bvrvt0br.js.map} +1 -1
- package/dist/dashboard/{specialists-DTAOVZpU.js → specialists-CD6GEKXk.js} +1 -1
- package/dist/dashboard/{specialists-D8sYZnPb.js → specialists-eFWpJRBc.js} +1 -1
- package/dist/dashboard/{stall-sweeper-9bRBZhrQ.js → stall-sweeper-D3fZvg7O.js} +3 -3
- package/dist/dashboard/{stall-sweeper-9bRBZhrQ.js.map → stall-sweeper-D3fZvg7O.js.map} +1 -1
- package/dist/dashboard/{state-migrate-J3tFJnbd.js → state-migrate-KZ2goscQ.js} +2 -2
- package/dist/dashboard/{state-migrate-J3tFJnbd.js.map → state-migrate-KZ2goscQ.js.map} +1 -1
- package/dist/dashboard/{stuck-remediation-BvTK_NYN.js → stuck-remediation-Cb2V1roj.js} +1 -1
- package/dist/dashboard/{stuck-remediation-4Rf_xMEb.js → stuck-remediation-CnoV0kcv.js} +5 -5
- package/dist/dashboard/{stuck-remediation-4Rf_xMEb.js.map → stuck-remediation-CnoV0kcv.js.map} +1 -1
- package/dist/dashboard/{summary-fork-PLcckTRE.js → summary-fork-CxuNBM0F.js} +2 -2
- package/dist/dashboard/{summary-fork-D-cylknJ.js → summary-fork-lC2vMXly.js} +4 -4
- package/dist/dashboard/{summary-fork-D-cylknJ.js.map → summary-fork-lC2vMXly.js.map} +1 -1
- package/dist/dashboard/{teardown-workspace-CTL16rnG.js → teardown-workspace-C-9murBw.js} +4 -4
- package/dist/dashboard/{teardown-workspace-CTL16rnG.js.map → teardown-workspace-C-9murBw.js.map} +1 -1
- package/dist/dashboard/{test-agent-queue-CMgYZiR_.js → test-agent-queue-YCBRzaAB.js} +3 -3
- package/dist/dashboard/{test-agent-queue-CMgYZiR_.js.map → test-agent-queue-YCBRzaAB.js.map} +1 -1
- package/dist/dashboard/{uat-failure-feedback-DS6W9X3I.js → uat-failure-feedback-B-1mL8Zp.js} +3 -3
- package/dist/dashboard/{uat-failure-feedback-DS6W9X3I.js.map → uat-failure-feedback-B-1mL8Zp.js.map} +1 -1
- package/dist/dashboard/{uat-promote-verification-CRsIHE_q.js → uat-promote-verification-BEBPcpPg.js} +2 -2
- package/dist/dashboard/{uat-promote-verification-CRsIHE_q.js.map → uat-promote-verification-BEBPcpPg.js.map} +1 -1
- package/dist/dashboard/{uat-train-BoA9r6_I.js → uat-train-BFo3OQUB.js} +1 -1
- package/dist/dashboard/{uat-train-Cd8CJeNe.js → uat-train-COgoJomz.js} +8 -8
- package/dist/dashboard/{uat-train-Cd8CJeNe.js.map → uat-train-COgoJomz.js.map} +1 -1
- package/dist/dashboard/{validate-BdmvTIym.js → validate-069_N1e2.js} +2 -2
- package/dist/dashboard/{validate-BdmvTIym.js.map → validate-069_N1e2.js.map} +1 -1
- package/dist/dashboard/{verification-runner-BO0K3xNA.js → verification-runner-CQnZyC5A.js} +1 -1
- package/dist/dashboard/{verification-runner-fEY1Qj-l.js → verification-runner-fy3wNrWn.js} +4 -4
- package/dist/dashboard/{verification-runner-fEY1Qj-l.js.map → verification-runner-fy3wNrWn.js.map} +1 -1
- package/dist/dashboard/workflows-Bk6Kt0ZQ.js +2 -0
- package/dist/dashboard/{workflows-CxVnygYU.js → workflows-N4QMkDzN.js} +10 -10
- package/dist/dashboard/{workflows-CxVnygYU.js.map → workflows-N4QMkDzN.js.map} +1 -1
- package/dist/dashboard/{workspaces-Cpz0xJWQ.js → workspaces-DmoyfHoU.js} +33 -33
- package/dist/dashboard/{workspaces-Cpz0xJWQ.js.map → workspaces-DmoyfHoU.js.map} +1 -1
- package/dist/{lifecycle-C6k0HWEO.js → lifecycle-DCFcjg7y.js} +1 -1
- package/dist/{lifecycle-D5oRpvvB.js → lifecycle-GkaHQJNK.js} +2 -2
- package/dist/{lifecycle-D5oRpvvB.js.map → lifecycle-GkaHQJNK.js.map} +1 -1
- package/dist/{merge-agent-Drufc7tU.js → merge-agent-CogNFasY.js} +2 -2
- package/dist/{merge-agent-Drufc7tU.js.map → merge-agent-CogNFasY.js.map} +1 -1
- package/dist/{merge-agent-CBOVY_eC.js → merge-agent-eteKQ9Rb.js} +1 -1
- package/package.json +1 -1
- package/dist/build-info-BgrwLUy6.js +0 -2
- package/dist/dashboard/agent-state-Ci6PhjOE.js.map +0 -1
- package/dist/dashboard/build-info-CYlxvZSQ.js +0 -2
- package/dist/dashboard/cli-ZAS502uC.js +0 -2
- package/dist/dashboard/conversation-service-Dlks_hSS.js +0 -3
- package/dist/dashboard/inspect-agent-DTuSg95i.js +0 -2
- package/dist/dashboard/merge-ops-Bgc-EScO.js +0 -2
- package/dist/dashboard/workflows-CgGNyl1z.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-idle-CAwB-K6M.js","names":["statSync","join","spawnAgentImpl","execAsync","statSync","existsSync","readFileSync"],"sources":["../../src/lib/runtimes/muse.ts","../../src/lib/runtimes/claude-code.ts","../../src/lib/cost-parsers/pi-parser.ts","../../src/lib/runtimes/pi.ts","../../src/lib/runtimes/acp.ts","../../src/lib/runtimes/index.ts","../../src/lib/cloister/agent-idle.ts"],"sourcesContent":["import { materializeMuseContext } from './muse-context.js';\n/** Native Muse Code runtime. Lifecycle operations use the shared delivery and tmux doors. */\nimport { statSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { listAgentStates } from '../agents/queries.js';\nimport { deliverAgentMessage } from '../agents/delivery.js';\nimport { waitForPromptReady } from '../agents/runtime-command.js';\nimport { prepareHarnessLaunch } from '../harness-binary.js';\nimport { generateLauncherScriptSync } from '../launcher-generator.js';\nimport { resolvePtySupervisorScriptPath } from '../channels/pty-supervisor-locate.js';\nimport { writePtyToken } from '../pty-token.js';\nimport { getOverdeckHome } from '../paths.js';\nimport { getPricingSync } from '../cost.js';\nimport { parseMuseSessionSync } from '../cost-parsers/muse-parser.js';\nimport { museSessionId, resolveMuseSessionPath, resolveMuseSessionPathSync } from './muse-session.js';\nimport { getRuntimeBehavior } from './behavior.js';\nimport { tmuxCreateSession, tmuxKillSession, tmuxSessionExists } from './tmux-cli.js';\nimport type { Agent, AgentRuntimeSync, CostBreakdown, Heartbeat, Session, SpawnConfig } from './types.js';\n\nexport class MuseRuntimeSync implements AgentRuntimeSync {\n readonly name = 'muse' as const;\n getHarnessBehavior() { return getRuntimeBehavior(this.name); }\n getSessionPath(agentId: string) { return resolveMuseSessionPathSync(agentId); }\n getLastActivity(agentId: string): Date | null {\n const path = this.getSessionPath(agentId);\n try { return path ? statSync(path).mtime : null; } catch { return null; }\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n const timestamp = this.getLastActivity(agentId);\n return timestamp ? { timestamp, agentId, source: 'jsonl', confidence: 'medium' } : null;\n }\n getTokenUsage(agentId: string) {\n const path = this.getSessionPath(agentId);\n return path ? parseMuseSessionSync(path)?.usage ?? null : null;\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n const path = this.getSessionPath(agentId);\n const session = path ? parseMuseSessionSync(path) : null;\n if (!session) return null;\n const pricing = getPricingSync('custom', session.model);\n if (!pricing) return null;\n return {\n inputCost: session.usage.inputTokens * pricing.inputPer1k / 1000,\n outputCost: session.usage.outputTokens * pricing.outputPer1k / 1000,\n cacheReadCost: (session.usage.cacheReadTokens ?? 0) * (pricing.cacheReadPer1k ?? 0) / 1000,\n cacheWriteCost: 0, totalCost: session.cost, currency: 'USD',\n };\n }\n async sendMessage(agentId: string, message: string): Promise<void> {\n const result = await deliverAgentMessage(agentId, message, 'muse-runtime');\n if (!result.ok) throw new Error(result.failure ?? 'Muse message delivery failed');\n }\n async killAgent(agentId: string): Promise<void> { await tmuxKillSession(agentId); }\n async isRunning(agentId: string): Promise<boolean> { return tmuxSessionExists(agentId); }\n async spawnAgent(config: SpawnConfig): Promise<Agent> {\n if (!config.model) throw new Error('Muse requires an explicitly configured model');\n if (await this.isRunning(config.agentId)) throw new Error(`Agent ${config.agentId} is already running`);\n const launch = await prepareHarnessLaunch('muse');\n const dir = join(getOverdeckHome(), 'agents', config.agentId);\n await mkdir(dir, { recursive: true });\n await writePtyToken(config.agentId);\n const launcher = join(dir, 'launcher.sh');\n const script = generateLauncherScriptSync({\n role: 'work', workingDir: config.workspace, harness: 'muse', museModel: config.model, museEffort: config.effort,\n museContextFile: await materializeMuseContext(config.agentId, config.workspace),\n museResumeSessionId: config.sessionId, overdeckEnv: { agentId: config.agentId },\n extraEnvExports: [launch.pathExport], useSupervisor: true,\n supervisorScriptPath: resolvePtySupervisorScriptPath(),\n });\n await writeFile(launcher, script, { mode: 0o700 });\n await tmuxCreateSession(config.agentId, config.workspace, `bash '${launcher.replace(/'/g, \"'\\\\''\")}'`, config.env);\n try {\n if (!await waitForPromptReady(config.agentId, 'muse', 60)) throw new Error('Muse startup timed out');\n const path = await resolveMuseSessionPath(config.agentId);\n if (!path) throw new Error('Muse started without a durable session log');\n if (config.prompt) await this.sendMessage(config.agentId, config.prompt);\n return { id: config.agentId, sessionId: museSessionId(path), runtime: 'muse',\n model: config.model, workspace: config.workspace, startedAt: new Date() };\n } catch (error) {\n await this.killAgent(config.agentId);\n throw error;\n }\n }\n listSessions(workspace?: string): Session[] {\n return listAgentStates().filter(state => state.harness === 'muse' && (!workspace || state.workspace === workspace)).flatMap(state => {\n const path = this.getSessionPath(state.id);\n const lastActivity = this.getLastActivity(state.id);\n if (!path || !lastActivity) return [];\n return [{ id: museSessionId(path), agentId: state.id, workspace: state.workspace,\n model: state.model, startedAt: new Date(state.startedAt), lastActivity,\n tokenUsage: this.getTokenUsage(state.id) ?? { inputTokens: 0, outputTokens: 0 } }];\n });\n }\n}\n\nexport function createMuseRuntimeSync(): MuseRuntimeSync { return new MuseRuntimeSync(); }\n","/**\n * Claude Code Runtime Implementation\n *\n * Implements AgentRuntime for Claude Code CLI.\n *\n * Session storage: ~/.claude/projects/<workspace-hash>/<session-id>.jsonl\n * Session index: ~/.claude/projects/<workspace-hash>/sessions-index.json\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { Effect } from 'effect';\nimport type {\n AgentRuntime,\n AgentRuntimeSync,\n AgentRuntimeError,\n HarnessBehavior,\n Heartbeat,\n TokenUsage,\n CostBreakdown,\n Session,\n SpawnConfig,\n Agent,\n ActivitySource,\n} from './types.js';\nimport { CLAUDE_CODE_BEHAVIOR } from './behavior.js';\nimport { getAgentStateSync, getAgentDir, spawnAgent as spawnAgentImpl, saveAgentStateSync, saveAgentRuntimeState, determineModel } from '../agents.js';\nimport { sessionExistsSync, killSessionSync, sendKeys, getAgentSessionsSync } from '../tmux.js';\nimport { parseClaudeSessionSync, getSessionFilesSync, getProjectDirsSync } from '../cost-parsers/jsonl-parser.js';\nimport { ProcessSpawnError, TmuxError, FsError } from '../errors.js';\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');\n\n/**\n * Claude Code session index entry\n */\ninterface SessionIndexEntry {\n sessionId: string;\n createdAt: string;\n updatedAt: string;\n filePath: string;\n}\n\n/**\n * Claude Code Runtime implementation\n */\nexport class ClaudeCodeRuntimeSync implements AgentRuntimeSync {\n readonly name = 'claude-code' as const;\n\n getHarnessBehavior(): HarnessBehavior {\n return CLAUDE_CODE_BEHAVIOR;\n }\n\n /**\n * Get the project directory for a workspace\n *\n * Claude Code hashes the workspace path to create project directories.\n * We need to find the project directory that contains sessions for this workspace.\n */\n private getProjectDirForWorkspace(workspace: string): string | null {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n return null;\n }\n\n // Get all project directories\n const projectDirs = getProjectDirsSync();\n\n for (const projectDir of projectDirs) {\n // Check if this project's sessions-index.json references the workspace\n const indexPath = join(projectDir, 'sessions-index.json');\n if (existsSync(indexPath)) {\n try {\n const indexContent = readFileSync(indexPath, 'utf-8');\n // Sessions index contains the workspace path\n if (indexContent.includes(workspace)) {\n return projectDir;\n }\n } catch {\n // Skip invalid index files\n }\n }\n }\n\n return null;\n }\n\n /**\n * Get the active session ID for an agent from the sessions index\n */\n private getActiveSessionId(projectDir: string): string | null {\n const indexPath = join(projectDir, 'sessions-index.json');\n if (!existsSync(indexPath)) {\n return null;\n }\n\n try {\n const indexContent = readFileSync(indexPath, 'utf-8');\n const index = JSON.parse(indexContent);\n\n // The sessions-index.json has a structure like:\n // { \"sessions\": [{ \"sessionId\": \"...\", \"filePath\": \"...\", ... }] }\n // Find the most recent session\n if (index.sessions && Array.isArray(index.sessions)) {\n const sessions = index.sessions as SessionIndexEntry[];\n if (sessions.length === 0) return null;\n\n // Sort by updatedAt and get the most recent\n const sorted = sessions.sort((a, b) => {\n return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();\n });\n\n return sorted[0].sessionId;\n }\n } catch {\n // Skip invalid index files\n }\n\n return null;\n }\n\n /**\n * Get the most recent JSONL file for a project\n */\n private getMostRecentJSONL(projectDir: string): string | null {\n const files = getSessionFilesSync(projectDir);\n return files.length > 0 ? files[0] : null;\n }\n\n /**\n * Get the session path for an agent\n */\n getSessionPath(agentId: string): string | null {\n const state = getAgentStateSync(agentId);\n if (!state) {\n return null;\n }\n\n const projectDir = this.getProjectDirForWorkspace(state.workspace);\n if (!projectDir) {\n return null;\n }\n\n // Try to get active session from index\n const sessionId = this.getActiveSessionId(projectDir);\n if (sessionId) {\n const sessionPath = join(projectDir, `${sessionId}.jsonl`);\n if (existsSync(sessionPath)) {\n return sessionPath;\n }\n }\n\n // Fall back to most recent JSONL file\n return this.getMostRecentJSONL(projectDir);\n }\n\n /**\n * Get last activity timestamp for an agent\n *\n * Uses passive detection via JSONL file modification time.\n */\n getLastActivity(agentId: string): Date | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath || !existsSync(sessionPath)) {\n return null;\n }\n\n try {\n const stat = statSync(sessionPath);\n return stat.mtime;\n } catch {\n return null;\n }\n }\n\n /**\n * Read active heartbeat file if it exists\n */\n private getActiveHeartbeat(agentId: string): Heartbeat | null {\n // Heartbeats are now in shared directory: ~/.overdeck/heartbeats/\n const heartbeatPath = join(homedir(), '.overdeck', 'heartbeats', `${agentId}.json`);\n if (!existsSync(heartbeatPath)) {\n return null;\n }\n\n try {\n const content = readFileSync(heartbeatPath, 'utf-8');\n const data = JSON.parse(content);\n\n // Check if heartbeat is recent (within 5 minutes)\n const timestamp = new Date(data.timestamp);\n const now = new Date();\n const ageMs = now.getTime() - timestamp.getTime();\n if (ageMs > 5 * 60 * 1000) {\n // Heartbeat is stale\n return null;\n }\n\n return {\n timestamp,\n agentId: data.agent_id || agentId,\n source: 'active-heartbeat',\n confidence: 'high',\n toolName: data.tool_name,\n lastAction: data.last_action,\n currentTask: data.current_task,\n gitBranch: data.git_branch,\n workspace: data.workspace,\n pid: data.pid,\n sessionId: data.session_id,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get passive heartbeat from file timestamps\n */\n private getPassiveHeartbeat(agentId: string): Heartbeat | null {\n const lastActivity = this.getLastActivity(agentId);\n if (!lastActivity) {\n return null;\n }\n\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n };\n }\n\n /**\n * Get heartbeat for an agent\n *\n * Tries active heartbeat first (if hooks configured), falls back to passive.\n */\n getHeartbeat(agentId: string): Heartbeat | null {\n // Try active heartbeat first\n const activeHeartbeat = this.getActiveHeartbeat(agentId);\n if (activeHeartbeat) {\n return activeHeartbeat;\n }\n\n // Fall back to passive detection\n return this.getPassiveHeartbeat(agentId);\n }\n\n /**\n * Get token usage for an agent's current session\n */\n getTokenUsage(agentId: string): TokenUsage | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionPath);\n if (!sessionUsage) {\n return null;\n }\n\n return sessionUsage.usage;\n }\n\n /**\n * Get cost breakdown for an agent's current session\n */\n getSessionCost(agentId: string): CostBreakdown | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionPath);\n if (!sessionUsage) {\n return null;\n }\n\n // Calculate breakdown based on token usage\n // Prices for Claude Sonnet 4 (most common)\n // TODO: Use actual model pricing from session\n const inputPrice = 3.0 / 1_000_000; // $3 per 1M input tokens\n const outputPrice = 15.0 / 1_000_000; // $15 per 1M output tokens\n const cacheReadPrice = 0.3 / 1_000_000; // $0.30 per 1M cache read tokens\n const cacheWritePrice = 3.75 / 1_000_000; // $3.75 per 1M cache write tokens\n\n const usage = sessionUsage.usage;\n const inputCost = usage.inputTokens * inputPrice;\n const outputCost = usage.outputTokens * outputPrice;\n const cacheReadCost = (usage.cacheReadTokens || 0) * cacheReadPrice;\n const cacheWriteCost = (usage.cacheWriteTokens || 0) * cacheWritePrice;\n\n return {\n inputCost,\n outputCost,\n cacheReadCost,\n cacheWriteCost,\n totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,\n currency: 'USD',\n };\n }\n\n /**\n * Send a message to a running agent\n */\n async sendMessage(agentId: string, message: string): Promise<void> {\n if (!sessionExistsSync(agentId)) {\n throw new Error(`Agent ${agentId} is not running`);\n }\n\n await Effect.runPromise(sendKeys(agentId, message));\n\n // Also save to mail queue for persistence\n const mailDir = join(getAgentDir(agentId), 'mail');\n mkdirSync(mailDir, { recursive: true });\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n writeFileSync(\n join(mailDir, `${timestamp}.md`),\n `# Message\\n\\n${message}\\n`\n );\n }\n\n /**\n * Kill an agent\n */\n killAgent(agentId: string): void {\n if (!sessionExistsSync(agentId)) {\n throw new Error(`Agent ${agentId} is not running`);\n }\n\n killSessionSync(agentId);\n\n // Reset runtime state so deacon / merge-agent busy-wait don't see a phantom active session\n saveAgentRuntimeState(agentId, { state: 'idle', lastActivity: new Date().toISOString() });\n\n // Update agent state\n const state = getAgentStateSync(agentId);\n if (state) {\n state.status = 'stopped';\n saveAgentStateSync(state);\n }\n }\n\n /**\n * Spawn a new agent\n */\n async spawnAgent(config: SpawnConfig): Promise<Agent> {\n // Use the existing spawnAgent implementation from agents.ts\n const state = await spawnAgentImpl({\n issueId: config.agentId.replace(/^agent-/, ''),\n workspace: config.workspace,\n harness: 'claude-code',\n model: determineModel({ model: config.model, role: 'work', spawnKey: `work:${config.agentId.replace(/^agent-/, '')}` }),\n role: 'work',\n prompt: config.prompt,\n startedBy: config.env?.['OVERDECK_AGENT_STARTED_BY'] ?? 'runtime:claude-code',\n });\n\n // Get the session ID (we'll need to look it up from the workspace)\n const projectDir = this.getProjectDirForWorkspace(config.workspace);\n const sessionId = projectDir ? this.getActiveSessionId(projectDir) : undefined;\n\n return {\n id: state.id,\n sessionId: sessionId || 'unknown',\n runtime: 'claude-code',\n model: state.model,\n workspace: state.workspace,\n startedAt: new Date(state.startedAt),\n };\n }\n\n /**\n * List all sessions for this runtime\n */\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = [];\n\n if (workspace) {\n // Get sessions for specific workspace\n const projectDir = this.getProjectDirForWorkspace(workspace);\n if (projectDir) {\n const files = getSessionFilesSync(projectDir);\n for (const file of files) {\n const session = this.parseSessionFile(file, workspace);\n if (session) {\n sessions.push(session);\n }\n }\n }\n } else {\n // Get all sessions\n const projectDirs = getProjectDirsSync();\n for (const projectDir of projectDirs) {\n const files = getSessionFilesSync(projectDir);\n for (const file of files) {\n const session = this.parseSessionFile(file);\n if (session) {\n sessions.push(session);\n }\n }\n }\n }\n\n return sessions;\n }\n\n /**\n * Parse a session file into a Session object\n */\n private parseSessionFile(file: string, workspace?: string): Session | null {\n const sessionUsage = parseClaudeSessionSync(file);\n if (!sessionUsage) {\n return null;\n }\n\n const stat = statSync(file);\n\n return {\n id: sessionUsage.sessionId,\n agentId: 'unknown', // We'd need to reverse-lookup from agent state\n workspace: workspace || 'unknown',\n model: sessionUsage.model,\n startedAt: new Date(sessionUsage.startTime),\n lastActivity: stat.mtime,\n tokenUsage: sessionUsage.usage,\n };\n }\n\n /**\n * Check if an agent is running\n */\n isRunning(agentId: string): boolean {\n return sessionExistsSync(agentId);\n }\n}\n\n/**\n * Create a Claude Code runtime instance\n */\nexport function createClaudeCodeRuntimeSync(): ClaudeCodeRuntimeSync {\n return new ClaudeCodeRuntimeSync();\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel adapter wrapping the legacy ClaudeCodeRuntime. The\n// promise/sync class above remains the canonical implementation used by\n// Cloister and the dashboard; this adapter is for new Effect-native callers.\n\n/**\n * Effect-channel variant of {@link ClaudeCodeRuntimeSync}. Lifts the async\n * send/kill/spawn methods into typed Effect channels (TmuxError /\n * ProcessSpawnError) while keeping sync introspection methods sync.\n */\nexport class ClaudeCodeRuntime implements AgentRuntime {\n readonly name = 'claude-code' as const;\n private readonly inner: ClaudeCodeRuntimeSync;\n\n constructor(inner: ClaudeCodeRuntimeSync = new ClaudeCodeRuntimeSync()) {\n this.inner = inner;\n }\n\n getSessionPath(agentId: string): string | null {\n return this.inner.getSessionPath(agentId);\n }\n getHarnessBehavior(): HarnessBehavior {\n return this.inner.getHarnessBehavior();\n }\n getLastActivity(agentId: string): Date | null {\n return this.inner.getLastActivity(agentId);\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n return this.inner.getHeartbeat(agentId);\n }\n getTokenUsage(agentId: string): TokenUsage | null {\n return this.inner.getTokenUsage(agentId);\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n return this.inner.getSessionCost(agentId);\n }\n listSessions(workspace?: string): Session[] {\n return this.inner.listSessions(workspace);\n }\n\n sendMessage(agentId: string, message: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.sendMessage(agentId, message),\n catch: (cause) =>\n new TmuxError({\n command: 'send-keys',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n killAgent(agentId: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.try({\n try: () => this.inner.killAgent(agentId),\n catch: (cause) =>\n new TmuxError({\n command: 'kill-session',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n spawnAgent(config: SpawnConfig): Effect.Effect<Agent, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.spawnAgent(config),\n catch: (cause) =>\n new ProcessSpawnError({\n command: 'claude',\n args: [],\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n isRunning(agentId: string): Effect.Effect<boolean> {\n return Effect.sync(() => this.inner.isRunning(agentId));\n }\n}\n\n/** Effect-flavored constructor companion to {@link createClaudeCodeRuntimeSync}. */\nexport function createClaudeCodeRuntime(): ClaudeCodeRuntime {\n return new ClaudeCodeRuntime();\n}\n","/**\n * Pi Coding Agent JSONL parser (PAN-636).\n *\n * Pi sessions live at:\n * ~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<session-id>.jsonl\n *\n * Format (Pi v3):\n * - First line: { type: \"session\", version: 3, id, timestamp, cwd }\n * - Subsequent: every entry has an opaque `id` and a `parentId`. parentId\n * is null for the first non-session entry.\n * - The graph is a tree (Pi supports forking/branching), not a list.\n *\n * To compute usage for the *active* branch we walk leaf -> root via parentId\n * from the latest leaf (max timestamp among entries that have no children).\n * Pi assistant messages report per-call usage — input/output tokens are NOT\n * cumulative across the conversation — so summing along the active branch is\n * correct without subtracting pre-compaction input.\n *\n * SessionUsage shape comes from jsonl-parser.ts (the Claude Code parser); we\n * map into it unchanged so all upstream consumers keep working.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { Effect } from 'effect';\nimport type { SessionUsage } from './jsonl-parser.js';\nimport type { TokenUsage } from '../cost.js';\nimport { FsError } from '../errors.js';\n\n// Minimal entry shape we care about. Anything else is ignored.\ninterface PiSessionRoot {\n type: 'session';\n version: number;\n id: string;\n timestamp: string;\n cwd?: string;\n}\n\ninterface PiUsage {\n input?: number;\n output?: number;\n cacheRead?: number;\n cacheWrite?: number;\n totalTokens?: number;\n cost?: {\n input?: number;\n output?: number;\n cacheRead?: number;\n cacheWrite?: number;\n total?: number;\n };\n}\n\ninterface PiMessageEntry {\n type: 'message';\n id: string;\n parentId: string | null;\n timestamp: string;\n message: {\n role: 'user' | 'assistant' | 'toolResult';\n content?: unknown;\n model?: string;\n provider?: string;\n usage?: PiUsage;\n };\n}\n\ninterface PiGenericEntry {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n // any other fields permitted; we only inspect them when type is known.\n [key: string]: unknown;\n}\n\ntype PiEntry = PiMessageEntry | PiGenericEntry;\n\nfunction isMessage(entry: PiEntry): entry is PiMessageEntry {\n return entry.type === 'message';\n}\n\nfunction isAssistantMessage(entry: PiEntry): entry is PiMessageEntry {\n return isMessage(entry) && entry.message?.role === 'assistant';\n}\n\nfunction tsMs(timestamp: string | undefined): number {\n if (!timestamp) return 0;\n const t = Date.parse(timestamp);\n return Number.isFinite(t) ? t : 0;\n}\n\ninterface ParseResult {\n ok: boolean;\n reason?: string;\n usage?: SessionUsage;\n}\n\n// Known top-level entry types. Anything else is logged once per session.\nconst KNOWN_TYPES: ReadonlySet<string> = new Set([\n 'session',\n 'message',\n 'model_change',\n 'thinking_level_change',\n 'compaction_start',\n 'compaction_end',\n 'session_before_compact',\n 'session_compact',\n 'session_before_switch',\n 'session_before_fork',\n 'session_start',\n]);\n\n/**\n * Parse a Pi session JSONL file into a SessionUsage record.\n *\n * Returns null if the file does not exist, is empty, or is unparseable. Errors\n * during line parsing are tolerated (the line is skipped); we never throw.\n */\nexport function parsePiSessionSync(filePath: string): SessionUsage | null {\n if (!existsSync(filePath)) return null;\n const result = parsePiSessionContent(readFileSync(filePath, 'utf8'), filePath);\n return result.ok ? result.usage! : null;\n}\n\n/**\n * Parse Pi session JSONL content. Exposed for tests and callers that already\n * have the file content in memory.\n */\nexport function parsePiSessionContent(content: string, filePath = '<inline>'): ParseResult {\n const lines = content.split('\\n').filter(l => l.trim().length > 0);\n if (lines.length === 0) {\n return { ok: false, reason: 'empty' };\n }\n\n let root: PiSessionRoot | null = null;\n const entriesById = new Map<string, PiEntry>();\n const childrenOf = new Map<string | null, string[]>();\n const unknownTypesSeen = new Set<string>();\n\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== 'object') continue;\n const entry = parsed as PiEntry;\n\n if (entry.type === 'session') {\n root = entry as unknown as PiSessionRoot;\n continue;\n }\n\n if (!entry.id) continue;\n\n if (!KNOWN_TYPES.has(entry.type)) {\n if (!unknownTypesSeen.has(entry.type)) {\n unknownTypesSeen.add(entry.type);\n // eslint-disable-next-line no-console\n console.debug(`[pi-parser] unknown entry type \"${entry.type}\" in ${filePath} — ignoring`);\n }\n // Still track the entry in the graph so downstream branches stay linked.\n }\n\n entriesById.set(entry.id, entry);\n const list = childrenOf.get(entry.parentId ?? null);\n if (list) list.push(entry.id);\n else childrenOf.set(entry.parentId ?? null, [entry.id]);\n }\n\n if (!root) {\n return { ok: false, reason: 'missing-session-root' };\n }\n\n if (entriesById.size === 0) {\n // Session with only the root entry — return a minimal SessionUsage.\n return {\n ok: true,\n usage: emptyUsage(filePath, root),\n };\n }\n\n // Find leaves: entries that have no children.\n const leaves: PiEntry[] = [];\n for (const entry of entriesById.values()) {\n const kids = childrenOf.get(entry.id);\n if (!kids || kids.length === 0) leaves.push(entry);\n }\n if (leaves.length === 0) {\n // Cycle or all entries have children — fall back to \"pick latest entry\".\n leaves.push(...entriesById.values());\n }\n\n // Pick the latest leaf by timestamp; tie-break by file order via id appearance.\n let latestLeaf: PiEntry = leaves[0]!;\n for (const leaf of leaves) {\n if (tsMs(leaf.timestamp) > tsMs(latestLeaf.timestamp)) {\n latestLeaf = leaf;\n }\n }\n\n // Walk leaf -> root via parentId, collecting entries in reverse-chronological order.\n const branch: PiEntry[] = [];\n const visited = new Set<string>();\n let cursor: PiEntry | undefined = latestLeaf;\n while (cursor) {\n if (visited.has(cursor.id)) break; // cycle guard\n visited.add(cursor.id);\n branch.push(cursor);\n if (!cursor.parentId) break;\n cursor = entriesById.get(cursor.parentId);\n }\n branch.reverse(); // chronological\n\n // Aggregate usage and per-model breakdown across assistant messages on the\n // active branch.\n const totals: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n let costV2 = 0;\n let messageCount = 0;\n let costRecomputedFromTokens = 0;\n const modelBreakdown: Record<\n string,\n { cost: number; inputTokens: number; outputTokens: number; messageCount: number }\n > = {};\n const modelsInOrder: string[] = [];\n let compactionCount = 0;\n\n let firstTs: string | null = null;\n let lastTs: string | null = null;\n\n for (const entry of branch) {\n if (entry.type === 'compaction_start' || entry.type === 'session_compact') {\n compactionCount += 1;\n }\n if (!firstTs && entry.timestamp) firstTs = entry.timestamp;\n if (entry.timestamp) lastTs = entry.timestamp;\n\n if (!isAssistantMessage(entry)) continue;\n const usage = entry.message.usage;\n if (!usage) continue;\n const model = entry.message.model || 'unknown';\n const input = usage.input ?? 0;\n const output = usage.output ?? 0;\n const cacheRead = usage.cacheRead ?? 0;\n const cacheWrite = usage.cacheWrite ?? 0;\n const cost = usage.cost?.total ?? 0;\n const localComputed =\n (usage.cost?.input ?? 0) +\n (usage.cost?.output ?? 0) +\n (usage.cost?.cacheRead ?? 0) +\n (usage.cost?.cacheWrite ?? 0);\n\n totals.inputTokens += input;\n totals.outputTokens += output;\n totals.cacheReadTokens = (totals.cacheReadTokens ?? 0) + cacheRead;\n totals.cacheWriteTokens = (totals.cacheWriteTokens ?? 0) + cacheWrite;\n costV2 += cost;\n costRecomputedFromTokens += localComputed;\n messageCount += 1;\n\n if (!modelBreakdown[model]) {\n modelBreakdown[model] = { cost: 0, inputTokens: 0, outputTokens: 0, messageCount: 0 };\n modelsInOrder.push(model);\n }\n const slot = modelBreakdown[model]!;\n slot.cost += cost;\n slot.inputTokens += input;\n slot.outputTokens += output;\n slot.messageCount += 1;\n }\n\n // Observability: log Pi-inline-cost vs locally-recomputed-from-cost-fields\n // delta. This catches cases where Pi's pricing tables drift inside Pi\n // itself; we always trust costV2 (Pi's inline total).\n if (Math.abs(costV2 - costRecomputedFromTokens) > 1e-9) {\n // eslint-disable-next-line no-console\n console.debug(\n `[pi-parser] inline cost vs sum-of-line-items delta=${(costV2 - costRecomputedFromTokens).toFixed(6)} in ${filePath}`,\n );\n }\n if (compactionCount > 0) {\n // eslint-disable-next-line no-console\n console.debug(\n `[pi-parser] active branch in ${filePath} has ${compactionCount} compaction event(s) — usage summed per-message (no double-count)`,\n );\n }\n\n const display =\n modelsInOrder.length === 0\n ? 'unknown'\n : modelsInOrder.length === 1\n ? modelsInOrder[0]!\n : modelsInOrder.join(' → ');\n\n return {\n ok: true,\n usage: {\n sessionId: root.id,\n sessionFile: filePath,\n startTime: firstTs ?? root.timestamp,\n endTime: lastTs ?? root.timestamp,\n model: display,\n usage: totals,\n cost: costV2,\n cost_v2: costV2,\n messageCount,\n modelBreakdown,\n },\n };\n}\n\nfunction emptyUsage(filePath: string, root: PiSessionRoot): SessionUsage {\n return {\n sessionId: root.id,\n sessionFile: filePath,\n startTime: root.timestamp,\n endTime: root.timestamp,\n model: 'unknown',\n usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },\n cost: 0,\n cost_v2: 0,\n messageCount: 0,\n modelBreakdown: {},\n };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** Effect variant of parsePiSession. */\nexport const parsePiSession = (\n filePath: string,\n): Effect.Effect<SessionUsage | null, FsError> =>\n Effect.try({\n try: () => parsePiSessionSync(filePath),\n catch: (cause) => new FsError({ path: filePath, operation: 'parsePiSession', cause }),\n });\n","/**\n * Pi Coding Agent runtime adapter (PAN-636).\n *\n * Implements AgentRuntime for the `pi` binary running in --mode rpc. The\n * adapter wires together three pieces:\n *\n * - pi-fifo.ts — per-agent rpc.in named pipe lifecycle\n * - launcher-generator — emits the `exec pi --mode rpc ... < <fifo>` script\n * - pi-extension — the vendored extension that writes ready.json,\n * heartbeats, and /pan-done markers from inside Pi\n *\n * Spawn flow:\n * 1. mkdir agent dir\n * 2. createPiFifo\n * 3. write launcher.sh via generateLauncherScript({ harness: 'pi', ... })\n * 4. tmux new-session running launcher.sh\n * 5. wait for ~/.overdeck/agents/<id>/ready.json (max 30s)\n *\n * Heartbeat sources (in priority order):\n * 1. fresh (<60s) ~/.overdeck/heartbeats/<id>.json — written by the\n * extension on tool_execution_end / turn_end\n * 2. mtime of the active session JSONL under <agentDir>/sessions/\n * 3. tmux pane activity timestamp\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { promisify } from 'node:util'\nimport { exec } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { Effect } from 'effect'\nimport type {\n AgentRuntime,\n AgentRuntimeSync,\n AgentRuntimeError,\n HarnessBehavior,\n Heartbeat,\n TokenUsage,\n CostBreakdown,\n Session,\n SpawnConfig,\n Agent,\n} from './types.js'\nimport { OHMYPI_BEHAVIOR } from './behavior.js'\nimport { sessionExists, killSession, createSession, listSessionsSync, getManagedTmuxSocketName } from '../tmux.js'\nimport { parsePiSessionSync } from '../cost-parsers/pi-parser.js'\nimport { prepareHarnessLaunch } from '../harness-binary.js'\nimport { generateLauncherScriptSync } from '../launcher-generator.js'\nimport { createPiFifo, destroyPiFifoSync, writePiCommandSync, piFifoPaths, PiNotReady } from './pi-fifo.js'\nimport { ProcessSpawnError, ProcessTimeoutError, TmuxError } from '../errors.js'\nimport { getOverdeckHome } from '../paths.js'\n\nconst execAsync = promisify(exec)\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nconst ACTIVE_HEARTBEAT_TTL_MS = 60_000\nconst SPAWN_READY_TIMEOUT_MS = 30_000\n\nfunction overdeckDir(): string {\n return getOverdeckHome()\n}\nfunction heartbeatsDir(): string {\n return join(overdeckDir(), 'heartbeats')\n}\nfunction agentsDir(): string {\n return join(overdeckDir(), 'agents')\n}\n\nexport class PiSpawnTimeout extends Error {\n readonly code = 'PI_SPAWN_TIMEOUT' as const\n constructor(agentId: string) {\n super(`Pi agent ${agentId} did not write ready.json within ${SPAWN_READY_TIMEOUT_MS}ms`)\n this.name = 'PiSpawnTimeout'\n }\n}\n\nexport interface PiSpawnConfig extends SpawnConfig {\n /** Absolute path to packages/pi-extension/dist/index.js. */\n piExtensionPath: string\n /** Optional extra args appended to the `pi` command line. */\n extraPiArgs?: string\n}\n\nfunction agentDirFor(agentId: string): string {\n return join(agentsDir(), agentId)\n}\n\nfunction piSessionDirFor(agentId: string): string {\n return join(agentDirFor(agentId), 'sessions')\n}\n\nfunction readyPathFor(agentId: string): string {\n return piFifoPaths(agentId).readyPath\n}\n\nfunction sessionIdPathFor(agentId: string): string {\n return join(agentDirFor(agentId), 'session.id')\n}\n\n/**\n * Read the persisted Pi session id for resume (PAN-636 workspace-3119).\n * Returns null when the file is absent (fresh agent or post-deep-wipe) or\n * unreadable. The pi-extension writes this file on every session_start with\n * a non-null sessionId; killAgent intentionally preserves it so the next\n * spawn can resume.\n */\nfunction readStoredSessionId(agentId: string): string | null {\n const path = sessionIdPathFor(agentId)\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf8').trim()\n return raw || null\n } catch {\n return null\n }\n}\n\n/**\n * PAN-1988: resolve the REAL pi session id from the freshest session JSONL. The `session.id` file\n * holds a spawn-time placeholder for non-claude harnesses (or can be missing), so resuming from it\n * makes pi drift into a fresh session and lose conversation history — the same bug fixed for codex's\n * rollout. The session JSONL always carries the real id (`parsed.sessionId`). Returns null when\n * there is no prior session on disk. Picks the freshest by mtime, tolerating nested cwd subdirs.\n */\nfunction resolveLatestPiSessionId(agentId: string): string | null {\n const root = piSessionDirFor(agentId)\n if (!existsSync(root)) return null\n const files: { path: string; mtime: number }[] = []\n walkJsonl(root, files)\n if (files.length === 0) return null\n files.sort((a, b) => b.mtime - a.mtime)\n const parsed = parsePiSessionSync(files[0]!.path)\n return parsed?.sessionId ?? null\n}\n\n// PAN-1989: Pi is legacy; no longer implements AgentRuntimeSync (removed from\n// RuntimeName union). Class is kept for backward-compat exports only.\nexport class PiRuntimeSync {\n readonly name: string = 'pi'\n\n getHarnessBehavior(): HarnessBehavior {\n return OHMYPI_BEHAVIOR\n }\n\n /** Resolve the latest Pi session JSONL for an agent. Pi nests files under\n * <agentDir>/sessions/<encoded-cwd>/<timestamp>_<id>.jsonl, but we tolerate\n * any depth and just pick the freshest .jsonl by mtime. */\n getSessionPath(agentId: string): string | null {\n const root = piSessionDirFor(agentId)\n if (!existsSync(root)) return null\n const files: { path: string; mtime: number }[] = []\n walkJsonl(root, files)\n if (files.length === 0) return null\n files.sort((a, b) => b.mtime - a.mtime)\n return files[0]!.path\n }\n\n getLastActivity(agentId: string): Date | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n try {\n return statSync(session).mtime\n } catch {\n return null\n }\n }\n\n getHeartbeat(agentId: string): Heartbeat | null {\n // 1. Fresh active heartbeat (<60s old).\n const heartbeatPath = join(heartbeatsDir(), `${agentId}.json`)\n if (existsSync(heartbeatPath)) {\n try {\n const data = JSON.parse(readFileSync(heartbeatPath, 'utf-8')) as {\n timestamp?: string\n tool_name?: string\n last_action?: string\n pid?: number\n }\n if (data.timestamp) {\n const ts = new Date(data.timestamp)\n if (Date.now() - ts.getTime() < ACTIVE_HEARTBEAT_TTL_MS) {\n return {\n timestamp: ts,\n agentId,\n source: 'active-heartbeat',\n confidence: 'high',\n toolName: data.tool_name,\n lastAction: data.last_action,\n pid: data.pid,\n }\n }\n }\n } catch {\n // fall through\n }\n }\n\n // 2. JSONL mtime.\n const lastActivity = this.getLastActivity(agentId)\n if (lastActivity) {\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n }\n }\n\n // 3. tmux session creation time (best-effort).\n try {\n const sess = listSessionsSync().find(s => s.name === agentId)\n if (sess) {\n return {\n timestamp: sess.created,\n agentId,\n source: 'tmux',\n confidence: 'low',\n }\n }\n } catch {\n // ignore\n }\n return null\n }\n\n getTokenUsage(agentId: string): TokenUsage | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n const parsed = parsePiSessionSync(session)\n return parsed?.usage ?? null\n }\n\n /** Pi reports per-message inline cost.total. We pass that through as the\n * totalCost and back out per-bucket costs from the parser's modelBreakdown\n * is not informative (Pi does not report cache vs input vs output cost\n * separately on a session level), so we leave the per-bucket fields zero. */\n getSessionCost(agentId: string): CostBreakdown | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n const parsed = parsePiSessionSync(session)\n if (!parsed) return null\n const totalCost = parsed.cost_v2 ?? parsed.cost ?? 0\n return {\n inputCost: 0,\n outputCost: 0,\n cacheReadCost: 0,\n cacheWriteCost: 0,\n totalCost,\n currency: 'USD',\n }\n }\n\n async sendMessage(agentId: string, message: string): Promise<void> {\n if (!existsSync(readyPathFor(agentId))) {\n throw new PiNotReady(`Pi agent ${agentId}: ready.json not present yet`)\n }\n // steer: Pi delivers immediately when idle and queues mid-turn; a bare\n // prompt is rejected with AgentBusyError while a run is active.\n writePiCommandSync(agentId, { id: randomUUID(), type: 'prompt', message, streamingBehavior: 'steer' })\n }\n\n /**\n * Kill a Pi agent via the documented escalation ladder (PAN-636 bead 8qco).\n *\n * 1. Send {cmd:'abort'} via the RPC fifo (graceful — Pi can flush\n * session.jsonl and write any pending heartbeat).\n * 2. Wait up to 2s for the tmux session to disappear on its own.\n * 3. SIGTERM the Pi process group via tmux send-signal -s TERM.\n * 4. Wait up to 5s for the tmux session to disappear.\n * 5. tmux kill-session (effectively SIGKILL on the pane).\n * 6. Always unlink rpc.in, ready.json, heartbeat, and completed marker.\n * NEVER touch session JSONL files under <agentDir>/sessions/ — per\n * the JSONL-is-sacred rule in CLAUDE.md.\n */\n async killAgent(agentId: string): Promise<void> {\n // Step 1: graceful RPC abort.\n try {\n writePiCommandSync(agentId, { id: randomUUID(), type: 'abort' })\n } catch {\n // No reader / not ready — fall through to signal escalation.\n }\n\n // Step 2: poll for tmux exit up to 2s.\n if (await pollUntilSessionGone(agentId, 2_000)) {\n cleanupPiTransientFiles(agentId)\n return\n }\n\n // Step 3: SIGTERM via tmux. Best-effort — if it fails the kill-session\n // fallback below will still take the pane down.\n try {\n await execAsync(`tmux -L ${shellQuote(getManagedTmuxSocketName())} send-keys -t ${agentId} C-c 2>/dev/null || true`)\n } catch {\n // ignore\n }\n // PAN-1798: never use cmdline-match kills (pkill -f) here. If the shared\n // tmux server was founded implicitly by this agent/conversation, its\n // cmdline embeds the session name and pkill would destroy the entire\n // server. Resolve the pane PID and signal only that process group.\n try {\n const { stdout } = await execAsync(\n `tmux -L ${shellQuote(getManagedTmuxSocketName())} list-panes -t ${shellQuote(agentId)} -F '#{pane_pid}' 2>/dev/null`\n )\n const pid = stdout.trim()\n if (pid) {\n await execAsync(`kill -TERM -- -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`)\n }\n } catch {\n // ignore\n }\n\n // Step 4: poll for tmux exit up to another 5s (7s total budget).\n if (await pollUntilSessionGone(agentId, 5_000)) {\n cleanupPiTransientFiles(agentId)\n return\n }\n\n // Step 5: SIGKILL fallback via tmux kill-session.\n try {\n if (await Effect.runPromise(sessionExists(agentId))) {\n await Effect.runPromise(killSession(agentId))\n }\n } finally {\n cleanupPiTransientFiles(agentId)\n }\n }\n\n async spawnAgent(config: SpawnConfig & { piExtensionPath?: string }): Promise<Agent> {\n const piExtensionPath = config.piExtensionPath\n if (!piExtensionPath) {\n throw new Error('PiRuntime.spawnAgent requires piExtensionPath in config')\n }\n\n const harnessLaunch = await prepareHarnessLaunch('ohmypi')\n const agentId = config.agentId\n const dir = agentDirFor(agentId)\n const sessionDir = piSessionDirFor(agentId)\n // Detect a prior spawn BEFORE mkdir creates the dir. We treat \"sessions/\n // contains at least one .jsonl\" as the canonical proof of a prior run,\n // since killAgent preserves session jsonls per the JSONL-is-sacred rule.\n const hadPriorSpawn = existsSync(sessionDir) &&\n readdirSync(sessionDir).some((f) => f.endsWith('.jsonl'))\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n mkdirSync(sessionDir, { recursive: true, mode: 0o700 })\n\n // Writer side relies on the fifo existing before tmux starts the launcher.\n const fifoPath = await Effect.runPromise(createPiFifo(agentId))\n\n const promptFile = config.prompt ? writeAgentPromptFile(agentId, config.prompt) : undefined\n\n // Resume into the previously-recorded Pi session if one exists (AC2).\n // AC3: when session.id is missing AFTER a prior spawn (sessions/*.jsonl\n // present) we fall back to a fresh session and warn so the dashboard\n // can surface the divergence. On a first-ever spawn — sessions/ is\n // empty — we stay silent.\n // PAN-1988: resume from the stored `session.id` when present, else recover the REAL session id\n // from the freshest session JSONL rather than spawning fresh and losing history (the old AC3\n // behavior threw the session away whenever the stored id was a spawn-time placeholder or\n // missing). Only when neither yields an id do we start fresh.\n const resumeSessionId = readStoredSessionId(agentId) ?? resolveLatestPiSessionId(agentId) ?? undefined\n if (!resumeSessionId && hadPriorSpawn) {\n console.warn(\n `[pi-runtime] ${agentId}: prior session.jsonl exists but no resumable session id found — spawning a fresh Pi session`,\n )\n }\n\n const launcherScript = generateLauncherScriptSync({\n role: 'work',\n workingDir: config.workspace,\n harness: 'ohmypi',\n piExtensionPath,\n piFifoPath: fifoPath,\n piSessionDir: sessionDir,\n model: config.model,\n promptFile,\n resumeSessionId,\n overdeckEnv: { agentId },\n setTerminalEnv: true,\n extraEnvExports: [harnessLaunch.pathExport],\n trapHup: true,\n })\n\n const launcherPath = join(dir, 'pi-launcher.sh')\n writeFileSync(launcherPath, launcherScript)\n chmodSync(launcherPath, 0o755)\n\n await Effect.runPromise(createSession(agentId, config.workspace, `bash ${launcherPath}`, {\n env: { OVERDECK_AGENT_ID: agentId },\n }))\n\n await waitForReady(agentId, SPAWN_READY_TIMEOUT_MS)\n\n const readyData = readReady(agentId)\n return {\n id: agentId,\n sessionId: readyData?.sessionId ?? 'unknown',\n runtime: 'ohmypi',\n model: config.model ?? 'unknown',\n workspace: config.workspace,\n startedAt: new Date(),\n }\n }\n\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = []\n const root = agentsDir()\n if (!existsSync(root)) return sessions\n for (const agentName of safeReaddir(root)) {\n const sessionRoot = piSessionDirFor(agentName)\n if (!existsSync(sessionRoot)) continue\n const files: { path: string; mtime: number }[] = []\n walkJsonl(sessionRoot, files)\n for (const file of files) {\n const parsed = parsePiSessionSync(file.path)\n if (!parsed) continue\n sessions.push({\n id: parsed.sessionId,\n agentId: agentName,\n workspace: workspace ?? 'unknown',\n model: parsed.model,\n startedAt: new Date(parsed.startTime),\n lastActivity: new Date(file.mtime),\n tokenUsage: parsed.usage,\n })\n }\n }\n return sessions\n }\n\n async isRunning(agentId: string): Promise<boolean> {\n return await Effect.runPromise(sessionExists(agentId))\n }\n}\n\nexport function createPiRuntimeSync(): PiRuntimeSync {\n return new PiRuntimeSync()\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel adapter wrapping the legacy PiRuntime. The class\n// above remains the canonical adapter used by Cloister; this variant is for\n// new Effect-native callers who want typed error channels.\n\n/**\n * Effect-channel variant of {@link PiRuntimeSync}. Lifts async kill/spawn/send\n * methods into typed Effects (ProcessSpawnError / ProcessTimeoutError /\n * TmuxError). PiSpawnTimeout is preserved as the legacy Error class; Effect\n * callers receive ProcessTimeoutError instead.\n */\n// PAN-1989: legacy — does not implement AgentRuntime (see PiRuntimeSync comment above).\nexport class PiRuntime {\n readonly name: string = 'pi'\n private readonly inner: PiRuntimeSync\n\n constructor(inner: PiRuntimeSync = new PiRuntimeSync()) {\n this.inner = inner\n }\n\n getSessionPath(agentId: string): string | null {\n return this.inner.getSessionPath(agentId)\n }\n getHarnessBehavior(): HarnessBehavior {\n return this.inner.getHarnessBehavior()\n }\n getLastActivity(agentId: string): Date | null {\n return this.inner.getLastActivity(agentId)\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n return this.inner.getHeartbeat(agentId)\n }\n getTokenUsage(agentId: string): TokenUsage | null {\n return this.inner.getTokenUsage(agentId)\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n return this.inner.getSessionCost(agentId)\n }\n listSessions(workspace?: string): Session[] {\n return this.inner.listSessions(workspace)\n }\n\n sendMessage(agentId: string, message: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => Promise.resolve(this.inner.sendMessage(agentId, message)),\n catch: (cause) =>\n new TmuxError({\n command: 'rpc-write',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n })\n }\n\n killAgent(agentId: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.killAgent(agentId),\n catch: (cause) =>\n new TmuxError({\n command: 'kill-session',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n })\n }\n\n spawnAgent(config: SpawnConfig): Effect.Effect<Agent, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.spawnAgent(config),\n catch: (cause) => {\n if (cause instanceof PiSpawnTimeout) {\n return new ProcessTimeoutError({\n command: 'pi',\n args: ['--mode', 'rpc'],\n timeoutMs: SPAWN_READY_TIMEOUT_MS,\n })\n }\n return new ProcessSpawnError({\n command: 'pi',\n args: ['--mode', 'rpc'],\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n })\n },\n })\n }\n\n isRunning(agentId: string): Effect.Effect<boolean> {\n return Effect.promise(() => this.inner.isRunning(agentId))\n }\n}\n\n/** Effect-flavored constructor companion to {@link createPiRuntimeSync}. */\nexport function createPiRuntime(): PiRuntime {\n return new PiRuntime()\n}\n\n// ────────────────────────────────────────────────────────────────────────\n// helpers\n// ────────────────────────────────────────────────────────────────────────\n\nasync function pollUntilSessionGone(agentId: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (!(await Effect.runPromise(sessionExists(agentId)))) return true\n await new Promise(r => setTimeout(r, 100))\n }\n return false\n}\n\n/**\n * Remove Pi's transient per-agent files: rpc.in fifo, ready.json,\n * heartbeats/<id>.json, completed marker. Never touches session JSONLs\n * under <agentDir>/sessions/ — those are sacred (CLAUDE.md).\n */\nfunction cleanupPiTransientFiles(agentId: string): void {\n try { destroyPiFifoSync(agentId) } catch { /* ignore */ }\n const dir = agentDirFor(agentId)\n for (const transient of ['ready.json', 'completed']) {\n const path = join(dir, transient)\n if (existsSync(path)) {\n try { unlinkSync(path) } catch { /* ignore */ }\n }\n }\n const heartbeatPath = join(heartbeatsDir(), `${agentId}.json`)\n if (existsSync(heartbeatPath)) {\n try { unlinkSync(heartbeatPath) } catch { /* ignore */ }\n }\n // Sacred: NEVER touch <dir>/sessions/*.jsonl — JSONLs are conversation history.\n}\n\nfunction readReady(agentId: string): { sessionId?: string; reason?: string } | null {\n const path = readyPathFor(agentId)\n if (!existsSync(path)) return null\n try {\n return JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return null\n }\n}\n\nasync function waitForReady(agentId: string, timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs\n const path = readyPathFor(agentId)\n while (Date.now() < deadline) {\n if (existsSync(path)) return\n await new Promise(r => setTimeout(r, 100))\n }\n throw new PiSpawnTimeout(agentId)\n}\n\nfunction writeAgentPromptFile(agentId: string, prompt: string): string {\n const promptPath = join(agentDirFor(agentId), 'prompt.txt')\n writeFileSync(promptPath, prompt)\n return promptPath\n}\n\nfunction safeReaddir(dir: string): string[] {\n try {\n return readdirSync(dir)\n } catch {\n return []\n }\n}\n\nfunction walkJsonl(dir: string, out: { path: string; mtime: number }[]): void {\n let entries: string[]\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const entry of entries) {\n const full = join(dir, entry)\n let stat\n try {\n stat = statSync(full)\n } catch {\n continue\n }\n if (stat.isDirectory()) {\n walkJsonl(full, out)\n } else if (stat.isFile() && entry.endsWith('.jsonl')) {\n out.push({ path: full, mtime: stat.mtime.getTime() })\n }\n }\n}\n\n// Keep imports referenced even when only the types are used.\nvoid execAsync\nvoid tmpdir\n","import { exec } from 'node:child_process'\nimport { getManagedTmuxSocketName } from '../tmux.js';\nimport { existsSync, readFileSync, rmSync, statSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { join } from 'node:path'\nimport { promisify } from 'node:util'\n\nimport type { AgentState } from '../agents/agent-state.js'\nimport { materializeAcpContextFile } from '../acp/context.js'\nimport { listAgentStates } from '../agents/queries.js'\nimport { BRIDGE_TOKEN_HEADER } from '../bridge-token.js'\nimport { prepareHarnessLaunch } from '../harness-binary.js'\nimport { resolveKimiNativeEffort } from '../kimi-effort.js'\nimport { getOverdeckHome, packageRoot } from '../paths.js'\nimport { getRuntimeBehavior } from './behavior.js'\nimport {\n tmuxCreateSession,\n tmuxKillSession,\n tmuxSessionExists,\n} from './tmux-cli.js'\nimport type {\n Agent,\n AgentRuntimeSync,\n CostBreakdown,\n HarnessBehavior,\n Heartbeat,\n Session,\n SpawnConfig,\n TokenUsage,\n} from './types.js'\n\nconst execAsync = promisify(exec)\nconst HOST_REQUEST_TIMEOUT_MS = 5_000\nconst SPAWN_READY_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 100\n\ninterface AcpHostResponse {\n readonly status: number\n readonly body: string\n}\n\nexport interface AcpSpawnConfig extends SpawnConfig {\n readonly provider?: string\n}\n\nexport interface AcpRuntimeOptions {\n readonly name?: 'acp' | 'opencode'\n readonly provider?: string\n readonly overdeckHome?: string\n readonly execCommand?: (command: string) => Promise<{ readonly stdout: string }>\n readonly prepareLaunch?: () => Promise<{ readonly binaryPath: string; readonly pathExport: string }>\n readonly listAgentStates?: () => AgentState[]\n}\n\nexport class AcpSpawnTimeout extends Error {\n readonly code = 'ACP_SPAWN_TIMEOUT' as const\n\n constructor(agentId: string) {\n super(`ACP agent ${agentId} did not write acp-session-id within ${SPAWN_READY_TIMEOUT_MS}ms`)\n this.name = 'AcpSpawnTimeout'\n }\n}\n\nexport class AcpRuntimeSync implements AgentRuntimeSync {\n readonly name: 'acp' | 'opencode'\n private readonly provider: string\n private readonly overdeckHome: string | undefined\n private readonly execCommand: (command: string) => Promise<{ readonly stdout: string }>\n private readonly prepareLaunch: () => Promise<{ readonly binaryPath: string; readonly pathExport: string }>\n private readonly resolveAgentStates: () => AgentState[]\n\n constructor(options: AcpRuntimeOptions = {}) {\n this.name = options.name ?? 'acp'\n this.provider = options.provider ?? (this.name === 'opencode' ? 'opencode' : 'kimi')\n this.overdeckHome = options.overdeckHome\n this.execCommand = options.execCommand ?? execAsync\n this.prepareLaunch = options.prepareLaunch ?? (() => prepareHarnessLaunch(this.name))\n this.resolveAgentStates = options.listAgentStates ?? (() => listAgentStates())\n }\n\n getHarnessBehavior(): HarnessBehavior {\n return getRuntimeBehavior(this.name)\n }\n\n getSessionPath(agentId: string): string {\n return join(this.home(), 'agents', agentId, 'acp-session.jsonl')\n }\n\n getLastActivity(agentId: string): Date | null {\n try {\n return statSync(this.getSessionPath(agentId)).mtime\n } catch {\n return null\n }\n }\n\n getHeartbeat(agentId: string): Heartbeat | null {\n const lastActivity = this.getLastActivity(agentId)\n if (!lastActivity) return null\n\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n }\n }\n\n getTokenUsage(_agentId: string): TokenUsage | null {\n return null\n }\n\n getSessionCost(_agentId: string): CostBreakdown | null {\n return null\n }\n\n async sendMessage(agentId: string, message: string): Promise<void> {\n const socketPath = this.socketPath(agentId)\n if (!existsSync(socketPath)) {\n throw new Error(`ACP agent ${agentId}: host socket is not available`)\n }\n const token = this.readToken(agentId)\n if (!token) {\n throw new Error(`ACP agent ${agentId}: missing acp-token`)\n }\n\n await postUnixSocketJson(socketPath, token, {\n op: 'message',\n content: message,\n }, HOST_REQUEST_TIMEOUT_MS)\n }\n\n async killAgent(agentId: string): Promise<void> {\n const socketPath = this.socketPath(agentId)\n const token = this.readToken(agentId)\n if (existsSync(socketPath) && token) {\n try {\n await postUnixSocketJson(socketPath, token, { op: 'interrupt' }, HOST_REQUEST_TIMEOUT_MS)\n } catch {\n // Best effort: the process signal ladder below still tears down the host.\n }\n }\n\n if (await pollUntilSessionGone(agentId, 2_000)) return\n\n let panePid: string | null = null\n try {\n const { stdout } = await this.execCommand(\n `tmux -L ${shellQuote(getManagedTmuxSocketName())} list-panes -t ${shellQuote(agentId)} -F '#{pane_pid}' 2>/dev/null`,\n )\n panePid = stdout.trim() || null\n if (panePid) {\n await this.execCommand(\n `kill -TERM -- -${panePid} 2>/dev/null || kill -TERM ${panePid} 2>/dev/null || true`,\n )\n }\n } catch {\n // Best effort: the remaining kill ladder still tears down the tmux session.\n }\n\n if (await pollUntilSessionGone(agentId, 5_000)) return\n if (panePid) {\n try {\n await this.execCommand(\n `kill -KILL -- -${panePid} 2>/dev/null || kill -KILL ${panePid} 2>/dev/null || true`,\n )\n } catch {\n // Best effort: tmuxKillSession below removes any remaining session.\n }\n }\n if (await tmuxSessionExists(agentId)) await tmuxKillSession(agentId)\n }\n\n async spawnAgent(config: AcpSpawnConfig): Promise<Agent> {\n if (this.name === 'opencode' && !config.model) throw new Error('OpenCode requires an explicit model')\n const provider = config.provider ?? (this.name === 'opencode' && config.model?.startsWith('opencode-go/') ? 'opencode-go' : this.provider)\n const { binaryPath } = await this.prepareLaunch()\n const contextFile = materializeAcpContextFile(\n join(this.home(), 'agents', config.agentId),\n config.workspace,\n this.name,\n )\n const command = [\n 'node',\n shellQuote(join(packageRoot, 'dist', 'acp-host.js')),\n '--agent',\n shellQuote(config.agentId),\n '--provider',\n shellQuote(provider),\n '--workspace',\n shellQuote(config.workspace),\n '--binary-path',\n shellQuote(binaryPath),\n '--context-file',\n shellQuote(contextFile),\n ]\n if (this.name === 'opencode' && config.effort) command.push('--effort', shellQuote(config.effort))\n if (config.sessionId) command.push('--resume', shellQuote(config.sessionId))\n if (config.model) command.push('--model', shellQuote(config.model))\n if (provider === 'kimi' && config.model) {\n const effort = resolveKimiNativeEffort(config.model, config.effort)\n if (effort) command.push('--effort', shellQuote(effort))\n }\n\n rmSync(this.agentPath(config.agentId, 'acp-session-id'), { force: true })\n rmSync(this.agentPath(config.agentId, 'acp-launch-error'), { force: true })\n\n await tmuxCreateSession(\n config.agentId,\n config.workspace,\n command.join(' '),\n {\n ...config.env,\n OVERDECK_AGENT_ID: config.agentId,\n },\n )\n\n try {\n const sessionId = await this.waitForSessionId(config.agentId)\n if (!sessionId) throw new AcpSpawnTimeout(config.agentId)\n\n if (config.prompt) await this.sendMessage(config.agentId, config.prompt)\n\n return {\n id: config.agentId,\n sessionId,\n runtime: this.name,\n model: config.model ?? '',\n workspace: config.workspace,\n startedAt: new Date(),\n }\n } catch (error) {\n if (await tmuxSessionExists(config.agentId)) await tmuxKillSession(config.agentId)\n throw error\n }\n }\n\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = []\n for (const state of this.resolveAgentStates()) {\n if (state.harness !== this.name) continue\n if (workspace && state.workspace !== workspace) continue\n\n const sessionId = this.readSessionId(state.id)\n if (!sessionId) continue\n\n const sessionPath = this.getSessionPath(state.id)\n let lastActivity: Date\n try {\n lastActivity = statSync(sessionPath).mtime\n } catch {\n try {\n lastActivity = statSync(this.agentPath(state.id, 'acp-session-id')).mtime\n } catch {\n continue\n }\n }\n\n sessions.push({\n id: sessionId,\n agentId: state.id,\n workspace: state.workspace,\n model: state.model ?? '',\n startedAt: new Date(state.startedAt),\n lastActivity,\n tokenUsage: { inputTokens: 0, outputTokens: 0 },\n })\n }\n return sessions\n }\n\n async isRunning(agentId: string): Promise<boolean> {\n return tmuxSessionExists(agentId)\n }\n\n private home(): string {\n return this.overdeckHome ?? getOverdeckHome()\n }\n\n private agentPath(agentId: string, file: string): string {\n return join(this.home(), 'agents', agentId, file)\n }\n\n private socketPath(agentId: string): string {\n return join(this.home(), 'sockets', `acp-${agentId}.sock`)\n }\n\n private readToken(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-token'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private readLaunchError(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-launch-error'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private readSessionId(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-session-id'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private async waitForSessionId(agentId: string): Promise<string | null> {\n const deadline = Date.now() + SPAWN_READY_TIMEOUT_MS\n while (Date.now() < deadline) {\n const launchError = this.readLaunchError(agentId)\n if (launchError) throw new Error(`ACP host ${agentId} failed to start: ${launchError}`)\n const sessionId = this.readSessionId(agentId)\n if (sessionId) return sessionId\n await delay(POLL_INTERVAL_MS)\n }\n return null\n }\n}\n\nasync function pollUntilSessionGone(agentId: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (!(await tmuxSessionExists(agentId))) return true\n await delay(POLL_INTERVAL_MS)\n }\n return false\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nfunction postUnixSocketJson(\n socketPath: string,\n token: string,\n body: unknown,\n timeoutMs?: number,\n): Promise<AcpHostResponse> {\n return new Promise((resolve, reject) => {\n let settled = false\n let timeout: NodeJS.Timeout | undefined\n const finish = (callback: () => void) => {\n if (settled) return\n settled = true\n if (timeout) clearTimeout(timeout)\n callback()\n }\n const client = request(\n {\n socketPath,\n path: '/',\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n [BRIDGE_TOKEN_HEADER]: token,\n },\n },\n (response) => {\n const chunks: Buffer[] = []\n response.on('data', (chunk: Buffer) => chunks.push(chunk))\n response.on('end', () => {\n const responseBody = Buffer.concat(chunks).toString('utf8')\n const status = response.statusCode ?? 0\n if (status < 200 || status >= 300) {\n finish(() => reject(new Error(`ACP host returned HTTP ${status}: ${responseBody}`)))\n return\n }\n finish(() => resolve({ status, body: responseBody }))\n })\n },\n )\n if (timeoutMs !== undefined) {\n timeout = setTimeout(() => {\n client.destroy(new Error(`ACP host request timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n timeout.unref?.()\n }\n client.once('error', (error) => finish(() => reject(error)))\n client.end(JSON.stringify(body))\n })\n}\n\nexport function createAcpRuntimeSync(options: AcpRuntimeOptions = {}): AcpRuntimeSync {\n return new AcpRuntimeSync(options)\n}\n","import { createMuseRuntimeSync } from './muse.js';\nexport { MuseRuntimeSync, createMuseRuntimeSync } from './muse.js';\n/**\n * Cloister Runtime Abstraction\n *\n * Export all runtime types and provide a unified registry for managing\n * multiple AI coding assistant runtimes.\n */\n\nexport * from './types.js';\nexport * from './behavior.js';\nexport {\n ClaudeCodeRuntimeSync,\n ClaudeCodeRuntime,\n createClaudeCodeRuntimeSync,\n createClaudeCodeRuntime,\n} from './claude-code.js';\nexport {\n PiRuntimeSync,\n PiRuntime,\n createPiRuntimeSync,\n createPiRuntime,\n PiSpawnTimeout,\n} from './pi.js';\nexport {\n OhmypiRuntimeSync,\n OhmypiRuntime,\n createOhmypiRuntimeSync,\n createOhmypiRuntime,\n OhmypiSpawnTimeout,\n} from './ohmypi.js';\nexport {\n CodexRuntimeSync,\n CodexRuntime,\n createCodexRuntimeSync,\n createCodexRuntime,\n} from './codex.js';\nexport {\n AcpRuntimeSync,\n createAcpRuntimeSync,\n AcpSpawnTimeout,\n} from './acp.js';\nexport {\n KimiCodeRuntimeSync,\n createKimiCodeRuntimeSync,\n KimiCodeSpawnTimeout,\n} from './kimi-code.js';\n\nimport type {\n AgentRuntimeSync,\n RuntimeName,\n RuntimeRegistry as RuntimeRegistryInterface,\n} from './types.js';\nimport { getAgentStateSync } from '../agents.js';\nimport { createClaudeCodeRuntimeSync } from './claude-code.js';\nimport { createPiRuntimeSync } from './pi.js';\nimport { createOhmypiRuntimeSync } from './ohmypi.js';\nimport { createCodexRuntimeSync } from './codex.js';\nimport { createAcpRuntimeSync } from './acp.js';\nimport { createKimiCodeRuntimeSync } from './kimi-code.js';\n\n/**\n * Runtime registry implementation\n *\n * Manages multiple runtime adapters and provides lookup by agent ID.\n */\nexport class RuntimeRegistry implements RuntimeRegistryInterface {\n private runtimes: Map<RuntimeName, AgentRuntimeSync> = new Map();\n\n /**\n * Register a runtime adapter\n */\n register(runtime: AgentRuntimeSync): void {\n this.runtimes.set(runtime.name, runtime);\n }\n\n /**\n * Get a runtime by name\n */\n get(name: RuntimeName): AgentRuntimeSync | undefined {\n return this.runtimes.get(name);\n }\n\n /**\n * Get all registered runtimes\n */\n getAll(): AgentRuntimeSync[] {\n return Array.from(this.runtimes.values());\n }\n\n /**\n * Get the runtime for a specific agent.\n *\n * Reads the agent's state file and dispatches by `state.harness`. When\n * the harness field is missing or carries a legacy value (e.g. 'claude'\n * from pre-PAN-636 wire format), we fall back to the claude-code runtime\n * to preserve back-compat (PAN-636 ac2).\n */\n getRuntimeForAgent(agentId: string): AgentRuntimeSync | null {\n const state = getAgentStateSync(agentId);\n if (!state) {\n return null;\n }\n // Read as plain string so legacy 'pi' rows (pre-PAN-1989) are handled below.\n const harness = (state as { harness?: string }).harness;\n if (harness === 'ohmypi' || harness === 'pi') {\n // 'pi' is a legacy harness value (PAN-1989); normalize to ohmypi adapter.\n return this.get('ohmypi') ?? null;\n }\n if (harness === 'codex') {\n return this.get('codex') ?? null;\n }\n if (harness === 'opencode') return this.get('opencode') ?? null;\n if (harness === 'acp') {\n return this.get('acp') ?? null;\n }\n if (harness === 'muse') return this.get('muse') ?? null;\n if (harness === 'kimi-code') {\n return this.get('kimi-code') ?? null;\n }\n return this.get('claude-code') ?? null;\n }\n}\n\n/**\n * Global runtime registry instance\n */\nlet globalRegistry: RuntimeRegistry | null = null;\n\n/**\n * Get the global runtime registry\n *\n * Creates a new registry if one doesn't exist.\n * Registers Claude Code runtime by default.\n */\nexport function getGlobalRegistry(): RuntimeRegistry {\n if (!globalRegistry) {\n globalRegistry = new RuntimeRegistry();\n\n // Register Claude Code (default), ohmypi (PAN-1989), Codex (PAN-1574), ACP,\n // and native Kimi Code (PAN-1837) runtimes.\n // Pi (PAN-636) is legacy and no longer registered — 'pi' harness is normalized\n // to 'ohmypi' by getRuntimeForAgent (see above) and normalizeHarness() in conversations.ts.\n globalRegistry.register(createClaudeCodeRuntimeSync());\n globalRegistry.register(createOhmypiRuntimeSync());\n globalRegistry.register(createCodexRuntimeSync());\n globalRegistry.register(createAcpRuntimeSync());\n globalRegistry.register(createAcpRuntimeSync({ name: 'opencode', provider: 'opencode' }));\n globalRegistry.register(createKimiCodeRuntimeSync());\n globalRegistry.register(createMuseRuntimeSync());\n }\n return globalRegistry;\n}\n\n/**\n * Set the global runtime registry\n *\n * Useful for testing or custom configurations.\n */\nexport function setGlobalRegistry(registry: RuntimeRegistry): void {\n globalRegistry = registry;\n}\n\n/**\n * Helper to get a runtime by name from the global registry\n */\nexport function getRuntime(name: RuntimeName): AgentRuntimeSync | undefined {\n return getGlobalRegistry().get(name);\n}\n\n/**\n * Helper to get the runtime for an agent from the global registry\n */\nexport function getRuntimeForAgent(agentId: string): AgentRuntimeSync | null {\n return getGlobalRegistry().getRuntimeForAgent(agentId);\n}\n","import { getAgentRuntimeStateSync } from '../agents.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { listPaneValuesSync } from '../tmux.js';\n\nexport function getAgentEffectiveLastActivityMs(agentId: string): number | null {\n const candidates: number[] = [];\n\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const runtimeMs = runtimeState?.lastActivity ? new Date(runtimeState.lastActivity).getTime() : NaN;\n if (Number.isFinite(runtimeMs)) candidates.push(runtimeMs);\n\n try {\n const tmuxActivity = listPaneValuesSync(agentId, '#{window_activity}')\n .map((value) => Number.parseInt(value.trim(), 10) * 1000)\n .filter(Number.isFinite);\n candidates.push(...tmuxActivity);\n } catch {\n // tmux activity is best-effort; fall through to transcript/runtime sources.\n }\n\n try {\n const heartbeat = getRuntimeForAgent(agentId)?.getHeartbeat(agentId);\n if (heartbeat?.timestamp) {\n const heartbeatMs = heartbeat.timestamp.getTime();\n if (Number.isFinite(heartbeatMs)) candidates.push(heartbeatMs);\n }\n } catch {\n // Runtime transcript lookup is best-effort.\n }\n\n return candidates.length > 0 ? Math.max(...candidates) : null;\n}\n\n/**\n * PAN-3677: work-product activity only — the runtime mirror (hook-driven) plus\n * the transcript heartbeat, deliberately EXCLUDING tmux `window_activity`.\n * Claude Code repaints its spinner / background-task panel every second while a\n * turn is in flight, so `window_activity` stays fresh even when the provider\n * call is hung and no work is happening. A mid-turn wedge detector keyed on\n * {@link getAgentEffectiveLastActivityMs} (which takes the MAX) would never see\n * the PAN-3677 planning wedge — the hung sessions looked busy on the pane for\n * minutes while producing no tool calls, no transcript writes, and no hook\n * events. This signal goes stale exactly when real work stops.\n */\nexport function getAgentWorkActivityMs(agentId: string): number | null {\n const candidates: number[] = [];\n\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const runtimeMs = runtimeState?.lastActivity ? new Date(runtimeState.lastActivity).getTime() : NaN;\n if (Number.isFinite(runtimeMs)) candidates.push(runtimeMs);\n\n try {\n const heartbeat = getRuntimeForAgent(agentId)?.getHeartbeat(agentId);\n if (heartbeat?.timestamp) {\n const heartbeatMs = heartbeat.timestamp.getTime();\n if (Number.isFinite(heartbeatMs)) candidates.push(heartbeatMs);\n }\n } catch {\n // Runtime transcript lookup is best-effort.\n }\n\n return candidates.length > 0 ? Math.max(...candidates) : null;\n}\n\nexport function isAgentIdleForNudge(\n agentId: string,\n staleActiveThresholdMs = 5 * 60 * 1000,\n now = Date.now(),\n): boolean {\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const effectiveLastActivityMs = getAgentEffectiveLastActivityMs(agentId);\n if (!runtimeState) {\n if (effectiveLastActivityMs === null) {\n console.log(`[deacon] ${agentId}: no runtime.json — skipping (hook not yet fired)`);\n return false;\n }\n return now - effectiveLastActivityMs > staleActiveThresholdMs;\n }\n if (runtimeState.state === 'suspended' || runtimeState.state === 'stopped') return false;\n // A human-blocked agent is not idle work — never nudge it.\n if (runtimeState.state === 'waiting-on-human') return false;\n if (runtimeState.state === 'idle') return true;\n\n // 'active' or 'uninitialized': the activity mirror is only trustworthy while\n // it is fresh. A STALE 'active' mirror means the activity hooks (including the\n // Stop hook) stopped firing — e.g. the agent ended its turn but the Stop hook\n // never updated the mirror to 'idle' (PAN-1574: the agent sat 'active' for 36h\n // after ending its turn, invisible to every idle-nudge patrol). In that case\n // fall back to an age-based idle judgment so recovery can act. A genuinely\n // working agent emits tool-activity events continuously and stays fresh, so it\n // is not affected.\n const lastActivityMs = effectiveLastActivityMs ?? new Date(runtimeState.lastActivity).getTime();\n const ageMs = now - lastActivityMs;\n return ageMs > staleActiveThresholdMs;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,IAAA,kBAAA,MAAA;;;AAEyB,SAAA,mBAAA,KAAA,KAAA;;;AACW,SAAA,2BAAA,QAAA;;;;AAGhC,MAAA;AAAM,UAAA,OAAA,WAAA,KAAA,CAAA,QAAA;;AAAqD,UAAA;;;;;AAI3D,SAAA,YAAA;;;;;;;;;AAIA,SAAA,OAAA,qBAAA,KAAA,EAAA,SAAA,OAAA;;;;;AAKA,MAAA,CAAA,QAAA,QAAA;;AAEA,MAAA,CAAA,QAAA,QAAA;AACA,SAAA;;;;;;;;;;;AASA,MAAA,CAAA,OAAA,GAAA,OAAA,IAAA,MAAA,OAAA,WAAA,+BAAA;;;AAEgD,QAAA,gBAAA,QAAA;;;AACG,SAAA,kBAAA,QAAA;;;AAEnD,MAAA,CAAA,OAAA,MAAA,OAAA,IAAA,MAAA,+CAAA;AACA,MAAA,MAAA,KAAA,UAAA,OAAA,QAAA,CAAA,OAAA,IAAA,MAAA,SAAA,OAAA,QAAA,qBAAA;;;AAGA,QAAA,MAAA,KAAA,EAAA,WAAA,MAAA,CAAA;AACA,QAAA,cAAA,OAAA,QAAA;;AASA,QAAA,UAAA,UAAA,2BAAA;;;;;;;;;;;;;AACA,QAAA,kBAAA,OAAA,SAAA,OAAA,WAAA,SAAA,SAAA,QAAA,MAAA,QAAA,CAAA,IAAA,OAAA,IAAA;AACA,MAAA;AACE,OAAA,CAAA,MAAA,mBAAA,OAAA,SAAA,QAAA,GAAA,CAAA,OAAA,IAAA,MAAA,yBAAA;;AAEA,OAAA,CAAA,KAAA,OAAA,IAAA,MAAA,6CAAA;AACA,OAAA,OAAA,OAAA,OAAA,KAAA,YAAA,OAAA,SAAA,OAAA,OAAA;AACA,UAAA;;;;;;;;;AAGA,SAAA,KAAA,UAAA,OAAA,QAAA;AACA,SAAA;;;;AAIF,SAAA,iBAAA,CAAA,QAAA,UAAA,MAAA,YAAA,WAAA,CAAA,aAAA,MAAA,cAAA,WAAA,CAAA,SAAA,UAAA;;;AAGE,OAAA,CAAA,QAAA,CAAA,aAAA,QAAA,EAAA;AACA,UAAA,CAAA;;;;;;;;;;;;;;;AAON,SAAA,wBAAA;AAA2D,QAAA,IAAA,iBAAA;;;;;;;;;;;;eCtEN;mBAG6D;AAGlH,MAAM,sBAAsBC,OAAK,SAAS,EAAE,WAAW,WAAW;;;;AAelE,IAAa,wBAAb,MAA+D;CAC7D,OAAgB;CAEhB,qBAAsC;AACpC,SAAO;;;;;;;;CAST,0BAAkC,WAAkC;AAClE,MAAI,CAAC,WAAW,oBAAoB,CAClC,QAAO;EAIT,MAAM,cAAc,oBAAoB;AAExC,OAAK,MAAM,cAAc,aAAa;GAEpC,MAAM,YAAYA,OAAK,YAAY,sBAAsB;AACzD,OAAI,WAAW,UAAU,CACvB,KAAI;AAGF,QAFqB,aAAa,WAAW,QAE7B,CAAC,SAAS,UAAU,CAClC,QAAO;WAEH;;AAMZ,SAAO;;;;;CAMT,mBAA2B,YAAmC;EAC5D,MAAM,YAAYA,OAAK,YAAY,sBAAsB;AACzD,MAAI,CAAC,WAAW,UAAU,CACxB,QAAO;AAGT,MAAI;GACF,MAAM,eAAe,aAAa,WAAW,QAAQ;GACrD,MAAM,QAAQ,KAAK,MAAM,aAAa;AAKtC,OAAI,MAAM,YAAY,MAAM,QAAQ,MAAM,SAAS,EAAE;IACnD,MAAM,WAAW,MAAM;AACvB,QAAI,SAAS,WAAW,EAAG,QAAO;AAOlC,WAJe,SAAS,MAAM,GAAG,MAAM;AACrC,YAAO,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS;MAG7D,CAAC,GAAG;;UAEb;AAIR,SAAO;;;;;CAMT,mBAA2B,YAAmC;EAC5D,MAAM,QAAQ,oBAAoB,WAAW;AAC7C,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK;;;;;CAMvC,eAAe,SAAgC;EAC7C,MAAM,QAAQ,kBAAkB,QAAQ;AACxC,MAAI,CAAC,MACH,QAAO;EAGT,MAAM,aAAa,KAAK,0BAA0B,MAAM,UAAU;AAClE,MAAI,CAAC,WACH,QAAO;EAIT,MAAM,YAAY,KAAK,mBAAmB,WAAW;AACrD,MAAI,WAAW;GACb,MAAM,cAAcA,OAAK,YAAY,GAAG,UAAU,QAAQ;AAC1D,OAAI,WAAW,YAAY,CACzB,QAAO;;AAKX,SAAO,KAAK,mBAAmB,WAAW;;;;;;;CAQ5C,gBAAgB,SAA8B;EAC5C,MAAM,cAAc,KAAK,eAAe,QAAQ;AAChD,MAAI,CAAC,eAAe,CAAC,WAAW,YAAY,CAC1C,QAAO;AAGT,MAAI;AAEF,UADa,SAAS,YACX,CAAC;UACN;AACN,UAAO;;;;;;CAOX,mBAA2B,SAAmC;EAE5D,MAAM,gBAAgBA,OAAK,SAAS,EAAE,aAAa,cAAc,GAAG,QAAQ,OAAO;AACnF,MAAI,CAAC,WAAW,cAAc,CAC5B,QAAO;AAGT,MAAI;GACF,MAAM,UAAU,aAAa,eAAe,QAAQ;GACpD,MAAM,OAAO,KAAK,MAAM,QAAQ;GAGhC,MAAM,YAAY,IAAI,KAAK,KAAK,UAAU;AAG1C,wBADc,IADE,MACC,EAAC,SAAS,GAAG,UAAU,SAAS,GACrC,MAAS,IAEnB,QAAO;AAGT,UAAO;IACL;IACA,SAAS,KAAK,YAAY;IAC1B,QAAQ;IACR,YAAY;IACZ,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,aAAa,KAAK;IAClB,WAAW,KAAK;IAChB,WAAW,KAAK;IAChB,KAAK,KAAK;IACV,WAAW,KAAK;IACjB;UACK;AACN,UAAO;;;;;;CAOX,oBAA4B,SAAmC;EAC7D,MAAM,eAAe,KAAK,gBAAgB,QAAQ;AAClD,MAAI,CAAC,aACH,QAAO;AAGT,SAAO;GACL,WAAW;GACX;GACA,QAAQ;GACR,YAAY;GACb;;;;;;;CAQH,aAAa,SAAmC;EAE9C,MAAM,kBAAkB,KAAK,mBAAmB,QAAQ;AACxD,MAAI,gBACF,QAAO;AAIT,SAAO,KAAK,oBAAoB,QAAQ;;;;;CAM1C,cAAc,SAAoC;EAChD,MAAM,cAAc,KAAK,eAAe,QAAQ;AAChD,MAAI,CAAC,YACH,QAAO;EAGT,MAAM,eAAe,uBAAuB,YAAY;AACxD,MAAI,CAAC,aACH,QAAO;AAGT,SAAO,aAAa;;;;;CAMtB,eAAe,SAAuC;EACpD,MAAM,cAAc,KAAK,eAAe,QAAQ;AAChD,MAAI,CAAC,YACH,QAAO;EAGT,MAAM,eAAe,uBAAuB,YAAY;AACxD,MAAI,CAAC,aACH,QAAO;EAMT,MAAM,aAAa,IAAM;EACzB,MAAM,cAAc,KAAO;EAC3B,MAAM,iBAAiB,KAAM;EAC7B,MAAM,kBAAkB,OAAO;EAE/B,MAAM,QAAQ,aAAa;EAC3B,MAAM,YAAY,MAAM,cAAc;EACtC,MAAM,aAAa,MAAM,eAAe;EACxC,MAAM,iBAAiB,MAAM,mBAAmB,KAAK;EACrD,MAAM,kBAAkB,MAAM,oBAAoB,KAAK;AAEvD,SAAO;GACL;GACA;GACA;GACA;GACA,WAAW,YAAY,aAAa,gBAAgB;GACpD,UAAU;GACX;;;;;CAMH,MAAM,YAAY,SAAiB,SAAgC;AACjE,MAAI,CAAC,kBAAkB,QAAQ,CAC7B,OAAM,IAAI,MAAM,SAAS,QAAQ,iBAAiB;AAGpD,QAAM,OAAO,WAAW,SAAS,SAAS,QAAQ,CAAC;EAGnD,MAAM,UAAUA,OAAK,YAAY,QAAQ,EAAE,OAAO;AAClD,YAAU,SAAS,EAAE,WAAW,MAAM,CAAC;AAGvC,gBACEA,OAAK,SAAS,oBAFE,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAEhC,CAAC,KAAK,EAChC,gBAAgB,QAAQ,IACzB;;;;;CAMH,UAAU,SAAuB;AAC/B,MAAI,CAAC,kBAAkB,QAAQ,CAC7B,OAAM,IAAI,MAAM,SAAS,QAAQ,iBAAiB;AAGpD,kBAAgB,QAAQ;AAGxB,wBAAsB,SAAS;GAAE,OAAO;GAAQ,+BAAc,IAAI,MAAM,EAAC,aAAa;GAAE,CAAC;EAGzF,MAAM,QAAQ,kBAAkB,QAAQ;AACxC,MAAI,OAAO;AACT,SAAM,SAAS;AACf,sBAAmB,MAAM;;;;;;CAO7B,MAAM,WAAW,QAAqC;EAEpD,MAAM,QAAQ,MAAMC,WAAe;GACjC,SAAS,OAAO,QAAQ,QAAQ,WAAW,GAAG;GAC9C,WAAW,OAAO;GAClB,SAAS;GACT,OAAO,eAAe;IAAE,OAAO,OAAO;IAAO,MAAM;IAAQ,UAAU,QAAQ,OAAO,QAAQ,QAAQ,WAAW,GAAG;IAAI,CAAC;GACvH,MAAM;GACN,QAAQ,OAAO;GACf,WAAW,OAAO,MAAM,gCAAgC;GACzD,CAAC;EAGF,MAAM,aAAa,KAAK,0BAA0B,OAAO,UAAU;EACnE,MAAM,YAAY,aAAa,KAAK,mBAAmB,WAAW,GAAG,KAAA;AAErE,SAAO;GACL,IAAI,MAAM;GACV,WAAW,aAAa;GACxB,SAAS;GACT,OAAO,MAAM;GACb,WAAW,MAAM;GACjB,WAAW,IAAI,KAAK,MAAM,UAAU;GACrC;;;;;CAMH,aAAa,WAA+B;EAC1C,MAAM,WAAsB,EAAE;AAE9B,MAAI,WAAW;GAEb,MAAM,aAAa,KAAK,0BAA0B,UAAU;AAC5D,OAAI,YAAY;IACd,MAAM,QAAQ,oBAAoB,WAAW;AAC7C,SAAK,MAAM,QAAQ,OAAO;KACxB,MAAM,UAAU,KAAK,iBAAiB,MAAM,UAAU;AACtD,SAAI,QACF,UAAS,KAAK,QAAQ;;;SAIvB;GAEL,MAAM,cAAc,oBAAoB;AACxC,QAAK,MAAM,cAAc,aAAa;IACpC,MAAM,QAAQ,oBAAoB,WAAW;AAC7C,SAAK,MAAM,QAAQ,OAAO;KACxB,MAAM,UAAU,KAAK,iBAAiB,KAAK;AAC3C,SAAI,QACF,UAAS,KAAK,QAAQ;;;;AAM9B,SAAO;;;;;CAMT,iBAAyB,MAAc,WAAoC;EACzE,MAAM,eAAe,uBAAuB,KAAK;AACjD,MAAI,CAAC,aACH,QAAO;EAGT,MAAM,OAAO,SAAS,KAAK;AAE3B,SAAO;GACL,IAAI,aAAa;GACjB,SAAS;GACT,WAAW,aAAa;GACxB,OAAO,aAAa;GACpB,WAAW,IAAI,KAAK,aAAa,UAAU;GAC3C,cAAc,KAAK;GACnB,YAAY,aAAa;GAC1B;;;;;CAMH,UAAU,SAA0B;AAClC,SAAO,kBAAkB,QAAQ;;;;;;AAOrC,SAAgB,8BAAqD;AACnE,QAAO,IAAI,uBAAuB;;;;aClaG;;;eCkBQ;WACmE;qBAEvD;yBACU;aAEW;YACnC;AAE3B,UAAU,KAAK;;;;;;;;;;ACtBjC,MAAA,YAAA,UAAA,KAAA;AACA,MAAA,0BAAA;AACA,MAAA,yBAAA;AACA,MAAA,mBAAA;AAoBA,IAAA,kBAAA,cAAA,MAAA;;;AAII,QAAA,aAAA,QAAA,uCAAA,uBAAA,IAAA;AACA,OAAA,OAAA;;;AAIJ,IAAA,iBAAA,MAAA;;;;;;;;AASI,OAAA,OAAA,QAAA,QAAA;AACA,OAAA,WAAA,QAAA,aAAA,KAAA,SAAA,aAAA,aAAA;AACA,OAAA,eAAA,QAAA;AACA,OAAA,cAAA,QAAA,eAAA;AACA,OAAA,gBAAA,QAAA,wBAAA,qBAAA,KAAA,KAAA;AACA,OAAA,qBAAA,QAAA,0BAAA,iBAAA;;;AAIA,SAAA,mBAAA,KAAA,KAAA;;;AAIA,SAAA,KAAA,KAAA,MAAA,EAAA,UAAA,SAAA,oBAAA;;;AAIA,MAAA;AACE,UAAA,WAAA,KAAA,eAAA,QAAA,CAAA,CAAA;;AAEA,UAAA;;;;;AAMF,MAAA,CAAA,aAAA,QAAA;AAEA,SAAA;;;;;;;;AASA,SAAA;;;AAIA,SAAA;;;;AAKA,MAAA,CAAA,aAAA,WAAA,CAAA,OAAA,IAAA,MAAA,aAAA,QAAA,gCAAA;;AAIA,MAAA,CAAA,MAAA,OAAA,IAAA,MAAA,aAAA,QAAA,qBAAA;AAIA,QAAA,mBAAA,YAAA,OAAA;;;;;;;;AASA,MAAA,aAAA,WAAA,IAAA,MAAA,KAAA;AAEI,SAAA,mBAAA,YAAA,OAAA,EAAA,IAAA,aAAA,EAAA,wBAAA;;AAMJ,MAAA,MAAA,qBAAA,SAAA,IAAA,CAAA;;AAGA,MAAA;;AAIE,aAAA,OAAA,MAAA,IAAA;AACA,OAAA,QAAA,OAAA,KAAA,YAAA,kBAAA,QAAA,6BAAA,QAAA,sBAAA;;AASF,MAAA,MAAA,qBAAA,SAAA,IAAA,CAAA;AACA,MAAA,QAAA,KAAA;AAEI,SAAA,KAAA,YAAA,kBAAA,QAAA,6BAAA,QAAA,sBAAA;;AAOJ,MAAA,MAAA,kBAAA,QAAA,CAAA,OAAA,gBAAA,QAAA;;;AAIA,MAAA,KAAA,SAAA,cAAA,CAAA,OAAA,MAAA,OAAA,IAAA,MAAA,sCAAA;;;;;;;;;;;;;;;;;;AAsBA,MAAA,KAAA,SAAA,cAAA,OAAA,OAAA,SAAA,KAAA,YAAA,WAAA,OAAA,OAAA,CAAA;AACA,MAAA,OAAA,UAAA,SAAA,KAAA,YAAA,WAAA,OAAA,UAAA,CAAA;AACA,MAAA,OAAA,MAAA,SAAA,KAAA,WAAA,WAAA,OAAA,MAAA,CAAA;AACA,MAAA,aAAA,UAAA,OAAA,OAAA;;AAEE,OAAA,OAAA,SAAA,KAAA,YAAA,WAAA,OAAA,CAAA;;AAGF,WAAA,KAAA,UAAA,OAAA,SAAA,iBAAA,EAAA,EAAA,OAAA,MAAA,CAAA;AACA,WAAA,KAAA,UAAA,OAAA,SAAA,mBAAA,EAAA,EAAA,OAAA,MAAA,CAAA;AAEA,QAAA,kBAAA,OAAA,SAAA,OAAA,WAAA,QAAA,KAAA,IAAA,EAAA;;;;AAUA,MAAA;;AAEE,OAAA,CAAA,UAAA,OAAA,IAAA,gBAAA,OAAA,QAAA;AAEA,OAAA,OAAA,OAAA,OAAA,KAAA,YAAA,OAAA,SAAA,OAAA,OAAA;AAEA,UAAA;;;;;;;;;AASA,OAAA,MAAA,kBAAA,OAAA,QAAA,CAAA,OAAA,gBAAA,OAAA,QAAA;AACA,SAAA;;;;;AAMF,OAAA,MAAA,SAAA,KAAA,oBAAA,EAAA;AACE,OAAA,MAAA,YAAA,KAAA,KAAA;AACA,OAAA,aAAA,MAAA,cAAA,UAAA;;AAGA,OAAA,CAAA,UAAA;;;AAIA,OAAA;AACE,mBAAA,WAAA,YAAA,CAAA;;AAEA,QAAA;AACE,oBAAA,WAAA,KAAA,UAAA,MAAA,IAAA,iBAAA,CAAA,CAAA;;AAEA;;;AAIJ,YAAA,KAAA;;;;;;;;;;;;;AAUF,SAAA;;;AAIA,SAAA,kBAAA,QAAA;;;AAIA,SAAA,KAAA,gBAAA,iBAAA;;;AAIA,SAAA,KAAA,KAAA,MAAA,EAAA,UAAA,SAAA,KAAA;;;AAIA,SAAA,KAAA,KAAA,MAAA,EAAA,WAAA,OAAA,QAAA,OAAA;;;AAIA,MAAA;AACE,UAAA,eAAA,KAAA,UAAA,SAAA,YAAA,EAAA,OAAA,CAAA,MAAA,IAAA;;AAEA,UAAA;;;;AAKF,MAAA;AACE,UAAA,eAAA,KAAA,UAAA,SAAA,mBAAA,EAAA,OAAA,CAAA,MAAA,IAAA;;AAEA,UAAA;;;;AAKF,MAAA;AACE,UAAA,eAAA,KAAA,UAAA,SAAA,iBAAA,EAAA,OAAA,CAAA,MAAA,IAAA;;AAEA,UAAA;;;;;AAMF,SAAA,KAAA,KAAA,GAAA,UAAA;;AAEE,OAAA,YAAA,OAAA,IAAA,MAAA,YAAA,QAAA,oBAAA,cAAA;;AAEA,OAAA,UAAA,QAAA;AACA,SAAA,MAAA,iBAAA;;AAEF,SAAA;;;AAIJ,eAAA,qBAAA,SAAA,WAAA;;AAEE,QAAA,KAAA,KAAA,GAAA,UAAA;AACE,MAAA,CAAA,MAAA,kBAAA,QAAA,CAAA,QAAA;AACA,QAAA,MAAA,iBAAA;;AAEF,QAAA;;AAGF,SAAA,MAAA,IAAA;AACE,QAAA,IAAA,SAAA,YAAA,WAAA,SAAA,GAAA,CAAA;;AAGF,SAAA,WAAA,OAAA;AACE,QAAA,IAAA,MAAA,QAAA,MAAA,QAAA,CAAA;;AAGF,SAAA,mBAAA,YAAA,OAAA,MAAA,WAAA;AAME,QAAA,IAAA,SAAA,SAAA,WAAA;;;;AAII,OAAA,QAAA;AACA,aAAA;AACA,OAAA,QAAA,cAAA,QAAA;AACA,aAAA;;;;;;;;;;;;AAcE,YAAA,GAAA,SAAA,UAAA,OAAA,KAAA,MAAA,CAAA;AACA,YAAA,GAAA,aAAA;;;AAGE,QAAA,SAAA,OAAA,UAAA,KAAA;AACE,kBAAA,uBAAA,IAAA,MAAA,0BAAA,OAAA,IAAA,eAAA,CAAA,CAAA;AACA;;AAEF,iBAAA,QAAA;;;;;;AAIN,MAAA,cAAA,KAAA,GAAA;AACE,aAAA,iBAAA;AACE,WAAA,wBAAA,IAAA,MAAA,oCAAA,UAAA,IAAA,CAAA;;AAEF,WAAA,SAAA;;AAEF,SAAA,KAAA,UAAA,UAAA,aAAA,OAAA,MAAA,CAAA,CAAA;AACA,SAAA,IAAA,KAAA,UAAA,KAAA,CAAA;;;AAIJ,SAAA,qBAAA,UAAA,EAAA,EAAA;AACE,QAAA,IAAA,eAAA,QAAA;;;;;aCnV+C;;;;;;AAajD,IAAa,kBAAb,MAAiE;CAC/D,2BAAuD,IAAI,KAAK;;;;CAKhE,SAAS,SAAiC;AACxC,OAAK,SAAS,IAAI,QAAQ,MAAM,QAAQ;;;;;CAM1C,IAAI,MAAiD;AACnD,SAAO,KAAK,SAAS,IAAI,KAAK;;;;;CAMhC,SAA6B;AAC3B,SAAO,MAAM,KAAK,KAAK,SAAS,QAAQ,CAAC;;;;;;;;;;CAW3C,mBAAmB,SAA0C;EAC3D,MAAM,QAAQ,kBAAkB,QAAQ;AACxC,MAAI,CAAC,MACH,QAAO;EAGT,MAAM,UAAW,MAA+B;AAChD,MAAI,YAAY,YAAY,YAAY,KAEtC,QAAO,KAAK,IAAI,SAAS,IAAI;AAE/B,MAAI,YAAY,QACd,QAAO,KAAK,IAAI,QAAQ,IAAI;AAE9B,MAAI,YAAY,WAAY,QAAO,KAAK,IAAI,WAAW,IAAI;AAC3D,MAAI,YAAY,MACd,QAAO,KAAK,IAAI,MAAM,IAAI;AAE5B,MAAI,YAAY,OAAQ,QAAO,KAAK,IAAI,OAAO,IAAI;AACnD,MAAI,YAAY,YACd,QAAO,KAAK,IAAI,YAAY,IAAI;AAElC,SAAO,KAAK,IAAI,cAAc,IAAI;;;;;;AAOtC,IAAI,iBAAyC;;;;;;;AAQ7C,SAAgB,oBAAqC;AACnD,KAAI,CAAC,gBAAgB;AACnB,mBAAiB,IAAI,iBAAiB;AAMtC,iBAAe,SAAS,6BAA6B,CAAC;AACtD,iBAAe,SAAS,yBAAyB,CAAC;AAClD,iBAAe,SAAS,wBAAwB,CAAC;AACjD,iBAAe,SAAS,sBAAsB,CAAC;AAC/C,iBAAe,SAAS,qBAAqB;GAAE,MAAM;GAAY,UAAU;GAAY,CAAC,CAAC;AACzF,iBAAe,SAAS,2BAA2B,CAAC;AACpD,iBAAe,SAAS,uBAAuB,CAAC;;AAElD,QAAO;;;;;AAsBT,SAAgB,mBAAmB,SAA0C;AAC3E,QAAO,mBAAmB,CAAC,mBAAmB,QAAQ;;;;aC9KA;WAER;AAEhD,SAAgB,gCAAgC,SAAgC;CAC9E,MAAM,aAAuB,EAAE;CAE/B,MAAM,eAAe,yBAAyB,QAAQ;CACtD,MAAM,YAAY,cAAc,eAAe,IAAI,KAAK,aAAa,aAAa,CAAC,SAAS,GAAG;AAC/F,KAAI,OAAO,SAAS,UAAU,CAAE,YAAW,KAAK,UAAU;AAE1D,KAAI;EACF,MAAM,eAAe,mBAAmB,SAAS,qBAAqB,CACnE,KAAK,UAAU,OAAO,SAAS,MAAM,MAAM,EAAE,GAAG,GAAG,IAAK,CACxD,OAAO,OAAO,SAAS;AAC1B,aAAW,KAAK,GAAG,aAAa;SAC1B;AAIR,KAAI;EACF,MAAM,YAAY,mBAAmB,QAAQ,EAAE,aAAa,QAAQ;AACpE,MAAI,WAAW,WAAW;GACxB,MAAM,cAAc,UAAU,UAAU,SAAS;AACjD,OAAI,OAAO,SAAS,YAAY,CAAE,YAAW,KAAK,YAAY;;SAE1D;AAIR,QAAO,WAAW,SAAS,IAAI,KAAK,IAAI,GAAG,WAAW,GAAG;;;;;;;;;;;;;AAc3D,SAAgB,uBAAuB,SAAgC;CACrE,MAAM,aAAuB,EAAE;CAE/B,MAAM,eAAe,yBAAyB,QAAQ;CACtD,MAAM,YAAY,cAAc,eAAe,IAAI,KAAK,aAAa,aAAa,CAAC,SAAS,GAAG;AAC/F,KAAI,OAAO,SAAS,UAAU,CAAE,YAAW,KAAK,UAAU;AAE1D,KAAI;EACF,MAAM,YAAY,mBAAmB,QAAQ,EAAE,aAAa,QAAQ;AACpE,MAAI,WAAW,WAAW;GACxB,MAAM,cAAc,UAAU,UAAU,SAAS;AACjD,OAAI,OAAO,SAAS,YAAY,CAAE,YAAW,KAAK,YAAY;;SAE1D;AAIR,QAAO,WAAW,SAAS,IAAI,KAAK,IAAI,GAAG,WAAW,GAAG;;AAG3D,SAAgB,oBACd,SACA,yBAAyB,MAAS,KAClC,MAAM,KAAK,KAAK,EACP;CACT,MAAM,eAAe,yBAAyB,QAAQ;CACtD,MAAM,0BAA0B,gCAAgC,QAAQ;AACxE,KAAI,CAAC,cAAc;AACjB,MAAI,4BAA4B,MAAM;AACpC,WAAQ,IAAI,YAAY,QAAQ,mDAAmD;AACnF,UAAO;;AAET,SAAO,MAAM,0BAA0B;;AAEzC,KAAI,aAAa,UAAU,eAAe,aAAa,UAAU,UAAW,QAAO;AAEnF,KAAI,aAAa,UAAU,mBAAoB,QAAO;AACtD,KAAI,aAAa,UAAU,OAAQ,QAAO;AAY1C,QADc,OADS,2BAA2B,IAAI,KAAK,aAAa,aAAa,CAAC,SAAS,IAEhF"}
|
|
1
|
+
{"version":3,"file":"agent-idle-D-Qy5BN_.js","names":["statSync","join","spawnAgentImpl","execAsync","statSync","existsSync","readFileSync"],"sources":["../../src/lib/runtimes/muse.ts","../../src/lib/runtimes/claude-code.ts","../../src/lib/cost-parsers/pi-parser.ts","../../src/lib/runtimes/pi.ts","../../src/lib/runtimes/acp.ts","../../src/lib/runtimes/index.ts","../../src/lib/cloister/agent-idle.ts"],"sourcesContent":["import { materializeMuseContext } from './muse-context.js';\n/** Native Muse Code runtime. Lifecycle operations use the shared delivery and tmux doors. */\nimport { statSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { listAgentStates } from '../agents/queries.js';\nimport { deliverAgentMessage } from '../agents/delivery.js';\nimport { waitForPromptReady } from '../agents/runtime-command.js';\nimport { prepareHarnessLaunch } from '../harness-binary.js';\nimport { generateLauncherScriptSync } from '../launcher-generator.js';\nimport { resolvePtySupervisorScriptPath } from '../channels/pty-supervisor-locate.js';\nimport { writePtyToken } from '../pty-token.js';\nimport { getOverdeckHome } from '../paths.js';\nimport { getPricingSync } from '../cost.js';\nimport { parseMuseSessionSync } from '../cost-parsers/muse-parser.js';\nimport { museSessionId, resolveMuseSessionPath, resolveMuseSessionPathSync } from './muse-session.js';\nimport { getRuntimeBehavior } from './behavior.js';\nimport { tmuxCreateSession, tmuxKillSession, tmuxSessionExists } from './tmux-cli.js';\nimport type { Agent, AgentRuntimeSync, CostBreakdown, Heartbeat, Session, SpawnConfig } from './types.js';\n\nexport class MuseRuntimeSync implements AgentRuntimeSync {\n readonly name = 'muse' as const;\n getHarnessBehavior() { return getRuntimeBehavior(this.name); }\n getSessionPath(agentId: string) { return resolveMuseSessionPathSync(agentId); }\n getLastActivity(agentId: string): Date | null {\n const path = this.getSessionPath(agentId);\n try { return path ? statSync(path).mtime : null; } catch { return null; }\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n const timestamp = this.getLastActivity(agentId);\n return timestamp ? { timestamp, agentId, source: 'jsonl', confidence: 'medium' } : null;\n }\n getTokenUsage(agentId: string) {\n const path = this.getSessionPath(agentId);\n return path ? parseMuseSessionSync(path)?.usage ?? null : null;\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n const path = this.getSessionPath(agentId);\n const session = path ? parseMuseSessionSync(path) : null;\n if (!session) return null;\n const pricing = getPricingSync('custom', session.model);\n if (!pricing) return null;\n return {\n inputCost: session.usage.inputTokens * pricing.inputPer1k / 1000,\n outputCost: session.usage.outputTokens * pricing.outputPer1k / 1000,\n cacheReadCost: (session.usage.cacheReadTokens ?? 0) * (pricing.cacheReadPer1k ?? 0) / 1000,\n cacheWriteCost: 0, totalCost: session.cost, currency: 'USD',\n };\n }\n async sendMessage(agentId: string, message: string): Promise<void> {\n const result = await deliverAgentMessage(agentId, message, 'muse-runtime');\n if (!result.ok) throw new Error(result.failure ?? 'Muse message delivery failed');\n }\n async killAgent(agentId: string): Promise<void> { await tmuxKillSession(agentId); }\n async isRunning(agentId: string): Promise<boolean> { return tmuxSessionExists(agentId); }\n async spawnAgent(config: SpawnConfig): Promise<Agent> {\n if (!config.model) throw new Error('Muse requires an explicitly configured model');\n if (await this.isRunning(config.agentId)) throw new Error(`Agent ${config.agentId} is already running`);\n const launch = await prepareHarnessLaunch('muse');\n const dir = join(getOverdeckHome(), 'agents', config.agentId);\n await mkdir(dir, { recursive: true });\n await writePtyToken(config.agentId);\n const launcher = join(dir, 'launcher.sh');\n const script = generateLauncherScriptSync({\n role: 'work', workingDir: config.workspace, harness: 'muse', museModel: config.model, museEffort: config.effort,\n museContextFile: await materializeMuseContext(config.agentId, config.workspace),\n museResumeSessionId: config.sessionId, overdeckEnv: { agentId: config.agentId },\n extraEnvExports: [launch.pathExport], useSupervisor: true,\n supervisorScriptPath: resolvePtySupervisorScriptPath(),\n });\n await writeFile(launcher, script, { mode: 0o700 });\n await tmuxCreateSession(config.agentId, config.workspace, `bash '${launcher.replace(/'/g, \"'\\\\''\")}'`, config.env);\n try {\n if (!await waitForPromptReady(config.agentId, 'muse', 60)) throw new Error('Muse startup timed out');\n const path = await resolveMuseSessionPath(config.agentId);\n if (!path) throw new Error('Muse started without a durable session log');\n if (config.prompt) await this.sendMessage(config.agentId, config.prompt);\n return { id: config.agentId, sessionId: museSessionId(path), runtime: 'muse',\n model: config.model, workspace: config.workspace, startedAt: new Date() };\n } catch (error) {\n await this.killAgent(config.agentId);\n throw error;\n }\n }\n listSessions(workspace?: string): Session[] {\n return listAgentStates().filter(state => state.harness === 'muse' && (!workspace || state.workspace === workspace)).flatMap(state => {\n const path = this.getSessionPath(state.id);\n const lastActivity = this.getLastActivity(state.id);\n if (!path || !lastActivity) return [];\n return [{ id: museSessionId(path), agentId: state.id, workspace: state.workspace,\n model: state.model, startedAt: new Date(state.startedAt), lastActivity,\n tokenUsage: this.getTokenUsage(state.id) ?? { inputTokens: 0, outputTokens: 0 } }];\n });\n }\n}\n\nexport function createMuseRuntimeSync(): MuseRuntimeSync { return new MuseRuntimeSync(); }\n","/**\n * Claude Code Runtime Implementation\n *\n * Implements AgentRuntime for Claude Code CLI.\n *\n * Session storage: ~/.claude/projects/<workspace-hash>/<session-id>.jsonl\n * Session index: ~/.claude/projects/<workspace-hash>/sessions-index.json\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { Effect } from 'effect';\nimport type {\n AgentRuntime,\n AgentRuntimeSync,\n AgentRuntimeError,\n HarnessBehavior,\n Heartbeat,\n TokenUsage,\n CostBreakdown,\n Session,\n SpawnConfig,\n Agent,\n ActivitySource,\n} from './types.js';\nimport { CLAUDE_CODE_BEHAVIOR } from './behavior.js';\nimport { getAgentStateSync, getAgentDir, spawnAgent as spawnAgentImpl, saveAgentStateSync, saveAgentRuntimeState, determineModel } from '../agents.js';\nimport { sessionExistsSync, killSessionSync, sendKeys, getAgentSessionsSync } from '../tmux.js';\nimport { parseClaudeSessionSync, getSessionFilesSync, getProjectDirsSync } from '../cost-parsers/jsonl-parser.js';\nimport { ProcessSpawnError, TmuxError, FsError } from '../errors.js';\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');\n\n/**\n * Claude Code session index entry\n */\ninterface SessionIndexEntry {\n sessionId: string;\n createdAt: string;\n updatedAt: string;\n filePath: string;\n}\n\n/**\n * Claude Code Runtime implementation\n */\nexport class ClaudeCodeRuntimeSync implements AgentRuntimeSync {\n readonly name = 'claude-code' as const;\n\n getHarnessBehavior(): HarnessBehavior {\n return CLAUDE_CODE_BEHAVIOR;\n }\n\n /**\n * Get the project directory for a workspace\n *\n * Claude Code hashes the workspace path to create project directories.\n * We need to find the project directory that contains sessions for this workspace.\n */\n private getProjectDirForWorkspace(workspace: string): string | null {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n return null;\n }\n\n // Get all project directories\n const projectDirs = getProjectDirsSync();\n\n for (const projectDir of projectDirs) {\n // Check if this project's sessions-index.json references the workspace\n const indexPath = join(projectDir, 'sessions-index.json');\n if (existsSync(indexPath)) {\n try {\n const indexContent = readFileSync(indexPath, 'utf-8');\n // Sessions index contains the workspace path\n if (indexContent.includes(workspace)) {\n return projectDir;\n }\n } catch {\n // Skip invalid index files\n }\n }\n }\n\n return null;\n }\n\n /**\n * Get the active session ID for an agent from the sessions index\n */\n private getActiveSessionId(projectDir: string): string | null {\n const indexPath = join(projectDir, 'sessions-index.json');\n if (!existsSync(indexPath)) {\n return null;\n }\n\n try {\n const indexContent = readFileSync(indexPath, 'utf-8');\n const index = JSON.parse(indexContent);\n\n // The sessions-index.json has a structure like:\n // { \"sessions\": [{ \"sessionId\": \"...\", \"filePath\": \"...\", ... }] }\n // Find the most recent session\n if (index.sessions && Array.isArray(index.sessions)) {\n const sessions = index.sessions as SessionIndexEntry[];\n if (sessions.length === 0) return null;\n\n // Sort by updatedAt and get the most recent\n const sorted = sessions.sort((a, b) => {\n return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();\n });\n\n return sorted[0].sessionId;\n }\n } catch {\n // Skip invalid index files\n }\n\n return null;\n }\n\n /**\n * Get the most recent JSONL file for a project\n */\n private getMostRecentJSONL(projectDir: string): string | null {\n const files = getSessionFilesSync(projectDir);\n return files.length > 0 ? files[0] : null;\n }\n\n /**\n * Get the session path for an agent\n */\n getSessionPath(agentId: string): string | null {\n const state = getAgentStateSync(agentId);\n if (!state) {\n return null;\n }\n\n const projectDir = this.getProjectDirForWorkspace(state.workspace);\n if (!projectDir) {\n return null;\n }\n\n // Try to get active session from index\n const sessionId = this.getActiveSessionId(projectDir);\n if (sessionId) {\n const sessionPath = join(projectDir, `${sessionId}.jsonl`);\n if (existsSync(sessionPath)) {\n return sessionPath;\n }\n }\n\n // Fall back to most recent JSONL file\n return this.getMostRecentJSONL(projectDir);\n }\n\n /**\n * Get last activity timestamp for an agent\n *\n * Uses passive detection via JSONL file modification time.\n */\n getLastActivity(agentId: string): Date | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath || !existsSync(sessionPath)) {\n return null;\n }\n\n try {\n const stat = statSync(sessionPath);\n return stat.mtime;\n } catch {\n return null;\n }\n }\n\n /**\n * Read active heartbeat file if it exists\n */\n private getActiveHeartbeat(agentId: string): Heartbeat | null {\n // Heartbeats are now in shared directory: ~/.overdeck/heartbeats/\n const heartbeatPath = join(homedir(), '.overdeck', 'heartbeats', `${agentId}.json`);\n if (!existsSync(heartbeatPath)) {\n return null;\n }\n\n try {\n const content = readFileSync(heartbeatPath, 'utf-8');\n const data = JSON.parse(content);\n\n // Check if heartbeat is recent (within 5 minutes)\n const timestamp = new Date(data.timestamp);\n const now = new Date();\n const ageMs = now.getTime() - timestamp.getTime();\n if (ageMs > 5 * 60 * 1000) {\n // Heartbeat is stale\n return null;\n }\n\n return {\n timestamp,\n agentId: data.agent_id || agentId,\n source: 'active-heartbeat',\n confidence: 'high',\n toolName: data.tool_name,\n lastAction: data.last_action,\n currentTask: data.current_task,\n gitBranch: data.git_branch,\n workspace: data.workspace,\n pid: data.pid,\n sessionId: data.session_id,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get passive heartbeat from file timestamps\n */\n private getPassiveHeartbeat(agentId: string): Heartbeat | null {\n const lastActivity = this.getLastActivity(agentId);\n if (!lastActivity) {\n return null;\n }\n\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n };\n }\n\n /**\n * Get heartbeat for an agent\n *\n * Tries active heartbeat first (if hooks configured), falls back to passive.\n */\n getHeartbeat(agentId: string): Heartbeat | null {\n // Try active heartbeat first\n const activeHeartbeat = this.getActiveHeartbeat(agentId);\n if (activeHeartbeat) {\n return activeHeartbeat;\n }\n\n // Fall back to passive detection\n return this.getPassiveHeartbeat(agentId);\n }\n\n /**\n * Get token usage for an agent's current session\n */\n getTokenUsage(agentId: string): TokenUsage | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionPath);\n if (!sessionUsage) {\n return null;\n }\n\n return sessionUsage.usage;\n }\n\n /**\n * Get cost breakdown for an agent's current session\n */\n getSessionCost(agentId: string): CostBreakdown | null {\n const sessionPath = this.getSessionPath(agentId);\n if (!sessionPath) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionPath);\n if (!sessionUsage) {\n return null;\n }\n\n // Calculate breakdown based on token usage\n // Prices for Claude Sonnet 4 (most common)\n // TODO: Use actual model pricing from session\n const inputPrice = 3.0 / 1_000_000; // $3 per 1M input tokens\n const outputPrice = 15.0 / 1_000_000; // $15 per 1M output tokens\n const cacheReadPrice = 0.3 / 1_000_000; // $0.30 per 1M cache read tokens\n const cacheWritePrice = 3.75 / 1_000_000; // $3.75 per 1M cache write tokens\n\n const usage = sessionUsage.usage;\n const inputCost = usage.inputTokens * inputPrice;\n const outputCost = usage.outputTokens * outputPrice;\n const cacheReadCost = (usage.cacheReadTokens || 0) * cacheReadPrice;\n const cacheWriteCost = (usage.cacheWriteTokens || 0) * cacheWritePrice;\n\n return {\n inputCost,\n outputCost,\n cacheReadCost,\n cacheWriteCost,\n totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,\n currency: 'USD',\n };\n }\n\n /**\n * Send a message to a running agent\n */\n async sendMessage(agentId: string, message: string): Promise<void> {\n if (!sessionExistsSync(agentId)) {\n throw new Error(`Agent ${agentId} is not running`);\n }\n\n await Effect.runPromise(sendKeys(agentId, message));\n\n // Also save to mail queue for persistence\n const mailDir = join(getAgentDir(agentId), 'mail');\n mkdirSync(mailDir, { recursive: true });\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n writeFileSync(\n join(mailDir, `${timestamp}.md`),\n `# Message\\n\\n${message}\\n`\n );\n }\n\n /**\n * Kill an agent\n */\n killAgent(agentId: string): void {\n if (!sessionExistsSync(agentId)) {\n throw new Error(`Agent ${agentId} is not running`);\n }\n\n killSessionSync(agentId);\n\n // Reset runtime state so deacon / merge-agent busy-wait don't see a phantom active session\n saveAgentRuntimeState(agentId, { state: 'idle', lastActivity: new Date().toISOString() });\n\n // Update agent state\n const state = getAgentStateSync(agentId);\n if (state) {\n state.status = 'stopped';\n saveAgentStateSync(state);\n }\n }\n\n /**\n * Spawn a new agent\n */\n async spawnAgent(config: SpawnConfig): Promise<Agent> {\n // Use the existing spawnAgent implementation from agents.ts\n const state = await spawnAgentImpl({\n issueId: config.agentId.replace(/^agent-/, ''),\n workspace: config.workspace,\n harness: 'claude-code',\n model: determineModel({ model: config.model, role: 'work', spawnKey: `work:${config.agentId.replace(/^agent-/, '')}` }),\n role: 'work',\n prompt: config.prompt,\n startedBy: config.env?.['OVERDECK_AGENT_STARTED_BY'] ?? 'runtime:claude-code',\n });\n\n // Get the session ID (we'll need to look it up from the workspace)\n const projectDir = this.getProjectDirForWorkspace(config.workspace);\n const sessionId = projectDir ? this.getActiveSessionId(projectDir) : undefined;\n\n return {\n id: state.id,\n sessionId: sessionId || 'unknown',\n runtime: 'claude-code',\n model: state.model,\n workspace: state.workspace,\n startedAt: new Date(state.startedAt),\n };\n }\n\n /**\n * List all sessions for this runtime\n */\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = [];\n\n if (workspace) {\n // Get sessions for specific workspace\n const projectDir = this.getProjectDirForWorkspace(workspace);\n if (projectDir) {\n const files = getSessionFilesSync(projectDir);\n for (const file of files) {\n const session = this.parseSessionFile(file, workspace);\n if (session) {\n sessions.push(session);\n }\n }\n }\n } else {\n // Get all sessions\n const projectDirs = getProjectDirsSync();\n for (const projectDir of projectDirs) {\n const files = getSessionFilesSync(projectDir);\n for (const file of files) {\n const session = this.parseSessionFile(file);\n if (session) {\n sessions.push(session);\n }\n }\n }\n }\n\n return sessions;\n }\n\n /**\n * Parse a session file into a Session object\n */\n private parseSessionFile(file: string, workspace?: string): Session | null {\n const sessionUsage = parseClaudeSessionSync(file);\n if (!sessionUsage) {\n return null;\n }\n\n const stat = statSync(file);\n\n return {\n id: sessionUsage.sessionId,\n agentId: 'unknown', // We'd need to reverse-lookup from agent state\n workspace: workspace || 'unknown',\n model: sessionUsage.model,\n startedAt: new Date(sessionUsage.startTime),\n lastActivity: stat.mtime,\n tokenUsage: sessionUsage.usage,\n };\n }\n\n /**\n * Check if an agent is running\n */\n isRunning(agentId: string): boolean {\n return sessionExistsSync(agentId);\n }\n}\n\n/**\n * Create a Claude Code runtime instance\n */\nexport function createClaudeCodeRuntimeSync(): ClaudeCodeRuntimeSync {\n return new ClaudeCodeRuntimeSync();\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel adapter wrapping the legacy ClaudeCodeRuntime. The\n// promise/sync class above remains the canonical implementation used by\n// Cloister and the dashboard; this adapter is for new Effect-native callers.\n\n/**\n * Effect-channel variant of {@link ClaudeCodeRuntimeSync}. Lifts the async\n * send/kill/spawn methods into typed Effect channels (TmuxError /\n * ProcessSpawnError) while keeping sync introspection methods sync.\n */\nexport class ClaudeCodeRuntime implements AgentRuntime {\n readonly name = 'claude-code' as const;\n private readonly inner: ClaudeCodeRuntimeSync;\n\n constructor(inner: ClaudeCodeRuntimeSync = new ClaudeCodeRuntimeSync()) {\n this.inner = inner;\n }\n\n getSessionPath(agentId: string): string | null {\n return this.inner.getSessionPath(agentId);\n }\n getHarnessBehavior(): HarnessBehavior {\n return this.inner.getHarnessBehavior();\n }\n getLastActivity(agentId: string): Date | null {\n return this.inner.getLastActivity(agentId);\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n return this.inner.getHeartbeat(agentId);\n }\n getTokenUsage(agentId: string): TokenUsage | null {\n return this.inner.getTokenUsage(agentId);\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n return this.inner.getSessionCost(agentId);\n }\n listSessions(workspace?: string): Session[] {\n return this.inner.listSessions(workspace);\n }\n\n sendMessage(agentId: string, message: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.sendMessage(agentId, message),\n catch: (cause) =>\n new TmuxError({\n command: 'send-keys',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n killAgent(agentId: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.try({\n try: () => this.inner.killAgent(agentId),\n catch: (cause) =>\n new TmuxError({\n command: 'kill-session',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n spawnAgent(config: SpawnConfig): Effect.Effect<Agent, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.spawnAgent(config),\n catch: (cause) =>\n new ProcessSpawnError({\n command: 'claude',\n args: [],\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n }\n\n isRunning(agentId: string): Effect.Effect<boolean> {\n return Effect.sync(() => this.inner.isRunning(agentId));\n }\n}\n\n/** Effect-flavored constructor companion to {@link createClaudeCodeRuntimeSync}. */\nexport function createClaudeCodeRuntime(): ClaudeCodeRuntime {\n return new ClaudeCodeRuntime();\n}\n","/**\n * Pi Coding Agent JSONL parser (PAN-636).\n *\n * Pi sessions live at:\n * ~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<session-id>.jsonl\n *\n * Format (Pi v3):\n * - First line: { type: \"session\", version: 3, id, timestamp, cwd }\n * - Subsequent: every entry has an opaque `id` and a `parentId`. parentId\n * is null for the first non-session entry.\n * - The graph is a tree (Pi supports forking/branching), not a list.\n *\n * To compute usage for the *active* branch we walk leaf -> root via parentId\n * from the latest leaf (max timestamp among entries that have no children).\n * Pi assistant messages report per-call usage — input/output tokens are NOT\n * cumulative across the conversation — so summing along the active branch is\n * correct without subtracting pre-compaction input.\n *\n * SessionUsage shape comes from jsonl-parser.ts (the Claude Code parser); we\n * map into it unchanged so all upstream consumers keep working.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { Effect } from 'effect';\nimport type { SessionUsage } from './jsonl-parser.js';\nimport type { TokenUsage } from '../cost.js';\nimport { FsError } from '../errors.js';\n\n// Minimal entry shape we care about. Anything else is ignored.\ninterface PiSessionRoot {\n type: 'session';\n version: number;\n id: string;\n timestamp: string;\n cwd?: string;\n}\n\ninterface PiUsage {\n input?: number;\n output?: number;\n cacheRead?: number;\n cacheWrite?: number;\n totalTokens?: number;\n cost?: {\n input?: number;\n output?: number;\n cacheRead?: number;\n cacheWrite?: number;\n total?: number;\n };\n}\n\ninterface PiMessageEntry {\n type: 'message';\n id: string;\n parentId: string | null;\n timestamp: string;\n message: {\n role: 'user' | 'assistant' | 'toolResult';\n content?: unknown;\n model?: string;\n provider?: string;\n usage?: PiUsage;\n };\n}\n\ninterface PiGenericEntry {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n // any other fields permitted; we only inspect them when type is known.\n [key: string]: unknown;\n}\n\ntype PiEntry = PiMessageEntry | PiGenericEntry;\n\nfunction isMessage(entry: PiEntry): entry is PiMessageEntry {\n return entry.type === 'message';\n}\n\nfunction isAssistantMessage(entry: PiEntry): entry is PiMessageEntry {\n return isMessage(entry) && entry.message?.role === 'assistant';\n}\n\nfunction tsMs(timestamp: string | undefined): number {\n if (!timestamp) return 0;\n const t = Date.parse(timestamp);\n return Number.isFinite(t) ? t : 0;\n}\n\ninterface ParseResult {\n ok: boolean;\n reason?: string;\n usage?: SessionUsage;\n}\n\n// Known top-level entry types. Anything else is logged once per session.\nconst KNOWN_TYPES: ReadonlySet<string> = new Set([\n 'session',\n 'message',\n 'model_change',\n 'thinking_level_change',\n 'compaction_start',\n 'compaction_end',\n 'session_before_compact',\n 'session_compact',\n 'session_before_switch',\n 'session_before_fork',\n 'session_start',\n]);\n\n/**\n * Parse a Pi session JSONL file into a SessionUsage record.\n *\n * Returns null if the file does not exist, is empty, or is unparseable. Errors\n * during line parsing are tolerated (the line is skipped); we never throw.\n */\nexport function parsePiSessionSync(filePath: string): SessionUsage | null {\n if (!existsSync(filePath)) return null;\n const result = parsePiSessionContent(readFileSync(filePath, 'utf8'), filePath);\n return result.ok ? result.usage! : null;\n}\n\n/**\n * Parse Pi session JSONL content. Exposed for tests and callers that already\n * have the file content in memory.\n */\nexport function parsePiSessionContent(content: string, filePath = '<inline>'): ParseResult {\n const lines = content.split('\\n').filter(l => l.trim().length > 0);\n if (lines.length === 0) {\n return { ok: false, reason: 'empty' };\n }\n\n let root: PiSessionRoot | null = null;\n const entriesById = new Map<string, PiEntry>();\n const childrenOf = new Map<string | null, string[]>();\n const unknownTypesSeen = new Set<string>();\n\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== 'object') continue;\n const entry = parsed as PiEntry;\n\n if (entry.type === 'session') {\n root = entry as unknown as PiSessionRoot;\n continue;\n }\n\n if (!entry.id) continue;\n\n if (!KNOWN_TYPES.has(entry.type)) {\n if (!unknownTypesSeen.has(entry.type)) {\n unknownTypesSeen.add(entry.type);\n // eslint-disable-next-line no-console\n console.debug(`[pi-parser] unknown entry type \"${entry.type}\" in ${filePath} — ignoring`);\n }\n // Still track the entry in the graph so downstream branches stay linked.\n }\n\n entriesById.set(entry.id, entry);\n const list = childrenOf.get(entry.parentId ?? null);\n if (list) list.push(entry.id);\n else childrenOf.set(entry.parentId ?? null, [entry.id]);\n }\n\n if (!root) {\n return { ok: false, reason: 'missing-session-root' };\n }\n\n if (entriesById.size === 0) {\n // Session with only the root entry — return a minimal SessionUsage.\n return {\n ok: true,\n usage: emptyUsage(filePath, root),\n };\n }\n\n // Find leaves: entries that have no children.\n const leaves: PiEntry[] = [];\n for (const entry of entriesById.values()) {\n const kids = childrenOf.get(entry.id);\n if (!kids || kids.length === 0) leaves.push(entry);\n }\n if (leaves.length === 0) {\n // Cycle or all entries have children — fall back to \"pick latest entry\".\n leaves.push(...entriesById.values());\n }\n\n // Pick the latest leaf by timestamp; tie-break by file order via id appearance.\n let latestLeaf: PiEntry = leaves[0]!;\n for (const leaf of leaves) {\n if (tsMs(leaf.timestamp) > tsMs(latestLeaf.timestamp)) {\n latestLeaf = leaf;\n }\n }\n\n // Walk leaf -> root via parentId, collecting entries in reverse-chronological order.\n const branch: PiEntry[] = [];\n const visited = new Set<string>();\n let cursor: PiEntry | undefined = latestLeaf;\n while (cursor) {\n if (visited.has(cursor.id)) break; // cycle guard\n visited.add(cursor.id);\n branch.push(cursor);\n if (!cursor.parentId) break;\n cursor = entriesById.get(cursor.parentId);\n }\n branch.reverse(); // chronological\n\n // Aggregate usage and per-model breakdown across assistant messages on the\n // active branch.\n const totals: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n let costV2 = 0;\n let messageCount = 0;\n let costRecomputedFromTokens = 0;\n const modelBreakdown: Record<\n string,\n { cost: number; inputTokens: number; outputTokens: number; messageCount: number }\n > = {};\n const modelsInOrder: string[] = [];\n let compactionCount = 0;\n\n let firstTs: string | null = null;\n let lastTs: string | null = null;\n\n for (const entry of branch) {\n if (entry.type === 'compaction_start' || entry.type === 'session_compact') {\n compactionCount += 1;\n }\n if (!firstTs && entry.timestamp) firstTs = entry.timestamp;\n if (entry.timestamp) lastTs = entry.timestamp;\n\n if (!isAssistantMessage(entry)) continue;\n const usage = entry.message.usage;\n if (!usage) continue;\n const model = entry.message.model || 'unknown';\n const input = usage.input ?? 0;\n const output = usage.output ?? 0;\n const cacheRead = usage.cacheRead ?? 0;\n const cacheWrite = usage.cacheWrite ?? 0;\n const cost = usage.cost?.total ?? 0;\n const localComputed =\n (usage.cost?.input ?? 0) +\n (usage.cost?.output ?? 0) +\n (usage.cost?.cacheRead ?? 0) +\n (usage.cost?.cacheWrite ?? 0);\n\n totals.inputTokens += input;\n totals.outputTokens += output;\n totals.cacheReadTokens = (totals.cacheReadTokens ?? 0) + cacheRead;\n totals.cacheWriteTokens = (totals.cacheWriteTokens ?? 0) + cacheWrite;\n costV2 += cost;\n costRecomputedFromTokens += localComputed;\n messageCount += 1;\n\n if (!modelBreakdown[model]) {\n modelBreakdown[model] = { cost: 0, inputTokens: 0, outputTokens: 0, messageCount: 0 };\n modelsInOrder.push(model);\n }\n const slot = modelBreakdown[model]!;\n slot.cost += cost;\n slot.inputTokens += input;\n slot.outputTokens += output;\n slot.messageCount += 1;\n }\n\n // Observability: log Pi-inline-cost vs locally-recomputed-from-cost-fields\n // delta. This catches cases where Pi's pricing tables drift inside Pi\n // itself; we always trust costV2 (Pi's inline total).\n if (Math.abs(costV2 - costRecomputedFromTokens) > 1e-9) {\n // eslint-disable-next-line no-console\n console.debug(\n `[pi-parser] inline cost vs sum-of-line-items delta=${(costV2 - costRecomputedFromTokens).toFixed(6)} in ${filePath}`,\n );\n }\n if (compactionCount > 0) {\n // eslint-disable-next-line no-console\n console.debug(\n `[pi-parser] active branch in ${filePath} has ${compactionCount} compaction event(s) — usage summed per-message (no double-count)`,\n );\n }\n\n const display =\n modelsInOrder.length === 0\n ? 'unknown'\n : modelsInOrder.length === 1\n ? modelsInOrder[0]!\n : modelsInOrder.join(' → ');\n\n return {\n ok: true,\n usage: {\n sessionId: root.id,\n sessionFile: filePath,\n startTime: firstTs ?? root.timestamp,\n endTime: lastTs ?? root.timestamp,\n model: display,\n usage: totals,\n cost: costV2,\n cost_v2: costV2,\n messageCount,\n modelBreakdown,\n },\n };\n}\n\nfunction emptyUsage(filePath: string, root: PiSessionRoot): SessionUsage {\n return {\n sessionId: root.id,\n sessionFile: filePath,\n startTime: root.timestamp,\n endTime: root.timestamp,\n model: 'unknown',\n usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },\n cost: 0,\n cost_v2: 0,\n messageCount: 0,\n modelBreakdown: {},\n };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** Effect variant of parsePiSession. */\nexport const parsePiSession = (\n filePath: string,\n): Effect.Effect<SessionUsage | null, FsError> =>\n Effect.try({\n try: () => parsePiSessionSync(filePath),\n catch: (cause) => new FsError({ path: filePath, operation: 'parsePiSession', cause }),\n });\n","/**\n * Pi Coding Agent runtime adapter (PAN-636).\n *\n * Implements AgentRuntime for the `pi` binary running in --mode rpc. The\n * adapter wires together three pieces:\n *\n * - pi-fifo.ts — per-agent rpc.in named pipe lifecycle\n * - launcher-generator — emits the `exec pi --mode rpc ... < <fifo>` script\n * - pi-extension — the vendored extension that writes ready.json,\n * heartbeats, and /pan-done markers from inside Pi\n *\n * Spawn flow:\n * 1. mkdir agent dir\n * 2. createPiFifo\n * 3. write launcher.sh via generateLauncherScript({ harness: 'pi', ... })\n * 4. tmux new-session running launcher.sh\n * 5. wait for ~/.overdeck/agents/<id>/ready.json (max 30s)\n *\n * Heartbeat sources (in priority order):\n * 1. fresh (<60s) ~/.overdeck/heartbeats/<id>.json — written by the\n * extension on tool_execution_end / turn_end\n * 2. mtime of the active session JSONL under <agentDir>/sessions/\n * 3. tmux pane activity timestamp\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { promisify } from 'node:util'\nimport { exec } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { Effect } from 'effect'\nimport type {\n AgentRuntime,\n AgentRuntimeSync,\n AgentRuntimeError,\n HarnessBehavior,\n Heartbeat,\n TokenUsage,\n CostBreakdown,\n Session,\n SpawnConfig,\n Agent,\n} from './types.js'\nimport { OHMYPI_BEHAVIOR } from './behavior.js'\nimport { sessionExists, killSession, createSession, listSessionsSync, getManagedTmuxSocketName } from '../tmux.js'\nimport { parsePiSessionSync } from '../cost-parsers/pi-parser.js'\nimport { prepareHarnessLaunch } from '../harness-binary.js'\nimport { generateLauncherScriptSync } from '../launcher-generator.js'\nimport { createPiFifo, destroyPiFifoSync, writePiCommandSync, piFifoPaths, PiNotReady } from './pi-fifo.js'\nimport { ProcessSpawnError, ProcessTimeoutError, TmuxError } from '../errors.js'\nimport { getOverdeckHome } from '../paths.js'\n\nconst execAsync = promisify(exec)\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nconst ACTIVE_HEARTBEAT_TTL_MS = 60_000\nconst SPAWN_READY_TIMEOUT_MS = 30_000\n\nfunction overdeckDir(): string {\n return getOverdeckHome()\n}\nfunction heartbeatsDir(): string {\n return join(overdeckDir(), 'heartbeats')\n}\nfunction agentsDir(): string {\n return join(overdeckDir(), 'agents')\n}\n\nexport class PiSpawnTimeout extends Error {\n readonly code = 'PI_SPAWN_TIMEOUT' as const\n constructor(agentId: string) {\n super(`Pi agent ${agentId} did not write ready.json within ${SPAWN_READY_TIMEOUT_MS}ms`)\n this.name = 'PiSpawnTimeout'\n }\n}\n\nexport interface PiSpawnConfig extends SpawnConfig {\n /** Absolute path to packages/pi-extension/dist/index.js. */\n piExtensionPath: string\n /** Optional extra args appended to the `pi` command line. */\n extraPiArgs?: string\n}\n\nfunction agentDirFor(agentId: string): string {\n return join(agentsDir(), agentId)\n}\n\nfunction piSessionDirFor(agentId: string): string {\n return join(agentDirFor(agentId), 'sessions')\n}\n\nfunction readyPathFor(agentId: string): string {\n return piFifoPaths(agentId).readyPath\n}\n\nfunction sessionIdPathFor(agentId: string): string {\n return join(agentDirFor(agentId), 'session.id')\n}\n\n/**\n * Read the persisted Pi session id for resume (PAN-636 workspace-3119).\n * Returns null when the file is absent (fresh agent or post-deep-wipe) or\n * unreadable. The pi-extension writes this file on every session_start with\n * a non-null sessionId; killAgent intentionally preserves it so the next\n * spawn can resume.\n */\nfunction readStoredSessionId(agentId: string): string | null {\n const path = sessionIdPathFor(agentId)\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf8').trim()\n return raw || null\n } catch {\n return null\n }\n}\n\n/**\n * PAN-1988: resolve the REAL pi session id from the freshest session JSONL. The `session.id` file\n * holds a spawn-time placeholder for non-claude harnesses (or can be missing), so resuming from it\n * makes pi drift into a fresh session and lose conversation history — the same bug fixed for codex's\n * rollout. The session JSONL always carries the real id (`parsed.sessionId`). Returns null when\n * there is no prior session on disk. Picks the freshest by mtime, tolerating nested cwd subdirs.\n */\nfunction resolveLatestPiSessionId(agentId: string): string | null {\n const root = piSessionDirFor(agentId)\n if (!existsSync(root)) return null\n const files: { path: string; mtime: number }[] = []\n walkJsonl(root, files)\n if (files.length === 0) return null\n files.sort((a, b) => b.mtime - a.mtime)\n const parsed = parsePiSessionSync(files[0]!.path)\n return parsed?.sessionId ?? null\n}\n\n// PAN-1989: Pi is legacy; no longer implements AgentRuntimeSync (removed from\n// RuntimeName union). Class is kept for backward-compat exports only.\nexport class PiRuntimeSync {\n readonly name: string = 'pi'\n\n getHarnessBehavior(): HarnessBehavior {\n return OHMYPI_BEHAVIOR\n }\n\n /** Resolve the latest Pi session JSONL for an agent. Pi nests files under\n * <agentDir>/sessions/<encoded-cwd>/<timestamp>_<id>.jsonl, but we tolerate\n * any depth and just pick the freshest .jsonl by mtime. */\n getSessionPath(agentId: string): string | null {\n const root = piSessionDirFor(agentId)\n if (!existsSync(root)) return null\n const files: { path: string; mtime: number }[] = []\n walkJsonl(root, files)\n if (files.length === 0) return null\n files.sort((a, b) => b.mtime - a.mtime)\n return files[0]!.path\n }\n\n getLastActivity(agentId: string): Date | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n try {\n return statSync(session).mtime\n } catch {\n return null\n }\n }\n\n getHeartbeat(agentId: string): Heartbeat | null {\n // 1. Fresh active heartbeat (<60s old).\n const heartbeatPath = join(heartbeatsDir(), `${agentId}.json`)\n if (existsSync(heartbeatPath)) {\n try {\n const data = JSON.parse(readFileSync(heartbeatPath, 'utf-8')) as {\n timestamp?: string\n tool_name?: string\n last_action?: string\n pid?: number\n }\n if (data.timestamp) {\n const ts = new Date(data.timestamp)\n if (Date.now() - ts.getTime() < ACTIVE_HEARTBEAT_TTL_MS) {\n return {\n timestamp: ts,\n agentId,\n source: 'active-heartbeat',\n confidence: 'high',\n toolName: data.tool_name,\n lastAction: data.last_action,\n pid: data.pid,\n }\n }\n }\n } catch {\n // fall through\n }\n }\n\n // 2. JSONL mtime.\n const lastActivity = this.getLastActivity(agentId)\n if (lastActivity) {\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n }\n }\n\n // 3. tmux session creation time (best-effort).\n try {\n const sess = listSessionsSync().find(s => s.name === agentId)\n if (sess) {\n return {\n timestamp: sess.created,\n agentId,\n source: 'tmux',\n confidence: 'low',\n }\n }\n } catch {\n // ignore\n }\n return null\n }\n\n getTokenUsage(agentId: string): TokenUsage | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n const parsed = parsePiSessionSync(session)\n return parsed?.usage ?? null\n }\n\n /** Pi reports per-message inline cost.total. We pass that through as the\n * totalCost and back out per-bucket costs from the parser's modelBreakdown\n * is not informative (Pi does not report cache vs input vs output cost\n * separately on a session level), so we leave the per-bucket fields zero. */\n getSessionCost(agentId: string): CostBreakdown | null {\n const session = this.getSessionPath(agentId)\n if (!session) return null\n const parsed = parsePiSessionSync(session)\n if (!parsed) return null\n const totalCost = parsed.cost_v2 ?? parsed.cost ?? 0\n return {\n inputCost: 0,\n outputCost: 0,\n cacheReadCost: 0,\n cacheWriteCost: 0,\n totalCost,\n currency: 'USD',\n }\n }\n\n async sendMessage(agentId: string, message: string): Promise<void> {\n if (!existsSync(readyPathFor(agentId))) {\n throw new PiNotReady(`Pi agent ${agentId}: ready.json not present yet`)\n }\n // steer: Pi delivers immediately when idle and queues mid-turn; a bare\n // prompt is rejected with AgentBusyError while a run is active.\n writePiCommandSync(agentId, { id: randomUUID(), type: 'prompt', message, streamingBehavior: 'steer' })\n }\n\n /**\n * Kill a Pi agent via the documented escalation ladder (PAN-636 bead 8qco).\n *\n * 1. Send {cmd:'abort'} via the RPC fifo (graceful — Pi can flush\n * session.jsonl and write any pending heartbeat).\n * 2. Wait up to 2s for the tmux session to disappear on its own.\n * 3. SIGTERM the Pi process group via tmux send-signal -s TERM.\n * 4. Wait up to 5s for the tmux session to disappear.\n * 5. tmux kill-session (effectively SIGKILL on the pane).\n * 6. Always unlink rpc.in, ready.json, heartbeat, and completed marker.\n * NEVER touch session JSONL files under <agentDir>/sessions/ — per\n * the JSONL-is-sacred rule in CLAUDE.md.\n */\n async killAgent(agentId: string): Promise<void> {\n // Step 1: graceful RPC abort.\n try {\n writePiCommandSync(agentId, { id: randomUUID(), type: 'abort' })\n } catch {\n // No reader / not ready — fall through to signal escalation.\n }\n\n // Step 2: poll for tmux exit up to 2s.\n if (await pollUntilSessionGone(agentId, 2_000)) {\n cleanupPiTransientFiles(agentId)\n return\n }\n\n // Step 3: SIGTERM via tmux. Best-effort — if it fails the kill-session\n // fallback below will still take the pane down.\n try {\n await execAsync(`tmux -L ${shellQuote(getManagedTmuxSocketName())} send-keys -t ${agentId} C-c 2>/dev/null || true`)\n } catch {\n // ignore\n }\n // PAN-1798: never use cmdline-match kills (pkill -f) here. If the shared\n // tmux server was founded implicitly by this agent/conversation, its\n // cmdline embeds the session name and pkill would destroy the entire\n // server. Resolve the pane PID and signal only that process group.\n try {\n const { stdout } = await execAsync(\n `tmux -L ${shellQuote(getManagedTmuxSocketName())} list-panes -t ${shellQuote(agentId)} -F '#{pane_pid}' 2>/dev/null`\n )\n const pid = stdout.trim()\n if (pid) {\n await execAsync(`kill -TERM -- -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`)\n }\n } catch {\n // ignore\n }\n\n // Step 4: poll for tmux exit up to another 5s (7s total budget).\n if (await pollUntilSessionGone(agentId, 5_000)) {\n cleanupPiTransientFiles(agentId)\n return\n }\n\n // Step 5: SIGKILL fallback via tmux kill-session.\n try {\n if (await Effect.runPromise(sessionExists(agentId))) {\n await Effect.runPromise(killSession(agentId))\n }\n } finally {\n cleanupPiTransientFiles(agentId)\n }\n }\n\n async spawnAgent(config: SpawnConfig & { piExtensionPath?: string }): Promise<Agent> {\n const piExtensionPath = config.piExtensionPath\n if (!piExtensionPath) {\n throw new Error('PiRuntime.spawnAgent requires piExtensionPath in config')\n }\n\n const harnessLaunch = await prepareHarnessLaunch('ohmypi')\n const agentId = config.agentId\n const dir = agentDirFor(agentId)\n const sessionDir = piSessionDirFor(agentId)\n // Detect a prior spawn BEFORE mkdir creates the dir. We treat \"sessions/\n // contains at least one .jsonl\" as the canonical proof of a prior run,\n // since killAgent preserves session jsonls per the JSONL-is-sacred rule.\n const hadPriorSpawn = existsSync(sessionDir) &&\n readdirSync(sessionDir).some((f) => f.endsWith('.jsonl'))\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n mkdirSync(sessionDir, { recursive: true, mode: 0o700 })\n\n // Writer side relies on the fifo existing before tmux starts the launcher.\n const fifoPath = await Effect.runPromise(createPiFifo(agentId))\n\n const promptFile = config.prompt ? writeAgentPromptFile(agentId, config.prompt) : undefined\n\n // Resume into the previously-recorded Pi session if one exists (AC2).\n // AC3: when session.id is missing AFTER a prior spawn (sessions/*.jsonl\n // present) we fall back to a fresh session and warn so the dashboard\n // can surface the divergence. On a first-ever spawn — sessions/ is\n // empty — we stay silent.\n // PAN-1988: resume from the stored `session.id` when present, else recover the REAL session id\n // from the freshest session JSONL rather than spawning fresh and losing history (the old AC3\n // behavior threw the session away whenever the stored id was a spawn-time placeholder or\n // missing). Only when neither yields an id do we start fresh.\n const resumeSessionId = readStoredSessionId(agentId) ?? resolveLatestPiSessionId(agentId) ?? undefined\n if (!resumeSessionId && hadPriorSpawn) {\n console.warn(\n `[pi-runtime] ${agentId}: prior session.jsonl exists but no resumable session id found — spawning a fresh Pi session`,\n )\n }\n\n const launcherScript = generateLauncherScriptSync({\n role: 'work',\n workingDir: config.workspace,\n harness: 'ohmypi',\n piExtensionPath,\n piFifoPath: fifoPath,\n piSessionDir: sessionDir,\n model: config.model,\n promptFile,\n resumeSessionId,\n overdeckEnv: { agentId },\n setTerminalEnv: true,\n extraEnvExports: [harnessLaunch.pathExport],\n trapHup: true,\n })\n\n const launcherPath = join(dir, 'pi-launcher.sh')\n writeFileSync(launcherPath, launcherScript)\n chmodSync(launcherPath, 0o755)\n\n await Effect.runPromise(createSession(agentId, config.workspace, `bash ${launcherPath}`, {\n env: { OVERDECK_AGENT_ID: agentId },\n }))\n\n await waitForReady(agentId, SPAWN_READY_TIMEOUT_MS)\n\n const readyData = readReady(agentId)\n return {\n id: agentId,\n sessionId: readyData?.sessionId ?? 'unknown',\n runtime: 'ohmypi',\n model: config.model ?? 'unknown',\n workspace: config.workspace,\n startedAt: new Date(),\n }\n }\n\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = []\n const root = agentsDir()\n if (!existsSync(root)) return sessions\n for (const agentName of safeReaddir(root)) {\n const sessionRoot = piSessionDirFor(agentName)\n if (!existsSync(sessionRoot)) continue\n const files: { path: string; mtime: number }[] = []\n walkJsonl(sessionRoot, files)\n for (const file of files) {\n const parsed = parsePiSessionSync(file.path)\n if (!parsed) continue\n sessions.push({\n id: parsed.sessionId,\n agentId: agentName,\n workspace: workspace ?? 'unknown',\n model: parsed.model,\n startedAt: new Date(parsed.startTime),\n lastActivity: new Date(file.mtime),\n tokenUsage: parsed.usage,\n })\n }\n }\n return sessions\n }\n\n async isRunning(agentId: string): Promise<boolean> {\n return await Effect.runPromise(sessionExists(agentId))\n }\n}\n\nexport function createPiRuntimeSync(): PiRuntimeSync {\n return new PiRuntimeSync()\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel adapter wrapping the legacy PiRuntime. The class\n// above remains the canonical adapter used by Cloister; this variant is for\n// new Effect-native callers who want typed error channels.\n\n/**\n * Effect-channel variant of {@link PiRuntimeSync}. Lifts async kill/spawn/send\n * methods into typed Effects (ProcessSpawnError / ProcessTimeoutError /\n * TmuxError). PiSpawnTimeout is preserved as the legacy Error class; Effect\n * callers receive ProcessTimeoutError instead.\n */\n// PAN-1989: legacy — does not implement AgentRuntime (see PiRuntimeSync comment above).\nexport class PiRuntime {\n readonly name: string = 'pi'\n private readonly inner: PiRuntimeSync\n\n constructor(inner: PiRuntimeSync = new PiRuntimeSync()) {\n this.inner = inner\n }\n\n getSessionPath(agentId: string): string | null {\n return this.inner.getSessionPath(agentId)\n }\n getHarnessBehavior(): HarnessBehavior {\n return this.inner.getHarnessBehavior()\n }\n getLastActivity(agentId: string): Date | null {\n return this.inner.getLastActivity(agentId)\n }\n getHeartbeat(agentId: string): Heartbeat | null {\n return this.inner.getHeartbeat(agentId)\n }\n getTokenUsage(agentId: string): TokenUsage | null {\n return this.inner.getTokenUsage(agentId)\n }\n getSessionCost(agentId: string): CostBreakdown | null {\n return this.inner.getSessionCost(agentId)\n }\n listSessions(workspace?: string): Session[] {\n return this.inner.listSessions(workspace)\n }\n\n sendMessage(agentId: string, message: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => Promise.resolve(this.inner.sendMessage(agentId, message)),\n catch: (cause) =>\n new TmuxError({\n command: 'rpc-write',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n })\n }\n\n killAgent(agentId: string): Effect.Effect<void, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.killAgent(agentId),\n catch: (cause) =>\n new TmuxError({\n command: 'kill-session',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n })\n }\n\n spawnAgent(config: SpawnConfig): Effect.Effect<Agent, AgentRuntimeError> {\n return Effect.tryPromise({\n try: () => this.inner.spawnAgent(config),\n catch: (cause) => {\n if (cause instanceof PiSpawnTimeout) {\n return new ProcessTimeoutError({\n command: 'pi',\n args: ['--mode', 'rpc'],\n timeoutMs: SPAWN_READY_TIMEOUT_MS,\n })\n }\n return new ProcessSpawnError({\n command: 'pi',\n args: ['--mode', 'rpc'],\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n })\n },\n })\n }\n\n isRunning(agentId: string): Effect.Effect<boolean> {\n return Effect.promise(() => this.inner.isRunning(agentId))\n }\n}\n\n/** Effect-flavored constructor companion to {@link createPiRuntimeSync}. */\nexport function createPiRuntime(): PiRuntime {\n return new PiRuntime()\n}\n\n// ────────────────────────────────────────────────────────────────────────\n// helpers\n// ────────────────────────────────────────────────────────────────────────\n\nasync function pollUntilSessionGone(agentId: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (!(await Effect.runPromise(sessionExists(agentId)))) return true\n await new Promise(r => setTimeout(r, 100))\n }\n return false\n}\n\n/**\n * Remove Pi's transient per-agent files: rpc.in fifo, ready.json,\n * heartbeats/<id>.json, completed marker. Never touches session JSONLs\n * under <agentDir>/sessions/ — those are sacred (CLAUDE.md).\n */\nfunction cleanupPiTransientFiles(agentId: string): void {\n try { destroyPiFifoSync(agentId) } catch { /* ignore */ }\n const dir = agentDirFor(agentId)\n for (const transient of ['ready.json', 'completed']) {\n const path = join(dir, transient)\n if (existsSync(path)) {\n try { unlinkSync(path) } catch { /* ignore */ }\n }\n }\n const heartbeatPath = join(heartbeatsDir(), `${agentId}.json`)\n if (existsSync(heartbeatPath)) {\n try { unlinkSync(heartbeatPath) } catch { /* ignore */ }\n }\n // Sacred: NEVER touch <dir>/sessions/*.jsonl — JSONLs are conversation history.\n}\n\nfunction readReady(agentId: string): { sessionId?: string; reason?: string } | null {\n const path = readyPathFor(agentId)\n if (!existsSync(path)) return null\n try {\n return JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return null\n }\n}\n\nasync function waitForReady(agentId: string, timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs\n const path = readyPathFor(agentId)\n while (Date.now() < deadline) {\n if (existsSync(path)) return\n await new Promise(r => setTimeout(r, 100))\n }\n throw new PiSpawnTimeout(agentId)\n}\n\nfunction writeAgentPromptFile(agentId: string, prompt: string): string {\n const promptPath = join(agentDirFor(agentId), 'prompt.txt')\n writeFileSync(promptPath, prompt)\n return promptPath\n}\n\nfunction safeReaddir(dir: string): string[] {\n try {\n return readdirSync(dir)\n } catch {\n return []\n }\n}\n\nfunction walkJsonl(dir: string, out: { path: string; mtime: number }[]): void {\n let entries: string[]\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const entry of entries) {\n const full = join(dir, entry)\n let stat\n try {\n stat = statSync(full)\n } catch {\n continue\n }\n if (stat.isDirectory()) {\n walkJsonl(full, out)\n } else if (stat.isFile() && entry.endsWith('.jsonl')) {\n out.push({ path: full, mtime: stat.mtime.getTime() })\n }\n }\n}\n\n// Keep imports referenced even when only the types are used.\nvoid execAsync\nvoid tmpdir\n","import { exec } from 'node:child_process'\nimport { getManagedTmuxSocketName } from '../tmux.js';\nimport { existsSync, readFileSync, rmSync, statSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { join } from 'node:path'\nimport { promisify } from 'node:util'\n\nimport type { AgentState } from '../agents/agent-state.js'\nimport { materializeAcpContextFile } from '../acp/context.js'\nimport { listAgentStates } from '../agents/queries.js'\nimport { BRIDGE_TOKEN_HEADER } from '../bridge-token.js'\nimport { prepareHarnessLaunch } from '../harness-binary.js'\nimport { resolveKimiNativeEffort } from '../kimi-effort.js'\nimport { getOverdeckHome, packageRoot } from '../paths.js'\nimport { getRuntimeBehavior } from './behavior.js'\nimport {\n tmuxCreateSession,\n tmuxKillSession,\n tmuxSessionExists,\n} from './tmux-cli.js'\nimport type {\n Agent,\n AgentRuntimeSync,\n CostBreakdown,\n HarnessBehavior,\n Heartbeat,\n Session,\n SpawnConfig,\n TokenUsage,\n} from './types.js'\n\nconst execAsync = promisify(exec)\nconst HOST_REQUEST_TIMEOUT_MS = 5_000\nconst SPAWN_READY_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 100\n\ninterface AcpHostResponse {\n readonly status: number\n readonly body: string\n}\n\nexport interface AcpSpawnConfig extends SpawnConfig {\n readonly provider?: string\n}\n\nexport interface AcpRuntimeOptions {\n readonly name?: 'acp' | 'opencode'\n readonly provider?: string\n readonly overdeckHome?: string\n readonly execCommand?: (command: string) => Promise<{ readonly stdout: string }>\n readonly prepareLaunch?: () => Promise<{ readonly binaryPath: string; readonly pathExport: string }>\n readonly listAgentStates?: () => AgentState[]\n}\n\nexport class AcpSpawnTimeout extends Error {\n readonly code = 'ACP_SPAWN_TIMEOUT' as const\n\n constructor(agentId: string) {\n super(`ACP agent ${agentId} did not write acp-session-id within ${SPAWN_READY_TIMEOUT_MS}ms`)\n this.name = 'AcpSpawnTimeout'\n }\n}\n\nexport class AcpRuntimeSync implements AgentRuntimeSync {\n readonly name: 'acp' | 'opencode'\n private readonly provider: string\n private readonly overdeckHome: string | undefined\n private readonly execCommand: (command: string) => Promise<{ readonly stdout: string }>\n private readonly prepareLaunch: () => Promise<{ readonly binaryPath: string; readonly pathExport: string }>\n private readonly resolveAgentStates: () => AgentState[]\n\n constructor(options: AcpRuntimeOptions = {}) {\n this.name = options.name ?? 'acp'\n this.provider = options.provider ?? (this.name === 'opencode' ? 'opencode' : 'kimi')\n this.overdeckHome = options.overdeckHome\n this.execCommand = options.execCommand ?? execAsync\n this.prepareLaunch = options.prepareLaunch ?? (() => prepareHarnessLaunch(this.name))\n this.resolveAgentStates = options.listAgentStates ?? (() => listAgentStates())\n }\n\n getHarnessBehavior(): HarnessBehavior {\n return getRuntimeBehavior(this.name)\n }\n\n getSessionPath(agentId: string): string {\n return join(this.home(), 'agents', agentId, 'acp-session.jsonl')\n }\n\n getLastActivity(agentId: string): Date | null {\n try {\n return statSync(this.getSessionPath(agentId)).mtime\n } catch {\n return null\n }\n }\n\n getHeartbeat(agentId: string): Heartbeat | null {\n const lastActivity = this.getLastActivity(agentId)\n if (!lastActivity) return null\n\n return {\n timestamp: lastActivity,\n agentId,\n source: 'jsonl',\n confidence: 'medium',\n }\n }\n\n getTokenUsage(_agentId: string): TokenUsage | null {\n return null\n }\n\n getSessionCost(_agentId: string): CostBreakdown | null {\n return null\n }\n\n async sendMessage(agentId: string, message: string): Promise<void> {\n const socketPath = this.socketPath(agentId)\n if (!existsSync(socketPath)) {\n throw new Error(`ACP agent ${agentId}: host socket is not available`)\n }\n const token = this.readToken(agentId)\n if (!token) {\n throw new Error(`ACP agent ${agentId}: missing acp-token`)\n }\n\n await postUnixSocketJson(socketPath, token, {\n op: 'message',\n content: message,\n }, HOST_REQUEST_TIMEOUT_MS)\n }\n\n async killAgent(agentId: string): Promise<void> {\n const socketPath = this.socketPath(agentId)\n const token = this.readToken(agentId)\n if (existsSync(socketPath) && token) {\n try {\n await postUnixSocketJson(socketPath, token, { op: 'interrupt' }, HOST_REQUEST_TIMEOUT_MS)\n } catch {\n // Best effort: the process signal ladder below still tears down the host.\n }\n }\n\n if (await pollUntilSessionGone(agentId, 2_000)) return\n\n let panePid: string | null = null\n try {\n const { stdout } = await this.execCommand(\n `tmux -L ${shellQuote(getManagedTmuxSocketName())} list-panes -t ${shellQuote(agentId)} -F '#{pane_pid}' 2>/dev/null`,\n )\n panePid = stdout.trim() || null\n if (panePid) {\n await this.execCommand(\n `kill -TERM -- -${panePid} 2>/dev/null || kill -TERM ${panePid} 2>/dev/null || true`,\n )\n }\n } catch {\n // Best effort: the remaining kill ladder still tears down the tmux session.\n }\n\n if (await pollUntilSessionGone(agentId, 5_000)) return\n if (panePid) {\n try {\n await this.execCommand(\n `kill -KILL -- -${panePid} 2>/dev/null || kill -KILL ${panePid} 2>/dev/null || true`,\n )\n } catch {\n // Best effort: tmuxKillSession below removes any remaining session.\n }\n }\n if (await tmuxSessionExists(agentId)) await tmuxKillSession(agentId)\n }\n\n async spawnAgent(config: AcpSpawnConfig): Promise<Agent> {\n if (this.name === 'opencode' && !config.model) throw new Error('OpenCode requires an explicit model')\n const provider = config.provider ?? (this.name === 'opencode' && config.model?.startsWith('opencode-go/') ? 'opencode-go' : this.provider)\n const { binaryPath } = await this.prepareLaunch()\n const contextFile = materializeAcpContextFile(\n join(this.home(), 'agents', config.agentId),\n config.workspace,\n this.name,\n )\n const command = [\n 'node',\n shellQuote(join(packageRoot, 'dist', 'acp-host.js')),\n '--agent',\n shellQuote(config.agentId),\n '--provider',\n shellQuote(provider),\n '--workspace',\n shellQuote(config.workspace),\n '--binary-path',\n shellQuote(binaryPath),\n '--context-file',\n shellQuote(contextFile),\n ]\n if (this.name === 'opencode' && config.effort) command.push('--effort', shellQuote(config.effort))\n if (config.sessionId) command.push('--resume', shellQuote(config.sessionId))\n if (config.model) command.push('--model', shellQuote(config.model))\n if (provider === 'kimi' && config.model) {\n const effort = resolveKimiNativeEffort(config.model, config.effort)\n if (effort) command.push('--effort', shellQuote(effort))\n }\n\n rmSync(this.agentPath(config.agentId, 'acp-session-id'), { force: true })\n rmSync(this.agentPath(config.agentId, 'acp-launch-error'), { force: true })\n\n await tmuxCreateSession(\n config.agentId,\n config.workspace,\n command.join(' '),\n {\n ...config.env,\n OVERDECK_AGENT_ID: config.agentId,\n },\n )\n\n try {\n const sessionId = await this.waitForSessionId(config.agentId)\n if (!sessionId) throw new AcpSpawnTimeout(config.agentId)\n\n if (config.prompt) await this.sendMessage(config.agentId, config.prompt)\n\n return {\n id: config.agentId,\n sessionId,\n runtime: this.name,\n model: config.model ?? '',\n workspace: config.workspace,\n startedAt: new Date(),\n }\n } catch (error) {\n if (await tmuxSessionExists(config.agentId)) await tmuxKillSession(config.agentId)\n throw error\n }\n }\n\n listSessions(workspace?: string): Session[] {\n const sessions: Session[] = []\n for (const state of this.resolveAgentStates()) {\n if (state.harness !== this.name) continue\n if (workspace && state.workspace !== workspace) continue\n\n const sessionId = this.readSessionId(state.id)\n if (!sessionId) continue\n\n const sessionPath = this.getSessionPath(state.id)\n let lastActivity: Date\n try {\n lastActivity = statSync(sessionPath).mtime\n } catch {\n try {\n lastActivity = statSync(this.agentPath(state.id, 'acp-session-id')).mtime\n } catch {\n continue\n }\n }\n\n sessions.push({\n id: sessionId,\n agentId: state.id,\n workspace: state.workspace,\n model: state.model ?? '',\n startedAt: new Date(state.startedAt),\n lastActivity,\n tokenUsage: { inputTokens: 0, outputTokens: 0 },\n })\n }\n return sessions\n }\n\n async isRunning(agentId: string): Promise<boolean> {\n return tmuxSessionExists(agentId)\n }\n\n private home(): string {\n return this.overdeckHome ?? getOverdeckHome()\n }\n\n private agentPath(agentId: string, file: string): string {\n return join(this.home(), 'agents', agentId, file)\n }\n\n private socketPath(agentId: string): string {\n return join(this.home(), 'sockets', `acp-${agentId}.sock`)\n }\n\n private readToken(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-token'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private readLaunchError(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-launch-error'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private readSessionId(agentId: string): string | null {\n try {\n return readFileSync(this.agentPath(agentId, 'acp-session-id'), 'utf8').trim() || null\n } catch {\n return null\n }\n }\n\n private async waitForSessionId(agentId: string): Promise<string | null> {\n const deadline = Date.now() + SPAWN_READY_TIMEOUT_MS\n while (Date.now() < deadline) {\n const launchError = this.readLaunchError(agentId)\n if (launchError) throw new Error(`ACP host ${agentId} failed to start: ${launchError}`)\n const sessionId = this.readSessionId(agentId)\n if (sessionId) return sessionId\n await delay(POLL_INTERVAL_MS)\n }\n return null\n }\n}\n\nasync function pollUntilSessionGone(agentId: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (!(await tmuxSessionExists(agentId))) return true\n await delay(POLL_INTERVAL_MS)\n }\n return false\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nfunction postUnixSocketJson(\n socketPath: string,\n token: string,\n body: unknown,\n timeoutMs?: number,\n): Promise<AcpHostResponse> {\n return new Promise((resolve, reject) => {\n let settled = false\n let timeout: NodeJS.Timeout | undefined\n const finish = (callback: () => void) => {\n if (settled) return\n settled = true\n if (timeout) clearTimeout(timeout)\n callback()\n }\n const client = request(\n {\n socketPath,\n path: '/',\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n [BRIDGE_TOKEN_HEADER]: token,\n },\n },\n (response) => {\n const chunks: Buffer[] = []\n response.on('data', (chunk: Buffer) => chunks.push(chunk))\n response.on('end', () => {\n const responseBody = Buffer.concat(chunks).toString('utf8')\n const status = response.statusCode ?? 0\n if (status < 200 || status >= 300) {\n finish(() => reject(new Error(`ACP host returned HTTP ${status}: ${responseBody}`)))\n return\n }\n finish(() => resolve({ status, body: responseBody }))\n })\n },\n )\n if (timeoutMs !== undefined) {\n timeout = setTimeout(() => {\n client.destroy(new Error(`ACP host request timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n timeout.unref?.()\n }\n client.once('error', (error) => finish(() => reject(error)))\n client.end(JSON.stringify(body))\n })\n}\n\nexport function createAcpRuntimeSync(options: AcpRuntimeOptions = {}): AcpRuntimeSync {\n return new AcpRuntimeSync(options)\n}\n","import { createMuseRuntimeSync } from './muse.js';\nexport { MuseRuntimeSync, createMuseRuntimeSync } from './muse.js';\n/**\n * Cloister Runtime Abstraction\n *\n * Export all runtime types and provide a unified registry for managing\n * multiple AI coding assistant runtimes.\n */\n\nexport * from './types.js';\nexport * from './behavior.js';\nexport {\n ClaudeCodeRuntimeSync,\n ClaudeCodeRuntime,\n createClaudeCodeRuntimeSync,\n createClaudeCodeRuntime,\n} from './claude-code.js';\nexport {\n PiRuntimeSync,\n PiRuntime,\n createPiRuntimeSync,\n createPiRuntime,\n PiSpawnTimeout,\n} from './pi.js';\nexport {\n OhmypiRuntimeSync,\n OhmypiRuntime,\n createOhmypiRuntimeSync,\n createOhmypiRuntime,\n OhmypiSpawnTimeout,\n} from './ohmypi.js';\nexport {\n CodexRuntimeSync,\n CodexRuntime,\n createCodexRuntimeSync,\n createCodexRuntime,\n} from './codex.js';\nexport {\n AcpRuntimeSync,\n createAcpRuntimeSync,\n AcpSpawnTimeout,\n} from './acp.js';\nexport {\n KimiCodeRuntimeSync,\n createKimiCodeRuntimeSync,\n KimiCodeSpawnTimeout,\n} from './kimi-code.js';\n\nimport type {\n AgentRuntimeSync,\n RuntimeName,\n RuntimeRegistry as RuntimeRegistryInterface,\n} from './types.js';\nimport { getAgentStateSync } from '../agents.js';\nimport { createClaudeCodeRuntimeSync } from './claude-code.js';\nimport { createPiRuntimeSync } from './pi.js';\nimport { createOhmypiRuntimeSync } from './ohmypi.js';\nimport { createCodexRuntimeSync } from './codex.js';\nimport { createAcpRuntimeSync } from './acp.js';\nimport { createKimiCodeRuntimeSync } from './kimi-code.js';\n\n/**\n * Runtime registry implementation\n *\n * Manages multiple runtime adapters and provides lookup by agent ID.\n */\nexport class RuntimeRegistry implements RuntimeRegistryInterface {\n private runtimes: Map<RuntimeName, AgentRuntimeSync> = new Map();\n\n /**\n * Register a runtime adapter\n */\n register(runtime: AgentRuntimeSync): void {\n this.runtimes.set(runtime.name, runtime);\n }\n\n /**\n * Get a runtime by name\n */\n get(name: RuntimeName): AgentRuntimeSync | undefined {\n return this.runtimes.get(name);\n }\n\n /**\n * Get all registered runtimes\n */\n getAll(): AgentRuntimeSync[] {\n return Array.from(this.runtimes.values());\n }\n\n /**\n * Get the runtime for a specific agent.\n *\n * Reads the agent's state file and dispatches by `state.harness`. When\n * the harness field is missing or carries a legacy value (e.g. 'claude'\n * from pre-PAN-636 wire format), we fall back to the claude-code runtime\n * to preserve back-compat (PAN-636 ac2).\n */\n getRuntimeForAgent(agentId: string): AgentRuntimeSync | null {\n const state = getAgentStateSync(agentId);\n if (!state) {\n return null;\n }\n // Read as plain string so legacy 'pi' rows (pre-PAN-1989) are handled below.\n const harness = (state as { harness?: string }).harness;\n if (harness === 'ohmypi' || harness === 'pi') {\n // 'pi' is a legacy harness value (PAN-1989); normalize to ohmypi adapter.\n return this.get('ohmypi') ?? null;\n }\n if (harness === 'codex') {\n return this.get('codex') ?? null;\n }\n if (harness === 'opencode') return this.get('opencode') ?? null;\n if (harness === 'acp') {\n return this.get('acp') ?? null;\n }\n if (harness === 'muse') return this.get('muse') ?? null;\n if (harness === 'kimi-code') {\n return this.get('kimi-code') ?? null;\n }\n return this.get('claude-code') ?? null;\n }\n}\n\n/**\n * Global runtime registry instance\n */\nlet globalRegistry: RuntimeRegistry | null = null;\n\n/**\n * Get the global runtime registry\n *\n * Creates a new registry if one doesn't exist.\n * Registers Claude Code runtime by default.\n */\nexport function getGlobalRegistry(): RuntimeRegistry {\n if (!globalRegistry) {\n globalRegistry = new RuntimeRegistry();\n\n // Register Claude Code (default), ohmypi (PAN-1989), Codex (PAN-1574), ACP,\n // and native Kimi Code (PAN-1837) runtimes.\n // Pi (PAN-636) is legacy and no longer registered — 'pi' harness is normalized\n // to 'ohmypi' by getRuntimeForAgent (see above) and normalizeHarness() in conversations.ts.\n globalRegistry.register(createClaudeCodeRuntimeSync());\n globalRegistry.register(createOhmypiRuntimeSync());\n globalRegistry.register(createCodexRuntimeSync());\n globalRegistry.register(createAcpRuntimeSync());\n globalRegistry.register(createAcpRuntimeSync({ name: 'opencode', provider: 'opencode' }));\n globalRegistry.register(createKimiCodeRuntimeSync());\n globalRegistry.register(createMuseRuntimeSync());\n }\n return globalRegistry;\n}\n\n/**\n * Set the global runtime registry\n *\n * Useful for testing or custom configurations.\n */\nexport function setGlobalRegistry(registry: RuntimeRegistry): void {\n globalRegistry = registry;\n}\n\n/**\n * Helper to get a runtime by name from the global registry\n */\nexport function getRuntime(name: RuntimeName): AgentRuntimeSync | undefined {\n return getGlobalRegistry().get(name);\n}\n\n/**\n * Helper to get the runtime for an agent from the global registry\n */\nexport function getRuntimeForAgent(agentId: string): AgentRuntimeSync | null {\n return getGlobalRegistry().getRuntimeForAgent(agentId);\n}\n","import { getAgentRuntimeStateSync } from '../agents.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { listPaneValuesSync } from '../tmux.js';\n\nexport function getAgentEffectiveLastActivityMs(agentId: string): number | null {\n const candidates: number[] = [];\n\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const runtimeMs = runtimeState?.lastActivity ? new Date(runtimeState.lastActivity).getTime() : NaN;\n if (Number.isFinite(runtimeMs)) candidates.push(runtimeMs);\n\n try {\n const tmuxActivity = listPaneValuesSync(agentId, '#{window_activity}')\n .map((value) => Number.parseInt(value.trim(), 10) * 1000)\n .filter(Number.isFinite);\n candidates.push(...tmuxActivity);\n } catch {\n // tmux activity is best-effort; fall through to transcript/runtime sources.\n }\n\n try {\n const heartbeat = getRuntimeForAgent(agentId)?.getHeartbeat(agentId);\n if (heartbeat?.timestamp) {\n const heartbeatMs = heartbeat.timestamp.getTime();\n if (Number.isFinite(heartbeatMs)) candidates.push(heartbeatMs);\n }\n } catch {\n // Runtime transcript lookup is best-effort.\n }\n\n return candidates.length > 0 ? Math.max(...candidates) : null;\n}\n\n/**\n * PAN-3677: work-product activity only — the runtime mirror (hook-driven) plus\n * the transcript heartbeat, deliberately EXCLUDING tmux `window_activity`.\n * Claude Code repaints its spinner / background-task panel every second while a\n * turn is in flight, so `window_activity` stays fresh even when the provider\n * call is hung and no work is happening. A mid-turn wedge detector keyed on\n * {@link getAgentEffectiveLastActivityMs} (which takes the MAX) would never see\n * the PAN-3677 planning wedge — the hung sessions looked busy on the pane for\n * minutes while producing no tool calls, no transcript writes, and no hook\n * events. This signal goes stale exactly when real work stops.\n */\nexport function getAgentWorkActivityMs(agentId: string): number | null {\n const candidates: number[] = [];\n\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const runtimeMs = runtimeState?.lastActivity ? new Date(runtimeState.lastActivity).getTime() : NaN;\n if (Number.isFinite(runtimeMs)) candidates.push(runtimeMs);\n\n try {\n const heartbeat = getRuntimeForAgent(agentId)?.getHeartbeat(agentId);\n if (heartbeat?.timestamp) {\n const heartbeatMs = heartbeat.timestamp.getTime();\n if (Number.isFinite(heartbeatMs)) candidates.push(heartbeatMs);\n }\n } catch {\n // Runtime transcript lookup is best-effort.\n }\n\n return candidates.length > 0 ? Math.max(...candidates) : null;\n}\n\nexport function isAgentIdleForNudge(\n agentId: string,\n staleActiveThresholdMs = 5 * 60 * 1000,\n now = Date.now(),\n): boolean {\n const runtimeState = getAgentRuntimeStateSync(agentId);\n const effectiveLastActivityMs = getAgentEffectiveLastActivityMs(agentId);\n if (!runtimeState) {\n if (effectiveLastActivityMs === null) {\n console.log(`[deacon] ${agentId}: no runtime.json — skipping (hook not yet fired)`);\n return false;\n }\n return now - effectiveLastActivityMs > staleActiveThresholdMs;\n }\n if (runtimeState.state === 'suspended' || runtimeState.state === 'stopped') return false;\n // A human-blocked agent is not idle work — never nudge it.\n if (runtimeState.state === 'waiting-on-human') return false;\n if (runtimeState.state === 'idle') return true;\n\n // 'active' or 'uninitialized': the activity mirror is only trustworthy while\n // it is fresh. A STALE 'active' mirror means the activity hooks (including the\n // Stop hook) stopped firing — e.g. the agent ended its turn but the Stop hook\n // never updated the mirror to 'idle' (PAN-1574: the agent sat 'active' for 36h\n // after ending its turn, invisible to every idle-nudge patrol). In that case\n // fall back to an age-based idle judgment so recovery can act. A genuinely\n // working agent emits tool-activity events continuously and stays fresh, so it\n // is not affected.\n const lastActivityMs = effectiveLastActivityMs ?? new Date(runtimeState.lastActivity).getTime();\n const ageMs = now - lastActivityMs;\n return ageMs > staleActiveThresholdMs;\n}\n"],"mappings":"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"}
|