@overdeck/core 0.45.10 → 0.45.11
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/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
- package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
- package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
- package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
- package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
- package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
- package/dist/cli/index.js +45 -45
- package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
- package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
- package/dist/cli-MTB9IOoR.js +3 -0
- package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
- package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
- package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
- package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
- package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
- package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
- package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
- package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
- package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
- package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
- package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
- package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
- package/dist/dashboard/cli-DCqy_wFa.js +3 -0
- package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
- package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
- package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
- package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
- package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
- package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
- package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
- package/dist/dashboard/deacon.js +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
- package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
- package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
- package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
- package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
- package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
- package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
- package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
- package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
- package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
- package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
- package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
- package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
- package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
- package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
- package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
- package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
- package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
- package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
- package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
- package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
- package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
- package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
- package/dist/dashboard/review-agent-DTINEffB.js +3 -0
- package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
- package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
- package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
- package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
- package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
- package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
- package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
- package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
- package/dist/dashboard/server.js +66 -55
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
- package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
- package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
- package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
- package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
- package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
- package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
- package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
- package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
- package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
- package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
- package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
- package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
- package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
- package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
- package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
- package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
- package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
- package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
- package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
- package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
- package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
- package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
- package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
- package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
- package/dist/dashboard/workflows-DVwXauef.js +2 -0
- package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
- package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
- package/dist/docs-index.sqlite +0 -0
- package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
- package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
- package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
- package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
- package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
- package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
- package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
- package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
- package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
- package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
- package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
- package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
- package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
- package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
- package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
- package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
- package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
- package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
- package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
- package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
- package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
- package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
- package/dist/records-backfill-BQcmTmwF.js +2 -0
- package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
- package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
- package/dist/resume-Bn4kDk9x.js +3 -0
- package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
- package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
- package/dist/review-agent-hLbRRxZF.js +3 -0
- package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
- package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
- package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
- package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
- package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
- package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
- package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
- package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
- package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
- package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
- package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
- package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
- package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
- package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
- package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
- package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
- package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
- package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
- package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
- package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
- package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
- package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
- package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
- package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
- package/dist/sync-CylE_SjB.js +2 -0
- package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
- package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
- package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
- package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
- package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
- package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
- package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
- package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
- package/package.json +1 -1
- package/scripts/file-size-baseline.txt +1 -1
- package/dist/cli-DBWG12XK.js +0 -3
- package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
- package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
- package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
- package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
- package/dist/dashboard/resume-DnIsmIgC.js +0 -3
- package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
- package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
- package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
- package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
- package/dist/records-backfill-DEgtRyKL.js +0 -2
- package/dist/resume-DGymuXZM.js +0 -3
- package/dist/review-agent-DjVadURa.js +0 -3
- package/dist/sync-4jqukkxz.js +0 -2
package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-compaction-B_6s9EvM.js","names":[],"sources":["../src/dashboard/server/services/codex-conversation-parser.ts","../src/dashboard/server/services/conversation/types.ts","../src/dashboard/server/services/conversation/compact-boundary.ts","../src/dashboard/server/services/conversation/parser.ts","../src/dashboard/server/services/conversation/activity-summary.ts","../src/dashboard/server/services/conversation/context-usage.ts","../src/dashboard/server/services/conversation/session-files.ts","../src/dashboard/server/services/conversation/watch.ts","../src/dashboard/server/services/conversation-service.ts","../src/dashboard/server/services/conversation-compaction.ts"],"sourcesContent":["/**\n * OpenAI Codex → chat panel adapter (PAN-1520).\n *\n * Codex conversations write a \"rollout\" JSONL under the per-agent\n * $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<uuid>-<threadId>.jsonl. The schema\n * is OpenAI's, not Claude Code's, so the Claude parser (and even the Pi parser,\n * which the path also happens to match) find zero message entries and the chat\n * panel renders blank. This module is the adapter — it reads a Codex rollout\n * and emits the same {@link ParseResult} shape the chat panel already consumes.\n *\n * Scope: chat panel display only.\n *\n * Codex rollout shape (cli ≥ 0.137.0):\n * - `type: 'session_meta' | 'turn_context'` — metadata, ignored for display.\n * - `type: 'event_msg'` with `payload.type`:\n * - `user_message` — the user's prompt (clean text in `payload.message`)\n * - `agent_message` — the assistant's visible reply (`payload.message`)\n * - `token_count` — cumulative usage in `payload.info.total_token_usage`\n * - `type: 'response_item'` with `payload.type`:\n * - `function_call` / `custom_tool_call` — tool invocation\n * - `function_call_output` / `custom_tool_call_output` — tool result\n * - `message` — the model's raw turn incl. injected AGENTS.md context;\n * skipped (user/assistant text comes from event_msg).\n * - `reasoning` — chain-of-thought; Codex encrypts it, so it is skipped.\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport type { ChatMessage, CompactBoundary, WorkLogEntry } from '@overdeck/contracts';\nimport type { ParseResult } from './conversation-service.js';\nimport { parseCodexSessionSync } from '../../../lib/cost-parsers/codex-parser.js';\n\ninterface CodexTokenUsage {\n input_tokens?: number;\n cached_input_tokens?: number;\n output_tokens?: number;\n reasoning_output_tokens?: number;\n total_tokens?: number;\n}\n\ninterface CodexPayload {\n type?: string;\n message?: string;\n name?: string;\n call_id?: string;\n arguments?: string;\n input?: unknown;\n output?: unknown;\n info?: { total_token_usage?: CodexTokenUsage };\n [k: string]: unknown;\n}\n\ninterface CodexEntry {\n type?: string;\n timestamp?: string;\n payload?: CodexPayload;\n [k: string]: unknown;\n}\n\n/** Flatten a Codex tool output (usually a string) into display text. */\nfunction extractToolOutput(output: unknown): string {\n if (typeof output === 'string') return output.trim();\n if (output && typeof output === 'object') {\n const obj = output as Record<string, unknown>;\n if (typeof obj['output'] === 'string') return (obj['output'] as string).trim();\n if (typeof obj['content'] === 'string') return (obj['content'] as string).trim();\n try {\n return JSON.stringify(output);\n } catch {\n return '';\n }\n }\n return '';\n}\n\n/**\n * Pull the shell command out of an `exec_command` tool call's JSON arguments so\n * it renders in the work log's command slot. Returns undefined for other tools.\n */\nfunction extractCommand(name: string, args: string): string | undefined {\n if (name !== 'exec_command' && name !== 'shell') return undefined;\n try {\n const parsed = JSON.parse(args) as Record<string, unknown>;\n const cmd = parsed['cmd'] ?? parsed['command'];\n if (typeof cmd === 'string') return cmd;\n if (Array.isArray(cmd)) return cmd.join(' ');\n } catch {\n // arguments weren't JSON — fall through\n }\n return undefined;\n}\n\n/**\n * Parse a Codex rollout JSONL into the ParseResult shape the chat panel\n * consumes. Always a full read (rollouts are small enough); incremental-parse\n * state fields are returned as empty stubs, matching the Pi adapter.\n */\nexport async function parseCodexConversationMessages(sessionFile: string): Promise<ParseResult> {\n const fileStats = await stat(sessionFile);\n const raw = await readFile(sessionFile, 'utf-8');\n\n const lines = raw.split('\\n').filter((line) => line.trim().length > 0);\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n const compactBoundaries: CompactBoundary[] = [];\n // Matches function_call → function_call_output by call_id within this parse.\n const toolCallsByCallId = new Map<string, WorkLogEntry>();\n let totalTokens = 0;\n let sequence = 0;\n\n for (const line of lines) {\n let entry: CodexEntry;\n try {\n entry = JSON.parse(line) as CodexEntry;\n } catch {\n continue;\n }\n if (!entry || typeof entry !== 'object') continue;\n const payload = entry.payload;\n if (!payload || typeof payload !== 'object') continue;\n const createdAt = entry.timestamp ?? new Date().toISOString();\n const ptype = payload.type;\n\n if (entry.type === 'event_msg') {\n if (ptype === 'user_message' || ptype === 'agent_message') {\n const text = typeof payload.message === 'string' ? payload.message.trim() : '';\n if (!text) continue;\n sequence += 1;\n messages.push({\n id: `codex-${ptype === 'user_message' ? 'user' : 'agent'}-${sequence}`,\n role: ptype === 'user_message' ? 'user' : 'assistant',\n text,\n createdAt,\n completedAt: createdAt,\n streaming: false,\n sequence,\n });\n } else if (ptype === 'token_count') {\n // Cumulative usage — the latest token_count carries the running total.\n const usage = payload.info?.total_token_usage;\n if (usage) {\n totalTokens = typeof usage.total_tokens === 'number'\n ? usage.total_tokens\n : (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);\n }\n }\n continue;\n }\n\n if (entry.type === 'response_item') {\n if (ptype === 'function_call' || ptype === 'custom_tool_call') {\n const callId = typeof payload.call_id === 'string' ? payload.call_id : '';\n const name = typeof payload.name === 'string' ? payload.name : 'tool';\n const args = typeof payload.arguments === 'string'\n ? payload.arguments\n : payload.input != null ? JSON.stringify(payload.input) : '';\n const command = extractCommand(name, args);\n sequence += 1;\n const wl: WorkLogEntry = {\n id: callId || `codex-tool-${sequence}`,\n createdAt,\n label: command ? 'Shell' : name,\n tone: 'tool',\n sequence,\n ...(command ? { command } : args ? { detail: args } : {}),\n };\n workLog.push(wl);\n if (callId) toolCallsByCallId.set(callId, wl);\n } else if (ptype === 'function_call_output' || ptype === 'custom_tool_call_output') {\n const callId = typeof payload.call_id === 'string' ? payload.call_id : '';\n const output = extractToolOutput(payload.output);\n const wl = callId ? toolCallsByCallId.get(callId) : undefined;\n if (wl) {\n if (output) (wl as { result?: string }).result = output;\n if (callId) toolCallsByCallId.delete(callId);\n } else if (output) {\n // Output with no matching call (truncated/rotated) — stand-alone entry.\n sequence += 1;\n workLog.push({\n id: callId || `codex-tool-out-${sequence}`,\n createdAt,\n label: 'Tool result',\n result: output,\n tone: 'tool',\n sequence,\n });\n }\n }\n // response_item 'message' (injected context) and 'reasoning' (encrypted)\n // carry nothing user-visible — intentionally skipped.\n continue;\n }\n }\n\n // Codex turns are written as complete agent_message events (not streamed\n // token-by-token into the rollout), so there is no partial-turn state to\n // surface — the chat panel never shows a stuck typing indicator.\n const streaming = false;\n\n // Cost is derived by the canonical Codex cost parser (single source of truth\n // for rollout pricing) so the conversation list shows real spend rather than\n // $0. token_count already gave us the cumulative throughput above.\n const usage = parseCodexSessionSync(sessionFile);\n const totalCost = usage?.cost_v2 ?? usage?.cost ?? 0;\n\n return {\n messages,\n workLog,\n byteOffset: fileStats.size,\n streaming,\n totalCost,\n totalTokens,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: fileStats.size,\n pendingToolUse: new Map(),\n unresolvedResults: new Map(),\n lastSequence: sequence,\n mtimeMs: fileStats.mtimeMs,\n planToolUseIds: new Set(),\n compactBoundaries,\n fileEditsByAssistantId: new Map(),\n };\n}\n","import type { ChatMessage, CompactBoundary, ProposedPlan, WorkLogEntry } from '@overdeck/contracts';\n\nexport interface ParseResult {\n messages: ChatMessage[];\n workLog: WorkLogEntry[];\n /** Byte offset after the last parsed line — pass back for incremental reads. */\n byteOffset: number;\n /** True when the last assistant message has no completedAt and file was modified recently. */\n streaming: boolean;\n /** Total estimated cost in USD computed from assistant message usage data (cache-discount aware). */\n totalCost: number;\n /** Total token throughput (input + output + cache read + cache write) across assistant messages. */\n totalTokens: number;\n /** Last assistant usage observed after the active compact boundary. */\n latestAssistantUsage: LatestAssistantUsage | null;\n /** Byte offset of the active compact boundary used for context-window usage. */\n contextBoundaryOffset: number;\n /** Bytes from the active compact boundary through EOF. */\n contextActiveBytes: number;\n /** Unpaired tool_use entries waiting for tool_result (persist across incremental calls). */\n pendingToolUse: Map<string, WorkLogEntry>;\n /** Pre-arrived tool_result entries waiting for tool_use (persist across incremental calls). */\n unresolvedResults: Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>;\n /** Last sequence number assigned (persist across incremental calls). */\n lastSequence: number;\n /** File modification time in ms from the stat call made during parsing. */\n mtimeMs: number;\n /** Active proposed plan (ExitPlanMode with no matching tool_result yet). */\n proposedPlan?: ProposedPlan;\n /** ExitPlanMode tool_use IDs (persist across incremental calls). */\n planToolUseIds?: Set<string>;\n /** Compact boundary markers detected in the JSONL. */\n compactBoundaries?: CompactBoundary[];\n /** Current permission mode (plan/default/bypassPermissions/acceptEdits). */\n permissionMode?: string;\n /** Map assistant message ID → file paths touched by file-modifying tool_use calls. */\n fileEditsByAssistantId?: Map<string, Array<{ tool: string; filePath: string }>>;\n /** ID of the current pendingAssistant message (carried across incremental parses for file-edit tracking). */\n pendingAssistantId?: string;\n /** Orphaned tool_use entry UUIDs awaiting re-keying (carried across incremental parses). */\n orphanToolUseIds?: Set<string>;\n /**\n * Request/message IDs whose usage has already been counted into totalCost/totalTokens.\n * Claude Code writes one API response across several JSONL lines (text block, tool_use\n * block, …) that each repeat the same `usage`; counting every line double-counts cost.\n * Carried across incremental parses so a response split across read boundaries is counted once.\n */\n countedUsageIds?: Set<string>;\n}\n\n/** State carried across incremental parseConversationMessages calls. */\nexport interface ParseState {\n pendingToolUse: Map<string, WorkLogEntry>;\n unresolvedResults: Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>;\n lastSequence: number;\n planToolUseIds?: Set<string>;\n proposedPlan?: ProposedPlan;\n /** Latest assistant usage observed after the active compact boundary. */\n latestAssistantUsage?: LatestAssistantUsage | null;\n /** Byte offset of the active compact boundary used for context-window usage. */\n contextBoundaryOffset?: number;\n /** Current permission mode (plan/default/bypassPermissions/acceptEdits). */\n permissionMode?: string;\n /** Map assistant message ID → file paths touched by file-modifying tool_use calls in that turn. */\n fileEditsByAssistantId?: Map<string, Array<{ tool: string; filePath: string }>>;\n /** ID of the current pendingAssistant message (carried across incremental parses for file-edit tracking). */\n pendingAssistantId?: string;\n /** Orphaned tool_use entry UUIDs awaiting re-keying (carried across incremental parses). */\n orphanToolUseIds?: Set<string>;\n /** Request/message IDs already counted into cost/tokens (see ParseResult.countedUsageIds). */\n countedUsageIds?: Set<string>;\n}\n\nexport interface ConversationActivitySummary {\n messages: ChatMessage[];\n streaming: boolean;\n isWorking: boolean;\n /** Tool name of the most recently pending tool call, if any (e.g. \"Bash\", \"Read\"). */\n currentTool: string | null;\n}\n\n/** Maximum bytes to read in a single incremental chunk (10 MB). */\nexport const MAX_READ_BYTES = 10 * 1024 * 1024;\nexport const MAX_FALLBACK_BYTES = 5 * 1024 * 1024;\n\nexport interface JsonlUsage {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n}\n\nexport interface JsonlEntry {\n type?: string;\n role?: string;\n message?: {\n id?: string;\n role?: string;\n content?: unknown[] | string;\n model?: string;\n stop_reason?: string | null;\n usage?: JsonlUsage;\n };\n timestamp?: string;\n sessionId?: string;\n uuid?: string;\n /** Claude Code per-API-request id. Stable across the multiple JSONL lines of one response. */\n requestId?: string;\n}\n\nexport interface ContentBlock {\n type?: string;\n text?: string;\n thinking?: string;\n id?: string;\n name?: string;\n input?: unknown;\n tool_use_id?: string;\n content?: unknown;\n is_error?: boolean;\n}\n\nexport interface LatestAssistantUsage {\n lastInputTokens: number;\n lastCacheReadTokens: number;\n lastCacheCreationTokens: number;\n maxObservedInputTokens: number;\n lastModel: string | null;\n lastTimestamp: string | null;\n}\n","import { open, stat } from 'node:fs/promises';\nimport { MAX_FALLBACK_BYTES, MAX_READ_BYTES } from './types.js';\n\n// ─── Compact boundary offset cache ───────────────────────────────────────────\n\n/**\n * In-memory cache mapping JSONL file path → last compact_boundary byte offset\n * and the byte offset up to which we've scanned complete lines.\n * Persists across requests so we don't re-scan the entire file each time.\n */\nconst COMPACT_OFFSET_CACHE_MAX = 100;\nconst compactOffsetCache = new Map<string, { boundaryOffset: number; scannedUpTo: number }>();\n\nfunction setCompactOffsetCache(sessionFile: string, value: { boundaryOffset: number; scannedUpTo: number }): void {\n compactOffsetCache.set(sessionFile, value);\n if (compactOffsetCache.size > COMPACT_OFFSET_CACHE_MAX) {\n const firstKey = compactOffsetCache.keys().next().value;\n if (firstKey !== undefined) {\n compactOffsetCache.delete(firstKey);\n }\n }\n}\n\n/**\n * Find the byte offset of the last `compact_boundary` system entry in a JSONL file.\n *\n * Uses an in-memory cache keyed by file path. When the file grows beyond\n * the cached scan position, only the new portion is scanned. Returns 0 if no boundary found.\n */\nexport async function findLastCompactBoundary(sessionFile: string): Promise<number> {\n const fileStats = await stat(sessionFile);\n const fileSize = fileStats.size;\n\n const cached = compactOffsetCache.get(sessionFile);\n\n // If file hasn't grown since last scan, return cached offset without reading\n if (cached && cached.scannedUpTo === fileSize) {\n return cached.boundaryOffset;\n }\n\n // No cache or file shrank — scan entire file in capped chunks\n if (!cached || fileSize < cached.scannedUpTo) {\n let lastBoundaryOffset = 0;\n let scanPos = 0;\n const fh = await open(sessionFile, 'r');\n try {\n while (scanPos < fileSize) {\n const toRead = Math.min(fileSize - scanPos, MAX_READ_BYTES);\n let buf: Buffer;\n let bytesRead: number;\n try {\n buf = Buffer.alloc(toRead);\n const result = await fh.read(buf, 0, toRead, scanPos);\n bytesRead = result.bytesRead;\n } catch {\n break;\n }\n\n if (bytesRead === 0) break;\n\n let scanBytes = bytesRead;\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n\n if (lastNewline !== -1) {\n scanBytes = lastNewline + 1;\n } else if (scanPos + bytesRead < fileSize) {\n // No newline in chunk and more file remains — read more, capped\n const remaining = Math.min(fileSize - scanPos, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n let fullBytesRead: number;\n try {\n const result = await fh.read(fullBuf, 0, remaining, scanPos);\n fullBytesRead = result.bytesRead;\n } catch {\n break;\n }\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n scanBytes = fullLastNewline + 1;\n buf = fullBuf;\n } else {\n scanBytes = fullBytesRead;\n buf = fullBuf;\n }\n }\n\n const text = buf.toString('utf-8', 0, scanBytes);\n let bytePos = scanPos;\n const lines = text.split('\\n');\n for (const line of lines) {\n if (line.trim()) {\n try {\n const cleanLine = line.replace(/\\r$/, '');\n const entry = JSON.parse(cleanLine);\n if (entry.type === 'system' && entry.subtype === 'compact_boundary') {\n lastBoundaryOffset = bytePos;\n }\n } catch { /* skip invalid lines */ }\n }\n bytePos += Buffer.byteLength(line, 'utf-8') + 1; // +1 for \\n\n }\n\n scanPos += scanBytes;\n }\n } finally {\n await fh.close();\n }\n\n setCompactOffsetCache(sessionFile, { boundaryOffset: lastBoundaryOffset, scannedUpTo: fileSize });\n return lastBoundaryOffset;\n }\n\n // File grew — read only the new portion, respecting complete-line boundaries\n const newBytes = fileSize - cached.scannedUpTo;\n const toRead = Math.min(newBytes, MAX_READ_BYTES);\n const fh = await open(sessionFile, 'r');\n try {\n let buf: Buffer;\n let bytesRead: number;\n try {\n buf = Buffer.alloc(toRead);\n const result = await fh.read(buf, 0, toRead, cached.scannedUpTo);\n bytesRead = result.bytesRead;\n } catch {\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n\n let scanBytes = 0;\n if (bytesRead > 0) {\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n if (lastNewline !== -1) {\n scanBytes = lastNewline + 1;\n } else if (cached.scannedUpTo + bytesRead < fileSize) {\n // No newline in chunk and more file remains — read to EOF, capped\n const remaining = Math.min(fileSize - cached.scannedUpTo, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n let fullBytesRead: number;\n try {\n const result = await fh.read(fullBuf, 0, remaining, cached.scannedUpTo);\n fullBytesRead = result.bytesRead;\n } catch {\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n scanBytes = fullLastNewline + 1;\n buf = fullBuf;\n } else {\n // No newline even at EOF — don't scan partial trailing line\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n } else {\n // At EOF with no newline — don't scan partial trailing line\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n }\n\n const text = buf.toString('utf-8', 0, scanBytes);\n\n let lastBoundaryOffset = cached.boundaryOffset;\n let bytePos = cached.scannedUpTo;\n const lines = text.split('\\n');\n for (const line of lines) {\n if (line.trim()) {\n try {\n const cleanLine = line.replace(/\\r$/, '');\n const entry = JSON.parse(cleanLine);\n if (entry.type === 'system' && entry.subtype === 'compact_boundary') {\n lastBoundaryOffset = bytePos;\n }\n } catch { /* skip invalid lines */ }\n }\n bytePos += Buffer.byteLength(line, 'utf-8') + 1; // +1 for \\n\n }\n\n setCompactOffsetCache(sessionFile, { boundaryOffset: lastBoundaryOffset, scannedUpTo: cached.scannedUpTo + scanBytes });\n return lastBoundaryOffset;\n } finally {\n await fh.close();\n }\n}\n","import { open, readFile, stat } from 'node:fs/promises';\nimport type { ChatMessage, CompactBoundary, ProposedPlan, WorkLogEntry } from '@overdeck/contracts';\nimport { calculateCostSync, getPricingSync } from '../../../../lib/cost.js';\nimport { summarizeToolInputForWorkLog } from '../format-tool-input.js';\nimport { findLastCompactBoundary } from './compact-boundary.js';\nimport { renderableUserText } from './message-filters.js';\nimport { providerFromModel } from './provider.js';\nimport {\n MAX_FALLBACK_BYTES,\n MAX_READ_BYTES,\n type ContentBlock,\n type JsonlEntry,\n type ParseResult,\n type ParseState,\n} from './types.js';\n\n// ─── JSONL parsing ────────────────────────────────────────────────────────────\n\n/**\n * Parse JSONL session file from a byte offset.\n *\n * Returns parsed messages, work log entries, new byte offset, and streaming status.\n * Safe to call repeatedly — incremental reads avoid re-parsing.\n */\nexport async function parseConversationMessages(\n sessionFile: string,\n fromByteOffset = 0,\n priorState?: ParseState,\n): Promise<ParseResult> {\n // Read only new content from the byte offset — avoids re-reading the entire JSONL every tick\n let fileStats: Awaited<ReturnType<typeof stat>>;\n try {\n fileStats = await stat(sessionFile);\n } catch {\n // The transcript file does not exist yet — a freshly-spawned conversation\n // whose runtime has not written its first JSONL line. Treat this as an empty\n // transcript (0 messages) instead of throwing, so a live subscriber can\n // attach the instant the conversation row exists and self-populate once the\n // file appears (watchConversation polls until it does). Mirrors the empty\n // result the truncation branch below returns.\n return {\n messages: [],\n workLog: [],\n byteOffset: 0,\n streaming: false,\n totalCost: 0,\n totalTokens: 0,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: 0,\n pendingToolUse: priorState?.pendingToolUse ?? new Map(),\n unresolvedResults: priorState?.unresolvedResults ?? new Map(),\n lastSequence: priorState?.lastSequence ?? 0,\n mtimeMs: 0,\n permissionMode: priorState?.permissionMode,\n fileEditsByAssistantId: new Map(),\n };\n }\n const fileSize = fileStats.size;\n\n // File was truncated or rotated since last read — signal reset to caller\n if (fromByteOffset > fileSize) {\n return {\n messages: [],\n workLog: [],\n byteOffset: 0,\n streaming: false,\n totalCost: 0,\n totalTokens: 0,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: fileStats.size,\n pendingToolUse: priorState?.pendingToolUse ?? new Map(),\n unresolvedResults: priorState?.unresolvedResults ?? new Map(),\n lastSequence: priorState?.lastSequence ?? 0,\n mtimeMs: fileStats.mtimeMs,\n permissionMode: priorState?.permissionMode,\n fileEditsByAssistantId: new Map(),\n };\n }\n\n if (fromByteOffset === 0 && !priorState && fileSize > MAX_READ_BYTES) {\n return parseEntireConversation(sessionFile);\n }\n\n const toRead = Math.max(0, Math.min(fileSize - fromByteOffset, MAX_READ_BYTES));\n const isIncremental = fromByteOffset > 0;\n\n let newText = '';\n let newByteOffset = fromByteOffset;\n\n if (toRead > 0) {\n const fh = await open(sessionFile, 'r');\n try {\n const buf = Buffer.alloc(toRead);\n const { bytesRead } = await fh.read(buf, 0, toRead, fromByteOffset);\n\n if (bytesRead > 0) {\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n\n if (lastNewline !== -1) {\n // At least one complete line — advance only past complete lines\n newText = buf.toString('utf-8', 0, lastNewline + 1);\n newByteOffset = fromByteOffset + lastNewline + 1;\n } else if (!isIncremental && fromByteOffset + bytesRead >= fileSize) {\n // Full parse at EOF with no trailing newline — include trailing bytes\n newText = buf.toString('utf-8', 0, bytesRead);\n newByteOffset = fromByteOffset + bytesRead;\n } else if (fromByteOffset + bytesRead >= fileSize) {\n // Incremental parse at EOF with no newline — don't advance, wait for completion\n newByteOffset = fromByteOffset;\n } else {\n // No newline in a full chunk and more file remains (line exceeds MAX_READ_BYTES).\n // Read more to find a newline boundary, but cap to avoid OOM on pathological files.\n const remaining = Math.min(fileSize - fromByteOffset, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n const { bytesRead: fullBytesRead } = await fh.read(fullBuf, 0, remaining, fromByteOffset);\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n newText = fullBuf.toString('utf-8', 0, fullLastNewline + 1);\n newByteOffset = fromByteOffset + fullLastNewline + 1;\n } else if (!isIncremental) {\n newText = fullBuf.toString('utf-8', 0, fullBytesRead);\n newByteOffset = fromByteOffset + fullBytesRead;\n } else {\n newByteOffset = fromByteOffset;\n }\n }\n }\n } finally {\n await fh.close();\n }\n }\n\n const lines = newText.split('\\n');\n\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n let totalCost = 0;\n let totalTokens = 0;\n let latestAssistantUsage = priorState?.latestAssistantUsage ?? null;\n let maxObservedInputTokens = latestAssistantUsage?.maxObservedInputTokens ?? 0;\n let contextBoundaryOffset = priorState?.contextBoundaryOffset ?? 0;\n\n // Pending assistant message being assembled from content blocks\n let pendingAssistant: ChatMessage | null = null;\n // Map tool_use id → WorkLogEntry (waiting for tool_result)\n const pendingToolUse = priorState?.pendingToolUse ?? new Map<string, WorkLogEntry>();\n // Map tool_use id → pre-arrived tool_result (waiting for tool_use)\n const unresolvedResults = priorState?.unresolvedResults ?? new Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>();\n // Monotonic sequence counter per JSONL line\n let sequence = priorState?.lastSequence ?? 0;\n // Plan mode: track ExitPlanMode tool_use id → ProposedPlan\n let proposedPlan: ProposedPlan | undefined = priorState?.proposedPlan;\n // Set of ExitPlanMode tool_use IDs so we can match tool_results to plans\n const planToolUseIds = priorState?.planToolUseIds ?? new Set<string>();\n // Compact boundary markers\n const compactBoundaries: CompactBoundary[] = [];\n // Track permission mode across incremental parses\n let permissionMode: string | undefined = priorState?.permissionMode;\n // Track file-modifying tool_use calls per assistant message for diff computation\n const fileEditsByAssistantId = new Map<string, Array<{ tool: string; filePath: string }>>();\n const FILE_EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);\n // Tool_use entries arrive with their own UUID, separate from the text entry that follows.\n // We track these UUIDs so we can re-key edits when the text entry merges them into one message.\n const orphanToolUseIds = priorState?.orphanToolUseIds\n ? new Set(priorState.orphanToolUseIds)\n : new Set<string>();\n // Request/message IDs whose usage has already been counted, so a response spread across\n // multiple JSONL lines (or across incremental read boundaries) is counted exactly once.\n const countedUsageIds = priorState?.countedUsageIds ?? new Set<string>();\n // Carry the prior assistant ID for file-edit association only. Do not\n // materialize it as a ChatMessage: a later flush before user/tool-result lines\n // would emit an invalid ID-only assistant delta.\n const pendingAssistantIdForEdits = priorState?.pendingAssistantId;\n\n let lineByteOffset = fromByteOffset;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const currentLineByteOffset = lineByteOffset;\n lineByteOffset += Buffer.byteLength(line, 'utf-8') + (i < lines.length - 1 ? 1 : 0);\n if (!line.trim()) continue;\n\n let entry: JsonlEntry;\n try {\n entry = JSON.parse(line.replace(/\\r$/, '')) as JsonlEntry;\n } catch {\n continue;\n }\n const lineSequence = sequence++;\n\n if (entry.type === 'user' && entry.message) {\n const msg = entry.message;\n const rawContent = msg.content;\n\n // Flush any pending assistant message\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n pendingAssistant = null;\n }\n\n // Content can be a string (plain text) or array of content blocks\n if (typeof rawContent === 'string' && rawContent.trim()) {\n const text = renderableUserText(rawContent);\n if (text !== null) {\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: entry.uuid ?? `user-${messages.length}`,\n role: 'user',\n text,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n } else if (Array.isArray(rawContent)) {\n // Collect tool_result blocks first (they complete pending WorkLogEntries)\n for (const block of rawContent as ContentBlock[]) {\n if (block.type === 'tool_result' && block.tool_use_id) {\n let resultText: string | undefined;\n if (typeof block.content === 'string') {\n resultText = block.content;\n } else if (Array.isArray(block.content)) {\n resultText = (block.content as Array<{ type?: string; text?: string }>)\n .filter(b => b.type === 'text' && b.text)\n .map(b => b.text)\n .join('\\n');\n }\n if (planToolUseIds.has(block.tool_use_id)) {\n if (proposedPlan && proposedPlan.id === block.tool_use_id) {\n const text = resultText ?? '';\n if (text.includes('approved')) {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n } else if (text.includes(\"doesn't want to proceed\") || text.includes('does not want')) {\n proposedPlan = { ...proposedPlan, status: 'rejected', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n } else {\n const pending = pendingToolUse.get(block.tool_use_id);\n if (pending) {\n pendingToolUse.delete(block.tool_use_id);\n workLog.push({\n ...pending,\n detail: block.is_error\n ? `Error: ${resultText ?? JSON.stringify(block.content)}`\n : pending.detail,\n result: resultText,\n tone: block.is_error ? 'error' : pending.tone,\n });\n } else {\n unresolvedResults.set(block.tool_use_id, {\n resultText,\n isError: block.is_error ?? false,\n rawContent: block.content,\n });\n }\n }\n }\n }\n\n // Collect all text blocks, joining with newlines to preserve multiline input\n const textBlocks: string[] = [];\n for (const block of rawContent as ContentBlock[]) {\n if (block.type === 'text' && block.text) {\n const text = renderableUserText(block.text);\n if (text !== null) textBlocks.push(text);\n }\n }\n if (textBlocks.length > 0) {\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: entry.uuid ?? `user-${messages.length}`,\n role: 'user',\n text: textBlocks.join('\\n'),\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n }\n } else if (entry.type === 'assistant' && entry.message) {\n const msg = entry.message;\n const content = Array.isArray(msg.content) ? msg.content : [];\n\n if (msg.usage) {\n const input = msg.usage.input_tokens ?? 0;\n const cacheRead = msg.usage.cache_read_input_tokens ?? 0;\n const cacheCreate = msg.usage.cache_creation_input_tokens ?? 0;\n const turnInput = input + cacheRead + cacheCreate;\n if (turnInput > maxObservedInputTokens) maxObservedInputTokens = turnInput;\n latestAssistantUsage = {\n lastInputTokens: input,\n lastCacheReadTokens: cacheRead,\n lastCacheCreationTokens: cacheCreate,\n maxObservedInputTokens,\n lastModel: msg.model ?? null,\n lastTimestamp: entry.timestamp ?? null,\n };\n }\n\n // Accumulate cost and token throughput from usage data.\n // Claude Code repeats the same `usage` on every JSONL line of one API response\n // (the text line, each tool_use line, …); dedup on requestId/message.id so each\n // response is counted once, otherwise multi-block turns inflate cost ~2-3×.\n const usageId = entry.requestId ?? msg.id;\n if (msg.usage && (usageId === undefined || !countedUsageIds.has(usageId))) {\n if (usageId !== undefined) countedUsageIds.add(usageId);\n totalTokens +=\n (msg.usage.input_tokens ?? 0) +\n (msg.usage.output_tokens ?? 0) +\n (msg.usage.cache_read_input_tokens ?? 0) +\n (msg.usage.cache_creation_input_tokens ?? 0);\n if (msg.model) {\n const pricing = getPricingSync(providerFromModel(msg.model), msg.model);\n if (pricing) {\n totalCost += calculateCostSync({\n inputTokens: msg.usage.input_tokens ?? 0,\n outputTokens: msg.usage.output_tokens ?? 0,\n cacheReadTokens: msg.usage.cache_read_input_tokens ?? 0,\n cacheWriteTokens: msg.usage.cache_creation_input_tokens ?? 0,\n }, pricing);\n }\n }\n }\n\n let assistantText = '';\n let blockIndex = 0;\n for (const block of content as ContentBlock[]) {\n if (block.type === 'text' && block.text) {\n assistantText += block.text;\n } else if (block.type === 'thinking' && block.thinking) {\n workLog.push({\n id: `${entry.uuid ?? msg.id ?? `asst-${messages.length}`}-thinking-${blockIndex}`,\n createdAt: entry.timestamp ?? new Date().toISOString(),\n label: 'thinking',\n detail: block.thinking,\n tone: 'thinking',\n sequence: lineSequence,\n });\n } else if (block.type === 'tool_use' && block.id) {\n if (block.name === 'ExitPlanMode') {\n const input = block.input as Record<string, unknown> | undefined;\n const planText = typeof input?.plan === 'string' ? input.plan : '';\n const planFilePath = typeof input?.planFilePath === 'string' ? input.planFilePath : undefined;\n proposedPlan = {\n id: block.id,\n plan: planText,\n planFilePath,\n status: 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n };\n planToolUseIds.add(block.id);\n const unresolved = unresolvedResults.get(block.id);\n if (unresolved) {\n unresolvedResults.delete(block.id);\n const resultText = unresolved.resultText ?? '';\n if (resultText.includes('approved')) {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n } else if (resultText.includes(\"doesn't want to proceed\") || resultText.includes('does not want')) {\n proposedPlan = { ...proposedPlan, status: 'rejected', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n } else if (block.name === 'EnterPlanMode') {\n // Skip — don't add to workLog\n } else {\n // Track file-modifying tools for diff computation\n if (block.name && FILE_EDIT_TOOLS.has(block.name) && block.input) {\n const input = block.input as Record<string, unknown>;\n const filePath = typeof input.file_path === 'string' ? input.file_path : undefined;\n if (filePath) {\n const asstId = pendingAssistant?.id ?? pendingAssistantIdForEdits ?? entry.uuid ?? msg.id ?? `asst-${messages.length}`;\n // If no pendingAssistant, this tool_use UUID is orphaned — the text entry\n // will merge it into the final message with a different UUID.\n if (!pendingAssistant) {\n orphanToolUseIds.add(asstId);\n }\n let edits = fileEditsByAssistantId.get(asstId);\n if (!edits) {\n edits = [];\n fileEditsByAssistantId.set(asstId, edits);\n }\n if (!edits.some(e => e.filePath === filePath)) {\n edits.push({ tool: block.name!, filePath });\n }\n }\n }\n // WorkLogEntry for the tool call. We pass the raw input dict\n // through as `toolInput` so the frontend can render per-tool\n // (Bash command as a fenced shell block, file tools as chips,\n // etc.) and pre-compute a short one-line summary for the\n // collapsed row via summarizeToolInputForWorkLog. See PAN-1459.\n const inputDict =\n block.input && typeof block.input === 'object' && !Array.isArray(block.input)\n ? (block.input as Record<string, unknown>)\n : undefined;\n const toolEntry: WorkLogEntry = {\n id: block.id,\n createdAt: entry.timestamp ?? new Date().toISOString(),\n label: block.name ?? 'tool',\n tone: 'tool',\n toolTitle: block.name,\n detail: summarizeToolInputForWorkLog(block.name, inputDict),\n toolInput: inputDict,\n sequence: lineSequence,\n };\n const unresolved = unresolvedResults.get(block.id);\n if (unresolved) {\n unresolvedResults.delete(block.id);\n workLog.push({\n ...toolEntry,\n detail: unresolved.isError\n ? `Error: ${unresolved.resultText ?? JSON.stringify(unresolved.rawContent)}`\n : toolEntry.detail,\n result: unresolved.resultText,\n tone: unresolved.isError ? 'error' : toolEntry.tone,\n });\n } else {\n pendingToolUse.set(block.id, toolEntry);\n }\n }\n }\n blockIndex++;\n }\n\n if (assistantText) {\n const newId = entry.uuid ?? msg.id ?? `asst-${messages.length}`;\n // Flush previous pending assistant\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n // Re-key file edits from old pendingAssistant UUID\n const oldId = pendingAssistant.id;\n if (oldId !== newId) {\n const edits = fileEditsByAssistantId.get(oldId);\n if (edits) {\n fileEditsByAssistantId.delete(oldId);\n fileEditsByAssistantId.set(newId, edits);\n }\n }\n }\n // Re-key orphaned tool_use UUIDs into the merged message's UUID\n for (const orphanId of orphanToolUseIds) {\n if (orphanId !== newId) {\n const edits = fileEditsByAssistantId.get(orphanId);\n if (edits) {\n fileEditsByAssistantId.delete(orphanId);\n const existing = fileEditsByAssistantId.get(newId);\n if (existing) {\n for (const e of edits) {\n if (!existing.some(x => x.filePath === e.filePath)) existing.push(e);\n }\n } else {\n fileEditsByAssistantId.set(newId, edits);\n }\n }\n }\n }\n orphanToolUseIds.clear();\n pendingAssistant = {\n id: entry.uuid ?? msg.id ?? `asst-${messages.length}`,\n role: 'assistant',\n text: assistantText,\n // createdAt = when the assistant response was generated (preserves\n // chronological order for interleaving with work log entries).\n // Duration start is computed separately in session-logic.ts.\n createdAt: entry.timestamp ?? new Date().toISOString(),\n // Any terminal stop reason (end_turn, max_tokens, stop_sequence) marks the response as done.\n // tool_use means more exchanges are coming, so leave completedAt unset.\n // Use || fallback in case entry.timestamp is null (not just undefined).\n completedAt: (msg.stop_reason && msg.stop_reason !== 'tool_use')\n ? (entry.timestamp || new Date().toISOString())\n : undefined,\n streaming: !msg.stop_reason,\n sequence: lineSequence,\n };\n }\n } else if (entry.type === 'system' && (entry as Record<string, unknown>).subtype === 'compact_boundary') {\n contextBoundaryOffset = currentLineByteOffset;\n latestAssistantUsage = null;\n maxObservedInputTokens = 0;\n const meta = (entry as Record<string, unknown>).compactMetadata as Record<string, unknown> | undefined;\n compactBoundaries.push({\n id: (entry as Record<string, unknown>).uuid as string ?? `compact-${lineSequence}`,\n timestamp: entry.timestamp ?? new Date().toISOString(),\n trigger: typeof meta?.trigger === 'string' ? meta.trigger : undefined,\n preTokens: typeof meta?.preTokens === 'number' ? meta.preTokens : undefined,\n model: typeof meta?.model === 'string' ? meta.model : undefined,\n });\n } else if (entry.type === 'permission-mode') {\n // Track permission mode transitions. Exiting 'plan' mode is an approval\n // signal when no explicit plan_mode_exit attachment was emitted.\n const newMode = (entry as Record<string, unknown>).permissionMode as string | undefined;\n if (newMode && permissionMode === 'plan' && newMode !== 'plan') {\n if (proposedPlan && proposedPlan.status === 'pending') {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n permissionMode = newMode;\n } else if (entry.type === 'attachment') {\n // Claude Code 2.1.121+ attachment-based plan mode protocol.\n // The agent writes the plan to ~/.claude/plans/<slug>.md and the runtime\n // emits a `plan_file_reference` attachment with the full content. Approval\n // is signalled by a subsequent `plan_mode_exit` attachment.\n const attachment = (entry as Record<string, unknown>).attachment as Record<string, unknown> | undefined;\n if (attachment?.type === 'plan_file_reference') {\n const planContent = typeof attachment.planContent === 'string' ? attachment.planContent : '';\n if (planContent) {\n const planFilePath = typeof attachment.planFilePath === 'string' ? attachment.planFilePath : undefined;\n const id = ((entry as Record<string, unknown>).uuid as string | undefined) ?? `plan-${lineSequence}`;\n // If permission mode has already exited 'plan', auto-approve since the\n // user already approved before this attachment arrived (or no exit was emitted).\n const alreadyApproved = permissionMode !== 'plan' && permissionMode !== undefined;\n proposedPlan = {\n id,\n plan: planContent,\n planFilePath,\n status: alreadyApproved ? 'approved' : 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n ...(alreadyApproved ? { resolvedAt: entry.timestamp ?? new Date().toISOString() } : {}),\n };\n }\n } else if (attachment?.type === 'plan_mode_exit') {\n if (proposedPlan && proposedPlan.status === 'pending') {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n } else if (attachment?.type === 'plan_mode') {\n // Claude Code plan_mode attachment: agent called EnterPlanMode, plan written\n // to an external file. Always start as 'pending' — the permission-mode\n // transition handler (plan → other) will mark it approved when that fires.\n const planFilePath = typeof attachment.planFilePath === 'string' ? attachment.planFilePath : undefined;\n if (planFilePath) {\n try {\n const planContent = (await readFile(planFilePath, 'utf-8')).trim();\n if (planContent) {\n const id = ((entry as Record<string, unknown>).uuid as string | undefined) ?? `plan-${lineSequence}`;\n proposedPlan = {\n id,\n plan: planContent,\n planFilePath,\n status: 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n };\n }\n } catch {\n // Plan file not yet written — will show once the agent finishes planning\n }\n }\n } else if (attachment?.type === 'queued_command' && attachment?.commandMode === 'prompt') {\n // User message sent as an interrupt while Claude was running — stored as\n // queued_command instead of a regular user entry. Render it in the timeline.\n const prompt = attachment.prompt;\n let text = '';\n if (typeof prompt === 'string') {\n text = prompt;\n } else if (Array.isArray(prompt)) {\n text = (prompt as Array<{ type?: string; text?: string }>)\n .filter((b) => b.type === 'text' && b.text)\n .map((b) => b.text)\n .join('\\n');\n }\n const renderableText = text ? renderableUserText(text) : null;\n if (renderableText !== null) {\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n pendingAssistant = null;\n }\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: ((entry as Record<string, unknown>).uuid as string | undefined) ?? `queued-${lineSequence}`,\n role: 'user',\n text: renderableText,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n } else if (attachment?.type === 'command_permissions') {\n // Session start / resume record — contains the allowedTools list.\n // Render as a system message so the user can see what permissions are in effect.\n const rawTools = Array.isArray(attachment.allowedTools)\n ? (attachment.allowedTools as unknown[]).filter((t) => typeof t === 'string') as string[]\n : [];\n const tools = rawTools.length > 0 ? rawTools.join(', ') : 'None';\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: ((entry as Record<string, unknown>).uuid as string | undefined) ?? `perm-${lineSequence}`,\n role: 'system',\n text: tools,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n }\n }\n\n // Push any remaining pending assistant\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n }\n\n // Flush any tool_use entries that never got a result (still running).\n // Only flush on non-incremental parses; incremental callers pass priorState\n // and will receive pendingToolUse back for the next call.\n if (!priorState) {\n for (const [id, entry] of pendingToolUse) {\n if (!planToolUseIds.has(id)) {\n workLog.push(entry);\n }\n }\n pendingToolUse.clear();\n }\n\n // Sort by (createdAt, sequence) so the conversation view matches terminal order.\n // Use direct string comparison (not localeCompare) — ISO 8601 timestamps sort lexicographically.\n // For incremental parses (the hot path), messages arrive in file order and are already\n // sorted — skip the O(n log n) sort entirely.\n if (!isIncremental) {\n messages.sort((a, b) => {\n if (a.createdAt < b.createdAt) return -1;\n if (a.createdAt > b.createdAt) return 1;\n return (a.sequence ?? 0) - (b.sequence ?? 0);\n });\n workLog.sort((a, b) => {\n if (a.createdAt < b.createdAt) return -1;\n if (a.createdAt > b.createdAt) return 1;\n return (a.sequence ?? 0) - (b.sequence ?? 0);\n });\n }\n\n // Streaming detection: agent is active if the last assistant message is incomplete,\n // or if there are pending/unresolved tools (mid-turn), or a user message arrived\n // (agent is about to respond), and the file was modified recently.\n let streaming = false;\n const lastMsg = messages[messages.length - 1];\n const assistantIncomplete = lastMsg?.role === 'assistant' && !lastMsg.completedAt;\n const fileRecent = Date.now() - fileStats.mtimeMs < 5000;\n if (assistantIncomplete && fileRecent) {\n streaming = true;\n } else if (lastMsg?.role === 'user' && fileRecent) {\n streaming = true;\n }\n\n return {\n messages,\n workLog,\n byteOffset: newByteOffset,\n streaming,\n totalCost,\n totalTokens,\n latestAssistantUsage,\n contextBoundaryOffset,\n contextActiveBytes: Math.max(0, fileStats.size - contextBoundaryOffset),\n pendingToolUse,\n unresolvedResults,\n lastSequence: sequence,\n mtimeMs: fileStats.mtimeMs,\n proposedPlan,\n planToolUseIds,\n compactBoundaries,\n permissionMode,\n fileEditsByAssistantId,\n pendingAssistantId: pendingAssistant?.id ?? pendingAssistantIdForEdits,\n orphanToolUseIds: orphanToolUseIds.size > 0 ? orphanToolUseIds : undefined,\n countedUsageIds,\n };\n}\n\n/**\n * Parse an ENTIRE JSONL transcript, regardless of size.\n *\n * `parseConversationMessages(file, offset, state)` reads at most MAX_READ_BYTES\n * (10 MB) in a single incremental call. This helper loops the same parser in\n * bounded chunks for callers that need a complete transcript snapshot.\n *\n * ParseState is threaded from the first chunk so tool_use/tool_result pairs can\n * span chunk boundaries. Per-chunk totals are safe to sum because\n * `countedUsageIds` is threaded, so a response split across a read boundary is\n * counted once.\n */\nexport async function parseEntireConversation(\n sessionFile: string,\n options: { flushPendingToolUse?: boolean } = {},\n): Promise<ParseResult> {\n let offset = 0;\n let priorState: ParseState = {\n pendingToolUse: new Map(),\n unresolvedResults: new Map(),\n lastSequence: 0,\n };\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n const compactBoundaries: CompactBoundary[] = [];\n let totalCost = 0;\n let totalTokens = 0;\n let last: ParseResult | null = null;\n\n // Bounded loop: at 10 MB/chunk this covers a 10 GB transcript. The real exit\n // is \"byteOffset stopped advancing\" (EOF or an incomplete trailing line).\n for (let guard = 0; guard < 1024; guard++) {\n const result = await parseConversationMessages(sessionFile, offset, priorState);\n messages.push(...result.messages);\n workLog.push(...result.workLog);\n if (result.compactBoundaries?.length) compactBoundaries.push(...result.compactBoundaries);\n totalCost += result.totalCost;\n totalTokens += result.totalTokens;\n last = result;\n\n if (result.byteOffset <= offset) break; // no progress → EOF\n offset = result.byteOffset;\n priorState = {\n pendingToolUse: result.pendingToolUse,\n unresolvedResults: result.unresolvedResults,\n lastSequence: result.lastSequence,\n planToolUseIds: result.planToolUseIds,\n proposedPlan: result.proposedPlan,\n latestAssistantUsage: result.latestAssistantUsage,\n contextBoundaryOffset: result.contextBoundaryOffset,\n permissionMode: result.permissionMode,\n fileEditsByAssistantId: result.fileEditsByAssistantId,\n pendingAssistantId: result.pendingAssistantId,\n orphanToolUseIds: result.orphanToolUseIds,\n countedUsageIds: result.countedUsageIds,\n };\n }\n\n if (!last) return parseConversationMessages(sessionFile, 0);\n\n if (options.flushPendingToolUse ?? true) {\n for (const [id, entry] of last.pendingToolUse) {\n if (!last.planToolUseIds?.has(id)) {\n workLog.push(entry);\n }\n }\n last.pendingToolUse.clear();\n }\n\n // Take terminal state (byteOffset, boundary offsets, pending maps, mtime, …)\n // from the final chunk; replace the accumulating fields with the full totals.\n return { ...last, messages, workLog, compactBoundaries, totalCost, totalTokens };\n}\n\nexport async function parseFromLastCompactBoundary(\n sessionFile: string,\n priorState?: ParseState,\n): Promise<ParseResult> {\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n return parseConversationMessages(sessionFile, boundaryOffset, priorState);\n}\n","import { stat } from 'node:fs/promises';\nimport { getHarnessBehavior } from '../../../../lib/runtimes/behavior.js';\nimport { parseCodexConversationMessages } from '../codex-conversation-parser.js';\nimport { isOhmypiSessionFile, parseOhmypiConversationMessages } from '../ohmypi-conversation-parser.js';\nimport { isPiSessionFile, parsePiConversationMessages } from '../pi-conversation-parser.js';\nimport { parseFromLastCompactBoundary } from './parser.js';\nimport type { ConversationActivitySummary } from './types.js';\n\n/**\n * PAN-1635: a conversation whose JSONL hasn't been written in this long is no\n * longer \"working\" — even if its last entry is a trailing user/meta line (e.g. a\n * post-compaction summary whose follow-up prompt was eaten by the compaction).\n * Generous enough not to flap a slow-but-live turn; finite so a stranded session\n * can't spin forever.\n */\nconst WORKING_STALENESS_MS = 180_000;\n\n/** In-memory cache mapping sessionFile path → { mtimeMs, size, summary } */\nconst ACTIVITY_SUMMARY_CACHE_MAX = 100;\nconst activitySummaryCache = new Map<string, { mtimeMs: number; size: number; summary: ConversationActivitySummary }>();\n\nexport async function summarizeConversationActivity(\n sessionFile: string,\n options: { harness?: string | null } = {},\n): Promise<ConversationActivitySummary> {\n const fileStats = await stat(sessionFile);\n const cacheKey = `${options.harness ?? 'claude-code'}:${sessionFile}`;\n const behavior = getHarnessBehavior(options.harness as Parameters<typeof getHarnessBehavior>[0]);\n const cached = activitySummaryCache.get(cacheKey);\n if (cached && cached.mtimeMs === fileStats.mtimeMs && cached.size === fileStats.size) {\n return cached.summary;\n }\n\n const parsed = behavior.transcriptKind === 'codex-rollout-jsonl' ? await parseCodexConversationMessages(sessionFile)\n : behavior.transcriptKind === 'ohmypi-jsonl' || isOhmypiSessionFile(sessionFile) ? await parseOhmypiConversationMessages(sessionFile)\n : isPiSessionFile(sessionFile) ? await parsePiConversationMessages(sessionFile)\n // Parse from the last compact boundary instead of the full file — avoids\n // re-reading potentially megabytes of history on every list enrichment tick.\n // Pass an empty priorState so pendingToolUse stays populated rather than being\n // flushed into workLog. This lets us detect genuinely pending tools.\n : await parseFromLastCompactBoundary(\n sessionFile,\n { pendingToolUse: new Map(), unresolvedResults: new Map(), lastSequence: 0 },\n );\n const { messages, streaming, pendingToolUse, workLog, mtimeMs } = parsed;\n const lastMsg = messages[messages.length - 1];\n // Agent is idle only when the last message is an assistant message with a terminal\n // completedAt (stop_reason was end_turn/max_tokens/stop_sequence). Any other state\n // — empty history, last message is user (tool result or prompt), or last message is\n // an assistant still streaming / waiting on tool_use — means the agent is working.\n //\n // PAN-1635: guard on file recency. After a compaction the only post-boundary\n // entries are user-role meta lines (compact summary, /compact echoes) with no\n // following assistant turn, so the raw heuristic below pegs isWorking true\n // forever. A session whose JSONL has been idle for WORKING_STALENESS_MS is not\n // working — mirrors the `fileRecent` guards on `streaming` and `currentTool`.\n // The window is generous (covers a long compaction / deep think that writes\n // nothing for a while); during a real in-progress compaction the PreCompact\n // hook's activity event keeps the card \"working\" independently.\n const workingFileRecent = Date.now() - mtimeMs < WORKING_STALENESS_MS;\n const isWorking = workingFileRecent && (\n messages.length === 0 ||\n lastMsg?.role === 'user' ||\n (lastMsg?.role === 'assistant' && !lastMsg.completedAt));\n\n // Find the most recent pending tool (tool_use sent but tool_result not yet received).\n // pendingToolUse holds the actual unpaired tool_uses, so this works correctly for\n // parallel tool calls where multiple tools are in flight simultaneously.\n // Time-bound: if the file hasn't been modified in 30s, the agent has likely crashed\n // and pending tools are stale — don't report a stale currentTool.\n const fileRecent = Date.now() - mtimeMs < 30_000;\n\n let currentTool: string | null = null;\n if (fileRecent) {\n let maxSequence = -1;\n for (const entry of pendingToolUse.values()) {\n if (entry.toolTitle && (entry.sequence ?? -1) > maxSequence) {\n maxSequence = entry.sequence ?? -1;\n currentTool = entry.toolTitle;\n }\n }\n if (!currentTool && behavior.transcriptKind === 'codex-rollout-jsonl') {\n for (const entry of workLog) {\n if (entry.tone === 'tool' && !entry.result && (entry.sequence ?? -1) > maxSequence) {\n maxSequence = entry.sequence ?? -1;\n currentTool = entry.label;\n }\n }\n }\n }\n\n const summary: ConversationActivitySummary = { messages, streaming, isWorking, currentTool };\n activitySummaryCache.set(cacheKey, { mtimeMs, size: fileStats.size, summary });\n if (activitySummaryCache.size > ACTIVITY_SUMMARY_CACHE_MAX) {\n const firstKey = activitySummaryCache.keys().next().value;\n if (firstKey !== undefined) {\n activitySummaryCache.delete(firstKey);\n }\n }\n return summary;\n}\n","import { open, stat } from 'node:fs/promises';\nimport type { ContextUsage } from '@overdeck/contracts';\nimport { MODEL_CAPABILITIES, resolveModelIdSync } from '../../../../lib/model-capabilities.js';\nimport { findLastCompactBoundary } from './compact-boundary.js';\nimport type { JsonlEntry, LatestAssistantUsage, ParseResult } from './types.js';\n\ntype ModelCapability = (typeof MODEL_CAPABILITIES)[keyof typeof MODEL_CAPABILITIES];\n\n/**\n * Compute \"how full is the context window right now?\" from the JSONL.\n *\n * Approach (matches Claude Code's terminal indicator):\n * 1. Find the last compact boundary so we only count tokens in the current\n * window (post-compaction). For never-compacted sessions, scan the whole\n * file.\n * 2. Find the last assistant message after the boundary with `usage` data.\n * 3. Sum `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`\n * from that message — those are the tokens the model actually saw on\n * its most recent turn, i.e. the live context size.\n * 4. Compare against the model's context window. If observed input ever\n * exceeded the model's default (e.g. Claude Code 1M extended-context\n * mode for Opus / Sonnet), promote the effective window to 1M.\n *\n * Previous implementation used `fileBytes / 4` as a token estimate. That\n * counts every byte of every tool result, MCP payload, and cached file\n * content in the JSONL, which routinely overshoots actual `input_tokens` by\n * 4-10x. Removed.\n *\n * Returns null when the model is unknown. Returns a zero-token snapshot when\n * the active window has no assistant usage yet.\n */\nfunction resolveContextCapability(model: string | null): ModelCapability | null {\n const normalizedModel = model?.trim();\n if (!normalizedModel) return null;\n\n const resolvedModel = resolveModelIdSync(normalizedModel);\n return Object.prototype.hasOwnProperty.call(MODEL_CAPABILITIES, resolvedModel)\n ? MODEL_CAPABILITIES[resolvedModel as keyof typeof MODEL_CAPABILITIES]\n : null;\n}\n\nfunction buildContextUsage(\n capability: ModelCapability,\n activeBytes: number,\n usageSummary: LatestAssistantUsage | null,\n): ContextUsage {\n if (!usageSummary) {\n // No assistant message with usage yet — return zeros against the\n // declared window so the meter renders an empty ring instead of nothing.\n return {\n activeBytes,\n estimatedTokens: 0,\n contextWindow: capability.contextWindow,\n percentUsed: 0,\n };\n }\n\n // input + cache_read + cache_creation = total context the model received\n // on the last turn. output_tokens is the next-turn input from the model's\n // perspective, but for \"how full is my window NOW?\" the input side is the\n // honest answer — Claude Code's terminal uses the same convention.\n const liveContextTokens =\n usageSummary.lastInputTokens +\n usageSummary.lastCacheReadTokens +\n usageSummary.lastCacheCreationTokens;\n\n // 1M-context detection. Claude Code's extended-context mode (the\n // `context-1m-2025-08-07` beta) lets Opus / Sonnet see up to 1,000,000\n // tokens. We can't read the request headers, but if we've ever seen the\n // model accept more than its default window we know extended context is\n // active. Round up to the next plausible tier rather than guessing.\n const observedCeiling = Math.max(usageSummary.maxObservedInputTokens, liveContextTokens);\n const effectiveContextWindow =\n observedCeiling > capability.contextWindow\n ? Math.max(1_000_000, capability.contextWindow)\n : capability.contextWindow;\n\n const percentUsed = Math.min(\n 100,\n Math.max(0, (liveContextTokens / effectiveContextWindow) * 100),\n );\n\n return {\n activeBytes,\n estimatedTokens: liveContextTokens,\n contextWindow: effectiveContextWindow,\n percentUsed,\n lastInputTokens: usageSummary.lastInputTokens,\n lastCacheReadTokens: usageSummary.lastCacheReadTokens,\n lastCacheCreationTokens: usageSummary.lastCacheCreationTokens,\n maxObservedInputTokens: usageSummary.maxObservedInputTokens,\n lastModel: usageSummary.lastModel,\n lastTurnAt: usageSummary.lastTimestamp,\n };\n}\n\nexport function contextUsageFromParseResult(\n result: Pick<ParseResult, 'contextActiveBytes' | 'latestAssistantUsage'>,\n model: string | null,\n): ContextUsage | null {\n const capability = resolveContextCapability(model);\n if (!capability) return null;\n return buildContextUsage(capability, result.contextActiveBytes, result.latestAssistantUsage);\n}\n\nexport async function computeContextUsage(sessionFile: string, model: string | null): Promise<ContextUsage | null> {\n const capability = resolveContextCapability(model);\n if (!capability) return null;\n\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n const fileStats = await stat(sessionFile);\n const activeBytes = Math.max(0, fileStats.size - boundaryOffset);\n\n const usageSummary = await readLatestAssistantUsage(sessionFile, boundaryOffset, activeBytes);\n return buildContextUsage(capability, activeBytes, usageSummary);\n}\n\n/**\n * Stream the JSONL between `boundaryOffset` and EOF, returning usage data\n * from the most recent assistant message + the highest input_tokens ever\n * observed (used for 1M-context detection).\n *\n * Async, line-buffered. Skips malformed lines silently.\n */\nasync function readLatestAssistantUsage(\n sessionFile: string,\n boundaryOffset: number,\n activeBytes: number,\n): Promise<LatestAssistantUsage | null> {\n if (activeBytes <= 0) return null;\n const fh = await open(sessionFile, 'r');\n try {\n const buffer = Buffer.alloc(activeBytes);\n await fh.read(buffer, 0, activeBytes, boundaryOffset);\n const lines = buffer.toString('utf-8').split('\\n');\n\n let result: LatestAssistantUsage | null = null;\n let maxObservedInputTokens = 0;\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: JsonlEntry;\n try {\n entry = JSON.parse(line.replace(/\\r$/, '')) as JsonlEntry;\n } catch {\n continue;\n }\n if (entry.type !== 'assistant' || !entry.message?.usage) continue;\n const u = entry.message.usage;\n const input = u.input_tokens ?? 0;\n const cacheRead = u.cache_read_input_tokens ?? 0;\n const cacheCreate = u.cache_creation_input_tokens ?? 0;\n // Track ceiling across the whole window so a one-off >200k turn flips\n // us to 1M-mode even if the latest turn dropped back down.\n const turnInput = input + cacheRead + cacheCreate;\n if (turnInput > maxObservedInputTokens) maxObservedInputTokens = turnInput;\n // Always replace — we want the *last* one in file order.\n result = {\n lastInputTokens: input,\n lastCacheReadTokens: cacheRead,\n lastCacheCreationTokens: cacheCreate,\n maxObservedInputTokens,\n lastModel: entry.message.model ?? null,\n lastTimestamp: entry.timestamp ?? null,\n };\n }\n\n if (result) result.maxObservedInputTokens = maxObservedInputTokens;\n return result;\n } finally {\n await fh.close();\n }\n}\n","import { readdir } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { encodeClaudeProjectDir } from '../../../../lib/paths.js';\n\n/**\n * Encode a filesystem path to the Claude Code project directory name.\n * Delegates to the shared encodeClaudeProjectDir() which matches\n * Claude Code's actual encoding (all non-alphanumeric chars → hyphens).\n */\nfunction encodeCwdToProjectDir(cwd: string): string {\n return encodeClaudeProjectDir(cwd);\n}\n\n/** Returns ~/.claude/projects/<encoded-cwd>/ */\nfunction claudeProjectDir(cwd: string): string {\n return join(homedir(), '.claude', 'projects', encodeCwdToProjectDir(cwd));\n}\n\n/**\n * Snapshot existing JSONL files, then poll for a NEW file that wasn't there before.\n *\n * The old approach checked mtime >= spawnTime, which matched any active session\n * (including the user's own Claude Code conversation). This approach is exact:\n * only a file that didn't exist before the spawn can be the new session.\n *\n * Call snapshotSessionFiles() BEFORE spawning, then pass the result to\n * discoverSessionFile() AFTER spawning.\n */\nexport async function snapshotSessionFiles(cwd: string): Promise<Set<string>> {\n const projectDir = claudeProjectDir(cwd);\n try {\n const entries = await readdir(projectDir);\n return new Set(entries.filter(e => e.endsWith('.jsonl')));\n } catch {\n return new Set();\n }\n}\n\n/**\n * Wait for a new JSONL session file that wasn't in the pre-spawn snapshot.\n *\n * Polls every 500ms for up to 60 seconds. Returns the absolute path when found.\n * Resolves with null if no new file appears within the timeout.\n */\nexport async function discoverSessionFile(\n cwd: string,\n existingFiles: Set<string>,\n timeoutMs = 60_000,\n): Promise<string | null> {\n const projectDir = claudeProjectDir(cwd);\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n try {\n const entries = await readdir(projectDir);\n for (const entry of entries) {\n if (!entry.endsWith('.jsonl')) continue;\n if (!existingFiles.has(entry)) {\n return join(projectDir, entry);\n }\n }\n } catch {\n // Project directory doesn't exist yet — Claude hasn't started\n }\n\n await new Promise<void>((r) => setTimeout(r, 500));\n }\n\n return null;\n}\n","import { watch } from 'node:fs/promises';\nimport { parseConversationMessages } from './parser.js';\nimport type { ParseResult, ParseState } from './types.js';\n\nexport function gateSnapshotEmission(\n fileWasReset: boolean,\n newMessageCount: number,\n highWaterCount: number,\n): { snapshot: boolean; highWaterCount: number; suppressedShrink: boolean } {\n if (!fileWasReset) {\n // Normal incremental delta — always a merge, never touches the high-water mark.\n return { snapshot: false, highWaterCount, suppressedShrink: false };\n }\n if (newMessageCount < highWaterCount) {\n // Transient truncate-rewrite read — downgrade to merge so the client keeps\n // its history. Do not lower the high-water mark.\n return { snapshot: false, highWaterCount, suppressedShrink: true };\n }\n // A genuine, non-shrinking full re-parse — safe to assert as the snapshot.\n return { snapshot: true, highWaterCount: newMessageCount, suppressedShrink: false };\n}\n\n// ─── File watcher ─────────────────────────────────────────────────────────────\n\nexport interface ConversationWatchHandle {\n stop: () => void;\n}\n\n/**\n * Watch a JSONL session file for appends. Calls `callback` with parse results\n * when new content is detected. Uses fs.watch with 500ms polling fallback.\n *\n * Returns a handle with `stop()` to clean up.\n */\nexport function watchConversation(\n sessionFile: string,\n callback: (result: ParseResult) => void | Promise<void>,\n options: { byteOffset?: number; priorState?: ParseState } = {},\n): ConversationWatchHandle {\n let byteOffset = options.byteOffset ?? 0;\n let priorState: ParseState | undefined = options.priorState;\n let stopped = false;\n let isParsing = false;\n let abortController: AbortController | null = null;\n let pollInterval: ReturnType<typeof setTimeout> | null = null;\n\n async function handleChange(): Promise<void> {\n if (stopped || isParsing) return;\n isParsing = true;\n try {\n const result = await parseConversationMessages(sessionFile, byteOffset, priorState);\n if (result.byteOffset < byteOffset) {\n // File was truncated or rotated — reset to full re-parse\n byteOffset = 0;\n priorState = undefined;\n const fullResult = await parseConversationMessages(sessionFile, 0);\n if (fullResult.byteOffset > 0) {\n byteOffset = fullResult.byteOffset;\n priorState = {\n pendingToolUse: fullResult.pendingToolUse,\n unresolvedResults: fullResult.unresolvedResults,\n lastSequence: fullResult.lastSequence,\n planToolUseIds: fullResult.planToolUseIds,\n proposedPlan: fullResult.proposedPlan,\n latestAssistantUsage: fullResult.latestAssistantUsage,\n contextBoundaryOffset: fullResult.contextBoundaryOffset,\n permissionMode: fullResult.permissionMode,\n countedUsageIds: fullResult.countedUsageIds,\n fileEditsByAssistantId: fullResult.fileEditsByAssistantId,\n pendingAssistantId: fullResult.pendingAssistantId,\n orphanToolUseIds: fullResult.orphanToolUseIds,\n };\n // Include in-flight tools so the live view shows pending work\n const workLog = [...fullResult.workLog, ...fullResult.pendingToolUse.values()];\n await callback({ ...fullResult, workLog });\n }\n } else if (result.byteOffset > byteOffset) {\n byteOffset = result.byteOffset;\n priorState = {\n pendingToolUse: result.pendingToolUse,\n unresolvedResults: result.unresolvedResults,\n lastSequence: result.lastSequence,\n planToolUseIds: result.planToolUseIds,\n proposedPlan: result.proposedPlan,\n latestAssistantUsage: result.latestAssistantUsage,\n contextBoundaryOffset: result.contextBoundaryOffset,\n permissionMode: result.permissionMode,\n countedUsageIds: result.countedUsageIds,\n fileEditsByAssistantId: result.fileEditsByAssistantId,\n pendingAssistantId: result.pendingAssistantId,\n orphanToolUseIds: result.orphanToolUseIds,\n };\n // Include in-flight tools so the live view shows pending work\n const workLog = [...result.workLog, ...result.pendingToolUse.values()];\n await callback({ ...result, workLog });\n }\n } catch {\n // File may have been rotated or is temporarily unavailable\n } finally {\n isParsing = false;\n }\n }\n\n // Try fs.watch first (inotify on Linux)\n // Create AbortController synchronously so stop() can always abort,\n // even if called before the async startWatch() body runs.\n abortController = new AbortController();\n\n async function startWatch(): Promise<void> {\n try {\n const watcher = watch(sessionFile, { signal: abortController!.signal });\n for await (const _event of watcher) {\n await handleChange();\n }\n } catch (err) {\n if (!stopped) {\n // Fallback to polling on watch failure\n startPolling();\n }\n }\n }\n\n function startPolling(): void {\n async function poll(): Promise<void> {\n if (stopped) return;\n await handleChange();\n pollInterval = setTimeout(poll, 500);\n }\n pollInterval = setTimeout(poll, 500);\n }\n\n // Start watch; polling is only a fallback when fs.watch itself fails.\n startWatch();\n\n return {\n stop() {\n stopped = true;\n if (abortController) {\n abortController.abort();\n abortController = null;\n }\n if (pollInterval !== null) {\n clearTimeout(pollInterval);\n pollInterval = null;\n }\n },\n };\n}\n","export type {\n ConversationActivitySummary,\n LatestAssistantUsage,\n ParseResult,\n ParseState,\n} from './conversation/types.js';\nexport { summarizeConversationActivity } from './conversation/activity-summary.js';\nexport { contextUsageFromParseResult, computeContextUsage } from './conversation/context-usage.js';\nexport { findLastCompactBoundary } from './conversation/compact-boundary.js';\nexport { parseConversationMessages, parseEntireConversation, parseFromLastCompactBoundary } from './conversation/parser.js';\nexport { snapshotSessionFiles, discoverSessionFile } from './conversation/session-files.js';\nexport { gateSnapshotEmission, watchConversation } from './conversation/watch.js';\nexport type { ConversationWatchHandle } from './conversation/watch.js';\n","import { Effect } from 'effect';\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\nimport { findLastCompactBoundary } from './conversation-service.js';\nimport { generateSmartSummary } from '../../../lib/conversations/smart-compaction.js';\nimport { generateFallbackSummary } from '../../../lib/conversations/summary-fork.js';\nimport { loadConfigSync } from '../../../lib/config-yaml.js';\nimport { getAgentRuntimeBaseCommand, getProviderExportsForModel } from '../../../lib/agents.js';\nimport { getEventStore } from '../event-store.js';\nimport { isBackgroundFeatureEnabled } from '../../../lib/background-ai/features.js';\n\nconst activeCompactions = new Set<string>();\nexport function isCompacting(sessionFile: string): boolean {\n return activeCompactions.has(sessionFile);\n}\n\nexport interface NativeCompactionResult {\n summary: string;\n tokensBefore: number;\n boundaryUuid: string;\n model: string;\n forkedSessionId: string;\n forkedSessionFile: string;\n}\n\nexport interface MaybeCompactBeforeRespawnOptions {\n sessionFile: string | null | undefined;\n cwd: string;\n shouldCompact: boolean;\n}\n\nexport function getConversationCompactionSettings() {\n const { config } = loadConfigSync();\n return {\n model: config.conversations.compactionModel,\n manualCompactMode: config.conversations.manualCompactMode,\n richCompaction: config.conversations.richCompaction,\n };\n}\n\nexport function shouldInterceptManualCompact(message: string): boolean {\n const trimmed = message.trim();\n if (trimmed !== '/compact') return false;\n return getConversationCompactionSettings().manualCompactMode === 'overdeck-native';\n}\n\nexport async function estimateContextTokens(sessionFile: string | null | undefined): Promise<number> {\n if (!sessionFile || !existsSync(sessionFile)) return 0;\n try {\n const content = await readFile(sessionFile, 'utf-8');\n const lines = content.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n try {\n const entry = JSON.parse(line);\n const usage = entry?.message?.usage ?? entry?.usage;\n if (usage && typeof usage.input_tokens === 'number') {\n const total = (usage.input_tokens ?? 0)\n + (usage.cache_creation_input_tokens ?? 0)\n + (usage.cache_read_input_tokens ?? 0);\n if (total > 0) return total;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch (err) {\n console.warn(`[conversation-compaction] estimateContextTokens failed for ${sessionFile}:`, err);\n }\n return 0;\n}\n\nfunction buildContinuationSummary(summary: string, model: string): string {\n return [\n 'This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.',\n '',\n summary,\n '',\n `Overdeck native compaction model: ${model}`,\n '',\n 'Continue from this summary without redoing already-completed work.',\n ].join('\\n');\n}\n\nexport async function compactConversationNative(sessionFile: string, conversationName?: string): Promise<NativeCompactionResult> {\n if (!existsSync(sessionFile)) {\n throw new Error(`Session file not found: ${sessionFile}`);\n }\n activeCompactions.add(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: true } });\n }\n try {\n return await doCompact(sessionFile);\n } finally {\n activeCompactions.delete(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: false } });\n }\n }\n}\n\nasync function doCompact(sessionFile: string): Promise<NativeCompactionResult> {\n\n const settings = getConversationCompactionSettings();\n const tokensBefore = await estimateContextTokens(sessionFile);\n\n let summary: string;\n try {\n const result = await Effect.runPromise(generateSmartSummary({\n jsonlPath: sessionFile,\n model: settings.model,\n richMode: settings.richCompaction,\n mode: 'fork',\n }));\n summary = result.summary;\n } catch (error) {\n console.warn(`[conversation-compaction] Smart summary failed, falling back to heuristic:`, error);\n summary = await Effect.runPromise(generateFallbackSummary(sessionFile));\n }\n\n const continuation = buildContinuationSummary(summary, settings.model);\n const boundaryUuid = randomUUID();\n const timestamp = new Date().toISOString();\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n\n const entries = [\n JSON.stringify({\n parentUuid: null,\n isSidechain: false,\n type: 'system',\n subtype: 'compact_boundary',\n content: 'Conversation compacted',\n isMeta: false,\n timestamp,\n uuid: boundaryUuid,\n level: 'info',\n compactMetadata: {\n trigger: 'overdeck-native',\n preTokens: tokensBefore,\n model: settings.model,\n previousBoundaryOffset: boundaryOffset,\n },\n }),\n JSON.stringify({\n parentUuid: boundaryUuid,\n isSidechain: false,\n type: 'user',\n message: {\n role: 'user',\n content: continuation,\n },\n isVisibleInTranscriptOnly: true,\n isCompactSummary: true,\n uuid: randomUUID(),\n timestamp,\n }),\n ];\n\n const forkedSessionId = randomUUID();\n const forkedSessionFile = join(dirname(sessionFile), `${forkedSessionId}.jsonl`);\n await writeFile(forkedSessionFile, `${entries.join('\\n')}\\n`, 'utf-8');\n\n return {\n summary: continuation,\n tokensBefore,\n boundaryUuid,\n model: settings.model,\n forkedSessionId,\n forkedSessionFile,\n };\n}\n\nexport async function maybeCompactBeforeRespawn(\n opts: MaybeCompactBeforeRespawnOptions,\n): Promise<{ forkedSessionId: string; forkedSessionFile: string } | null> {\n if (!opts.sessionFile || !existsSync(opts.sessionFile)) return null;\n if (!opts.shouldCompact) {\n console.log('[conversation-compaction] Skipping compact (shouldCompact=false)');\n return null;\n }\n // Background AI gate: low-cost mode (or the summaryFork toggle) skips\n // automatic LLM compaction before respawn.\n if (!isBackgroundFeatureEnabled('summaryFork')) return null;\n\n const tokens = await estimateContextTokens(opts.sessionFile);\n console.log(`[conversation-compaction] Compacting before respawn (shouldCompact=true, tokens=${tokens})`);\n const result = await compactConversationNative(opts.sessionFile);\n return { forkedSessionId: result.forkedSessionId, forkedSessionFile: result.forkedSessionFile };\n}\n\nexport async function buildCompactionRuntimeInfo(model: string): Promise<{ command: string; exports: string }> {\n return {\n command: await getAgentRuntimeBaseCommand(model),\n exports: await getProviderExportsForModel(model),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"conversation-compaction-BQvZh09I.js","names":[],"sources":["../src/dashboard/server/services/codex-conversation-parser.ts","../src/dashboard/server/services/conversation/types.ts","../src/dashboard/server/services/conversation/compact-boundary.ts","../src/dashboard/server/services/conversation/parser.ts","../src/dashboard/server/services/conversation/activity-summary.ts","../src/dashboard/server/services/conversation/context-usage.ts","../src/dashboard/server/services/conversation/session-files.ts","../src/dashboard/server/services/conversation/watch.ts","../src/dashboard/server/services/conversation-service.ts","../src/dashboard/server/services/conversation-compaction.ts"],"sourcesContent":["/**\n * OpenAI Codex → chat panel adapter (PAN-1520).\n *\n * Codex conversations write a \"rollout\" JSONL under the per-agent\n * $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<uuid>-<threadId>.jsonl. The schema\n * is OpenAI's, not Claude Code's, so the Claude parser (and even the Pi parser,\n * which the path also happens to match) find zero message entries and the chat\n * panel renders blank. This module is the adapter — it reads a Codex rollout\n * and emits the same {@link ParseResult} shape the chat panel already consumes.\n *\n * Scope: chat panel display only.\n *\n * Codex rollout shape (cli ≥ 0.137.0):\n * - `type: 'session_meta' | 'turn_context'` — metadata, ignored for display.\n * - `type: 'event_msg'` with `payload.type`:\n * - `user_message` — the user's prompt (clean text in `payload.message`)\n * - `agent_message` — the assistant's visible reply (`payload.message`)\n * - `token_count` — cumulative usage in `payload.info.total_token_usage`\n * - `type: 'response_item'` with `payload.type`:\n * - `function_call` / `custom_tool_call` — tool invocation\n * - `function_call_output` / `custom_tool_call_output` — tool result\n * - `message` — the model's raw turn incl. injected AGENTS.md context;\n * skipped (user/assistant text comes from event_msg).\n * - `reasoning` — chain-of-thought; Codex encrypts it, so it is skipped.\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport type { ChatMessage, CompactBoundary, WorkLogEntry } from '@overdeck/contracts';\nimport type { ParseResult } from './conversation-service.js';\nimport { parseCodexSessionSync } from '../../../lib/cost-parsers/codex-parser.js';\n\ninterface CodexTokenUsage {\n input_tokens?: number;\n cached_input_tokens?: number;\n output_tokens?: number;\n reasoning_output_tokens?: number;\n total_tokens?: number;\n}\n\ninterface CodexPayload {\n type?: string;\n message?: string;\n name?: string;\n call_id?: string;\n arguments?: string;\n input?: unknown;\n output?: unknown;\n info?: { total_token_usage?: CodexTokenUsage };\n [k: string]: unknown;\n}\n\ninterface CodexEntry {\n type?: string;\n timestamp?: string;\n payload?: CodexPayload;\n [k: string]: unknown;\n}\n\n/** Flatten a Codex tool output (usually a string) into display text. */\nfunction extractToolOutput(output: unknown): string {\n if (typeof output === 'string') return output.trim();\n if (output && typeof output === 'object') {\n const obj = output as Record<string, unknown>;\n if (typeof obj['output'] === 'string') return (obj['output'] as string).trim();\n if (typeof obj['content'] === 'string') return (obj['content'] as string).trim();\n try {\n return JSON.stringify(output);\n } catch {\n return '';\n }\n }\n return '';\n}\n\n/**\n * Pull the shell command out of an `exec_command` tool call's JSON arguments so\n * it renders in the work log's command slot. Returns undefined for other tools.\n */\nfunction extractCommand(name: string, args: string): string | undefined {\n if (name !== 'exec_command' && name !== 'shell') return undefined;\n try {\n const parsed = JSON.parse(args) as Record<string, unknown>;\n const cmd = parsed['cmd'] ?? parsed['command'];\n if (typeof cmd === 'string') return cmd;\n if (Array.isArray(cmd)) return cmd.join(' ');\n } catch {\n // arguments weren't JSON — fall through\n }\n return undefined;\n}\n\n/**\n * Parse a Codex rollout JSONL into the ParseResult shape the chat panel\n * consumes. Always a full read (rollouts are small enough); incremental-parse\n * state fields are returned as empty stubs, matching the Pi adapter.\n */\nexport async function parseCodexConversationMessages(sessionFile: string): Promise<ParseResult> {\n const fileStats = await stat(sessionFile);\n const raw = await readFile(sessionFile, 'utf-8');\n\n const lines = raw.split('\\n').filter((line) => line.trim().length > 0);\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n const compactBoundaries: CompactBoundary[] = [];\n // Matches function_call → function_call_output by call_id within this parse.\n const toolCallsByCallId = new Map<string, WorkLogEntry>();\n let totalTokens = 0;\n let sequence = 0;\n\n for (const line of lines) {\n let entry: CodexEntry;\n try {\n entry = JSON.parse(line) as CodexEntry;\n } catch {\n continue;\n }\n if (!entry || typeof entry !== 'object') continue;\n const payload = entry.payload;\n if (!payload || typeof payload !== 'object') continue;\n const createdAt = entry.timestamp ?? new Date().toISOString();\n const ptype = payload.type;\n\n if (entry.type === 'event_msg') {\n if (ptype === 'user_message' || ptype === 'agent_message') {\n const text = typeof payload.message === 'string' ? payload.message.trim() : '';\n if (!text) continue;\n sequence += 1;\n messages.push({\n id: `codex-${ptype === 'user_message' ? 'user' : 'agent'}-${sequence}`,\n role: ptype === 'user_message' ? 'user' : 'assistant',\n text,\n createdAt,\n completedAt: createdAt,\n streaming: false,\n sequence,\n });\n } else if (ptype === 'token_count') {\n // Cumulative usage — the latest token_count carries the running total.\n const usage = payload.info?.total_token_usage;\n if (usage) {\n totalTokens = typeof usage.total_tokens === 'number'\n ? usage.total_tokens\n : (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);\n }\n }\n continue;\n }\n\n if (entry.type === 'response_item') {\n if (ptype === 'function_call' || ptype === 'custom_tool_call') {\n const callId = typeof payload.call_id === 'string' ? payload.call_id : '';\n const name = typeof payload.name === 'string' ? payload.name : 'tool';\n const args = typeof payload.arguments === 'string'\n ? payload.arguments\n : payload.input != null ? JSON.stringify(payload.input) : '';\n const command = extractCommand(name, args);\n sequence += 1;\n const wl: WorkLogEntry = {\n id: callId || `codex-tool-${sequence}`,\n createdAt,\n label: command ? 'Shell' : name,\n tone: 'tool',\n sequence,\n ...(command ? { command } : args ? { detail: args } : {}),\n };\n workLog.push(wl);\n if (callId) toolCallsByCallId.set(callId, wl);\n } else if (ptype === 'function_call_output' || ptype === 'custom_tool_call_output') {\n const callId = typeof payload.call_id === 'string' ? payload.call_id : '';\n const output = extractToolOutput(payload.output);\n const wl = callId ? toolCallsByCallId.get(callId) : undefined;\n if (wl) {\n if (output) (wl as { result?: string }).result = output;\n if (callId) toolCallsByCallId.delete(callId);\n } else if (output) {\n // Output with no matching call (truncated/rotated) — stand-alone entry.\n sequence += 1;\n workLog.push({\n id: callId || `codex-tool-out-${sequence}`,\n createdAt,\n label: 'Tool result',\n result: output,\n tone: 'tool',\n sequence,\n });\n }\n }\n // response_item 'message' (injected context) and 'reasoning' (encrypted)\n // carry nothing user-visible — intentionally skipped.\n continue;\n }\n }\n\n // Codex turns are written as complete agent_message events (not streamed\n // token-by-token into the rollout), so there is no partial-turn state to\n // surface — the chat panel never shows a stuck typing indicator.\n const streaming = false;\n\n // Cost is derived by the canonical Codex cost parser (single source of truth\n // for rollout pricing) so the conversation list shows real spend rather than\n // $0. token_count already gave us the cumulative throughput above.\n const usage = parseCodexSessionSync(sessionFile);\n const totalCost = usage?.cost_v2 ?? usage?.cost ?? 0;\n\n return {\n messages,\n workLog,\n byteOffset: fileStats.size,\n streaming,\n totalCost,\n totalTokens,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: fileStats.size,\n pendingToolUse: new Map(),\n unresolvedResults: new Map(),\n lastSequence: sequence,\n mtimeMs: fileStats.mtimeMs,\n planToolUseIds: new Set(),\n compactBoundaries,\n fileEditsByAssistantId: new Map(),\n };\n}\n","import type { ChatMessage, CompactBoundary, ProposedPlan, WorkLogEntry } from '@overdeck/contracts';\n\nexport interface ParseResult {\n messages: ChatMessage[];\n workLog: WorkLogEntry[];\n /** Byte offset after the last parsed line — pass back for incremental reads. */\n byteOffset: number;\n /** True when the last assistant message has no completedAt and file was modified recently. */\n streaming: boolean;\n /** Total estimated cost in USD computed from assistant message usage data (cache-discount aware). */\n totalCost: number;\n /** Total token throughput (input + output + cache read + cache write) across assistant messages. */\n totalTokens: number;\n /** Last assistant usage observed after the active compact boundary. */\n latestAssistantUsage: LatestAssistantUsage | null;\n /** Byte offset of the active compact boundary used for context-window usage. */\n contextBoundaryOffset: number;\n /** Bytes from the active compact boundary through EOF. */\n contextActiveBytes: number;\n /** Unpaired tool_use entries waiting for tool_result (persist across incremental calls). */\n pendingToolUse: Map<string, WorkLogEntry>;\n /** Pre-arrived tool_result entries waiting for tool_use (persist across incremental calls). */\n unresolvedResults: Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>;\n /** Last sequence number assigned (persist across incremental calls). */\n lastSequence: number;\n /** File modification time in ms from the stat call made during parsing. */\n mtimeMs: number;\n /** Active proposed plan (ExitPlanMode with no matching tool_result yet). */\n proposedPlan?: ProposedPlan;\n /** ExitPlanMode tool_use IDs (persist across incremental calls). */\n planToolUseIds?: Set<string>;\n /** Compact boundary markers detected in the JSONL. */\n compactBoundaries?: CompactBoundary[];\n /** Current permission mode (plan/default/bypassPermissions/acceptEdits). */\n permissionMode?: string;\n /** Map assistant message ID → file paths touched by file-modifying tool_use calls. */\n fileEditsByAssistantId?: Map<string, Array<{ tool: string; filePath: string }>>;\n /** ID of the current pendingAssistant message (carried across incremental parses for file-edit tracking). */\n pendingAssistantId?: string;\n /** Orphaned tool_use entry UUIDs awaiting re-keying (carried across incremental parses). */\n orphanToolUseIds?: Set<string>;\n /**\n * Request/message IDs whose usage has already been counted into totalCost/totalTokens.\n * Claude Code writes one API response across several JSONL lines (text block, tool_use\n * block, …) that each repeat the same `usage`; counting every line double-counts cost.\n * Carried across incremental parses so a response split across read boundaries is counted once.\n */\n countedUsageIds?: Set<string>;\n}\n\n/** State carried across incremental parseConversationMessages calls. */\nexport interface ParseState {\n pendingToolUse: Map<string, WorkLogEntry>;\n unresolvedResults: Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>;\n lastSequence: number;\n planToolUseIds?: Set<string>;\n proposedPlan?: ProposedPlan;\n /** Latest assistant usage observed after the active compact boundary. */\n latestAssistantUsage?: LatestAssistantUsage | null;\n /** Byte offset of the active compact boundary used for context-window usage. */\n contextBoundaryOffset?: number;\n /** Current permission mode (plan/default/bypassPermissions/acceptEdits). */\n permissionMode?: string;\n /** Map assistant message ID → file paths touched by file-modifying tool_use calls in that turn. */\n fileEditsByAssistantId?: Map<string, Array<{ tool: string; filePath: string }>>;\n /** ID of the current pendingAssistant message (carried across incremental parses for file-edit tracking). */\n pendingAssistantId?: string;\n /** Orphaned tool_use entry UUIDs awaiting re-keying (carried across incremental parses). */\n orphanToolUseIds?: Set<string>;\n /** Request/message IDs already counted into cost/tokens (see ParseResult.countedUsageIds). */\n countedUsageIds?: Set<string>;\n}\n\nexport interface ConversationActivitySummary {\n messages: ChatMessage[];\n streaming: boolean;\n isWorking: boolean;\n /** Tool name of the most recently pending tool call, if any (e.g. \"Bash\", \"Read\"). */\n currentTool: string | null;\n}\n\n/** Maximum bytes to read in a single incremental chunk (10 MB). */\nexport const MAX_READ_BYTES = 10 * 1024 * 1024;\nexport const MAX_FALLBACK_BYTES = 5 * 1024 * 1024;\n\nexport interface JsonlUsage {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n}\n\nexport interface JsonlEntry {\n type?: string;\n role?: string;\n message?: {\n id?: string;\n role?: string;\n content?: unknown[] | string;\n model?: string;\n stop_reason?: string | null;\n usage?: JsonlUsage;\n };\n timestamp?: string;\n sessionId?: string;\n uuid?: string;\n /** Claude Code per-API-request id. Stable across the multiple JSONL lines of one response. */\n requestId?: string;\n}\n\nexport interface ContentBlock {\n type?: string;\n text?: string;\n thinking?: string;\n id?: string;\n name?: string;\n input?: unknown;\n tool_use_id?: string;\n content?: unknown;\n is_error?: boolean;\n}\n\nexport interface LatestAssistantUsage {\n lastInputTokens: number;\n lastCacheReadTokens: number;\n lastCacheCreationTokens: number;\n maxObservedInputTokens: number;\n lastModel: string | null;\n lastTimestamp: string | null;\n}\n","import { open, stat } from 'node:fs/promises';\nimport { MAX_FALLBACK_BYTES, MAX_READ_BYTES } from './types.js';\n\n// ─── Compact boundary offset cache ───────────────────────────────────────────\n\n/**\n * In-memory cache mapping JSONL file path → last compact_boundary byte offset\n * and the byte offset up to which we've scanned complete lines.\n * Persists across requests so we don't re-scan the entire file each time.\n */\nconst COMPACT_OFFSET_CACHE_MAX = 100;\nconst compactOffsetCache = new Map<string, { boundaryOffset: number; scannedUpTo: number }>();\n\nfunction setCompactOffsetCache(sessionFile: string, value: { boundaryOffset: number; scannedUpTo: number }): void {\n compactOffsetCache.set(sessionFile, value);\n if (compactOffsetCache.size > COMPACT_OFFSET_CACHE_MAX) {\n const firstKey = compactOffsetCache.keys().next().value;\n if (firstKey !== undefined) {\n compactOffsetCache.delete(firstKey);\n }\n }\n}\n\n/**\n * Find the byte offset of the last `compact_boundary` system entry in a JSONL file.\n *\n * Uses an in-memory cache keyed by file path. When the file grows beyond\n * the cached scan position, only the new portion is scanned. Returns 0 if no boundary found.\n */\nexport async function findLastCompactBoundary(sessionFile: string): Promise<number> {\n const fileStats = await stat(sessionFile);\n const fileSize = fileStats.size;\n\n const cached = compactOffsetCache.get(sessionFile);\n\n // If file hasn't grown since last scan, return cached offset without reading\n if (cached && cached.scannedUpTo === fileSize) {\n return cached.boundaryOffset;\n }\n\n // No cache or file shrank — scan entire file in capped chunks\n if (!cached || fileSize < cached.scannedUpTo) {\n let lastBoundaryOffset = 0;\n let scanPos = 0;\n const fh = await open(sessionFile, 'r');\n try {\n while (scanPos < fileSize) {\n const toRead = Math.min(fileSize - scanPos, MAX_READ_BYTES);\n let buf: Buffer;\n let bytesRead: number;\n try {\n buf = Buffer.alloc(toRead);\n const result = await fh.read(buf, 0, toRead, scanPos);\n bytesRead = result.bytesRead;\n } catch {\n break;\n }\n\n if (bytesRead === 0) break;\n\n let scanBytes = bytesRead;\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n\n if (lastNewline !== -1) {\n scanBytes = lastNewline + 1;\n } else if (scanPos + bytesRead < fileSize) {\n // No newline in chunk and more file remains — read more, capped\n const remaining = Math.min(fileSize - scanPos, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n let fullBytesRead: number;\n try {\n const result = await fh.read(fullBuf, 0, remaining, scanPos);\n fullBytesRead = result.bytesRead;\n } catch {\n break;\n }\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n scanBytes = fullLastNewline + 1;\n buf = fullBuf;\n } else {\n scanBytes = fullBytesRead;\n buf = fullBuf;\n }\n }\n\n const text = buf.toString('utf-8', 0, scanBytes);\n let bytePos = scanPos;\n const lines = text.split('\\n');\n for (const line of lines) {\n if (line.trim()) {\n try {\n const cleanLine = line.replace(/\\r$/, '');\n const entry = JSON.parse(cleanLine);\n if (entry.type === 'system' && entry.subtype === 'compact_boundary') {\n lastBoundaryOffset = bytePos;\n }\n } catch { /* skip invalid lines */ }\n }\n bytePos += Buffer.byteLength(line, 'utf-8') + 1; // +1 for \\n\n }\n\n scanPos += scanBytes;\n }\n } finally {\n await fh.close();\n }\n\n setCompactOffsetCache(sessionFile, { boundaryOffset: lastBoundaryOffset, scannedUpTo: fileSize });\n return lastBoundaryOffset;\n }\n\n // File grew — read only the new portion, respecting complete-line boundaries\n const newBytes = fileSize - cached.scannedUpTo;\n const toRead = Math.min(newBytes, MAX_READ_BYTES);\n const fh = await open(sessionFile, 'r');\n try {\n let buf: Buffer;\n let bytesRead: number;\n try {\n buf = Buffer.alloc(toRead);\n const result = await fh.read(buf, 0, toRead, cached.scannedUpTo);\n bytesRead = result.bytesRead;\n } catch {\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n\n let scanBytes = 0;\n if (bytesRead > 0) {\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n if (lastNewline !== -1) {\n scanBytes = lastNewline + 1;\n } else if (cached.scannedUpTo + bytesRead < fileSize) {\n // No newline in chunk and more file remains — read to EOF, capped\n const remaining = Math.min(fileSize - cached.scannedUpTo, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n let fullBytesRead: number;\n try {\n const result = await fh.read(fullBuf, 0, remaining, cached.scannedUpTo);\n fullBytesRead = result.bytesRead;\n } catch {\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n scanBytes = fullLastNewline + 1;\n buf = fullBuf;\n } else {\n // No newline even at EOF — don't scan partial trailing line\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n } else {\n // At EOF with no newline — don't scan partial trailing line\n setCompactOffsetCache(sessionFile, { boundaryOffset: cached.boundaryOffset, scannedUpTo: fileSize });\n return cached.boundaryOffset;\n }\n }\n\n const text = buf.toString('utf-8', 0, scanBytes);\n\n let lastBoundaryOffset = cached.boundaryOffset;\n let bytePos = cached.scannedUpTo;\n const lines = text.split('\\n');\n for (const line of lines) {\n if (line.trim()) {\n try {\n const cleanLine = line.replace(/\\r$/, '');\n const entry = JSON.parse(cleanLine);\n if (entry.type === 'system' && entry.subtype === 'compact_boundary') {\n lastBoundaryOffset = bytePos;\n }\n } catch { /* skip invalid lines */ }\n }\n bytePos += Buffer.byteLength(line, 'utf-8') + 1; // +1 for \\n\n }\n\n setCompactOffsetCache(sessionFile, { boundaryOffset: lastBoundaryOffset, scannedUpTo: cached.scannedUpTo + scanBytes });\n return lastBoundaryOffset;\n } finally {\n await fh.close();\n }\n}\n","import { open, readFile, stat } from 'node:fs/promises';\nimport type { ChatMessage, CompactBoundary, ProposedPlan, WorkLogEntry } from '@overdeck/contracts';\nimport { calculateCostSync, getPricingSync } from '../../../../lib/cost.js';\nimport { summarizeToolInputForWorkLog } from '../format-tool-input.js';\nimport { findLastCompactBoundary } from './compact-boundary.js';\nimport { renderableUserText } from './message-filters.js';\nimport { providerFromModel } from './provider.js';\nimport {\n MAX_FALLBACK_BYTES,\n MAX_READ_BYTES,\n type ContentBlock,\n type JsonlEntry,\n type ParseResult,\n type ParseState,\n} from './types.js';\n\n// ─── JSONL parsing ────────────────────────────────────────────────────────────\n\n/**\n * Parse JSONL session file from a byte offset.\n *\n * Returns parsed messages, work log entries, new byte offset, and streaming status.\n * Safe to call repeatedly — incremental reads avoid re-parsing.\n */\nexport async function parseConversationMessages(\n sessionFile: string,\n fromByteOffset = 0,\n priorState?: ParseState,\n): Promise<ParseResult> {\n // Read only new content from the byte offset — avoids re-reading the entire JSONL every tick\n let fileStats: Awaited<ReturnType<typeof stat>>;\n try {\n fileStats = await stat(sessionFile);\n } catch {\n // The transcript file does not exist yet — a freshly-spawned conversation\n // whose runtime has not written its first JSONL line. Treat this as an empty\n // transcript (0 messages) instead of throwing, so a live subscriber can\n // attach the instant the conversation row exists and self-populate once the\n // file appears (watchConversation polls until it does). Mirrors the empty\n // result the truncation branch below returns.\n return {\n messages: [],\n workLog: [],\n byteOffset: 0,\n streaming: false,\n totalCost: 0,\n totalTokens: 0,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: 0,\n pendingToolUse: priorState?.pendingToolUse ?? new Map(),\n unresolvedResults: priorState?.unresolvedResults ?? new Map(),\n lastSequence: priorState?.lastSequence ?? 0,\n mtimeMs: 0,\n permissionMode: priorState?.permissionMode,\n fileEditsByAssistantId: new Map(),\n };\n }\n const fileSize = fileStats.size;\n\n // File was truncated or rotated since last read — signal reset to caller\n if (fromByteOffset > fileSize) {\n return {\n messages: [],\n workLog: [],\n byteOffset: 0,\n streaming: false,\n totalCost: 0,\n totalTokens: 0,\n latestAssistantUsage: null,\n contextBoundaryOffset: 0,\n contextActiveBytes: fileStats.size,\n pendingToolUse: priorState?.pendingToolUse ?? new Map(),\n unresolvedResults: priorState?.unresolvedResults ?? new Map(),\n lastSequence: priorState?.lastSequence ?? 0,\n mtimeMs: fileStats.mtimeMs,\n permissionMode: priorState?.permissionMode,\n fileEditsByAssistantId: new Map(),\n };\n }\n\n if (fromByteOffset === 0 && !priorState && fileSize > MAX_READ_BYTES) {\n return parseEntireConversation(sessionFile);\n }\n\n const toRead = Math.max(0, Math.min(fileSize - fromByteOffset, MAX_READ_BYTES));\n const isIncremental = fromByteOffset > 0;\n\n let newText = '';\n let newByteOffset = fromByteOffset;\n\n if (toRead > 0) {\n const fh = await open(sessionFile, 'r');\n try {\n const buf = Buffer.alloc(toRead);\n const { bytesRead } = await fh.read(buf, 0, toRead, fromByteOffset);\n\n if (bytesRead > 0) {\n const lastNewline = buf.lastIndexOf('\\n', bytesRead - 1);\n\n if (lastNewline !== -1) {\n // At least one complete line — advance only past complete lines\n newText = buf.toString('utf-8', 0, lastNewline + 1);\n newByteOffset = fromByteOffset + lastNewline + 1;\n } else if (!isIncremental && fromByteOffset + bytesRead >= fileSize) {\n // Full parse at EOF with no trailing newline — include trailing bytes\n newText = buf.toString('utf-8', 0, bytesRead);\n newByteOffset = fromByteOffset + bytesRead;\n } else if (fromByteOffset + bytesRead >= fileSize) {\n // Incremental parse at EOF with no newline — don't advance, wait for completion\n newByteOffset = fromByteOffset;\n } else {\n // No newline in a full chunk and more file remains (line exceeds MAX_READ_BYTES).\n // Read more to find a newline boundary, but cap to avoid OOM on pathological files.\n const remaining = Math.min(fileSize - fromByteOffset, MAX_FALLBACK_BYTES);\n const fullBuf = Buffer.alloc(remaining);\n const { bytesRead: fullBytesRead } = await fh.read(fullBuf, 0, remaining, fromByteOffset);\n const fullLastNewline = fullBuf.lastIndexOf('\\n', fullBytesRead - 1);\n if (fullLastNewline !== -1) {\n newText = fullBuf.toString('utf-8', 0, fullLastNewline + 1);\n newByteOffset = fromByteOffset + fullLastNewline + 1;\n } else if (!isIncremental) {\n newText = fullBuf.toString('utf-8', 0, fullBytesRead);\n newByteOffset = fromByteOffset + fullBytesRead;\n } else {\n newByteOffset = fromByteOffset;\n }\n }\n }\n } finally {\n await fh.close();\n }\n }\n\n const lines = newText.split('\\n');\n\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n let totalCost = 0;\n let totalTokens = 0;\n let latestAssistantUsage = priorState?.latestAssistantUsage ?? null;\n let maxObservedInputTokens = latestAssistantUsage?.maxObservedInputTokens ?? 0;\n let contextBoundaryOffset = priorState?.contextBoundaryOffset ?? 0;\n\n // Pending assistant message being assembled from content blocks\n let pendingAssistant: ChatMessage | null = null;\n // Map tool_use id → WorkLogEntry (waiting for tool_result)\n const pendingToolUse = priorState?.pendingToolUse ?? new Map<string, WorkLogEntry>();\n // Map tool_use id → pre-arrived tool_result (waiting for tool_use)\n const unresolvedResults = priorState?.unresolvedResults ?? new Map<string, { resultText?: string; isError: boolean; rawContent: unknown }>();\n // Monotonic sequence counter per JSONL line\n let sequence = priorState?.lastSequence ?? 0;\n // Plan mode: track ExitPlanMode tool_use id → ProposedPlan\n let proposedPlan: ProposedPlan | undefined = priorState?.proposedPlan;\n // Set of ExitPlanMode tool_use IDs so we can match tool_results to plans\n const planToolUseIds = priorState?.planToolUseIds ?? new Set<string>();\n // Compact boundary markers\n const compactBoundaries: CompactBoundary[] = [];\n // Track permission mode across incremental parses\n let permissionMode: string | undefined = priorState?.permissionMode;\n // Track file-modifying tool_use calls per assistant message for diff computation\n const fileEditsByAssistantId = new Map<string, Array<{ tool: string; filePath: string }>>();\n const FILE_EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);\n // Tool_use entries arrive with their own UUID, separate from the text entry that follows.\n // We track these UUIDs so we can re-key edits when the text entry merges them into one message.\n const orphanToolUseIds = priorState?.orphanToolUseIds\n ? new Set(priorState.orphanToolUseIds)\n : new Set<string>();\n // Request/message IDs whose usage has already been counted, so a response spread across\n // multiple JSONL lines (or across incremental read boundaries) is counted exactly once.\n const countedUsageIds = priorState?.countedUsageIds ?? new Set<string>();\n // Carry the prior assistant ID for file-edit association only. Do not\n // materialize it as a ChatMessage: a later flush before user/tool-result lines\n // would emit an invalid ID-only assistant delta.\n const pendingAssistantIdForEdits = priorState?.pendingAssistantId;\n\n let lineByteOffset = fromByteOffset;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const currentLineByteOffset = lineByteOffset;\n lineByteOffset += Buffer.byteLength(line, 'utf-8') + (i < lines.length - 1 ? 1 : 0);\n if (!line.trim()) continue;\n\n let entry: JsonlEntry;\n try {\n entry = JSON.parse(line.replace(/\\r$/, '')) as JsonlEntry;\n } catch {\n continue;\n }\n const lineSequence = sequence++;\n\n if (entry.type === 'user' && entry.message) {\n const msg = entry.message;\n const rawContent = msg.content;\n\n // Flush any pending assistant message\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n pendingAssistant = null;\n }\n\n // Content can be a string (plain text) or array of content blocks\n if (typeof rawContent === 'string' && rawContent.trim()) {\n const text = renderableUserText(rawContent);\n if (text !== null) {\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: entry.uuid ?? `user-${messages.length}`,\n role: 'user',\n text,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n } else if (Array.isArray(rawContent)) {\n // Collect tool_result blocks first (they complete pending WorkLogEntries)\n for (const block of rawContent as ContentBlock[]) {\n if (block.type === 'tool_result' && block.tool_use_id) {\n let resultText: string | undefined;\n if (typeof block.content === 'string') {\n resultText = block.content;\n } else if (Array.isArray(block.content)) {\n resultText = (block.content as Array<{ type?: string; text?: string }>)\n .filter(b => b.type === 'text' && b.text)\n .map(b => b.text)\n .join('\\n');\n }\n if (planToolUseIds.has(block.tool_use_id)) {\n if (proposedPlan && proposedPlan.id === block.tool_use_id) {\n const text = resultText ?? '';\n if (text.includes('approved')) {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n } else if (text.includes(\"doesn't want to proceed\") || text.includes('does not want')) {\n proposedPlan = { ...proposedPlan, status: 'rejected', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n } else {\n const pending = pendingToolUse.get(block.tool_use_id);\n if (pending) {\n pendingToolUse.delete(block.tool_use_id);\n workLog.push({\n ...pending,\n detail: block.is_error\n ? `Error: ${resultText ?? JSON.stringify(block.content)}`\n : pending.detail,\n result: resultText,\n tone: block.is_error ? 'error' : pending.tone,\n });\n } else {\n unresolvedResults.set(block.tool_use_id, {\n resultText,\n isError: block.is_error ?? false,\n rawContent: block.content,\n });\n }\n }\n }\n }\n\n // Collect all text blocks, joining with newlines to preserve multiline input\n const textBlocks: string[] = [];\n for (const block of rawContent as ContentBlock[]) {\n if (block.type === 'text' && block.text) {\n const text = renderableUserText(block.text);\n if (text !== null) textBlocks.push(text);\n }\n }\n if (textBlocks.length > 0) {\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: entry.uuid ?? `user-${messages.length}`,\n role: 'user',\n text: textBlocks.join('\\n'),\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n }\n } else if (entry.type === 'assistant' && entry.message) {\n const msg = entry.message;\n const content = Array.isArray(msg.content) ? msg.content : [];\n\n if (msg.usage) {\n const input = msg.usage.input_tokens ?? 0;\n const cacheRead = msg.usage.cache_read_input_tokens ?? 0;\n const cacheCreate = msg.usage.cache_creation_input_tokens ?? 0;\n const turnInput = input + cacheRead + cacheCreate;\n if (turnInput > maxObservedInputTokens) maxObservedInputTokens = turnInput;\n latestAssistantUsage = {\n lastInputTokens: input,\n lastCacheReadTokens: cacheRead,\n lastCacheCreationTokens: cacheCreate,\n maxObservedInputTokens,\n lastModel: msg.model ?? null,\n lastTimestamp: entry.timestamp ?? null,\n };\n }\n\n // Accumulate cost and token throughput from usage data.\n // Claude Code repeats the same `usage` on every JSONL line of one API response\n // (the text line, each tool_use line, …); dedup on requestId/message.id so each\n // response is counted once, otherwise multi-block turns inflate cost ~2-3×.\n const usageId = entry.requestId ?? msg.id;\n if (msg.usage && (usageId === undefined || !countedUsageIds.has(usageId))) {\n if (usageId !== undefined) countedUsageIds.add(usageId);\n totalTokens +=\n (msg.usage.input_tokens ?? 0) +\n (msg.usage.output_tokens ?? 0) +\n (msg.usage.cache_read_input_tokens ?? 0) +\n (msg.usage.cache_creation_input_tokens ?? 0);\n if (msg.model) {\n const pricing = getPricingSync(providerFromModel(msg.model), msg.model);\n if (pricing) {\n totalCost += calculateCostSync({\n inputTokens: msg.usage.input_tokens ?? 0,\n outputTokens: msg.usage.output_tokens ?? 0,\n cacheReadTokens: msg.usage.cache_read_input_tokens ?? 0,\n cacheWriteTokens: msg.usage.cache_creation_input_tokens ?? 0,\n }, pricing);\n }\n }\n }\n\n let assistantText = '';\n let blockIndex = 0;\n for (const block of content as ContentBlock[]) {\n if (block.type === 'text' && block.text) {\n assistantText += block.text;\n } else if (block.type === 'thinking' && block.thinking) {\n workLog.push({\n id: `${entry.uuid ?? msg.id ?? `asst-${messages.length}`}-thinking-${blockIndex}`,\n createdAt: entry.timestamp ?? new Date().toISOString(),\n label: 'thinking',\n detail: block.thinking,\n tone: 'thinking',\n sequence: lineSequence,\n });\n } else if (block.type === 'tool_use' && block.id) {\n if (block.name === 'ExitPlanMode') {\n const input = block.input as Record<string, unknown> | undefined;\n const planText = typeof input?.plan === 'string' ? input.plan : '';\n const planFilePath = typeof input?.planFilePath === 'string' ? input.planFilePath : undefined;\n proposedPlan = {\n id: block.id,\n plan: planText,\n planFilePath,\n status: 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n };\n planToolUseIds.add(block.id);\n const unresolved = unresolvedResults.get(block.id);\n if (unresolved) {\n unresolvedResults.delete(block.id);\n const resultText = unresolved.resultText ?? '';\n if (resultText.includes('approved')) {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n } else if (resultText.includes(\"doesn't want to proceed\") || resultText.includes('does not want')) {\n proposedPlan = { ...proposedPlan, status: 'rejected', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n } else if (block.name === 'EnterPlanMode') {\n // Skip — don't add to workLog\n } else {\n // Track file-modifying tools for diff computation\n if (block.name && FILE_EDIT_TOOLS.has(block.name) && block.input) {\n const input = block.input as Record<string, unknown>;\n const filePath = typeof input.file_path === 'string' ? input.file_path : undefined;\n if (filePath) {\n const asstId = pendingAssistant?.id ?? pendingAssistantIdForEdits ?? entry.uuid ?? msg.id ?? `asst-${messages.length}`;\n // If no pendingAssistant, this tool_use UUID is orphaned — the text entry\n // will merge it into the final message with a different UUID.\n if (!pendingAssistant) {\n orphanToolUseIds.add(asstId);\n }\n let edits = fileEditsByAssistantId.get(asstId);\n if (!edits) {\n edits = [];\n fileEditsByAssistantId.set(asstId, edits);\n }\n if (!edits.some(e => e.filePath === filePath)) {\n edits.push({ tool: block.name!, filePath });\n }\n }\n }\n // WorkLogEntry for the tool call. We pass the raw input dict\n // through as `toolInput` so the frontend can render per-tool\n // (Bash command as a fenced shell block, file tools as chips,\n // etc.) and pre-compute a short one-line summary for the\n // collapsed row via summarizeToolInputForWorkLog. See PAN-1459.\n const inputDict =\n block.input && typeof block.input === 'object' && !Array.isArray(block.input)\n ? (block.input as Record<string, unknown>)\n : undefined;\n const toolEntry: WorkLogEntry = {\n id: block.id,\n createdAt: entry.timestamp ?? new Date().toISOString(),\n label: block.name ?? 'tool',\n tone: 'tool',\n toolTitle: block.name,\n detail: summarizeToolInputForWorkLog(block.name, inputDict),\n toolInput: inputDict,\n sequence: lineSequence,\n };\n const unresolved = unresolvedResults.get(block.id);\n if (unresolved) {\n unresolvedResults.delete(block.id);\n workLog.push({\n ...toolEntry,\n detail: unresolved.isError\n ? `Error: ${unresolved.resultText ?? JSON.stringify(unresolved.rawContent)}`\n : toolEntry.detail,\n result: unresolved.resultText,\n tone: unresolved.isError ? 'error' : toolEntry.tone,\n });\n } else {\n pendingToolUse.set(block.id, toolEntry);\n }\n }\n }\n blockIndex++;\n }\n\n if (assistantText) {\n const newId = entry.uuid ?? msg.id ?? `asst-${messages.length}`;\n // Flush previous pending assistant\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n // Re-key file edits from old pendingAssistant UUID\n const oldId = pendingAssistant.id;\n if (oldId !== newId) {\n const edits = fileEditsByAssistantId.get(oldId);\n if (edits) {\n fileEditsByAssistantId.delete(oldId);\n fileEditsByAssistantId.set(newId, edits);\n }\n }\n }\n // Re-key orphaned tool_use UUIDs into the merged message's UUID\n for (const orphanId of orphanToolUseIds) {\n if (orphanId !== newId) {\n const edits = fileEditsByAssistantId.get(orphanId);\n if (edits) {\n fileEditsByAssistantId.delete(orphanId);\n const existing = fileEditsByAssistantId.get(newId);\n if (existing) {\n for (const e of edits) {\n if (!existing.some(x => x.filePath === e.filePath)) existing.push(e);\n }\n } else {\n fileEditsByAssistantId.set(newId, edits);\n }\n }\n }\n }\n orphanToolUseIds.clear();\n pendingAssistant = {\n id: entry.uuid ?? msg.id ?? `asst-${messages.length}`,\n role: 'assistant',\n text: assistantText,\n // createdAt = when the assistant response was generated (preserves\n // chronological order for interleaving with work log entries).\n // Duration start is computed separately in session-logic.ts.\n createdAt: entry.timestamp ?? new Date().toISOString(),\n // Any terminal stop reason (end_turn, max_tokens, stop_sequence) marks the response as done.\n // tool_use means more exchanges are coming, so leave completedAt unset.\n // Use || fallback in case entry.timestamp is null (not just undefined).\n completedAt: (msg.stop_reason && msg.stop_reason !== 'tool_use')\n ? (entry.timestamp || new Date().toISOString())\n : undefined,\n streaming: !msg.stop_reason,\n sequence: lineSequence,\n };\n }\n } else if (entry.type === 'system' && (entry as Record<string, unknown>).subtype === 'compact_boundary') {\n contextBoundaryOffset = currentLineByteOffset;\n latestAssistantUsage = null;\n maxObservedInputTokens = 0;\n const meta = (entry as Record<string, unknown>).compactMetadata as Record<string, unknown> | undefined;\n compactBoundaries.push({\n id: (entry as Record<string, unknown>).uuid as string ?? `compact-${lineSequence}`,\n timestamp: entry.timestamp ?? new Date().toISOString(),\n trigger: typeof meta?.trigger === 'string' ? meta.trigger : undefined,\n preTokens: typeof meta?.preTokens === 'number' ? meta.preTokens : undefined,\n model: typeof meta?.model === 'string' ? meta.model : undefined,\n });\n } else if (entry.type === 'permission-mode') {\n // Track permission mode transitions. Exiting 'plan' mode is an approval\n // signal when no explicit plan_mode_exit attachment was emitted.\n const newMode = (entry as Record<string, unknown>).permissionMode as string | undefined;\n if (newMode && permissionMode === 'plan' && newMode !== 'plan') {\n if (proposedPlan && proposedPlan.status === 'pending') {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n }\n permissionMode = newMode;\n } else if (entry.type === 'attachment') {\n // Claude Code 2.1.121+ attachment-based plan mode protocol.\n // The agent writes the plan to ~/.claude/plans/<slug>.md and the runtime\n // emits a `plan_file_reference` attachment with the full content. Approval\n // is signalled by a subsequent `plan_mode_exit` attachment.\n const attachment = (entry as Record<string, unknown>).attachment as Record<string, unknown> | undefined;\n if (attachment?.type === 'plan_file_reference') {\n const planContent = typeof attachment.planContent === 'string' ? attachment.planContent : '';\n if (planContent) {\n const planFilePath = typeof attachment.planFilePath === 'string' ? attachment.planFilePath : undefined;\n const id = ((entry as Record<string, unknown>).uuid as string | undefined) ?? `plan-${lineSequence}`;\n // If permission mode has already exited 'plan', auto-approve since the\n // user already approved before this attachment arrived (or no exit was emitted).\n const alreadyApproved = permissionMode !== 'plan' && permissionMode !== undefined;\n proposedPlan = {\n id,\n plan: planContent,\n planFilePath,\n status: alreadyApproved ? 'approved' : 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n ...(alreadyApproved ? { resolvedAt: entry.timestamp ?? new Date().toISOString() } : {}),\n };\n }\n } else if (attachment?.type === 'plan_mode_exit') {\n if (proposedPlan && proposedPlan.status === 'pending') {\n proposedPlan = { ...proposedPlan, status: 'approved', resolvedAt: entry.timestamp ?? new Date().toISOString() };\n }\n } else if (attachment?.type === 'plan_mode') {\n // Claude Code plan_mode attachment: agent called EnterPlanMode, plan written\n // to an external file. Always start as 'pending' — the permission-mode\n // transition handler (plan → other) will mark it approved when that fires.\n const planFilePath = typeof attachment.planFilePath === 'string' ? attachment.planFilePath : undefined;\n if (planFilePath) {\n try {\n const planContent = (await readFile(planFilePath, 'utf-8')).trim();\n if (planContent) {\n const id = ((entry as Record<string, unknown>).uuid as string | undefined) ?? `plan-${lineSequence}`;\n proposedPlan = {\n id,\n plan: planContent,\n planFilePath,\n status: 'pending',\n createdAt: entry.timestamp ?? new Date().toISOString(),\n };\n }\n } catch {\n // Plan file not yet written — will show once the agent finishes planning\n }\n }\n } else if (attachment?.type === 'queued_command' && attachment?.commandMode === 'prompt') {\n // User message sent as an interrupt while Claude was running — stored as\n // queued_command instead of a regular user entry. Render it in the timeline.\n const prompt = attachment.prompt;\n let text = '';\n if (typeof prompt === 'string') {\n text = prompt;\n } else if (Array.isArray(prompt)) {\n text = (prompt as Array<{ type?: string; text?: string }>)\n .filter((b) => b.type === 'text' && b.text)\n .map((b) => b.text)\n .join('\\n');\n }\n const renderableText = text ? renderableUserText(text) : null;\n if (renderableText !== null) {\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n pendingAssistant = null;\n }\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: ((entry as Record<string, unknown>).uuid as string | undefined) ?? `queued-${lineSequence}`,\n role: 'user',\n text: renderableText,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n } else if (attachment?.type === 'command_permissions') {\n // Session start / resume record — contains the allowedTools list.\n // Render as a system message so the user can see what permissions are in effect.\n const rawTools = Array.isArray(attachment.allowedTools)\n ? (attachment.allowedTools as unknown[]).filter((t) => typeof t === 'string') as string[]\n : [];\n const tools = rawTools.length > 0 ? rawTools.join(', ') : 'None';\n const ts = entry.timestamp ?? new Date().toISOString();\n messages.push({\n id: ((entry as Record<string, unknown>).uuid as string | undefined) ?? `perm-${lineSequence}`,\n role: 'system',\n text: tools,\n createdAt: ts,\n sequence: lineSequence,\n });\n }\n }\n }\n\n // Push any remaining pending assistant\n if (pendingAssistant) {\n messages.push(pendingAssistant);\n }\n\n // Flush any tool_use entries that never got a result (still running).\n // Only flush on non-incremental parses; incremental callers pass priorState\n // and will receive pendingToolUse back for the next call.\n if (!priorState) {\n for (const [id, entry] of pendingToolUse) {\n if (!planToolUseIds.has(id)) {\n workLog.push(entry);\n }\n }\n pendingToolUse.clear();\n }\n\n // Sort by (createdAt, sequence) so the conversation view matches terminal order.\n // Use direct string comparison (not localeCompare) — ISO 8601 timestamps sort lexicographically.\n // For incremental parses (the hot path), messages arrive in file order and are already\n // sorted — skip the O(n log n) sort entirely.\n if (!isIncremental) {\n messages.sort((a, b) => {\n if (a.createdAt < b.createdAt) return -1;\n if (a.createdAt > b.createdAt) return 1;\n return (a.sequence ?? 0) - (b.sequence ?? 0);\n });\n workLog.sort((a, b) => {\n if (a.createdAt < b.createdAt) return -1;\n if (a.createdAt > b.createdAt) return 1;\n return (a.sequence ?? 0) - (b.sequence ?? 0);\n });\n }\n\n // Streaming detection: agent is active if the last assistant message is incomplete,\n // or if there are pending/unresolved tools (mid-turn), or a user message arrived\n // (agent is about to respond), and the file was modified recently.\n let streaming = false;\n const lastMsg = messages[messages.length - 1];\n const assistantIncomplete = lastMsg?.role === 'assistant' && !lastMsg.completedAt;\n const fileRecent = Date.now() - fileStats.mtimeMs < 5000;\n if (assistantIncomplete && fileRecent) {\n streaming = true;\n } else if (lastMsg?.role === 'user' && fileRecent) {\n streaming = true;\n }\n\n return {\n messages,\n workLog,\n byteOffset: newByteOffset,\n streaming,\n totalCost,\n totalTokens,\n latestAssistantUsage,\n contextBoundaryOffset,\n contextActiveBytes: Math.max(0, fileStats.size - contextBoundaryOffset),\n pendingToolUse,\n unresolvedResults,\n lastSequence: sequence,\n mtimeMs: fileStats.mtimeMs,\n proposedPlan,\n planToolUseIds,\n compactBoundaries,\n permissionMode,\n fileEditsByAssistantId,\n pendingAssistantId: pendingAssistant?.id ?? pendingAssistantIdForEdits,\n orphanToolUseIds: orphanToolUseIds.size > 0 ? orphanToolUseIds : undefined,\n countedUsageIds,\n };\n}\n\n/**\n * Parse an ENTIRE JSONL transcript, regardless of size.\n *\n * `parseConversationMessages(file, offset, state)` reads at most MAX_READ_BYTES\n * (10 MB) in a single incremental call. This helper loops the same parser in\n * bounded chunks for callers that need a complete transcript snapshot.\n *\n * ParseState is threaded from the first chunk so tool_use/tool_result pairs can\n * span chunk boundaries. Per-chunk totals are safe to sum because\n * `countedUsageIds` is threaded, so a response split across a read boundary is\n * counted once.\n */\nexport async function parseEntireConversation(\n sessionFile: string,\n options: { flushPendingToolUse?: boolean } = {},\n): Promise<ParseResult> {\n let offset = 0;\n let priorState: ParseState = {\n pendingToolUse: new Map(),\n unresolvedResults: new Map(),\n lastSequence: 0,\n };\n const messages: ChatMessage[] = [];\n const workLog: WorkLogEntry[] = [];\n const compactBoundaries: CompactBoundary[] = [];\n let totalCost = 0;\n let totalTokens = 0;\n let last: ParseResult | null = null;\n\n // Bounded loop: at 10 MB/chunk this covers a 10 GB transcript. The real exit\n // is \"byteOffset stopped advancing\" (EOF or an incomplete trailing line).\n for (let guard = 0; guard < 1024; guard++) {\n const result = await parseConversationMessages(sessionFile, offset, priorState);\n messages.push(...result.messages);\n workLog.push(...result.workLog);\n if (result.compactBoundaries?.length) compactBoundaries.push(...result.compactBoundaries);\n totalCost += result.totalCost;\n totalTokens += result.totalTokens;\n last = result;\n\n if (result.byteOffset <= offset) break; // no progress → EOF\n offset = result.byteOffset;\n priorState = {\n pendingToolUse: result.pendingToolUse,\n unresolvedResults: result.unresolvedResults,\n lastSequence: result.lastSequence,\n planToolUseIds: result.planToolUseIds,\n proposedPlan: result.proposedPlan,\n latestAssistantUsage: result.latestAssistantUsage,\n contextBoundaryOffset: result.contextBoundaryOffset,\n permissionMode: result.permissionMode,\n fileEditsByAssistantId: result.fileEditsByAssistantId,\n pendingAssistantId: result.pendingAssistantId,\n orphanToolUseIds: result.orphanToolUseIds,\n countedUsageIds: result.countedUsageIds,\n };\n }\n\n if (!last) return parseConversationMessages(sessionFile, 0);\n\n if (options.flushPendingToolUse ?? true) {\n for (const [id, entry] of last.pendingToolUse) {\n if (!last.planToolUseIds?.has(id)) {\n workLog.push(entry);\n }\n }\n last.pendingToolUse.clear();\n }\n\n // Take terminal state (byteOffset, boundary offsets, pending maps, mtime, …)\n // from the final chunk; replace the accumulating fields with the full totals.\n return { ...last, messages, workLog, compactBoundaries, totalCost, totalTokens };\n}\n\nexport async function parseFromLastCompactBoundary(\n sessionFile: string,\n priorState?: ParseState,\n): Promise<ParseResult> {\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n return parseConversationMessages(sessionFile, boundaryOffset, priorState);\n}\n","import { stat } from 'node:fs/promises';\nimport { getHarnessBehavior } from '../../../../lib/runtimes/behavior.js';\nimport { parseCodexConversationMessages } from '../codex-conversation-parser.js';\nimport { isOhmypiSessionFile, parseOhmypiConversationMessages } from '../ohmypi-conversation-parser.js';\nimport { isPiSessionFile, parsePiConversationMessages } from '../pi-conversation-parser.js';\nimport { parseFromLastCompactBoundary } from './parser.js';\nimport type { ConversationActivitySummary } from './types.js';\n\n/**\n * PAN-1635: a conversation whose JSONL hasn't been written in this long is no\n * longer \"working\" — even if its last entry is a trailing user/meta line (e.g. a\n * post-compaction summary whose follow-up prompt was eaten by the compaction).\n * Generous enough not to flap a slow-but-live turn; finite so a stranded session\n * can't spin forever.\n */\nconst WORKING_STALENESS_MS = 180_000;\n\n/** In-memory cache mapping sessionFile path → { mtimeMs, size, summary } */\nconst ACTIVITY_SUMMARY_CACHE_MAX = 100;\nconst activitySummaryCache = new Map<string, { mtimeMs: number; size: number; summary: ConversationActivitySummary }>();\n\nexport async function summarizeConversationActivity(\n sessionFile: string,\n options: { harness?: string | null } = {},\n): Promise<ConversationActivitySummary> {\n const fileStats = await stat(sessionFile);\n const cacheKey = `${options.harness ?? 'claude-code'}:${sessionFile}`;\n const behavior = getHarnessBehavior(options.harness as Parameters<typeof getHarnessBehavior>[0]);\n const cached = activitySummaryCache.get(cacheKey);\n if (cached && cached.mtimeMs === fileStats.mtimeMs && cached.size === fileStats.size) {\n return cached.summary;\n }\n\n const parsed = behavior.transcriptKind === 'codex-rollout-jsonl' ? await parseCodexConversationMessages(sessionFile)\n : behavior.transcriptKind === 'ohmypi-jsonl' || isOhmypiSessionFile(sessionFile) ? await parseOhmypiConversationMessages(sessionFile)\n : isPiSessionFile(sessionFile) ? await parsePiConversationMessages(sessionFile)\n // Parse from the last compact boundary instead of the full file — avoids\n // re-reading potentially megabytes of history on every list enrichment tick.\n // Pass an empty priorState so pendingToolUse stays populated rather than being\n // flushed into workLog. This lets us detect genuinely pending tools.\n : await parseFromLastCompactBoundary(\n sessionFile,\n { pendingToolUse: new Map(), unresolvedResults: new Map(), lastSequence: 0 },\n );\n const { messages, streaming, pendingToolUse, workLog, mtimeMs } = parsed;\n const lastMsg = messages[messages.length - 1];\n // Agent is idle only when the last message is an assistant message with a terminal\n // completedAt (stop_reason was end_turn/max_tokens/stop_sequence). Any other state\n // — empty history, last message is user (tool result or prompt), or last message is\n // an assistant still streaming / waiting on tool_use — means the agent is working.\n //\n // PAN-1635: guard on file recency. After a compaction the only post-boundary\n // entries are user-role meta lines (compact summary, /compact echoes) with no\n // following assistant turn, so the raw heuristic below pegs isWorking true\n // forever. A session whose JSONL has been idle for WORKING_STALENESS_MS is not\n // working — mirrors the `fileRecent` guards on `streaming` and `currentTool`.\n // The window is generous (covers a long compaction / deep think that writes\n // nothing for a while); during a real in-progress compaction the PreCompact\n // hook's activity event keeps the card \"working\" independently.\n const workingFileRecent = Date.now() - mtimeMs < WORKING_STALENESS_MS;\n const isWorking = workingFileRecent && (\n messages.length === 0 ||\n lastMsg?.role === 'user' ||\n (lastMsg?.role === 'assistant' && !lastMsg.completedAt));\n\n // Find the most recent pending tool (tool_use sent but tool_result not yet received).\n // pendingToolUse holds the actual unpaired tool_uses, so this works correctly for\n // parallel tool calls where multiple tools are in flight simultaneously.\n // Time-bound: if the file hasn't been modified in 30s, the agent has likely crashed\n // and pending tools are stale — don't report a stale currentTool.\n const fileRecent = Date.now() - mtimeMs < 30_000;\n\n let currentTool: string | null = null;\n if (fileRecent) {\n let maxSequence = -1;\n for (const entry of pendingToolUse.values()) {\n if (entry.toolTitle && (entry.sequence ?? -1) > maxSequence) {\n maxSequence = entry.sequence ?? -1;\n currentTool = entry.toolTitle;\n }\n }\n if (!currentTool && behavior.transcriptKind === 'codex-rollout-jsonl') {\n for (const entry of workLog) {\n if (entry.tone === 'tool' && !entry.result && (entry.sequence ?? -1) > maxSequence) {\n maxSequence = entry.sequence ?? -1;\n currentTool = entry.label;\n }\n }\n }\n }\n\n const summary: ConversationActivitySummary = { messages, streaming, isWorking, currentTool };\n activitySummaryCache.set(cacheKey, { mtimeMs, size: fileStats.size, summary });\n if (activitySummaryCache.size > ACTIVITY_SUMMARY_CACHE_MAX) {\n const firstKey = activitySummaryCache.keys().next().value;\n if (firstKey !== undefined) {\n activitySummaryCache.delete(firstKey);\n }\n }\n return summary;\n}\n","import { open, stat } from 'node:fs/promises';\nimport type { ContextUsage } from '@overdeck/contracts';\nimport { MODEL_CAPABILITIES, resolveModelIdSync } from '../../../../lib/model-capabilities.js';\nimport { findLastCompactBoundary } from './compact-boundary.js';\nimport type { JsonlEntry, LatestAssistantUsage, ParseResult } from './types.js';\n\ntype ModelCapability = (typeof MODEL_CAPABILITIES)[keyof typeof MODEL_CAPABILITIES];\n\n/**\n * Compute \"how full is the context window right now?\" from the JSONL.\n *\n * Approach (matches Claude Code's terminal indicator):\n * 1. Find the last compact boundary so we only count tokens in the current\n * window (post-compaction). For never-compacted sessions, scan the whole\n * file.\n * 2. Find the last assistant message after the boundary with `usage` data.\n * 3. Sum `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`\n * from that message — those are the tokens the model actually saw on\n * its most recent turn, i.e. the live context size.\n * 4. Compare against the model's context window. If observed input ever\n * exceeded the model's default (e.g. Claude Code 1M extended-context\n * mode for Opus / Sonnet), promote the effective window to 1M.\n *\n * Previous implementation used `fileBytes / 4` as a token estimate. That\n * counts every byte of every tool result, MCP payload, and cached file\n * content in the JSONL, which routinely overshoots actual `input_tokens` by\n * 4-10x. Removed.\n *\n * Returns null when the model is unknown. Returns a zero-token snapshot when\n * the active window has no assistant usage yet.\n */\nfunction resolveContextCapability(model: string | null): ModelCapability | null {\n const normalizedModel = model?.trim();\n if (!normalizedModel) return null;\n\n const resolvedModel = resolveModelIdSync(normalizedModel);\n return Object.prototype.hasOwnProperty.call(MODEL_CAPABILITIES, resolvedModel)\n ? MODEL_CAPABILITIES[resolvedModel as keyof typeof MODEL_CAPABILITIES]\n : null;\n}\n\nfunction buildContextUsage(\n capability: ModelCapability,\n activeBytes: number,\n usageSummary: LatestAssistantUsage | null,\n): ContextUsage {\n if (!usageSummary) {\n // No assistant message with usage yet — return zeros against the\n // declared window so the meter renders an empty ring instead of nothing.\n return {\n activeBytes,\n estimatedTokens: 0,\n contextWindow: capability.contextWindow,\n percentUsed: 0,\n };\n }\n\n // input + cache_read + cache_creation = total context the model received\n // on the last turn. output_tokens is the next-turn input from the model's\n // perspective, but for \"how full is my window NOW?\" the input side is the\n // honest answer — Claude Code's terminal uses the same convention.\n const liveContextTokens =\n usageSummary.lastInputTokens +\n usageSummary.lastCacheReadTokens +\n usageSummary.lastCacheCreationTokens;\n\n // 1M-context detection. Claude Code's extended-context mode (the\n // `context-1m-2025-08-07` beta) lets Opus / Sonnet see up to 1,000,000\n // tokens. We can't read the request headers, but if we've ever seen the\n // model accept more than its default window we know extended context is\n // active. Round up to the next plausible tier rather than guessing.\n const observedCeiling = Math.max(usageSummary.maxObservedInputTokens, liveContextTokens);\n const effectiveContextWindow =\n observedCeiling > capability.contextWindow\n ? Math.max(1_000_000, capability.contextWindow)\n : capability.contextWindow;\n\n const percentUsed = Math.min(\n 100,\n Math.max(0, (liveContextTokens / effectiveContextWindow) * 100),\n );\n\n return {\n activeBytes,\n estimatedTokens: liveContextTokens,\n contextWindow: effectiveContextWindow,\n percentUsed,\n lastInputTokens: usageSummary.lastInputTokens,\n lastCacheReadTokens: usageSummary.lastCacheReadTokens,\n lastCacheCreationTokens: usageSummary.lastCacheCreationTokens,\n maxObservedInputTokens: usageSummary.maxObservedInputTokens,\n lastModel: usageSummary.lastModel,\n lastTurnAt: usageSummary.lastTimestamp,\n };\n}\n\nexport function contextUsageFromParseResult(\n result: Pick<ParseResult, 'contextActiveBytes' | 'latestAssistantUsage'>,\n model: string | null,\n): ContextUsage | null {\n const capability = resolveContextCapability(model);\n if (!capability) return null;\n return buildContextUsage(capability, result.contextActiveBytes, result.latestAssistantUsage);\n}\n\nexport async function computeContextUsage(sessionFile: string, model: string | null): Promise<ContextUsage | null> {\n const capability = resolveContextCapability(model);\n if (!capability) return null;\n\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n const fileStats = await stat(sessionFile);\n const activeBytes = Math.max(0, fileStats.size - boundaryOffset);\n\n const usageSummary = await readLatestAssistantUsage(sessionFile, boundaryOffset, activeBytes);\n return buildContextUsage(capability, activeBytes, usageSummary);\n}\n\n/**\n * Stream the JSONL between `boundaryOffset` and EOF, returning usage data\n * from the most recent assistant message + the highest input_tokens ever\n * observed (used for 1M-context detection).\n *\n * Async, line-buffered. Skips malformed lines silently.\n */\nasync function readLatestAssistantUsage(\n sessionFile: string,\n boundaryOffset: number,\n activeBytes: number,\n): Promise<LatestAssistantUsage | null> {\n if (activeBytes <= 0) return null;\n const fh = await open(sessionFile, 'r');\n try {\n const buffer = Buffer.alloc(activeBytes);\n await fh.read(buffer, 0, activeBytes, boundaryOffset);\n const lines = buffer.toString('utf-8').split('\\n');\n\n let result: LatestAssistantUsage | null = null;\n let maxObservedInputTokens = 0;\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: JsonlEntry;\n try {\n entry = JSON.parse(line.replace(/\\r$/, '')) as JsonlEntry;\n } catch {\n continue;\n }\n if (entry.type !== 'assistant' || !entry.message?.usage) continue;\n const u = entry.message.usage;\n const input = u.input_tokens ?? 0;\n const cacheRead = u.cache_read_input_tokens ?? 0;\n const cacheCreate = u.cache_creation_input_tokens ?? 0;\n // Track ceiling across the whole window so a one-off >200k turn flips\n // us to 1M-mode even if the latest turn dropped back down.\n const turnInput = input + cacheRead + cacheCreate;\n if (turnInput > maxObservedInputTokens) maxObservedInputTokens = turnInput;\n // Always replace — we want the *last* one in file order.\n result = {\n lastInputTokens: input,\n lastCacheReadTokens: cacheRead,\n lastCacheCreationTokens: cacheCreate,\n maxObservedInputTokens,\n lastModel: entry.message.model ?? null,\n lastTimestamp: entry.timestamp ?? null,\n };\n }\n\n if (result) result.maxObservedInputTokens = maxObservedInputTokens;\n return result;\n } finally {\n await fh.close();\n }\n}\n","import { readdir } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { encodeClaudeProjectDir } from '../../../../lib/paths.js';\n\n/**\n * Encode a filesystem path to the Claude Code project directory name.\n * Delegates to the shared encodeClaudeProjectDir() which matches\n * Claude Code's actual encoding (all non-alphanumeric chars → hyphens).\n */\nfunction encodeCwdToProjectDir(cwd: string): string {\n return encodeClaudeProjectDir(cwd);\n}\n\n/** Returns ~/.claude/projects/<encoded-cwd>/ */\nfunction claudeProjectDir(cwd: string): string {\n return join(homedir(), '.claude', 'projects', encodeCwdToProjectDir(cwd));\n}\n\n/**\n * Snapshot existing JSONL files, then poll for a NEW file that wasn't there before.\n *\n * The old approach checked mtime >= spawnTime, which matched any active session\n * (including the user's own Claude Code conversation). This approach is exact:\n * only a file that didn't exist before the spawn can be the new session.\n *\n * Call snapshotSessionFiles() BEFORE spawning, then pass the result to\n * discoverSessionFile() AFTER spawning.\n */\nexport async function snapshotSessionFiles(cwd: string): Promise<Set<string>> {\n const projectDir = claudeProjectDir(cwd);\n try {\n const entries = await readdir(projectDir);\n return new Set(entries.filter(e => e.endsWith('.jsonl')));\n } catch {\n return new Set();\n }\n}\n\n/**\n * Wait for a new JSONL session file that wasn't in the pre-spawn snapshot.\n *\n * Polls every 500ms for up to 60 seconds. Returns the absolute path when found.\n * Resolves with null if no new file appears within the timeout.\n */\nexport async function discoverSessionFile(\n cwd: string,\n existingFiles: Set<string>,\n timeoutMs = 60_000,\n): Promise<string | null> {\n const projectDir = claudeProjectDir(cwd);\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n try {\n const entries = await readdir(projectDir);\n for (const entry of entries) {\n if (!entry.endsWith('.jsonl')) continue;\n if (!existingFiles.has(entry)) {\n return join(projectDir, entry);\n }\n }\n } catch {\n // Project directory doesn't exist yet — Claude hasn't started\n }\n\n await new Promise<void>((r) => setTimeout(r, 500));\n }\n\n return null;\n}\n","import { watch } from 'node:fs/promises';\nimport { parseConversationMessages } from './parser.js';\nimport type { ParseResult, ParseState } from './types.js';\n\nexport function gateSnapshotEmission(\n fileWasReset: boolean,\n newMessageCount: number,\n highWaterCount: number,\n): { snapshot: boolean; highWaterCount: number; suppressedShrink: boolean } {\n if (!fileWasReset) {\n // Normal incremental delta — always a merge, never touches the high-water mark.\n return { snapshot: false, highWaterCount, suppressedShrink: false };\n }\n if (newMessageCount < highWaterCount) {\n // Transient truncate-rewrite read — downgrade to merge so the client keeps\n // its history. Do not lower the high-water mark.\n return { snapshot: false, highWaterCount, suppressedShrink: true };\n }\n // A genuine, non-shrinking full re-parse — safe to assert as the snapshot.\n return { snapshot: true, highWaterCount: newMessageCount, suppressedShrink: false };\n}\n\n// ─── File watcher ─────────────────────────────────────────────────────────────\n\nexport interface ConversationWatchHandle {\n stop: () => void;\n}\n\n/**\n * Watch a JSONL session file for appends. Calls `callback` with parse results\n * when new content is detected. Uses fs.watch with 500ms polling fallback.\n *\n * Returns a handle with `stop()` to clean up.\n */\nexport function watchConversation(\n sessionFile: string,\n callback: (result: ParseResult) => void | Promise<void>,\n options: { byteOffset?: number; priorState?: ParseState } = {},\n): ConversationWatchHandle {\n let byteOffset = options.byteOffset ?? 0;\n let priorState: ParseState | undefined = options.priorState;\n let stopped = false;\n let isParsing = false;\n let abortController: AbortController | null = null;\n let pollInterval: ReturnType<typeof setTimeout> | null = null;\n\n async function handleChange(): Promise<void> {\n if (stopped || isParsing) return;\n isParsing = true;\n try {\n const result = await parseConversationMessages(sessionFile, byteOffset, priorState);\n if (result.byteOffset < byteOffset) {\n // File was truncated or rotated — reset to full re-parse\n byteOffset = 0;\n priorState = undefined;\n const fullResult = await parseConversationMessages(sessionFile, 0);\n if (fullResult.byteOffset > 0) {\n byteOffset = fullResult.byteOffset;\n priorState = {\n pendingToolUse: fullResult.pendingToolUse,\n unresolvedResults: fullResult.unresolvedResults,\n lastSequence: fullResult.lastSequence,\n planToolUseIds: fullResult.planToolUseIds,\n proposedPlan: fullResult.proposedPlan,\n latestAssistantUsage: fullResult.latestAssistantUsage,\n contextBoundaryOffset: fullResult.contextBoundaryOffset,\n permissionMode: fullResult.permissionMode,\n countedUsageIds: fullResult.countedUsageIds,\n fileEditsByAssistantId: fullResult.fileEditsByAssistantId,\n pendingAssistantId: fullResult.pendingAssistantId,\n orphanToolUseIds: fullResult.orphanToolUseIds,\n };\n // Include in-flight tools so the live view shows pending work\n const workLog = [...fullResult.workLog, ...fullResult.pendingToolUse.values()];\n await callback({ ...fullResult, workLog });\n }\n } else if (result.byteOffset > byteOffset) {\n byteOffset = result.byteOffset;\n priorState = {\n pendingToolUse: result.pendingToolUse,\n unresolvedResults: result.unresolvedResults,\n lastSequence: result.lastSequence,\n planToolUseIds: result.planToolUseIds,\n proposedPlan: result.proposedPlan,\n latestAssistantUsage: result.latestAssistantUsage,\n contextBoundaryOffset: result.contextBoundaryOffset,\n permissionMode: result.permissionMode,\n countedUsageIds: result.countedUsageIds,\n fileEditsByAssistantId: result.fileEditsByAssistantId,\n pendingAssistantId: result.pendingAssistantId,\n orphanToolUseIds: result.orphanToolUseIds,\n };\n // Include in-flight tools so the live view shows pending work\n const workLog = [...result.workLog, ...result.pendingToolUse.values()];\n await callback({ ...result, workLog });\n }\n } catch {\n // File may have been rotated or is temporarily unavailable\n } finally {\n isParsing = false;\n }\n }\n\n // Try fs.watch first (inotify on Linux)\n // Create AbortController synchronously so stop() can always abort,\n // even if called before the async startWatch() body runs.\n abortController = new AbortController();\n\n async function startWatch(): Promise<void> {\n try {\n const watcher = watch(sessionFile, { signal: abortController!.signal });\n for await (const _event of watcher) {\n await handleChange();\n }\n } catch (err) {\n if (!stopped) {\n // Fallback to polling on watch failure\n startPolling();\n }\n }\n }\n\n function startPolling(): void {\n async function poll(): Promise<void> {\n if (stopped) return;\n await handleChange();\n pollInterval = setTimeout(poll, 500);\n }\n pollInterval = setTimeout(poll, 500);\n }\n\n // Start watch; polling is only a fallback when fs.watch itself fails.\n startWatch();\n\n return {\n stop() {\n stopped = true;\n if (abortController) {\n abortController.abort();\n abortController = null;\n }\n if (pollInterval !== null) {\n clearTimeout(pollInterval);\n pollInterval = null;\n }\n },\n };\n}\n","export type {\n ConversationActivitySummary,\n LatestAssistantUsage,\n ParseResult,\n ParseState,\n} from './conversation/types.js';\nexport { summarizeConversationActivity } from './conversation/activity-summary.js';\nexport { contextUsageFromParseResult, computeContextUsage } from './conversation/context-usage.js';\nexport { findLastCompactBoundary } from './conversation/compact-boundary.js';\nexport { parseConversationMessages, parseEntireConversation, parseFromLastCompactBoundary } from './conversation/parser.js';\nexport { snapshotSessionFiles, discoverSessionFile } from './conversation/session-files.js';\nexport { gateSnapshotEmission, watchConversation } from './conversation/watch.js';\nexport type { ConversationWatchHandle } from './conversation/watch.js';\n","import { Effect } from 'effect';\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\nimport { findLastCompactBoundary } from './conversation-service.js';\nimport { generateSmartSummary } from '../../../lib/conversations/smart-compaction.js';\nimport { generateFallbackSummary } from '../../../lib/conversations/summary-fork.js';\nimport { loadConfigSync } from '../../../lib/config-yaml.js';\nimport { getAgentRuntimeBaseCommand, getProviderExportsForModel } from '../../../lib/agents.js';\nimport { getEventStore } from '../event-store.js';\nimport { isBackgroundFeatureEnabled } from '../../../lib/background-ai/features.js';\n\nconst activeCompactions = new Set<string>();\nexport function isCompacting(sessionFile: string): boolean {\n return activeCompactions.has(sessionFile);\n}\n\nexport interface NativeCompactionResult {\n summary: string;\n tokensBefore: number;\n boundaryUuid: string;\n model: string;\n forkedSessionId: string;\n forkedSessionFile: string;\n}\n\nexport interface MaybeCompactBeforeRespawnOptions {\n sessionFile: string | null | undefined;\n cwd: string;\n shouldCompact: boolean;\n}\n\nexport function getConversationCompactionSettings() {\n const { config } = loadConfigSync();\n return {\n model: config.conversations.compactionModel,\n manualCompactMode: config.conversations.manualCompactMode,\n richCompaction: config.conversations.richCompaction,\n };\n}\n\nexport function shouldInterceptManualCompact(message: string): boolean {\n const trimmed = message.trim();\n if (trimmed !== '/compact') return false;\n return getConversationCompactionSettings().manualCompactMode === 'overdeck-native';\n}\n\nexport async function estimateContextTokens(sessionFile: string | null | undefined): Promise<number> {\n if (!sessionFile || !existsSync(sessionFile)) return 0;\n try {\n const content = await readFile(sessionFile, 'utf-8');\n const lines = content.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n try {\n const entry = JSON.parse(line);\n const usage = entry?.message?.usage ?? entry?.usage;\n if (usage && typeof usage.input_tokens === 'number') {\n const total = (usage.input_tokens ?? 0)\n + (usage.cache_creation_input_tokens ?? 0)\n + (usage.cache_read_input_tokens ?? 0);\n if (total > 0) return total;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch (err) {\n console.warn(`[conversation-compaction] estimateContextTokens failed for ${sessionFile}:`, err);\n }\n return 0;\n}\n\nfunction buildContinuationSummary(summary: string, model: string): string {\n return [\n 'This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.',\n '',\n summary,\n '',\n `Overdeck native compaction model: ${model}`,\n '',\n 'Continue from this summary without redoing already-completed work.',\n ].join('\\n');\n}\n\nexport async function compactConversationNative(sessionFile: string, conversationName?: string): Promise<NativeCompactionResult> {\n if (!existsSync(sessionFile)) {\n throw new Error(`Session file not found: ${sessionFile}`);\n }\n activeCompactions.add(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: true } });\n }\n try {\n return await doCompact(sessionFile);\n } finally {\n activeCompactions.delete(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: false } });\n }\n }\n}\n\nasync function doCompact(sessionFile: string): Promise<NativeCompactionResult> {\n\n const settings = getConversationCompactionSettings();\n const tokensBefore = await estimateContextTokens(sessionFile);\n\n let summary: string;\n try {\n const result = await Effect.runPromise(generateSmartSummary({\n jsonlPath: sessionFile,\n model: settings.model,\n richMode: settings.richCompaction,\n mode: 'fork',\n }));\n summary = result.summary;\n } catch (error) {\n console.warn(`[conversation-compaction] Smart summary failed, falling back to heuristic:`, error);\n summary = await Effect.runPromise(generateFallbackSummary(sessionFile));\n }\n\n const continuation = buildContinuationSummary(summary, settings.model);\n const boundaryUuid = randomUUID();\n const timestamp = new Date().toISOString();\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n\n const entries = [\n JSON.stringify({\n parentUuid: null,\n isSidechain: false,\n type: 'system',\n subtype: 'compact_boundary',\n content: 'Conversation compacted',\n isMeta: false,\n timestamp,\n uuid: boundaryUuid,\n level: 'info',\n compactMetadata: {\n trigger: 'overdeck-native',\n preTokens: tokensBefore,\n model: settings.model,\n previousBoundaryOffset: boundaryOffset,\n },\n }),\n JSON.stringify({\n parentUuid: boundaryUuid,\n isSidechain: false,\n type: 'user',\n message: {\n role: 'user',\n content: continuation,\n },\n isVisibleInTranscriptOnly: true,\n isCompactSummary: true,\n uuid: randomUUID(),\n timestamp,\n }),\n ];\n\n const forkedSessionId = randomUUID();\n const forkedSessionFile = join(dirname(sessionFile), `${forkedSessionId}.jsonl`);\n await writeFile(forkedSessionFile, `${entries.join('\\n')}\\n`, 'utf-8');\n\n return {\n summary: continuation,\n tokensBefore,\n boundaryUuid,\n model: settings.model,\n forkedSessionId,\n forkedSessionFile,\n };\n}\n\nexport async function maybeCompactBeforeRespawn(\n opts: MaybeCompactBeforeRespawnOptions,\n): Promise<{ forkedSessionId: string; forkedSessionFile: string } | null> {\n if (!opts.sessionFile || !existsSync(opts.sessionFile)) return null;\n if (!opts.shouldCompact) {\n console.log('[conversation-compaction] Skipping compact (shouldCompact=false)');\n return null;\n }\n // Background AI gate: low-cost mode (or the summaryFork toggle) skips\n // automatic LLM compaction before respawn.\n if (!isBackgroundFeatureEnabled('summaryFork')) return null;\n\n const tokens = await estimateContextTokens(opts.sessionFile);\n console.log(`[conversation-compaction] Compacting before respawn (shouldCompact=true, tokens=${tokens})`);\n const result = await compactConversationNative(opts.sessionFile);\n return { forkedSessionId: result.forkedSessionId, forkedSessionFile: result.forkedSessionFile };\n}\n\nexport async function buildCompactionRuntimeInfo(model: string): Promise<{ command: string; exports: string }> {\n return {\n command: await getAgentRuntimeBaseCommand(model),\n exports: await getProviderExportsForModel(model),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { O as listSessionNames, w as killSession, x as init_tmux } from "./tmux-ePNhMIMU.js";
|
|
2
|
-
import { c as init_review_status, s as getReviewStatusSync, u as loadReviewStatuses } from "./review-status-
|
|
3
|
-
import { Jn as saveAgentStateSync, Pn as getAgentStateSync, Vn as markAgentStoppedState, er as init_persistent_logger, nr as logDeaconEventSync, t as init_agents } from "./agents-
|
|
2
|
+
import { c as init_review_status, s as getReviewStatusSync, u as loadReviewStatuses } from "./review-status-B5ZASOo4.js";
|
|
3
|
+
import { Jn as saveAgentStateSync, Pn as getAgentStateSync, Vn as markAgentStoppedState, er as init_persistent_logger, nr as logDeaconEventSync, t as init_agents } from "./agents-C7V4odQ6.js";
|
|
4
4
|
import { o as selectMergedAdvancingSessions, t as init_reap_terminal_sessions } from "./reap-terminal-sessions-CAmURC2h.js";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
6
|
//#region ../../lib/cloister/advancing-selfheal.ts
|
|
@@ -134,4 +134,4 @@ async function runBootAdvancingSelfHeal(deps = defaultBootAdvancingSelfHealDeps)
|
|
|
134
134
|
//#endregion
|
|
135
135
|
export { checkMergedAdvancingSessions, markAdvancingSessionStopped, reconcileInFlightJournals, runBootAdvancingSelfHeal };
|
|
136
136
|
|
|
137
|
-
//# sourceMappingURL=advancing-selfheal-
|
|
137
|
+
//# sourceMappingURL=advancing-selfheal-BezNfM3P.js.map
|
package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"advancing-selfheal-
|
|
1
|
+
{"version":3,"file":"advancing-selfheal-BezNfM3P.js","names":["agents.getAgentStateSync","agents.saveAgentStateSync","agents.markAgentStoppedState"],"sources":["../../src/lib/cloister/advancing-selfheal.ts"],"sourcesContent":["import { Effect } from 'effect';\nimport * as agents from '../agents.js';\nimport type { AgentState } from '../agents.js';\nimport { logDeaconEventSync } from '../persistent-logger.js';\nimport {\n getReviewStatusSync,\n loadReviewStatuses,\n type ReviewStatus,\n} from '../review-status.js';\nimport { killSession, listSessionNames } from '../tmux.js';\nimport { selectMergedAdvancingSessions } from './reap-terminal-sessions.js';\n\nconst JOURNAL_RECONCILE_STATES = new Set([\n 'pending',\n 'reviewing',\n 'passed',\n 'failed',\n 'blocked',\n 'testing',\n 'skipped',\n 'dispatch_failed',\n]);\n\nexport interface AdvancingSelfHealDeps {\n loadReviewStatuses(): Record<string, ReviewStatus>;\n getReviewStatusSync(issueId: string): ReviewStatus | null;\n listSessionNames(): Promise<readonly string[]>;\n getAgentStateSync(session: string): AgentState | null;\n saveAgentStateSync(state: AgentState): void;\n markAgentStoppedState(state: AgentState): AgentState;\n warn(message: string): void;\n}\n\nconst defaultDeps: AdvancingSelfHealDeps = {\n loadReviewStatuses,\n getReviewStatusSync,\n listSessionNames: () => Effect.runPromise(listSessionNames()),\n getAgentStateSync: (session) => agents.getAgentStateSync(session),\n saveAgentStateSync: (state) => agents.saveAgentStateSync(state),\n markAgentStoppedState: (state) => agents.markAgentStoppedState(state),\n warn: (message) => console.warn(message),\n};\n\nfunction isReconciliableStatus(status: ReviewStatus): boolean {\n if (status.mergeStatus === 'merged') return false;\n return JOURNAL_RECONCILE_STATES.has(status.reviewStatus)\n && JOURNAL_RECONCILE_STATES.has(status.testStatus);\n}\n\nexport function parseAdvancingIssueId(sessionName: string): string | null {\n const match = sessionName.match(/^agent-([a-z0-9]+-\\d+)-(?:review|test|ship)(?:-.+)?$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction statusChanged(before: ReviewStatus | null | undefined, after: ReviewStatus | null): boolean {\n return JSON.stringify(before ?? null) !== JSON.stringify(after ?? null);\n}\n\n/**\n * Host-side sweep for journaled review/test verdicts whose HTTP status POST\n * failed. getReviewStatusSync owns the journal -> DB reconcile; this patrol\n * step makes sure in-flight and tmux-alive advancing issues are read by the\n * host even when the agent has gone idle.\n */\nexport async function reconcileInFlightJournals(\n deps: AdvancingSelfHealDeps = defaultDeps,\n): Promise<string[]> {\n const actions: string[] = [];\n const statuses = deps.loadReviewStatuses();\n const issueIds = new Set<string>();\n\n for (const [issueId, status] of Object.entries(statuses)) {\n if (!isReconciliableStatus(status)) continue;\n issueIds.add(issueId.toUpperCase());\n }\n\n let sessionNames: readonly string[] = [];\n try {\n sessionNames = await deps.listSessionNames();\n } catch (error) {\n deps.warn(`[deacon] reconcileInFlightJournals: failed to list tmux sessions: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n for (const sessionName of sessionNames) {\n const issueId = parseAdvancingIssueId(sessionName);\n if (!issueId) continue;\n const status = statuses[issueId];\n if (status && !isReconciliableStatus(status)) continue;\n issueIds.add(issueId);\n }\n\n for (const issueId of issueIds) {\n const before = statuses[issueId] ?? null;\n try {\n const after = deps.getReviewStatusSync(issueId);\n if (!statusChanged(before, after)) continue;\n actions.push(`Reconciled journaled advancing verdict for ${issueId}`);\n } catch (error) {\n deps.warn(`[deacon] reconcileInFlightJournals: failed to reconcile ${issueId}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n return actions;\n}\n\nexport function markAdvancingSessionStopped(\n session: string,\n deps: Pick<AdvancingSelfHealDeps, 'getAgentStateSync' | 'saveAgentStateSync' | 'markAgentStoppedState'> = defaultDeps,\n): boolean {\n const state = deps.getAgentStateSync(session);\n if (!state || state.status === 'stopped') return false;\n\n const stopped = deps.markAgentStoppedState(state);\n deps.saveAgentStateSync(stopped);\n return true;\n}\n\n/**\n * PAN-2341: Reap advancing-role sessions for already-merged issues.\n *\n * The warm-by-default lifecycle (PAN-2579) intentionally leaves review/test/ship\n * panes alive, but once the issue is MERGED those sessions have no remaining role\n * in the pipeline (close-out territory). Kill the tmux session and\n * mark the agents-table row stopped so countRunningAgents() frees the slot.\n */\nexport async function checkMergedAdvancingSessions(): Promise<string[]> {\n const actions: string[] = [];\n try {\n const statuses = loadReviewStatuses();\n const aliveSessions = await Effect.runPromise(listSessionNames());\n const toKill = selectMergedAdvancingSessions(statuses, [...aliveSessions]);\n\n for (const session of toKill) {\n try {\n await Effect.runPromise(killSession(session));\n } catch (err) {\n console.warn(`[deacon] Failed to reap merged advancing session ${session}: ${err instanceof Error ? err.message : String(err)}`);\n }\n const markedStopped = markAdvancingSessionStopped(session);\n actions.push(\n markedStopped\n ? `Reaped merged advancing session ${session} (issue merged, row stopped)`\n : `Reaped merged advancing session ${session} (issue merged)`,\n );\n console.log(`[deacon] Reaped merged advancing session ${session} (PAN-2341)`);\n logDeaconEventSync(`checkMergedAdvancingSessions: reaped ${session} — issue merged but advancing session alive (PAN-2341)`);\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error('[deacon] Error reaping merged advancing sessions:', msg);\n }\n return actions;\n}\n\nexport interface BootAdvancingSelfHealDeps {\n reconcileInFlightJournals(): Promise<string[]>;\n checkMergedAdvancingSessions(): Promise<string[]>;\n log(message: string): void;\n}\n\nconst defaultBootAdvancingSelfHealDeps: BootAdvancingSelfHealDeps = {\n reconcileInFlightJournals,\n checkMergedAdvancingSessions,\n log: logDeaconEventSync,\n};\n\nexport async function runBootAdvancingSelfHeal(\n deps: BootAdvancingSelfHealDeps = defaultBootAdvancingSelfHealDeps,\n): Promise<string[]> {\n const actions: string[] = [];\n const steps: Array<[string, () => Promise<string[]>]> = [\n ['reconcileInFlightJournals', deps.reconcileInFlightJournals],\n ['checkMergedAdvancingSessions', deps.checkMergedAdvancingSessions],\n ];\n\n for (const [name, step] of steps) {\n try {\n const stepActions = await step();\n actions.push(...stepActions);\n if (stepActions.length > 0) {\n deps.log(`startDeacon: ${name} boot self-heal actions: ${stepActions.join('; ')}`);\n }\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n deps.log(`startDeacon: ${name} boot self-heal failed: ${msg}`);\n }\n }\n\n return actions;\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Dn as clearAgentTroubledSync, Fn as init_agent_state, Pn as getAgentStateSync, Tn as clearAgentPausedSync, Xn as setAgentPausedSync } from "./agents-
|
|
1
|
+
import { Dn as clearAgentTroubledSync, Fn as init_agent_state, Pn as getAgentStateSync, Tn as clearAgentPausedSync, Xn as setAgentPausedSync } from "./agents-C7V4odQ6.js";
|
|
2
2
|
init_agent_state();
|
|
3
3
|
export { clearAgentPausedSync, clearAgentTroubledSync, getAgentStateSync, setAgentPausedSync };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as AgentStateServiceLive, r as getRuntimeSnapshot, t as AgentStateService } from "./agent-state-service-
|
|
1
|
+
import { n as AgentStateServiceLive, r as getRuntimeSnapshot, t as AgentStateService } from "./agent-state-service-tXUdp6UF.js";
|
|
2
2
|
export { AgentStateService, AgentStateServiceLive, getRuntimeSnapshot };
|
|
@@ -101,7 +101,7 @@ const AgentStateServiceLive = Layer.effect(AgentStateService, Effect.gen(functio
|
|
|
101
101
|
const store = yield* Effect.promise(() => initEventStore());
|
|
102
102
|
const ref = yield* SubscriptionRef.make({});
|
|
103
103
|
const seedFromSources = Effect.gen(function* () {
|
|
104
|
-
const { reconstructCacheAuto } = yield* Effect.promise(() => import("./reconstruct-cache-
|
|
104
|
+
const { reconstructCacheAuto } = yield* Effect.promise(() => import("./reconstruct-cache-C2pCltOq.js"));
|
|
105
105
|
const seeded = (yield* Effect.promise(() => reconstructCacheAuto())).agentRuntimeById;
|
|
106
106
|
if (Object.keys(seeded).length > 0) {
|
|
107
107
|
yield* SubscriptionRef.update(ref, (current) => mergeRuntimeBySequence(current, seeded));
|
|
@@ -150,4 +150,4 @@ function applyEventToRef(ref, ev) {
|
|
|
150
150
|
//#endregion
|
|
151
151
|
export { AgentStateServiceLive as n, getRuntimeSnapshot as r, AgentStateService as t };
|
|
152
152
|
|
|
153
|
-
//# sourceMappingURL=agent-state-service-
|
|
153
|
+
//# sourceMappingURL=agent-state-service-tXUdp6UF.js.map
|