airaknit 1.1.2-rc.9
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/LICENSE +84 -0
- package/README.md +202 -0
- package/bin/airaknit +9 -0
- package/bin/airaknit-project +14 -0
- package/bin/kanna +9 -0
- package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
- package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
- package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
- package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
- package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
- package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
- package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
- package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
- package/dist/client/assets/arc-B07zg7ol.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
- package/dist/client/assets/array-BGFCBI0e.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
- package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
- package/dist/client/assets/button-DO50qOGv.js +1 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
- package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
- package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
- package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
- package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
- package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
- package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
- package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
- package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
- package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
- package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
- package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
- package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
- package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
- package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
- package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
- package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
- package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
- package/dist/client/assets/client-BrKWI4CM.js +1 -0
- package/dist/client/assets/client-CGgNRU9w.js +1 -0
- package/dist/client/assets/client-DMSLRzg9.js +6 -0
- package/dist/client/assets/clone-DWcL7whJ.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
- package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
- package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
- package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
- package/dist/client/assets/dist-CuB4kiSK.js +1 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
- package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
- package/dist/client/assets/index-CetCiuqP.js +105 -0
- package/dist/client/assets/index-N29Mip7A.css +1 -0
- package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
- package/dist/client/assets/init-B8gtcn7T.js +1 -0
- package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
- package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
- package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
- package/dist/client/assets/katex-B94qP8b6.js +265 -0
- package/dist/client/assets/lib--QVjyxmL.js +29 -0
- package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
- package/dist/client/assets/line-DCrYfLBn.js +1 -0
- package/dist/client/assets/linear-_4upLmeo.js +1 -0
- package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
- package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
- package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
- package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
- package/dist/client/assets/path-DZF-JdEe.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
- package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
- package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
- package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
- package/dist/client/assets/react-CClhXMB2.js +1 -0
- package/dist/client/assets/react-Dd6D81m0.js +1 -0
- package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
- package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
- package/dist/client/assets/src-DeTlMJAU.js +1 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
- package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +15 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +31 -0
- package/dist/client/manifest.webmanifest +12 -0
- package/dist/client/sw.js +32 -0
- package/package.json +122 -0
- package/src/nats/auth-callout/callout-config.test.ts +93 -0
- package/src/nats/auth-callout/callout-config.ts +109 -0
- package/src/nats/auth-callout/callout.integration.test.ts +332 -0
- package/src/nats/auth-callout/keys.ts +103 -0
- package/src/nats/auth-callout/responder.ts +241 -0
- package/src/nats/auth-callout/scope-policy.test.ts +159 -0
- package/src/nats/auth-callout/scope-policy.ts +210 -0
- package/src/nats/auth-callout/token.test.ts +163 -0
- package/src/nats/auth-callout/token.ts +157 -0
- package/src/nats/nats-daemon-callout.ts +194 -0
- package/src/nats/nats-daemon.test.ts +77 -0
- package/src/nats/nats-daemon.ts +50 -0
- package/src/nats/nats-token.test.ts +61 -0
- package/src/nats/nats-token.ts +59 -0
- package/src/runner/coordination-mcp-integration.test.ts +134 -0
- package/src/runner/nats-coordination-client.test.ts +49 -0
- package/src/runner/nats-coordination-client.ts +94 -0
- package/src/runner/runner-agent.test.ts +469 -0
- package/src/runner/runner-agent.ts +453 -0
- package/src/runner/runner-credential.test.ts +93 -0
- package/src/runner/runner-credential.ts +82 -0
- package/src/runner/runner-nats.test.ts +495 -0
- package/src/runner/runner-nats.ts +323 -0
- package/src/runner/runner-pair.test.ts +107 -0
- package/src/runner/runner-pair.ts +81 -0
- package/src/runner/runner.test.ts +135 -0
- package/src/runner/runner.ts +212 -0
- package/src/runner/turn-factories.test.ts +97 -0
- package/src/runner/turn-factories.ts +475 -0
- package/src/server/agent-config-journey.test.ts +106 -0
- package/src/server/agent.ts +8 -0
- package/src/server/auto-continue/auth-error-detector.ts +66 -0
- package/src/server/auto-continue/limit-detector.ts +194 -0
- package/src/server/bm25.test.ts +92 -0
- package/src/server/bm25.ts +101 -0
- package/src/server/chat-events-jetstream.test.ts +135 -0
- package/src/server/claude-harness.ts +360 -0
- package/src/server/claude-pty/agent-normalizers.ts +309 -0
- package/src/server/claude-pty/auth.test.ts +38 -0
- package/src/server/claude-pty/auth.ts +32 -0
- package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
- package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
- package/src/server/claude-pty/driver.test.ts +902 -0
- package/src/server/claude-pty/driver.ts +807 -0
- package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
- package/src/server/claude-pty/jsonl-path.test.ts +114 -0
- package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
- package/src/server/claude-pty/jsonl-to-event.ts +174 -0
- package/src/server/claude-pty/output-ring.test.ts +35 -0
- package/src/server/claude-pty/output-ring.ts +25 -0
- package/src/server/claude-pty/parity-matrix.test.ts +227 -0
- package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
- package/src/server/claude-pty/pid-registry.test.ts +122 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
- package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
- package/src/server/claude-pty/pty-instance-registry.ts +166 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
- package/src/server/claude-pty/pty-process.adapter.ts +66 -0
- package/src/server/claude-pty/pty-process.test.ts +49 -0
- package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
- package/src/server/claude-pty/resolve-binary.test.ts +118 -0
- package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
- package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
- package/src/server/claude-pty/settings-writer.test.ts +22 -0
- package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
- package/src/server/claude-pty/smoke-test.test.ts +191 -0
- package/src/server/claude-pty/smoke-test.ts +185 -0
- package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
- package/src/server/claude-pty/tool-callback.ts +274 -0
- package/src/server/claude-pty/tui-control.test.ts +272 -0
- package/src/server/claude-pty/tui-control.ts +182 -0
- package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
- package/src/server/claude-pty/tui-source.adapter.ts +343 -0
- package/src/server/claude-pty/tunnel-gateway.ts +12 -0
- package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
- package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
- package/src/server/claude-pty-mcp/history-primer.ts +90 -0
- package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
- package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
- package/src/server/claude-pty-mcp/mcp.ts +412 -0
- package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
- package/src/server/claude-pty-mcp/paths.ts +24 -0
- package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
- package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
- package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
- package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
- package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
- package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
- package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
- package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
- package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
- package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
- package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
- package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
- package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
- package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
- package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
- package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
- package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
- package/src/server/claude-pty-mcp/uploads.ts +38 -0
- package/src/server/claude-turn.test.ts +176 -0
- package/src/server/cli-runtime.test.ts +456 -0
- package/src/server/cli-runtime.ts +374 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +78 -0
- package/src/server/client-log-forwarder.test.ts +74 -0
- package/src/server/client-log-forwarder.ts +75 -0
- package/src/server/codex-app-server-protocol.ts +449 -0
- package/src/server/codex-app-server.test.ts +2990 -0
- package/src/server/codex-app-server.ts +1713 -0
- package/src/server/coordination-integration.test.ts +63 -0
- package/src/server/coordination-mcp.test.ts +149 -0
- package/src/server/coordination-mcp.ts +197 -0
- package/src/server/delegation-coordinator.test.ts +675 -0
- package/src/server/delegation-coordinator.ts +454 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store-agent-config.test.ts +124 -0
- package/src/server/event-store-coordination.test.ts +149 -0
- package/src/server/event-store-profile.test.ts +132 -0
- package/src/server/event-store-repo.test.ts +154 -0
- package/src/server/event-store-runner-team.test.ts +104 -0
- package/src/server/event-store.test.ts +342 -0
- package/src/server/event-store.ts +2208 -0
- package/src/server/events.ts +379 -0
- package/src/server/extension-router.test.ts +183 -0
- package/src/server/extension-router.ts +114 -0
- package/src/server/extensions/agents/server.test.ts +191 -0
- package/src/server/extensions/agents/server.ts +108 -0
- package/src/server/extensions/c3/server.test.ts +284 -0
- package/src/server/extensions/c3/server.ts +212 -0
- package/src/server/extensions/code/server.test.ts +200 -0
- package/src/server/extensions/code/server.ts +150 -0
- package/src/server/extensions.config.ts +10 -0
- package/src/server/external-open.ts +69 -0
- package/src/server/generate-fork-context.ts +58 -0
- package/src/server/generate-merge-context.test.ts +290 -0
- package/src/server/generate-merge-context.ts +141 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-clone-policy.test.ts +138 -0
- package/src/server/git-clone-policy.ts +27 -0
- package/src/server/harness-types.ts +1 -0
- package/src/server/journey-verification.test.ts +640 -0
- package/src/server/journey-verification.ts +195 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/nats-auth.test.ts +92 -0
- package/src/server/nats-auth.ts +6 -0
- package/src/server/nats-bind-guard.test.ts +71 -0
- package/src/server/nats-bind-guard.ts +42 -0
- package/src/server/nats-bridge.test.ts +141 -0
- package/src/server/nats-bridge.ts +111 -0
- package/src/server/nats-connector.test.ts +56 -0
- package/src/server/nats-connector.ts +71 -0
- package/src/server/nats-daemon-manager.test.ts +76 -0
- package/src/server/nats-daemon-manager.ts +174 -0
- package/src/server/nats-publisher.test.ts +356 -0
- package/src/server/nats-publisher.ts +271 -0
- package/src/server/nats-responders.test.ts +1018 -0
- package/src/server/nats-responders.ts +925 -0
- package/src/server/nats-streams.test.ts +112 -0
- package/src/server/nats-streams.ts +129 -0
- package/src/server/oauth-pool/oauth-responders.ts +185 -0
- package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
- package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
- package/src/server/orchestration.test.ts +1063 -0
- package/src/server/orchestration.ts +716 -0
- package/src/server/pairing-endpoints.test.ts +171 -0
- package/src/server/pairing-store.test.ts +154 -0
- package/src/server/pairing-store.ts +124 -0
- package/src/server/paths.ts +27 -0
- package/src/server/pr3-liveness.test.ts +252 -0
- package/src/server/process-utils.ts +10 -0
- package/src/server/project-cli.ts +180 -0
- package/src/server/provider-catalog.test.ts +177 -0
- package/src/server/provider-catalog.ts +146 -0
- package/src/server/pty-responders.ts +345 -0
- package/src/server/push-notifications.test.ts +234 -0
- package/src/server/push-notifications.ts +188 -0
- package/src/server/quick-response.test.ts +196 -0
- package/src/server/quick-response.ts +154 -0
- package/src/server/read-models-agent-config.test.ts +59 -0
- package/src/server/read-models-coordination.test.ts +69 -0
- package/src/server/read-models.test.ts +332 -0
- package/src/server/read-models.ts +258 -0
- package/src/server/repo-journey.test.ts +96 -0
- package/src/server/repo-manager.test.ts +118 -0
- package/src/server/repo-manager.ts +97 -0
- package/src/server/repo-status.test.ts +54 -0
- package/src/server/repo-status.ts +82 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/runner-incompatible-gate.test.ts +383 -0
- package/src/server/runner-manager.test.ts +72 -0
- package/src/server/runner-manager.ts +312 -0
- package/src/server/runner-pairing-urls.test.ts +59 -0
- package/src/server/runner-pairing-urls.ts +67 -0
- package/src/server/runner-proxy.test.ts +456 -0
- package/src/server/runner-proxy.ts +494 -0
- package/src/server/runner-router.test.ts +429 -0
- package/src/server/runner-router.ts +212 -0
- package/src/server/runner-routing.test.ts +584 -0
- package/src/server/runtime-registry.test.ts +436 -0
- package/src/server/runtime-registry.ts +307 -0
- package/src/server/sandbox-health.test.ts +127 -0
- package/src/server/sandbox-health.ts +94 -0
- package/src/server/sandbox-journey.test.ts +232 -0
- package/src/server/sandbox-manager.test.ts +146 -0
- package/src/server/sandbox-manager.ts +159 -0
- package/src/server/server.test.ts +287 -0
- package/src/server/server.ts +1108 -0
- package/src/server/session-discovery.test.ts +129 -0
- package/src/server/session-discovery.ts +475 -0
- package/src/server/session-index.test.ts +362 -0
- package/src/server/session-index.ts +119 -0
- package/src/server/session-seed.ts +288 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/skill-discovery.test.ts +201 -0
- package/src/server/skill-discovery.ts +77 -0
- package/src/server/storage/test-helpers.ts +67 -0
- package/src/server/terminal-manager.test.ts +309 -0
- package/src/server/terminal-manager.ts +354 -0
- package/src/server/transcript-consumer.test.ts +339 -0
- package/src/server/transcript-consumer.ts +162 -0
- package/src/server/transcript-search.test.ts +193 -0
- package/src/server/transcript-search.ts +83 -0
- package/src/server/transcript-utils.ts +52 -0
- package/src/server/update-manager.test.ts +107 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/workflow-engine.test.ts +251 -0
- package/src/server/workflow-engine.ts +169 -0
- package/src/server/workflow-mcp.ts +49 -0
- package/src/server/workflow-store.test.ts +155 -0
- package/src/server/workflow-store.ts +139 -0
- package/src/server/workspace-agent-integration.test.ts +167 -0
- package/src/server/workspace-agent-routes.test.ts +127 -0
- package/src/server/workspace-agent-routes.ts +89 -0
- package/src/server/workspace-agent.test.ts +103 -0
- package/src/server/workspace-agent.ts +102 -0
- package/src/server/workspace-cli.test.ts +79 -0
- package/src/server/workspace-config-manager.test.ts +109 -0
- package/src/server/workspace-config-manager.ts +83 -0
- package/src/server/workspace-directory-policy.test.ts +109 -0
- package/src/server/workspace-directory-policy.ts +56 -0
- package/src/shared/agent-config-types.ts +25 -0
- package/src/shared/branding.test.ts +42 -0
- package/src/shared/branding.ts +54 -0
- package/src/shared/compression.test.ts +85 -0
- package/src/shared/compression.ts +42 -0
- package/src/shared/coordination-store.test.ts +24 -0
- package/src/shared/coordination-store.ts +26 -0
- package/src/shared/dev-ports.test.ts +84 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/extension-types.ts +45 -0
- package/src/shared/fork-presets.ts +54 -0
- package/src/shared/harness-types.test.ts +15 -0
- package/src/shared/harness-types.ts +21 -0
- package/src/shared/log-sink.test.ts +112 -0
- package/src/shared/log-sink.ts +185 -0
- package/src/shared/mention-pattern.ts +7 -0
- package/src/shared/merge-presets.ts +41 -0
- package/src/shared/nats-subjects.test.ts +61 -0
- package/src/shared/nats-subjects.ts +131 -0
- package/src/shared/permission-policy.ts +136 -0
- package/src/shared/ports.ts +3 -0
- package/src/shared/preset-types.ts +15 -0
- package/src/shared/profile-types.ts +49 -0
- package/src/shared/projectFileUrl.ts +36 -0
- package/src/shared/protocol.ts +153 -0
- package/src/shared/pty-instance.ts +43 -0
- package/src/shared/puggy/diagnostics/result.ts +18 -0
- package/src/shared/puggy/expressions/evaluate.ts +292 -0
- package/src/shared/puggy/index.test.ts +101 -0
- package/src/shared/puggy/index.ts +69 -0
- package/src/shared/puggy/parser/ast.ts +110 -0
- package/src/shared/puggy/parser/parser.ts +624 -0
- package/src/shared/puggy/renderer/html.ts +447 -0
- package/src/shared/runner-protocol.test.ts +277 -0
- package/src/shared/runner-protocol.ts +210 -0
- package/src/shared/runner-team-types.ts +73 -0
- package/src/shared/runtime-types.ts +48 -0
- package/src/shared/sandbox-types.ts +53 -0
- package/src/shared/tailwind-build.test.ts +12 -0
- package/src/shared/tinkaria-system-prompt.ts +101 -0
- package/src/shared/tools.test.ts +335 -0
- package/src/shared/tools.ts +397 -0
- package/src/shared/transcript-entries.ts +27 -0
- package/src/shared/transcript-render.test.ts +225 -0
- package/src/shared/transcript-render.ts +467 -0
- package/src/shared/types.ts +1031 -0
- package/src/shared/vite-config.test.ts +47 -0
- package/src/shared/web-context.test.ts +110 -0
- package/src/shared/web-context.ts +76 -0
- package/src/shared/workflow-types.ts +51 -0
- package/src/shared/workspace-types.ts +100 -0
|
@@ -0,0 +1,2208 @@
|
|
|
1
|
+
import { appendFile, mkdir, rename, writeFile } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { getDataDir, LOG_PREFIX } from "../shared/branding"
|
|
5
|
+
import type { AgentConfig, AgentConfigRecord } from "../shared/agent-config-types"
|
|
6
|
+
import type { ProviderProfile, WorkspaceProfileOverride } from "../shared/profile-types"
|
|
7
|
+
import type { TeamMember } from "../shared/runner-team-types"
|
|
8
|
+
import type { AgentProvider, TranscriptEntry } from "../shared/types"
|
|
9
|
+
import { STORE_VERSION, compareIndependentWorkspaces } from "../shared/types"
|
|
10
|
+
import {
|
|
11
|
+
type ChatEvent,
|
|
12
|
+
type AgentConfigEvent,
|
|
13
|
+
type CoordinationEvent,
|
|
14
|
+
type MessageEvent,
|
|
15
|
+
type WorkspaceEvent,
|
|
16
|
+
type ProviderProfileEvent,
|
|
17
|
+
type ExtensionPreferenceEvent,
|
|
18
|
+
type RunnerTeamEvent,
|
|
19
|
+
type SandboxEvent,
|
|
20
|
+
type WorkflowEvent,
|
|
21
|
+
type SnapshotFile,
|
|
22
|
+
type StoreEvent,
|
|
23
|
+
type StoreState,
|
|
24
|
+
type RepoEvent,
|
|
25
|
+
type RepoRecord,
|
|
26
|
+
type TurnEvent,
|
|
27
|
+
type QueuedChatTurnRecord,
|
|
28
|
+
cloneTranscriptEntries,
|
|
29
|
+
createEmptyCoordinationState,
|
|
30
|
+
createEmptyState,
|
|
31
|
+
} from "./events"
|
|
32
|
+
import type { WorkflowRunState } from "../shared/workflow-types"
|
|
33
|
+
import type { SandboxRecord } from "../shared/sandbox-types"
|
|
34
|
+
import { resolveLocalPath } from "./paths"
|
|
35
|
+
|
|
36
|
+
const COMPACTION_THRESHOLD_BYTES = 2 * 1024 * 1024
|
|
37
|
+
|
|
38
|
+
interface LegacyTranscriptStats {
|
|
39
|
+
hasLegacyData: boolean
|
|
40
|
+
sources: Array<"snapshot" | "messages_log">
|
|
41
|
+
chatCount: number
|
|
42
|
+
entryCount: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class EventStore {
|
|
46
|
+
readonly dataDir: string
|
|
47
|
+
readonly state: StoreState = createEmptyState()
|
|
48
|
+
private writeChain = Promise.resolve()
|
|
49
|
+
private storageReset = false
|
|
50
|
+
private readonly snapshotPath: string
|
|
51
|
+
private readonly projectsLogPath: string
|
|
52
|
+
private readonly chatsLogPath: string
|
|
53
|
+
private readonly messagesLogPath: string
|
|
54
|
+
private readonly turnsLogPath: string
|
|
55
|
+
private readonly transcriptsDir: string
|
|
56
|
+
private legacyMessagesByChatId = new Map<string, TranscriptEntry[]>()
|
|
57
|
+
private snapshotHasLegacyMessages = false
|
|
58
|
+
private transcriptCache = new Map<string, TranscriptEntry[]>()
|
|
59
|
+
private static readonly TRANSCRIPT_CACHE_MAX = 5
|
|
60
|
+
private readonly coordinationLogPath: string
|
|
61
|
+
private readonly agentConfigsLogPath: string
|
|
62
|
+
private readonly reposLogPath: string
|
|
63
|
+
private readonly workflowsLogPath: string
|
|
64
|
+
private readonly sandboxLogPath: string
|
|
65
|
+
private readonly profilesLogPath: string
|
|
66
|
+
private readonly extensionPrefsLogPath: string
|
|
67
|
+
private readonly runnerTeamsLogPath: string
|
|
68
|
+
private readonly ptySubagentLogPath: string
|
|
69
|
+
private readonly ptyToolRequestsLogPath: string
|
|
70
|
+
private readonly ptySessionTokensLogPath: string
|
|
71
|
+
private readonly _ptySessionTokensByChatId = new Map<string, Map<string, string>>()
|
|
72
|
+
|
|
73
|
+
constructor(dataDir = getDataDir(homedir())) {
|
|
74
|
+
this.dataDir = dataDir
|
|
75
|
+
this.snapshotPath = path.join(this.dataDir, "snapshot.json")
|
|
76
|
+
this.projectsLogPath = path.join(this.dataDir, "projects.jsonl")
|
|
77
|
+
this.chatsLogPath = path.join(this.dataDir, "chats.jsonl")
|
|
78
|
+
this.messagesLogPath = path.join(this.dataDir, "messages.jsonl")
|
|
79
|
+
this.turnsLogPath = path.join(this.dataDir, "turns.jsonl")
|
|
80
|
+
this.transcriptsDir = path.join(this.dataDir, "transcripts")
|
|
81
|
+
this.coordinationLogPath = path.join(this.dataDir, "coordination.jsonl")
|
|
82
|
+
this.agentConfigsLogPath = path.join(this.dataDir, "agent-configs.jsonl")
|
|
83
|
+
this.reposLogPath = path.join(this.dataDir, "repos.jsonl")
|
|
84
|
+
this.workflowsLogPath = path.join(this.dataDir, "workflows.jsonl")
|
|
85
|
+
this.sandboxLogPath = path.join(this.dataDir, "sandbox.jsonl")
|
|
86
|
+
this.profilesLogPath = path.join(this.dataDir, "profiles.jsonl")
|
|
87
|
+
this.extensionPrefsLogPath = path.join(this.dataDir, "extension-prefs.jsonl")
|
|
88
|
+
this.runnerTeamsLogPath = path.join(this.dataDir, "runner-teams.jsonl")
|
|
89
|
+
this.ptySubagentLogPath = path.join(this.dataDir, "pty-subagent.jsonl")
|
|
90
|
+
this.ptyToolRequestsLogPath = path.join(this.dataDir, "pty-tool-requests.jsonl")
|
|
91
|
+
this.ptySessionTokensLogPath = path.join(this.dataDir, "pty-session-tokens.jsonl")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async initialize() {
|
|
95
|
+
await mkdir(this.dataDir, { recursive: true })
|
|
96
|
+
await mkdir(this.transcriptsDir, { recursive: true })
|
|
97
|
+
await this.ensureFile(this.projectsLogPath)
|
|
98
|
+
await this.ensureFile(this.chatsLogPath)
|
|
99
|
+
await this.ensureFile(this.messagesLogPath)
|
|
100
|
+
await this.ensureFile(this.turnsLogPath)
|
|
101
|
+
await this.ensureFile(this.coordinationLogPath)
|
|
102
|
+
await this.ensureFile(this.agentConfigsLogPath)
|
|
103
|
+
await this.ensureFile(this.reposLogPath)
|
|
104
|
+
await this.ensureFile(this.workflowsLogPath)
|
|
105
|
+
await this.ensureFile(this.sandboxLogPath)
|
|
106
|
+
await this.ensureFile(this.profilesLogPath)
|
|
107
|
+
await this.ensureFile(this.extensionPrefsLogPath)
|
|
108
|
+
await this.ensureFile(this.runnerTeamsLogPath)
|
|
109
|
+
await this.ensureFile(this.ptySubagentLogPath)
|
|
110
|
+
await this.ensureFile(this.ptyToolRequestsLogPath)
|
|
111
|
+
await this.ensureFile(this.ptySessionTokensLogPath)
|
|
112
|
+
await this.replayPtyLogs()
|
|
113
|
+
await this.loadSnapshot()
|
|
114
|
+
await this.replayLogs()
|
|
115
|
+
if (!(await this.hasLegacyTranscriptData()) && await this.shouldCompact()) {
|
|
116
|
+
await this.compact()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async ensureFile(filePath: string) {
|
|
121
|
+
const file = Bun.file(filePath)
|
|
122
|
+
if (!(await file.exists())) {
|
|
123
|
+
await Bun.write(filePath, "")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async clearStorage() {
|
|
128
|
+
if (this.storageReset) return
|
|
129
|
+
this.storageReset = true
|
|
130
|
+
this.resetState()
|
|
131
|
+
this.clearLegacyTranscriptState()
|
|
132
|
+
await Promise.all([
|
|
133
|
+
Bun.write(this.snapshotPath, ""),
|
|
134
|
+
Bun.write(this.projectsLogPath, ""),
|
|
135
|
+
Bun.write(this.chatsLogPath, ""),
|
|
136
|
+
Bun.write(this.messagesLogPath, ""),
|
|
137
|
+
Bun.write(this.turnsLogPath, ""),
|
|
138
|
+
Bun.write(this.coordinationLogPath, ""),
|
|
139
|
+
Bun.write(this.agentConfigsLogPath, ""),
|
|
140
|
+
Bun.write(this.reposLogPath, ""),
|
|
141
|
+
Bun.write(this.workflowsLogPath, ""),
|
|
142
|
+
Bun.write(this.sandboxLogPath, ""),
|
|
143
|
+
Bun.write(this.profilesLogPath, ""),
|
|
144
|
+
Bun.write(this.extensionPrefsLogPath, ""),
|
|
145
|
+
Bun.write(this.runnerTeamsLogPath, ""),
|
|
146
|
+
])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async loadSnapshot() {
|
|
150
|
+
const file = Bun.file(this.snapshotPath)
|
|
151
|
+
if (!(await file.exists())) return
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const text = await file.text()
|
|
155
|
+
if (!text.trim()) return
|
|
156
|
+
const parsed = JSON.parse(text) as SnapshotFile
|
|
157
|
+
if (parsed.v !== STORE_VERSION) {
|
|
158
|
+
console.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
|
|
159
|
+
await this.clearStorage()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
for (const project of parsed.workspaces) {
|
|
163
|
+
this.state.workspacesById.set(project.id, { ...project })
|
|
164
|
+
this.state.workspaceIdsByPath.set(project.localPath, project.id)
|
|
165
|
+
}
|
|
166
|
+
if (parsed.independentWorkspaces?.length) {
|
|
167
|
+
for (const ws of parsed.independentWorkspaces) {
|
|
168
|
+
this.state.independentWorkspacesById.set(ws.id, { ...ws })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const chat of parsed.chats) {
|
|
172
|
+
this.state.chatsById.set(chat.id, { ...chat, unread: chat.unread ?? false, model: chat.model ?? null })
|
|
173
|
+
}
|
|
174
|
+
if (parsed.queuedTurns?.length) {
|
|
175
|
+
for (const queued of parsed.queuedTurns) {
|
|
176
|
+
this.state.queuedTurnsByChat.set(queued.chatId, { ...queued })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (parsed.coordination?.length) {
|
|
180
|
+
for (const entry of parsed.coordination) {
|
|
181
|
+
const coord = createEmptyCoordinationState()
|
|
182
|
+
for (const todo of entry.todos) coord.todos.set(todo.id, todo)
|
|
183
|
+
for (const claim of entry.claims) coord.claims.set(claim.id, claim)
|
|
184
|
+
for (const wt of entry.worktrees) coord.worktrees.set(wt.id, wt)
|
|
185
|
+
for (const rule of entry.rules) coord.rules.set(rule.id, rule)
|
|
186
|
+
this.state.coordinationByWorkspace.set(entry.workspaceId, coord)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (parsed.agentConfigs?.length) {
|
|
190
|
+
for (const entry of parsed.agentConfigs) {
|
|
191
|
+
const configMap = new Map<string, AgentConfigRecord>()
|
|
192
|
+
for (const record of entry.records) configMap.set(record.id, record)
|
|
193
|
+
this.state.agentConfigsByWorkspace.set(entry.workspaceId, configMap)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (parsed.repos?.length) {
|
|
197
|
+
for (const repo of parsed.repos) {
|
|
198
|
+
this.state.reposById.set(repo.id, { ...repo })
|
|
199
|
+
if (repo.localPath) this.state.reposByPath.set(repo.localPath, repo.id)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (parsed.workflowRuns?.length) {
|
|
203
|
+
for (const entry of parsed.workflowRuns) {
|
|
204
|
+
const runsMap = new Map<string, import("../shared/workflow-types").WorkflowRunState>()
|
|
205
|
+
for (const run of entry.runs) runsMap.set(run.runId, run)
|
|
206
|
+
this.state.workflowRunsByWorkspace.set(entry.workspaceId, runsMap)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (parsed.sandboxes?.length) {
|
|
210
|
+
for (const sandbox of parsed.sandboxes) {
|
|
211
|
+
this.state.sandboxByWorkspace.set(sandbox.workspaceId, sandbox)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (parsed.providerProfiles?.length) {
|
|
215
|
+
for (const record of parsed.providerProfiles) {
|
|
216
|
+
this.state.providerProfiles.set(record.id, { ...record })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (parsed.workspaceProfileOverrides?.length) {
|
|
220
|
+
for (const override of parsed.workspaceProfileOverrides) {
|
|
221
|
+
const wsMap = this.state.workspaceProfileOverrides.get(override.workspaceId) ?? new Map<string, WorkspaceProfileOverride>()
|
|
222
|
+
wsMap.set(override.profileId, { ...override })
|
|
223
|
+
this.state.workspaceProfileOverrides.set(override.workspaceId, wsMap)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (parsed.extensionPreferences?.length) {
|
|
227
|
+
for (const pref of parsed.extensionPreferences) {
|
|
228
|
+
this.state.extensionPreferences.set(pref.extensionId, { ...pref })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (parsed.teamMembers?.length) {
|
|
232
|
+
for (const member of parsed.teamMembers) {
|
|
233
|
+
this.state.teamMembers.set(member.id, { ...member })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (parsed.runnerLabels?.length) {
|
|
237
|
+
for (const label of parsed.runnerLabels) {
|
|
238
|
+
this.state.runnerLabels.set(label.runnerId, { ...label })
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (parsed.messages?.length) {
|
|
242
|
+
this.snapshotHasLegacyMessages = true
|
|
243
|
+
for (const messageSet of parsed.messages) {
|
|
244
|
+
this.legacyMessagesByChatId.set(messageSet.chatId, cloneTranscriptEntries(messageSet.entries))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, error)
|
|
249
|
+
await this.clearStorage()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private resetState() {
|
|
254
|
+
this.state.workspacesById.clear()
|
|
255
|
+
this.state.workspaceIdsByPath.clear()
|
|
256
|
+
this.state.chatsById.clear()
|
|
257
|
+
this.state.queuedTurnsByChat.clear()
|
|
258
|
+
this.state.coordinationByWorkspace.clear()
|
|
259
|
+
this.state.agentConfigsByWorkspace.clear()
|
|
260
|
+
this.state.reposById.clear()
|
|
261
|
+
this.state.reposByPath.clear()
|
|
262
|
+
this.state.workflowRunsByWorkspace.clear()
|
|
263
|
+
this.state.sandboxByWorkspace.clear()
|
|
264
|
+
this.state.providerProfiles.clear()
|
|
265
|
+
this.state.workspaceProfileOverrides.clear()
|
|
266
|
+
this.state.extensionPreferences.clear()
|
|
267
|
+
this.state.teamMembers.clear()
|
|
268
|
+
this.state.runnerLabels.clear()
|
|
269
|
+
this.transcriptCache.clear()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private clearLegacyTranscriptState() {
|
|
273
|
+
this.legacyMessagesByChatId.clear()
|
|
274
|
+
this.snapshotHasLegacyMessages = false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async replayLogs() {
|
|
278
|
+
if (this.storageReset) return
|
|
279
|
+
await this.replayLog<WorkspaceEvent>(this.projectsLogPath)
|
|
280
|
+
if (this.storageReset) return
|
|
281
|
+
await this.replayLog<ChatEvent>(this.chatsLogPath)
|
|
282
|
+
if (this.storageReset) return
|
|
283
|
+
await this.replayLog<MessageEvent>(this.messagesLogPath)
|
|
284
|
+
if (this.storageReset) return
|
|
285
|
+
await this.replayLog<TurnEvent>(this.turnsLogPath)
|
|
286
|
+
if (this.storageReset) return
|
|
287
|
+
await this.replayLog<CoordinationEvent>(this.coordinationLogPath)
|
|
288
|
+
if (this.storageReset) return
|
|
289
|
+
await this.replayLog<AgentConfigEvent>(this.agentConfigsLogPath)
|
|
290
|
+
if (this.storageReset) return
|
|
291
|
+
await this.replayLog<RepoEvent>(this.reposLogPath)
|
|
292
|
+
if (this.storageReset) return
|
|
293
|
+
await this.replayLog<WorkflowEvent>(this.workflowsLogPath)
|
|
294
|
+
if (this.storageReset) return
|
|
295
|
+
await this.replayLog<SandboxEvent>(this.sandboxLogPath)
|
|
296
|
+
if (this.storageReset) return
|
|
297
|
+
await this.replayLog<ProviderProfileEvent>(this.profilesLogPath)
|
|
298
|
+
if (this.storageReset) return
|
|
299
|
+
await this.replayLog<ExtensionPreferenceEvent>(this.extensionPrefsLogPath)
|
|
300
|
+
if (this.storageReset) return
|
|
301
|
+
await this.replayLog<RunnerTeamEvent>(this.runnerTeamsLogPath)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async replayLog<TEvent extends StoreEvent>(filePath: string) {
|
|
305
|
+
const file = Bun.file(filePath)
|
|
306
|
+
if (!(await file.exists())) return
|
|
307
|
+
const text = await file.text()
|
|
308
|
+
if (!text.trim()) return
|
|
309
|
+
|
|
310
|
+
const lines = text.split("\n")
|
|
311
|
+
let lastNonEmpty = -1
|
|
312
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
313
|
+
if (lines[index].trim()) {
|
|
314
|
+
lastNonEmpty = index
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
320
|
+
const line = lines[index].trim()
|
|
321
|
+
if (!line) continue
|
|
322
|
+
try {
|
|
323
|
+
const event = JSON.parse(line) as Partial<StoreEvent>
|
|
324
|
+
if (event.v !== STORE_VERSION) {
|
|
325
|
+
console.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
|
|
326
|
+
await this.clearStorage()
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
this.applyEvent(event as StoreEvent)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (index === lastNonEmpty) {
|
|
332
|
+
console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
console.warn(`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`, error)
|
|
336
|
+
await this.clearStorage()
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private applyEvent(event: StoreEvent) {
|
|
343
|
+
switch (event.type) {
|
|
344
|
+
case "workspace_opened": {
|
|
345
|
+
const localPath = resolveLocalPath(event.localPath)
|
|
346
|
+
const project = {
|
|
347
|
+
id: event.workspaceId,
|
|
348
|
+
localPath,
|
|
349
|
+
title: event.title,
|
|
350
|
+
createdAt: event.timestamp,
|
|
351
|
+
updatedAt: event.timestamp,
|
|
352
|
+
}
|
|
353
|
+
this.state.workspacesById.set(project.id, project)
|
|
354
|
+
this.state.workspaceIdsByPath.set(localPath, project.id)
|
|
355
|
+
break
|
|
356
|
+
}
|
|
357
|
+
case "workspace_removed": {
|
|
358
|
+
const project = this.state.workspacesById.get(event.workspaceId)
|
|
359
|
+
if (!project) break
|
|
360
|
+
project.deletedAt = event.timestamp
|
|
361
|
+
project.updatedAt = event.timestamp
|
|
362
|
+
this.state.workspaceIdsByPath.delete(project.localPath)
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
case "independent_workspace_created": {
|
|
366
|
+
this.state.independentWorkspacesById.set(event.workspaceId, {
|
|
367
|
+
id: event.workspaceId,
|
|
368
|
+
name: event.name,
|
|
369
|
+
createdAt: event.timestamp,
|
|
370
|
+
updatedAt: event.timestamp,
|
|
371
|
+
})
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
case "independent_workspace_deleted": {
|
|
375
|
+
this.state.independentWorkspacesById.delete(event.workspaceId)
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
case "independent_workspace_renamed": {
|
|
379
|
+
const ws = this.state.independentWorkspacesById.get(event.workspaceId)
|
|
380
|
+
if (ws) {
|
|
381
|
+
ws.name = event.name
|
|
382
|
+
ws.updatedAt = event.timestamp
|
|
383
|
+
}
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
case "independent_workspace_pin_toggled": {
|
|
387
|
+
const ws = this.state.independentWorkspacesById.get(event.workspaceId)
|
|
388
|
+
if (ws) {
|
|
389
|
+
ws.pinned = event.pinned
|
|
390
|
+
ws.updatedAt = event.timestamp
|
|
391
|
+
}
|
|
392
|
+
break
|
|
393
|
+
}
|
|
394
|
+
case "independent_workspaces_reordered": {
|
|
395
|
+
event.orderedWorkspaceIds.forEach((id, index) => {
|
|
396
|
+
const ws = this.state.independentWorkspacesById.get(id)
|
|
397
|
+
if (ws) {
|
|
398
|
+
ws.sortOrder = index
|
|
399
|
+
ws.updatedAt = event.timestamp
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
break
|
|
403
|
+
}
|
|
404
|
+
case "chat_created": {
|
|
405
|
+
const chat = {
|
|
406
|
+
id: event.chatId,
|
|
407
|
+
workspaceId: event.workspaceId,
|
|
408
|
+
repoId: event.repoId ?? null,
|
|
409
|
+
title: event.title,
|
|
410
|
+
createdAt: event.timestamp,
|
|
411
|
+
updatedAt: event.timestamp,
|
|
412
|
+
unread: false,
|
|
413
|
+
provider: null,
|
|
414
|
+
model: null,
|
|
415
|
+
planMode: false,
|
|
416
|
+
sessionToken: null,
|
|
417
|
+
lastTurnOutcome: null,
|
|
418
|
+
}
|
|
419
|
+
this.state.chatsById.set(chat.id, chat)
|
|
420
|
+
break
|
|
421
|
+
}
|
|
422
|
+
case "chat_renamed": {
|
|
423
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
424
|
+
if (!chat) break
|
|
425
|
+
chat.title = event.title
|
|
426
|
+
chat.updatedAt = event.timestamp
|
|
427
|
+
break
|
|
428
|
+
}
|
|
429
|
+
case "chat_deleted": {
|
|
430
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
431
|
+
if (!chat) break
|
|
432
|
+
chat.deletedAt = event.timestamp
|
|
433
|
+
chat.updatedAt = event.timestamp
|
|
434
|
+
this.state.queuedTurnsByChat.delete(event.chatId)
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
case "chat_provider_set": {
|
|
438
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
439
|
+
if (!chat) break
|
|
440
|
+
chat.provider = event.provider
|
|
441
|
+
chat.updatedAt = event.timestamp
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
case "chat_model_set": {
|
|
445
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
446
|
+
if (!chat) break
|
|
447
|
+
chat.model = event.model
|
|
448
|
+
chat.updatedAt = event.timestamp
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
case "chat_plan_mode_set": {
|
|
452
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
453
|
+
if (!chat) break
|
|
454
|
+
chat.planMode = event.planMode
|
|
455
|
+
chat.updatedAt = event.timestamp
|
|
456
|
+
break
|
|
457
|
+
}
|
|
458
|
+
case "chat_read_state_set": {
|
|
459
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
460
|
+
if (!chat) break
|
|
461
|
+
chat.unread = event.unread
|
|
462
|
+
chat.updatedAt = event.timestamp
|
|
463
|
+
break
|
|
464
|
+
}
|
|
465
|
+
case "chat_runner_set": {
|
|
466
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
467
|
+
if (!chat) break
|
|
468
|
+
chat.runnerId = event.runnerId
|
|
469
|
+
chat.updatedAt = event.timestamp
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
case "message_appended": {
|
|
473
|
+
this.applyMessageMetadata(event.chatId, event.entry)
|
|
474
|
+
const existing = this.legacyMessagesByChatId.get(event.chatId) ?? []
|
|
475
|
+
existing.push({ ...event.entry })
|
|
476
|
+
this.legacyMessagesByChatId.set(event.chatId, existing)
|
|
477
|
+
break
|
|
478
|
+
}
|
|
479
|
+
case "turn_started": {
|
|
480
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
481
|
+
if (!chat) break
|
|
482
|
+
chat.updatedAt = event.timestamp
|
|
483
|
+
break
|
|
484
|
+
}
|
|
485
|
+
case "turn_finished": {
|
|
486
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
487
|
+
if (!chat) break
|
|
488
|
+
chat.updatedAt = event.timestamp
|
|
489
|
+
chat.lastTurnOutcome = "success"
|
|
490
|
+
chat.unread = true
|
|
491
|
+
break
|
|
492
|
+
}
|
|
493
|
+
case "turn_failed": {
|
|
494
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
495
|
+
if (!chat) break
|
|
496
|
+
chat.updatedAt = event.timestamp
|
|
497
|
+
chat.lastTurnOutcome = "failed"
|
|
498
|
+
chat.unread = true
|
|
499
|
+
break
|
|
500
|
+
}
|
|
501
|
+
case "turn_cancelled": {
|
|
502
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
503
|
+
if (!chat) break
|
|
504
|
+
chat.updatedAt = event.timestamp
|
|
505
|
+
chat.lastTurnOutcome = "cancelled"
|
|
506
|
+
break
|
|
507
|
+
}
|
|
508
|
+
case "session_token_set": {
|
|
509
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
510
|
+
if (!chat) break
|
|
511
|
+
chat.sessionToken = event.sessionToken
|
|
512
|
+
chat.updatedAt = event.timestamp
|
|
513
|
+
break
|
|
514
|
+
}
|
|
515
|
+
case "chat_turn_queued": {
|
|
516
|
+
const existing = this.state.queuedTurnsByChat.get(event.chatId)
|
|
517
|
+
const current = existing?.content.trim() ?? ""
|
|
518
|
+
const next = event.content.trim()
|
|
519
|
+
const content = current && next ? `${current}\n\n${next}` : next || current
|
|
520
|
+
if (!content) break
|
|
521
|
+
this.state.queuedTurnsByChat.set(event.chatId, {
|
|
522
|
+
chatId: event.chatId,
|
|
523
|
+
provider: event.provider ?? existing?.provider,
|
|
524
|
+
content,
|
|
525
|
+
model: event.model ?? existing?.model,
|
|
526
|
+
modelOptions: event.modelOptions ?? existing?.modelOptions,
|
|
527
|
+
effort: event.effort ?? existing?.effort,
|
|
528
|
+
planMode: event.planMode ?? existing?.planMode,
|
|
529
|
+
updatedAt: event.timestamp,
|
|
530
|
+
})
|
|
531
|
+
break
|
|
532
|
+
}
|
|
533
|
+
case "chat_queued_turn_cleared": {
|
|
534
|
+
this.state.queuedTurnsByChat.delete(event.chatId)
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
case "todo_added": {
|
|
538
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
539
|
+
coord.todos.set(event.todoId, {
|
|
540
|
+
id: event.todoId,
|
|
541
|
+
description: event.description,
|
|
542
|
+
priority: event.priority,
|
|
543
|
+
status: "open",
|
|
544
|
+
claimedBy: null,
|
|
545
|
+
outputs: [],
|
|
546
|
+
createdBy: event.createdBy,
|
|
547
|
+
createdAt: new Date(event.timestamp).toISOString(),
|
|
548
|
+
updatedAt: new Date(event.timestamp).toISOString(),
|
|
549
|
+
})
|
|
550
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
case "todo_claimed": {
|
|
554
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
555
|
+
const todo = coord.todos.get(event.todoId)
|
|
556
|
+
if (!todo) break
|
|
557
|
+
todo.status = "claimed"
|
|
558
|
+
todo.claimedBy = event.claimedBy
|
|
559
|
+
todo.updatedAt = new Date(event.timestamp).toISOString()
|
|
560
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
561
|
+
break
|
|
562
|
+
}
|
|
563
|
+
case "todo_completed": {
|
|
564
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
565
|
+
const todo = coord.todos.get(event.todoId)
|
|
566
|
+
if (!todo) break
|
|
567
|
+
todo.status = "complete"
|
|
568
|
+
todo.outputs = event.outputs
|
|
569
|
+
todo.updatedAt = new Date(event.timestamp).toISOString()
|
|
570
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
571
|
+
break
|
|
572
|
+
}
|
|
573
|
+
case "todo_abandoned": {
|
|
574
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
575
|
+
const todo = coord.todos.get(event.todoId)
|
|
576
|
+
if (!todo) break
|
|
577
|
+
todo.status = "abandoned"
|
|
578
|
+
todo.updatedAt = new Date(event.timestamp).toISOString()
|
|
579
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
case "claim_created": {
|
|
583
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
584
|
+
coord.claims.set(event.claimId, {
|
|
585
|
+
id: event.claimId,
|
|
586
|
+
intent: event.intent,
|
|
587
|
+
files: event.files,
|
|
588
|
+
sessionId: event.sessionId,
|
|
589
|
+
status: "active",
|
|
590
|
+
conflictsWith: null,
|
|
591
|
+
createdAt: new Date(event.timestamp).toISOString(),
|
|
592
|
+
})
|
|
593
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
594
|
+
break
|
|
595
|
+
}
|
|
596
|
+
case "claim_released": {
|
|
597
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
598
|
+
const claim = coord.claims.get(event.claimId)
|
|
599
|
+
if (!claim) break
|
|
600
|
+
claim.status = "released"
|
|
601
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
602
|
+
break
|
|
603
|
+
}
|
|
604
|
+
case "claim_conflict_detected": {
|
|
605
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
606
|
+
const claim = coord.claims.get(event.claimId)
|
|
607
|
+
if (!claim) break
|
|
608
|
+
claim.status = "conflict"
|
|
609
|
+
claim.conflictsWith = event.conflictsWith
|
|
610
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
case "worktree_created": {
|
|
614
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
615
|
+
coord.worktrees.set(event.worktreeId, {
|
|
616
|
+
id: event.worktreeId,
|
|
617
|
+
branch: event.branch,
|
|
618
|
+
baseBranch: event.baseBranch,
|
|
619
|
+
path: event.path,
|
|
620
|
+
assignedTo: null,
|
|
621
|
+
status: "ready",
|
|
622
|
+
createdAt: new Date(event.timestamp).toISOString(),
|
|
623
|
+
})
|
|
624
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
625
|
+
break
|
|
626
|
+
}
|
|
627
|
+
case "worktree_assigned": {
|
|
628
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
629
|
+
const wt = coord.worktrees.get(event.worktreeId)
|
|
630
|
+
if (!wt) break
|
|
631
|
+
wt.assignedTo = event.sessionId
|
|
632
|
+
wt.status = "assigned"
|
|
633
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
634
|
+
break
|
|
635
|
+
}
|
|
636
|
+
case "worktree_removed": {
|
|
637
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
638
|
+
const wt = coord.worktrees.get(event.worktreeId)
|
|
639
|
+
if (!wt) break
|
|
640
|
+
wt.status = "removed"
|
|
641
|
+
wt.assignedTo = null
|
|
642
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
643
|
+
break
|
|
644
|
+
}
|
|
645
|
+
case "rule_set": {
|
|
646
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
647
|
+
coord.rules.set(event.ruleId, {
|
|
648
|
+
id: event.ruleId,
|
|
649
|
+
content: event.content,
|
|
650
|
+
setBy: event.setBy,
|
|
651
|
+
updatedAt: new Date(event.timestamp).toISOString(),
|
|
652
|
+
})
|
|
653
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
654
|
+
break
|
|
655
|
+
}
|
|
656
|
+
case "rule_removed": {
|
|
657
|
+
const coord = this.getOrCreateCoordination(event.workspaceId)
|
|
658
|
+
coord.rules.delete(event.ruleId)
|
|
659
|
+
coord.lastUpdated = new Date(event.timestamp).toISOString()
|
|
660
|
+
break
|
|
661
|
+
}
|
|
662
|
+
case "agent_config_saved": {
|
|
663
|
+
const workspaceMap = this.state.agentConfigsByWorkspace.get(event.workspaceId) ?? new Map<string, AgentConfigRecord>()
|
|
664
|
+
const existing = workspaceMap.get(event.agentId)
|
|
665
|
+
workspaceMap.set(event.agentId, {
|
|
666
|
+
id: event.agentId,
|
|
667
|
+
workspaceId: event.workspaceId,
|
|
668
|
+
config: event.config,
|
|
669
|
+
createdAt: existing?.createdAt ?? event.timestamp,
|
|
670
|
+
updatedAt: event.timestamp,
|
|
671
|
+
lastCommitHash: existing?.lastCommitHash,
|
|
672
|
+
})
|
|
673
|
+
this.state.agentConfigsByWorkspace.set(event.workspaceId, workspaceMap)
|
|
674
|
+
break
|
|
675
|
+
}
|
|
676
|
+
case "agent_config_committed": {
|
|
677
|
+
const record = this.state.agentConfigsByWorkspace.get(event.workspaceId)?.get(event.agentId)
|
|
678
|
+
if (record) record.lastCommitHash = event.commitHash
|
|
679
|
+
break
|
|
680
|
+
}
|
|
681
|
+
case "agent_config_removed": {
|
|
682
|
+
this.state.agentConfigsByWorkspace.get(event.workspaceId)?.delete(event.agentId)
|
|
683
|
+
break
|
|
684
|
+
}
|
|
685
|
+
case "repo_added": {
|
|
686
|
+
const repo: RepoRecord = {
|
|
687
|
+
id: event.id,
|
|
688
|
+
workspaceId: event.workspaceId,
|
|
689
|
+
origin: event.origin,
|
|
690
|
+
localPath: event.localPath,
|
|
691
|
+
label: event.label,
|
|
692
|
+
status: "cloned",
|
|
693
|
+
branch: event.branch,
|
|
694
|
+
createdAt: event.timestamp,
|
|
695
|
+
updatedAt: event.timestamp,
|
|
696
|
+
}
|
|
697
|
+
this.state.reposById.set(event.id, repo)
|
|
698
|
+
if (event.localPath) this.state.reposByPath.set(event.localPath, event.id)
|
|
699
|
+
break
|
|
700
|
+
}
|
|
701
|
+
case "repo_clone_started": {
|
|
702
|
+
const repo: RepoRecord = {
|
|
703
|
+
id: event.id,
|
|
704
|
+
workspaceId: event.workspaceId,
|
|
705
|
+
origin: event.origin,
|
|
706
|
+
localPath: event.targetPath,
|
|
707
|
+
label: event.label,
|
|
708
|
+
status: "pending",
|
|
709
|
+
branch: null,
|
|
710
|
+
createdAt: event.timestamp,
|
|
711
|
+
updatedAt: event.timestamp,
|
|
712
|
+
}
|
|
713
|
+
this.state.reposById.set(event.id, repo)
|
|
714
|
+
this.state.reposByPath.set(event.targetPath, event.id)
|
|
715
|
+
break
|
|
716
|
+
}
|
|
717
|
+
case "repo_cloned": {
|
|
718
|
+
const repo = this.state.reposById.get(event.id)
|
|
719
|
+
if (!repo) break
|
|
720
|
+
repo.status = "cloned"
|
|
721
|
+
repo.localPath = event.localPath
|
|
722
|
+
repo.branch = event.branch
|
|
723
|
+
repo.updatedAt = event.timestamp
|
|
724
|
+
this.state.reposByPath.set(event.localPath, event.id)
|
|
725
|
+
break
|
|
726
|
+
}
|
|
727
|
+
case "repo_clone_failed": {
|
|
728
|
+
const repo = this.state.reposById.get(event.id)
|
|
729
|
+
if (!repo) break
|
|
730
|
+
repo.status = "error"
|
|
731
|
+
repo.updatedAt = event.timestamp
|
|
732
|
+
break
|
|
733
|
+
}
|
|
734
|
+
case "repo_removed": {
|
|
735
|
+
const repo = this.state.reposById.get(event.id)
|
|
736
|
+
if (repo) {
|
|
737
|
+
this.state.reposByPath.delete(repo.localPath)
|
|
738
|
+
this.state.reposById.delete(event.id)
|
|
739
|
+
// Re-parent orphaned chats
|
|
740
|
+
for (const chat of this.state.chatsById.values()) {
|
|
741
|
+
if (chat.repoId === event.id) {
|
|
742
|
+
chat.repoId = null
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
break
|
|
747
|
+
}
|
|
748
|
+
case "repo_label_updated": {
|
|
749
|
+
const repo = this.state.reposById.get(event.id)
|
|
750
|
+
if (!repo) break
|
|
751
|
+
repo.label = event.label
|
|
752
|
+
repo.updatedAt = event.timestamp
|
|
753
|
+
break
|
|
754
|
+
}
|
|
755
|
+
case "workflow_started": {
|
|
756
|
+
const runsMap = this.state.workflowRunsByWorkspace.get(event.workspaceId) ?? new Map<string, WorkflowRunState>()
|
|
757
|
+
runsMap.set(event.runId, {
|
|
758
|
+
runId: event.runId,
|
|
759
|
+
workflowId: event.workflowId,
|
|
760
|
+
workspaceId: event.workspaceId,
|
|
761
|
+
targetRepoIds: event.targetRepoIds,
|
|
762
|
+
status: "running",
|
|
763
|
+
steps: [],
|
|
764
|
+
startedAt: event.timestamp,
|
|
765
|
+
triggeredBy: event.triggeredBy,
|
|
766
|
+
})
|
|
767
|
+
this.state.workflowRunsByWorkspace.set(event.workspaceId, runsMap)
|
|
768
|
+
break
|
|
769
|
+
}
|
|
770
|
+
case "workflow_step_started": {
|
|
771
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
772
|
+
if (!run) break
|
|
773
|
+
run.steps.push({
|
|
774
|
+
stepIndex: event.stepIndex,
|
|
775
|
+
mcp_tool: event.mcp_tool,
|
|
776
|
+
repoId: event.repoId,
|
|
777
|
+
status: "running",
|
|
778
|
+
startedAt: event.timestamp,
|
|
779
|
+
})
|
|
780
|
+
break
|
|
781
|
+
}
|
|
782
|
+
case "workflow_step_completed": {
|
|
783
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
784
|
+
if (!run) break
|
|
785
|
+
const step = run.steps.find((s) => s.stepIndex === event.stepIndex && s.repoId === event.repoId)
|
|
786
|
+
if (step) {
|
|
787
|
+
step.status = "completed"
|
|
788
|
+
step.output = event.output
|
|
789
|
+
step.completedAt = event.timestamp
|
|
790
|
+
}
|
|
791
|
+
break
|
|
792
|
+
}
|
|
793
|
+
case "workflow_step_failed": {
|
|
794
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
795
|
+
if (!run) break
|
|
796
|
+
const step = run.steps.find((s) => s.stepIndex === event.stepIndex && s.repoId === event.repoId)
|
|
797
|
+
if (step) {
|
|
798
|
+
step.status = "failed"
|
|
799
|
+
step.error = event.error
|
|
800
|
+
step.completedAt = event.timestamp
|
|
801
|
+
}
|
|
802
|
+
break
|
|
803
|
+
}
|
|
804
|
+
case "workflow_completed": {
|
|
805
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
806
|
+
if (!run) break
|
|
807
|
+
run.status = "completed"
|
|
808
|
+
run.completedAt = event.timestamp
|
|
809
|
+
break
|
|
810
|
+
}
|
|
811
|
+
case "workflow_failed": {
|
|
812
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
813
|
+
if (!run) break
|
|
814
|
+
run.status = "failed"
|
|
815
|
+
run.error = event.error
|
|
816
|
+
run.failedStep = event.failedStep
|
|
817
|
+
run.completedAt = event.timestamp
|
|
818
|
+
break
|
|
819
|
+
}
|
|
820
|
+
case "workflow_cancelled": {
|
|
821
|
+
const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
|
|
822
|
+
if (!run) break
|
|
823
|
+
run.status = "cancelled"
|
|
824
|
+
run.completedAt = event.timestamp
|
|
825
|
+
break
|
|
826
|
+
}
|
|
827
|
+
case "sandbox_created": {
|
|
828
|
+
const record: SandboxRecord = {
|
|
829
|
+
id: event.id,
|
|
830
|
+
workspaceId: event.workspaceId,
|
|
831
|
+
containerId: null,
|
|
832
|
+
status: "creating",
|
|
833
|
+
resourceLimits: event.resourceLimits,
|
|
834
|
+
natsUrl: "",
|
|
835
|
+
createdAt: event.timestamp,
|
|
836
|
+
updatedAt: event.timestamp,
|
|
837
|
+
lastHealthCheck: null,
|
|
838
|
+
error: null,
|
|
839
|
+
}
|
|
840
|
+
this.state.sandboxByWorkspace.set(event.workspaceId, record)
|
|
841
|
+
break
|
|
842
|
+
}
|
|
843
|
+
case "sandbox_started": {
|
|
844
|
+
const existing = this.findSandboxById(event.id)
|
|
845
|
+
if (existing) {
|
|
846
|
+
existing.containerId = event.containerId
|
|
847
|
+
existing.natsUrl = event.natsUrl
|
|
848
|
+
existing.status = "running"
|
|
849
|
+
existing.updatedAt = event.timestamp
|
|
850
|
+
}
|
|
851
|
+
break
|
|
852
|
+
}
|
|
853
|
+
case "sandbox_stopped": {
|
|
854
|
+
const existing = this.findSandboxById(event.id)
|
|
855
|
+
if (existing) {
|
|
856
|
+
existing.status = "stopped"
|
|
857
|
+
existing.updatedAt = event.timestamp
|
|
858
|
+
}
|
|
859
|
+
break
|
|
860
|
+
}
|
|
861
|
+
case "sandbox_destroyed": {
|
|
862
|
+
for (const [key, sb] of this.state.sandboxByWorkspace) {
|
|
863
|
+
if (sb.id === event.id) { this.state.sandboxByWorkspace.delete(key); break }
|
|
864
|
+
}
|
|
865
|
+
break
|
|
866
|
+
}
|
|
867
|
+
case "sandbox_error": {
|
|
868
|
+
const existing = this.findSandboxById(event.id)
|
|
869
|
+
if (existing) {
|
|
870
|
+
existing.status = "error"
|
|
871
|
+
existing.error = event.error
|
|
872
|
+
existing.updatedAt = event.timestamp
|
|
873
|
+
}
|
|
874
|
+
break
|
|
875
|
+
}
|
|
876
|
+
case "sandbox_health_updated": {
|
|
877
|
+
const existing = this.findSandboxById(event.id)
|
|
878
|
+
if (existing) {
|
|
879
|
+
existing.lastHealthCheck = event.timestamp
|
|
880
|
+
existing.updatedAt = event.timestamp
|
|
881
|
+
}
|
|
882
|
+
break
|
|
883
|
+
}
|
|
884
|
+
case "provider_profile_saved": {
|
|
885
|
+
const existingProfile = this.state.providerProfiles.get(event.profileId)
|
|
886
|
+
this.state.providerProfiles.set(event.profileId, {
|
|
887
|
+
id: event.profileId,
|
|
888
|
+
profile: event.profile,
|
|
889
|
+
createdAt: existingProfile?.createdAt ?? event.timestamp,
|
|
890
|
+
updatedAt: event.timestamp,
|
|
891
|
+
})
|
|
892
|
+
break
|
|
893
|
+
}
|
|
894
|
+
case "provider_profile_removed": {
|
|
895
|
+
this.state.providerProfiles.delete(event.profileId)
|
|
896
|
+
for (const [, overrides] of this.state.workspaceProfileOverrides) {
|
|
897
|
+
overrides.delete(event.profileId)
|
|
898
|
+
}
|
|
899
|
+
break
|
|
900
|
+
}
|
|
901
|
+
case "team_member_saved": {
|
|
902
|
+
this.state.teamMembers.set(event.memberId, event.member)
|
|
903
|
+
break
|
|
904
|
+
}
|
|
905
|
+
case "team_member_removed": {
|
|
906
|
+
this.state.teamMembers.delete(event.memberId)
|
|
907
|
+
// Cascade: unassign any runner that pointed at the removed member.
|
|
908
|
+
for (const [runnerId, label] of this.state.runnerLabels) {
|
|
909
|
+
if (label.memberId === event.memberId) {
|
|
910
|
+
this.state.runnerLabels.set(runnerId, { ...label, memberId: null, updatedAt: event.timestamp })
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
break
|
|
914
|
+
}
|
|
915
|
+
case "runner_label_set": {
|
|
916
|
+
// Upsert by runnerId. A label with neither a name nor a member is still
|
|
917
|
+
// a valid record (the reducer never prunes — removal is explicit).
|
|
918
|
+
this.state.runnerLabels.set(event.runnerId, {
|
|
919
|
+
runnerId: event.runnerId,
|
|
920
|
+
name: event.name,
|
|
921
|
+
memberId: event.memberId,
|
|
922
|
+
updatedAt: event.timestamp,
|
|
923
|
+
})
|
|
924
|
+
break
|
|
925
|
+
}
|
|
926
|
+
case "runner_label_removed": {
|
|
927
|
+
this.state.runnerLabels.delete(event.runnerId)
|
|
928
|
+
break
|
|
929
|
+
}
|
|
930
|
+
case "workspace_profile_override_set": {
|
|
931
|
+
const wsOverrides = this.state.workspaceProfileOverrides.get(event.workspaceId) ?? new Map<string, WorkspaceProfileOverride>()
|
|
932
|
+
wsOverrides.set(event.profileId, {
|
|
933
|
+
profileId: event.profileId,
|
|
934
|
+
workspaceId: event.workspaceId,
|
|
935
|
+
overrides: event.overrides,
|
|
936
|
+
updatedAt: event.timestamp,
|
|
937
|
+
})
|
|
938
|
+
this.state.workspaceProfileOverrides.set(event.workspaceId, wsOverrides)
|
|
939
|
+
break
|
|
940
|
+
}
|
|
941
|
+
case "workspace_profile_override_removed": {
|
|
942
|
+
this.state.workspaceProfileOverrides.get(event.workspaceId)?.delete(event.profileId)
|
|
943
|
+
break
|
|
944
|
+
}
|
|
945
|
+
case "extension_preference_set": {
|
|
946
|
+
this.state.extensionPreferences.set(event.extensionId, {
|
|
947
|
+
extensionId: event.extensionId,
|
|
948
|
+
enabled: event.enabled,
|
|
949
|
+
updatedAt: event.timestamp,
|
|
950
|
+
})
|
|
951
|
+
break
|
|
952
|
+
}
|
|
953
|
+
case "delegation_initiated":
|
|
954
|
+
case "delegation_completed":
|
|
955
|
+
// Audit-only — KV is the source of truth for delegation state
|
|
956
|
+
break
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private findSandboxById(id: string): SandboxRecord | undefined {
|
|
961
|
+
for (const sb of this.state.sandboxByWorkspace.values()) {
|
|
962
|
+
if (sb.id === id) return sb
|
|
963
|
+
}
|
|
964
|
+
return undefined
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private getOrCreateCoordination(workspaceId: string) {
|
|
968
|
+
let coord = this.state.coordinationByWorkspace.get(workspaceId)
|
|
969
|
+
if (!coord) {
|
|
970
|
+
coord = createEmptyCoordinationState()
|
|
971
|
+
this.state.coordinationByWorkspace.set(workspaceId, coord)
|
|
972
|
+
}
|
|
973
|
+
return coord
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private applyMessageMetadata(chatId: string, entry: TranscriptEntry) {
|
|
977
|
+
const chat = this.state.chatsById.get(chatId)
|
|
978
|
+
if (!chat) return
|
|
979
|
+
if (entry.kind === "user_prompt") {
|
|
980
|
+
chat.lastMessageAt = entry.createdAt
|
|
981
|
+
}
|
|
982
|
+
chat.updatedAt = Math.max(chat.updatedAt, entry.createdAt)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private append<TEvent extends StoreEvent>(filePath: string, event: TEvent) {
|
|
986
|
+
const payload = `${JSON.stringify(event)}\n`
|
|
987
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
988
|
+
await appendFile(filePath, payload, "utf8")
|
|
989
|
+
this.applyEvent(event)
|
|
990
|
+
})
|
|
991
|
+
return this.writeChain
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private transcriptPath(chatId: string) {
|
|
995
|
+
return path.join(this.transcriptsDir, `${chatId}.jsonl`)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private async loadTranscriptFromDisk(chatId: string): Promise<TranscriptEntry[]> {
|
|
999
|
+
const transcriptPath = this.transcriptPath(chatId)
|
|
1000
|
+
const file = Bun.file(transcriptPath)
|
|
1001
|
+
if (!await file.exists()) {
|
|
1002
|
+
return []
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const text = await file.text()
|
|
1006
|
+
if (!text.trim()) return []
|
|
1007
|
+
|
|
1008
|
+
const entries: TranscriptEntry[] = []
|
|
1009
|
+
for (const rawLine of text.split("\n")) {
|
|
1010
|
+
const line = rawLine.trim()
|
|
1011
|
+
if (!line) continue
|
|
1012
|
+
try {
|
|
1013
|
+
entries.push(JSON.parse(line) as TranscriptEntry)
|
|
1014
|
+
} catch (_err: unknown) {
|
|
1015
|
+
// Skip malformed JSONL lines — one bad line must not crash transcript loading
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return entries
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private setTranscriptCache(chatId: string, entries: TranscriptEntry[]) {
|
|
1022
|
+
this.transcriptCache.delete(chatId) // Remove to refresh insertion order
|
|
1023
|
+
this.transcriptCache.set(chatId, entries)
|
|
1024
|
+
if (this.transcriptCache.size > EventStore.TRANSCRIPT_CACHE_MAX) {
|
|
1025
|
+
const oldest = this.transcriptCache.keys().next().value
|
|
1026
|
+
if (oldest) this.transcriptCache.delete(oldest)
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async openProject(localPath: string, title?: string) {
|
|
1031
|
+
const normalized = resolveLocalPath(localPath)
|
|
1032
|
+
const existingId = this.state.workspaceIdsByPath.get(normalized)
|
|
1033
|
+
if (existingId) {
|
|
1034
|
+
const existing = this.state.workspacesById.get(existingId)
|
|
1035
|
+
if (existing && !existing.deletedAt) {
|
|
1036
|
+
return existing
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const workspaceId = crypto.randomUUID()
|
|
1041
|
+
const event: WorkspaceEvent = {
|
|
1042
|
+
v: STORE_VERSION,
|
|
1043
|
+
type: "workspace_opened",
|
|
1044
|
+
timestamp: Date.now(),
|
|
1045
|
+
workspaceId,
|
|
1046
|
+
localPath: normalized,
|
|
1047
|
+
title: title?.trim() || path.basename(normalized) || normalized,
|
|
1048
|
+
}
|
|
1049
|
+
await this.append(this.projectsLogPath, event)
|
|
1050
|
+
return this.state.workspacesById.get(workspaceId)!
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async removeProject(workspaceId: string) {
|
|
1054
|
+
const project = this.getProject(workspaceId)
|
|
1055
|
+
if (!project) {
|
|
1056
|
+
throw new Error("Project not found")
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const event: WorkspaceEvent = {
|
|
1060
|
+
v: STORE_VERSION,
|
|
1061
|
+
type: "workspace_removed",
|
|
1062
|
+
timestamp: Date.now(),
|
|
1063
|
+
workspaceId,
|
|
1064
|
+
}
|
|
1065
|
+
await this.append(this.projectsLogPath, event)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async createIndependentWorkspace(name: string) {
|
|
1069
|
+
const workspaceId = crypto.randomUUID()
|
|
1070
|
+
const event: WorkspaceEvent = {
|
|
1071
|
+
v: STORE_VERSION,
|
|
1072
|
+
type: "independent_workspace_created",
|
|
1073
|
+
timestamp: Date.now(),
|
|
1074
|
+
workspaceId,
|
|
1075
|
+
name: name.trim(),
|
|
1076
|
+
}
|
|
1077
|
+
await this.append(this.projectsLogPath, event)
|
|
1078
|
+
return this.state.independentWorkspacesById.get(workspaceId)!
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async deleteIndependentWorkspace(workspaceId: string) {
|
|
1082
|
+
const workspace = this.state.independentWorkspacesById.get(workspaceId)
|
|
1083
|
+
if (!workspace) {
|
|
1084
|
+
throw new Error("Independent workspace not found")
|
|
1085
|
+
}
|
|
1086
|
+
const event: WorkspaceEvent = {
|
|
1087
|
+
v: STORE_VERSION,
|
|
1088
|
+
type: "independent_workspace_deleted",
|
|
1089
|
+
timestamp: Date.now(),
|
|
1090
|
+
workspaceId,
|
|
1091
|
+
}
|
|
1092
|
+
await this.append(this.projectsLogPath, event)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async renameIndependentWorkspace(workspaceId: string, name: string) {
|
|
1096
|
+
if (!this.state.independentWorkspacesById.has(workspaceId)) {
|
|
1097
|
+
throw new Error("Independent workspace not found")
|
|
1098
|
+
}
|
|
1099
|
+
const event: WorkspaceEvent = {
|
|
1100
|
+
v: STORE_VERSION,
|
|
1101
|
+
type: "independent_workspace_renamed",
|
|
1102
|
+
timestamp: Date.now(),
|
|
1103
|
+
workspaceId,
|
|
1104
|
+
name: name.trim(),
|
|
1105
|
+
}
|
|
1106
|
+
await this.append(this.projectsLogPath, event)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async setIndependentWorkspacePinned(workspaceId: string, pinned: boolean) {
|
|
1110
|
+
if (!this.state.independentWorkspacesById.has(workspaceId)) {
|
|
1111
|
+
throw new Error("Independent workspace not found")
|
|
1112
|
+
}
|
|
1113
|
+
const event: WorkspaceEvent = {
|
|
1114
|
+
v: STORE_VERSION,
|
|
1115
|
+
type: "independent_workspace_pin_toggled",
|
|
1116
|
+
timestamp: Date.now(),
|
|
1117
|
+
workspaceId,
|
|
1118
|
+
pinned,
|
|
1119
|
+
}
|
|
1120
|
+
await this.append(this.projectsLogPath, event)
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async reorderIndependentWorkspaces(orderedWorkspaceIds: string[]) {
|
|
1124
|
+
for (const id of orderedWorkspaceIds) {
|
|
1125
|
+
if (!this.state.independentWorkspacesById.has(id)) {
|
|
1126
|
+
throw new Error("Independent workspace not found")
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const event: WorkspaceEvent = {
|
|
1130
|
+
v: STORE_VERSION,
|
|
1131
|
+
type: "independent_workspaces_reordered",
|
|
1132
|
+
timestamp: Date.now(),
|
|
1133
|
+
orderedWorkspaceIds,
|
|
1134
|
+
}
|
|
1135
|
+
await this.append(this.projectsLogPath, event)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
listIndependentWorkspaces() {
|
|
1139
|
+
return [...this.state.independentWorkspacesById.values()].sort(compareIndependentWorkspaces)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async createChat(workspaceId: string, repoId?: string, chatId?: string) {
|
|
1143
|
+
const project = this.state.workspacesById.get(workspaceId)
|
|
1144
|
+
if (!project || project.deletedAt) {
|
|
1145
|
+
throw new Error("Project not found")
|
|
1146
|
+
}
|
|
1147
|
+
const nextChatId = chatId ?? crypto.randomUUID()
|
|
1148
|
+
const existing = this.state.chatsById.get(nextChatId)
|
|
1149
|
+
if (existing && !existing.deletedAt) {
|
|
1150
|
+
if (existing.workspaceId !== workspaceId) {
|
|
1151
|
+
throw new Error("Chat already exists for another project")
|
|
1152
|
+
}
|
|
1153
|
+
return existing
|
|
1154
|
+
}
|
|
1155
|
+
const event: ChatEvent = {
|
|
1156
|
+
v: STORE_VERSION,
|
|
1157
|
+
type: "chat_created",
|
|
1158
|
+
timestamp: Date.now(),
|
|
1159
|
+
chatId: nextChatId,
|
|
1160
|
+
workspaceId,
|
|
1161
|
+
title: "New Chat",
|
|
1162
|
+
...(repoId ? { repoId } : {}),
|
|
1163
|
+
}
|
|
1164
|
+
await this.append(this.chatsLogPath, event)
|
|
1165
|
+
return this.state.chatsById.get(nextChatId)!
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async renameChat(chatId: string, title: string) {
|
|
1169
|
+
const trimmed = title.trim()
|
|
1170
|
+
if (!trimmed) return
|
|
1171
|
+
const chat = this.requireChat(chatId)
|
|
1172
|
+
if (chat.title === trimmed) return
|
|
1173
|
+
const event: ChatEvent = {
|
|
1174
|
+
v: STORE_VERSION,
|
|
1175
|
+
type: "chat_renamed",
|
|
1176
|
+
timestamp: Date.now(),
|
|
1177
|
+
chatId,
|
|
1178
|
+
title: trimmed,
|
|
1179
|
+
}
|
|
1180
|
+
await this.append(this.chatsLogPath, event)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async deleteChat(chatId: string) {
|
|
1184
|
+
this.requireChat(chatId)
|
|
1185
|
+
const event: ChatEvent = {
|
|
1186
|
+
v: STORE_VERSION,
|
|
1187
|
+
type: "chat_deleted",
|
|
1188
|
+
timestamp: Date.now(),
|
|
1189
|
+
chatId,
|
|
1190
|
+
}
|
|
1191
|
+
await this.append(this.chatsLogPath, event)
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async setChatProvider(chatId: string, provider: AgentProvider) {
|
|
1195
|
+
const chat = this.requireChat(chatId)
|
|
1196
|
+
if (chat.provider === provider) return
|
|
1197
|
+
const event: ChatEvent = {
|
|
1198
|
+
v: STORE_VERSION,
|
|
1199
|
+
type: "chat_provider_set",
|
|
1200
|
+
timestamp: Date.now(),
|
|
1201
|
+
chatId,
|
|
1202
|
+
provider,
|
|
1203
|
+
}
|
|
1204
|
+
await this.append(this.chatsLogPath, event)
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async setChatRunner(chatId: string, runnerId: string | null) {
|
|
1208
|
+
const chat = this.requireChat(chatId)
|
|
1209
|
+
if ((chat.runnerId ?? null) === runnerId) return
|
|
1210
|
+
const event: ChatEvent = {
|
|
1211
|
+
v: STORE_VERSION,
|
|
1212
|
+
type: "chat_runner_set",
|
|
1213
|
+
timestamp: Date.now(),
|
|
1214
|
+
chatId,
|
|
1215
|
+
runnerId,
|
|
1216
|
+
}
|
|
1217
|
+
await this.append(this.chatsLogPath, event)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async setPlanMode(chatId: string, planMode: boolean) {
|
|
1221
|
+
const chat = this.requireChat(chatId)
|
|
1222
|
+
if (chat.planMode === planMode) return
|
|
1223
|
+
const event: ChatEvent = {
|
|
1224
|
+
v: STORE_VERSION,
|
|
1225
|
+
type: "chat_plan_mode_set",
|
|
1226
|
+
timestamp: Date.now(),
|
|
1227
|
+
chatId,
|
|
1228
|
+
planMode,
|
|
1229
|
+
}
|
|
1230
|
+
await this.append(this.chatsLogPath, event)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async setChatModel(chatId: string, model: string | null) {
|
|
1234
|
+
const chat = this.requireChat(chatId)
|
|
1235
|
+
if ((chat.model ?? null) === model) return
|
|
1236
|
+
const event: ChatEvent = {
|
|
1237
|
+
v: STORE_VERSION,
|
|
1238
|
+
type: "chat_model_set",
|
|
1239
|
+
timestamp: Date.now(),
|
|
1240
|
+
chatId,
|
|
1241
|
+
model,
|
|
1242
|
+
}
|
|
1243
|
+
await this.append(this.chatsLogPath, event)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async setChatReadState(chatId: string, unread: boolean) {
|
|
1247
|
+
const chat = this.requireChat(chatId)
|
|
1248
|
+
if (chat.unread === unread) return
|
|
1249
|
+
const event: ChatEvent = {
|
|
1250
|
+
v: STORE_VERSION,
|
|
1251
|
+
type: "chat_read_state_set",
|
|
1252
|
+
timestamp: Date.now(),
|
|
1253
|
+
chatId,
|
|
1254
|
+
unread,
|
|
1255
|
+
}
|
|
1256
|
+
await this.append(this.chatsLogPath, event)
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
appendMessage(chatId: string, entry: TranscriptEntry) {
|
|
1260
|
+
this.requireChat(chatId)
|
|
1261
|
+
// In-memory first so NATS publish fires without waiting for disk I/O
|
|
1262
|
+
this.applyMessageMetadata(chatId, entry)
|
|
1263
|
+
const cached = this.transcriptCache.get(chatId)
|
|
1264
|
+
if (cached) {
|
|
1265
|
+
cached.push({ ...entry })
|
|
1266
|
+
}
|
|
1267
|
+
const payload = `${JSON.stringify(entry)}\n`
|
|
1268
|
+
const transcriptPath = this.transcriptPath(chatId)
|
|
1269
|
+
this.writeChain = this.writeChain.then(() => appendFile(transcriptPath, payload, "utf8"))
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async recordTurnStarted(chatId: string) {
|
|
1273
|
+
this.requireChat(chatId)
|
|
1274
|
+
const event: TurnEvent = {
|
|
1275
|
+
v: STORE_VERSION,
|
|
1276
|
+
type: "turn_started",
|
|
1277
|
+
timestamp: Date.now(),
|
|
1278
|
+
chatId,
|
|
1279
|
+
}
|
|
1280
|
+
await this.append(this.turnsLogPath, event)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async recordTurnFinished(chatId: string) {
|
|
1284
|
+
this.requireChat(chatId)
|
|
1285
|
+
const event: TurnEvent = {
|
|
1286
|
+
v: STORE_VERSION,
|
|
1287
|
+
type: "turn_finished",
|
|
1288
|
+
timestamp: Date.now(),
|
|
1289
|
+
chatId,
|
|
1290
|
+
}
|
|
1291
|
+
await this.append(this.turnsLogPath, event)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async recordTurnFailed(chatId: string, error: string) {
|
|
1295
|
+
this.requireChat(chatId)
|
|
1296
|
+
const event: TurnEvent = {
|
|
1297
|
+
v: STORE_VERSION,
|
|
1298
|
+
type: "turn_failed",
|
|
1299
|
+
timestamp: Date.now(),
|
|
1300
|
+
chatId,
|
|
1301
|
+
error,
|
|
1302
|
+
}
|
|
1303
|
+
await this.append(this.turnsLogPath, event)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async recordTurnCancelled(chatId: string) {
|
|
1307
|
+
this.requireChat(chatId)
|
|
1308
|
+
const event: TurnEvent = {
|
|
1309
|
+
v: STORE_VERSION,
|
|
1310
|
+
type: "turn_cancelled",
|
|
1311
|
+
timestamp: Date.now(),
|
|
1312
|
+
chatId,
|
|
1313
|
+
}
|
|
1314
|
+
await this.append(this.turnsLogPath, event)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async enqueueQueuedTurn(args: {
|
|
1318
|
+
chatId: string
|
|
1319
|
+
provider?: AgentProvider
|
|
1320
|
+
content: string
|
|
1321
|
+
model?: string
|
|
1322
|
+
modelOptions?: import("../shared/types").ModelOptions
|
|
1323
|
+
effort?: string
|
|
1324
|
+
planMode?: boolean
|
|
1325
|
+
}) {
|
|
1326
|
+
this.requireChat(args.chatId)
|
|
1327
|
+
const content = args.content.trim()
|
|
1328
|
+
if (!content) return
|
|
1329
|
+
const event: TurnEvent = {
|
|
1330
|
+
v: STORE_VERSION,
|
|
1331
|
+
type: "chat_turn_queued",
|
|
1332
|
+
timestamp: Date.now(),
|
|
1333
|
+
chatId: args.chatId,
|
|
1334
|
+
provider: args.provider,
|
|
1335
|
+
content,
|
|
1336
|
+
model: args.model,
|
|
1337
|
+
modelOptions: args.modelOptions,
|
|
1338
|
+
effort: args.effort,
|
|
1339
|
+
planMode: args.planMode,
|
|
1340
|
+
}
|
|
1341
|
+
await this.append(this.turnsLogPath, event)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
getQueuedTurn(chatId: string): QueuedChatTurnRecord | null {
|
|
1345
|
+
const queued = this.state.queuedTurnsByChat.get(chatId)
|
|
1346
|
+
return queued ? { ...queued } : null
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
async clearQueuedTurn(chatId: string) {
|
|
1350
|
+
if (!this.state.queuedTurnsByChat.has(chatId)) return
|
|
1351
|
+
const event: TurnEvent = {
|
|
1352
|
+
v: STORE_VERSION,
|
|
1353
|
+
type: "chat_queued_turn_cleared",
|
|
1354
|
+
timestamp: Date.now(),
|
|
1355
|
+
chatId,
|
|
1356
|
+
}
|
|
1357
|
+
await this.append(this.turnsLogPath, event)
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
async recordDelegationInitiated(workspaceId: string, args: { delegationId: string; parentChatId: string; childChatId: string; mode: "blocking" | "background"; resume: "immediate" | "gate" }) {
|
|
1361
|
+
const event: TurnEvent = {
|
|
1362
|
+
v: STORE_VERSION,
|
|
1363
|
+
type: "delegation_initiated",
|
|
1364
|
+
timestamp: Date.now(),
|
|
1365
|
+
delegationId: args.delegationId,
|
|
1366
|
+
parentChatId: args.parentChatId,
|
|
1367
|
+
childChatId: args.childChatId,
|
|
1368
|
+
workspaceId,
|
|
1369
|
+
mode: args.mode,
|
|
1370
|
+
resume: args.resume,
|
|
1371
|
+
}
|
|
1372
|
+
await this.append(this.turnsLogPath, event)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async recordDelegationCompleted(workspaceId: string, args: { delegationId: string; parentChatId: string; childChatId: string; outcome: "completed" | "failed" | "orphaned" | "stale" }) {
|
|
1376
|
+
const event: TurnEvent = {
|
|
1377
|
+
v: STORE_VERSION,
|
|
1378
|
+
type: "delegation_completed",
|
|
1379
|
+
timestamp: Date.now(),
|
|
1380
|
+
delegationId: args.delegationId,
|
|
1381
|
+
parentChatId: args.parentChatId,
|
|
1382
|
+
childChatId: args.childChatId,
|
|
1383
|
+
workspaceId,
|
|
1384
|
+
outcome: args.outcome,
|
|
1385
|
+
}
|
|
1386
|
+
await this.append(this.turnsLogPath, event)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
async setSessionToken(chatId: string, sessionToken: string | null) {
|
|
1390
|
+
const chat = this.requireChat(chatId)
|
|
1391
|
+
if (chat.sessionToken === sessionToken) return
|
|
1392
|
+
const event: TurnEvent = {
|
|
1393
|
+
v: STORE_VERSION,
|
|
1394
|
+
type: "session_token_set",
|
|
1395
|
+
timestamp: Date.now(),
|
|
1396
|
+
chatId,
|
|
1397
|
+
sessionToken,
|
|
1398
|
+
}
|
|
1399
|
+
await this.append(this.turnsLogPath, event)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// --- Coordination mutation methods ---
|
|
1403
|
+
|
|
1404
|
+
async addTodo(workspaceId: string, todoId: string, description: string, priority: "high" | "normal" | "low", createdBy: string) {
|
|
1405
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_added", timestamp: Date.now(), workspaceId, todoId, description, priority, createdBy }
|
|
1406
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
async claimTodo(workspaceId: string, todoId: string, claimedBy: string) {
|
|
1410
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_claimed", timestamp: Date.now(), workspaceId, todoId, claimedBy }
|
|
1411
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async completeTodo(workspaceId: string, todoId: string, outputs: string[]) {
|
|
1415
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_completed", timestamp: Date.now(), workspaceId, todoId, outputs }
|
|
1416
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async abandonTodo(workspaceId: string, todoId: string) {
|
|
1420
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_abandoned", timestamp: Date.now(), workspaceId, todoId }
|
|
1421
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
async createClaim(workspaceId: string, claimId: string, intent: string, files: string[], sessionId: string) {
|
|
1425
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "claim_created", timestamp: Date.now(), workspaceId, claimId, intent, files, sessionId }
|
|
1426
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1427
|
+
|
|
1428
|
+
// Auto-detect file overlap with existing active claims.
|
|
1429
|
+
// INVARIANT: append() updates in-memory state synchronously via applyEvent(),
|
|
1430
|
+
// so coordinationByWorkspace is already current when we read it here.
|
|
1431
|
+
// Only the first overlapping claim triggers a conflict event (intentional —
|
|
1432
|
+
// downstream can trace the full conflict chain via claim_conflict_detected events).
|
|
1433
|
+
const coord = this.state.coordinationByWorkspace.get(workspaceId)
|
|
1434
|
+
if (coord) {
|
|
1435
|
+
const fileSet = new Set(files)
|
|
1436
|
+
for (const [existingId, existing] of coord.claims) {
|
|
1437
|
+
if (existingId === claimId || existing.status !== "active") continue
|
|
1438
|
+
const overlapping = existing.files.filter((f) => fileSet.has(f))
|
|
1439
|
+
if (overlapping.length > 0) {
|
|
1440
|
+
const conflictEvent: CoordinationEvent = {
|
|
1441
|
+
v: STORE_VERSION, type: "claim_conflict_detected", timestamp: Date.now(),
|
|
1442
|
+
workspaceId, claimId, conflictsWith: existingId, overlappingFiles: overlapping,
|
|
1443
|
+
}
|
|
1444
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, conflictEvent)
|
|
1445
|
+
break
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async releaseClaim(workspaceId: string, claimId: string) {
|
|
1452
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "claim_released", timestamp: Date.now(), workspaceId, claimId }
|
|
1453
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async createWorktree(workspaceId: string, worktreeId: string, branch: string, baseBranch: string, wtPath: string) {
|
|
1457
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_created", timestamp: Date.now(), workspaceId, worktreeId, branch, baseBranch, path: wtPath }
|
|
1458
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async assignWorktree(workspaceId: string, worktreeId: string, sessionId: string) {
|
|
1462
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_assigned", timestamp: Date.now(), workspaceId, worktreeId, sessionId }
|
|
1463
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async removeWorktree(workspaceId: string, worktreeId: string) {
|
|
1467
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_removed", timestamp: Date.now(), workspaceId, worktreeId }
|
|
1468
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
async setRule(workspaceId: string, ruleId: string, content: string, setBy: string) {
|
|
1472
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "rule_set", timestamp: Date.now(), workspaceId, ruleId, content, setBy }
|
|
1473
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async removeRule(workspaceId: string, ruleId: string) {
|
|
1477
|
+
const event: CoordinationEvent = { v: STORE_VERSION, type: "rule_removed", timestamp: Date.now(), workspaceId, ruleId }
|
|
1478
|
+
await this.append<CoordinationEvent>(this.coordinationLogPath, event)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// --- Agent config mutation methods ---
|
|
1482
|
+
|
|
1483
|
+
async saveAgentConfig(workspaceId: string, agentId: string, config: AgentConfig) {
|
|
1484
|
+
const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_saved", timestamp: Date.now(), workspaceId, agentId, config }
|
|
1485
|
+
await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async commitAgentConfig(workspaceId: string, agentId: string, commitHash: string) {
|
|
1489
|
+
const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_committed", timestamp: Date.now(), workspaceId, agentId, commitHash }
|
|
1490
|
+
await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
async removeAgentConfig(workspaceId: string, agentId: string) {
|
|
1494
|
+
const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_removed", timestamp: Date.now(), workspaceId, agentId }
|
|
1495
|
+
await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// --- Provider profile mutation methods ---
|
|
1499
|
+
|
|
1500
|
+
async saveProviderProfile(profileId: string, profile: ProviderProfile) {
|
|
1501
|
+
const event: ProviderProfileEvent = { v: STORE_VERSION, type: "provider_profile_saved", timestamp: Date.now(), profileId, profile }
|
|
1502
|
+
await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
async removeProviderProfile(profileId: string) {
|
|
1506
|
+
const event: ProviderProfileEvent = { v: STORE_VERSION, type: "provider_profile_removed", timestamp: Date.now(), profileId }
|
|
1507
|
+
await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
async setWorkspaceProfileOverride(workspaceId: string, profileId: string, overrides: Partial<Omit<ProviderProfile, "id" | "provider">>) {
|
|
1511
|
+
const event: ProviderProfileEvent = { v: STORE_VERSION, type: "workspace_profile_override_set", timestamp: Date.now(), workspaceId, profileId, overrides }
|
|
1512
|
+
await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async removeWorkspaceProfileOverride(workspaceId: string, profileId: string) {
|
|
1516
|
+
const event: ProviderProfileEvent = { v: STORE_VERSION, type: "workspace_profile_override_removed", timestamp: Date.now(), workspaceId, profileId }
|
|
1517
|
+
await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// --- Extension preference mutation methods ---
|
|
1521
|
+
|
|
1522
|
+
async setExtensionPreference(extensionId: string, enabled: boolean) {
|
|
1523
|
+
const event: ExtensionPreferenceEvent = { v: STORE_VERSION, type: "extension_preference_set", timestamp: Date.now(), extensionId, enabled }
|
|
1524
|
+
await this.append<ExtensionPreferenceEvent>(this.extensionPrefsLogPath, event)
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// --- Runner team mutation methods (US-RTN) ---
|
|
1528
|
+
|
|
1529
|
+
async saveTeamMember(member: TeamMember) {
|
|
1530
|
+
const event: RunnerTeamEvent = { v: STORE_VERSION, type: "team_member_saved", timestamp: Date.now(), memberId: member.id, member }
|
|
1531
|
+
await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async removeTeamMember(memberId: string) {
|
|
1535
|
+
const event: RunnerTeamEvent = { v: STORE_VERSION, type: "team_member_removed", timestamp: Date.now(), memberId }
|
|
1536
|
+
await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
async setRunnerLabel(runnerId: string, name: string | null, memberId: string | null) {
|
|
1540
|
+
const event: RunnerTeamEvent = { v: STORE_VERSION, type: "runner_label_set", timestamp: Date.now(), runnerId, name, memberId }
|
|
1541
|
+
await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async removeRunnerLabel(runnerId: string) {
|
|
1545
|
+
const event: RunnerTeamEvent = { v: STORE_VERSION, type: "runner_label_removed", timestamp: Date.now(), runnerId }
|
|
1546
|
+
await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// --- Repo mutation methods ---
|
|
1550
|
+
|
|
1551
|
+
async addRepo(id: string, workspaceId: string, localPath: string, origin: string | null, label: string | null, branch: string | null) {
|
|
1552
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_added", timestamp: Date.now(), id, workspaceId, localPath, origin, label, branch }
|
|
1553
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
async startRepoClone(id: string, workspaceId: string, origin: string, targetPath: string, label: string | null) {
|
|
1557
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_clone_started", timestamp: Date.now(), id, workspaceId, origin, targetPath, label }
|
|
1558
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
async markRepoCloned(id: string, localPath: string, branch: string | null) {
|
|
1562
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_cloned", timestamp: Date.now(), id, localPath, branch }
|
|
1563
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async markRepoCloneFailed(id: string, error: string) {
|
|
1567
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_clone_failed", timestamp: Date.now(), id, error }
|
|
1568
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async removeRepo(id: string, workspaceId: string) {
|
|
1572
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_removed", timestamp: Date.now(), id, workspaceId }
|
|
1573
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
async updateRepoLabel(id: string, label: string) {
|
|
1577
|
+
const event: RepoEvent = { v: STORE_VERSION, type: "repo_label_updated", timestamp: Date.now(), id, label }
|
|
1578
|
+
await this.append<RepoEvent>(this.reposLogPath, event)
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// --- Workflow mutation methods ---
|
|
1582
|
+
|
|
1583
|
+
async emitWorkflowStarted(runId: string, workflowId: string, workspaceId: string, targetRepoIds: string[], triggeredBy: string) {
|
|
1584
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_started", timestamp: Date.now(), runId, workflowId, workspaceId, targetRepoIds, triggeredBy }
|
|
1585
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async emitWorkflowStepStarted(runId: string, workspaceId: string, stepIndex: number, mcpTool: string, repoId?: string) {
|
|
1589
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_started", timestamp: Date.now(), runId, workspaceId, stepIndex, mcp_tool: mcpTool, ...(repoId !== undefined ? { repoId } : {}) }
|
|
1590
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
async emitWorkflowStepCompleted(runId: string, workspaceId: string, stepIndex: number, output: string, repoId?: string) {
|
|
1594
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_completed", timestamp: Date.now(), runId, workspaceId, stepIndex, output, ...(repoId !== undefined ? { repoId } : {}) }
|
|
1595
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async emitWorkflowStepFailed(runId: string, workspaceId: string, stepIndex: number, error: string, repoId?: string) {
|
|
1599
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_failed", timestamp: Date.now(), runId, workspaceId, stepIndex, error, ...(repoId !== undefined ? { repoId } : {}) }
|
|
1600
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async emitWorkflowCompleted(runId: string, workspaceId: string) {
|
|
1604
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_completed", timestamp: Date.now(), runId, workspaceId }
|
|
1605
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
async emitWorkflowFailed(runId: string, workspaceId: string, error: string, failedStep: number) {
|
|
1609
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_failed", timestamp: Date.now(), runId, workspaceId, error, failedStep }
|
|
1610
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
async emitWorkflowCancelled(runId: string, workspaceId: string) {
|
|
1614
|
+
const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_cancelled", timestamp: Date.now(), runId, workspaceId }
|
|
1615
|
+
await this.append<WorkflowEvent>(this.workflowsLogPath, event)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// --- Sandbox mutation methods ---
|
|
1619
|
+
|
|
1620
|
+
async emitSandboxCreated(id: string, workspaceId: string, resourceLimits: import("../shared/sandbox-types").ResourceLimits) {
|
|
1621
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_created", timestamp: Date.now(), id, workspaceId, resourceLimits })
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
async emitSandboxStarted(id: string, containerId: string, natsUrl: string) {
|
|
1625
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_started", timestamp: Date.now(), id, containerId, natsUrl })
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async emitSandboxStopped(id: string, reason: string) {
|
|
1629
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_stopped", timestamp: Date.now(), id, reason })
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
async emitSandboxDestroyed(id: string) {
|
|
1633
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_destroyed", timestamp: Date.now(), id })
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async emitSandboxError(id: string, error: string) {
|
|
1637
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_error", timestamp: Date.now(), id, error })
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async emitSandboxHealthUpdated(id: string, health: import("../shared/sandbox-types").SandboxHealthReport) {
|
|
1641
|
+
await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_health_updated", timestamp: Date.now(), id, health })
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
getProject(workspaceId: string) {
|
|
1645
|
+
const project = this.state.workspacesById.get(workspaceId)
|
|
1646
|
+
if (!project || project.deletedAt) return null
|
|
1647
|
+
return project
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
requireChat(chatId: string) {
|
|
1651
|
+
const chat = this.state.chatsById.get(chatId)
|
|
1652
|
+
if (!chat || chat.deletedAt) {
|
|
1653
|
+
throw new Error("Chat not found")
|
|
1654
|
+
}
|
|
1655
|
+
return chat
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
getChat(chatId: string) {
|
|
1659
|
+
const chat = this.state.chatsById.get(chatId)
|
|
1660
|
+
if (!chat || chat.deletedAt) return null
|
|
1661
|
+
return chat
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async getMessages(chatId: string, options?: { offset?: number; limit?: number }) {
|
|
1665
|
+
let entries: TranscriptEntry[]
|
|
1666
|
+
|
|
1667
|
+
if (this.transcriptCache.has(chatId)) {
|
|
1668
|
+
entries = this.transcriptCache.get(chatId)!
|
|
1669
|
+
} else {
|
|
1670
|
+
const legacyEntries = this.legacyMessagesByChatId.get(chatId)
|
|
1671
|
+
if (legacyEntries) {
|
|
1672
|
+
this.setTranscriptCache(chatId, cloneTranscriptEntries(legacyEntries))
|
|
1673
|
+
entries = this.transcriptCache.get(chatId)!
|
|
1674
|
+
} else {
|
|
1675
|
+
// Drain pending writes before reading from disk to ensure consistency
|
|
1676
|
+
await this.writeChain
|
|
1677
|
+
entries = await this.loadTranscriptFromDisk(chatId)
|
|
1678
|
+
this.setTranscriptCache(chatId, entries)
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (options?.offset !== undefined || options?.limit !== undefined) {
|
|
1683
|
+
const start = options.offset ?? 0
|
|
1684
|
+
const end = options.limit !== undefined ? start + options.limit : undefined
|
|
1685
|
+
return cloneTranscriptEntries(entries.slice(start, end))
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
return cloneTranscriptEntries(entries)
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
async getMessageCount(chatId: string): Promise<number> {
|
|
1692
|
+
if (this.transcriptCache.has(chatId)) {
|
|
1693
|
+
return this.transcriptCache.get(chatId)!.length
|
|
1694
|
+
}
|
|
1695
|
+
const legacyEntries = this.legacyMessagesByChatId.get(chatId)
|
|
1696
|
+
if (legacyEntries) {
|
|
1697
|
+
return legacyEntries.length
|
|
1698
|
+
}
|
|
1699
|
+
await this.writeChain
|
|
1700
|
+
const entries = await this.loadTranscriptFromDisk(chatId)
|
|
1701
|
+
this.setTranscriptCache(chatId, entries)
|
|
1702
|
+
return entries.length
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
listProjects() {
|
|
1706
|
+
return [...this.state.workspacesById.values()].filter((project) => !project.deletedAt)
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
listChatsByProject(workspaceId: string) {
|
|
1710
|
+
return [...this.state.chatsById.values()]
|
|
1711
|
+
.filter((chat) => chat.workspaceId === workspaceId && !chat.deletedAt)
|
|
1712
|
+
.sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
getChatCount(workspaceId: string) {
|
|
1716
|
+
return this.listChatsByProject(workspaceId).length
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> {
|
|
1720
|
+
const messagesLogSize = await Bun.file(this.messagesLogPath).size
|
|
1721
|
+
const sources: LegacyTranscriptStats["sources"] = []
|
|
1722
|
+
if (this.snapshotHasLegacyMessages) {
|
|
1723
|
+
sources.push("snapshot")
|
|
1724
|
+
}
|
|
1725
|
+
if (messagesLogSize > 0) {
|
|
1726
|
+
sources.push("messages_log")
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
let entryCount = 0
|
|
1730
|
+
for (const entries of this.legacyMessagesByChatId.values()) {
|
|
1731
|
+
entryCount += entries.length
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return {
|
|
1735
|
+
hasLegacyData: sources.length > 0 || this.legacyMessagesByChatId.size > 0,
|
|
1736
|
+
sources,
|
|
1737
|
+
chatCount: this.legacyMessagesByChatId.size,
|
|
1738
|
+
entryCount,
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async hasLegacyTranscriptData() {
|
|
1743
|
+
return (await this.getLegacyTranscriptStats()).hasLegacyData
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
private createSnapshot(): SnapshotFile {
|
|
1747
|
+
const coordination: SnapshotFile["coordination"] = []
|
|
1748
|
+
for (const [workspaceId, coord] of this.state.coordinationByWorkspace) {
|
|
1749
|
+
coordination.push({
|
|
1750
|
+
workspaceId,
|
|
1751
|
+
todos: [...coord.todos.values()],
|
|
1752
|
+
claims: [...coord.claims.values()],
|
|
1753
|
+
worktrees: [...coord.worktrees.values()],
|
|
1754
|
+
rules: [...coord.rules.values()],
|
|
1755
|
+
})
|
|
1756
|
+
}
|
|
1757
|
+
const agentConfigs: SnapshotFile["agentConfigs"] = []
|
|
1758
|
+
for (const [workspaceId, configMap] of this.state.agentConfigsByWorkspace) {
|
|
1759
|
+
if (configMap.size > 0) {
|
|
1760
|
+
agentConfigs.push({ workspaceId, records: [...configMap.values()] })
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
v: STORE_VERSION,
|
|
1765
|
+
generatedAt: Date.now(),
|
|
1766
|
+
workspaces: this.listProjects().map((project) => ({ ...project })),
|
|
1767
|
+
...(this.state.independentWorkspacesById.size > 0 ? { independentWorkspaces: this.listIndependentWorkspaces() } : {}),
|
|
1768
|
+
chats: [...this.state.chatsById.values()]
|
|
1769
|
+
.filter((chat) => !chat.deletedAt)
|
|
1770
|
+
.map((chat) => ({ ...chat })),
|
|
1771
|
+
...(this.state.queuedTurnsByChat.size > 0 ? {
|
|
1772
|
+
queuedTurns: [...this.state.queuedTurnsByChat.values()].map((queued) => ({ ...queued })),
|
|
1773
|
+
} : {}),
|
|
1774
|
+
...(coordination.length > 0 ? { coordination } : {}),
|
|
1775
|
+
...(agentConfigs.length > 0 ? { agentConfigs } : {}),
|
|
1776
|
+
...(this.state.reposById.size > 0 ? { repos: [...this.state.reposById.values()] } : {}),
|
|
1777
|
+
...(this.state.workflowRunsByWorkspace.size > 0 ? {
|
|
1778
|
+
workflowRuns: [...this.state.workflowRunsByWorkspace.entries()].map(([workspaceId, runsMap]) => ({
|
|
1779
|
+
workspaceId,
|
|
1780
|
+
runs: [...runsMap.values()],
|
|
1781
|
+
})),
|
|
1782
|
+
} : {}),
|
|
1783
|
+
...(this.state.sandboxByWorkspace.size > 0 ? {
|
|
1784
|
+
sandboxes: [...this.state.sandboxByWorkspace.values()],
|
|
1785
|
+
} : {}),
|
|
1786
|
+
...(this.state.providerProfiles.size > 0 ? {
|
|
1787
|
+
providerProfiles: [...this.state.providerProfiles.values()],
|
|
1788
|
+
} : {}),
|
|
1789
|
+
...(this.state.workspaceProfileOverrides.size > 0 ? {
|
|
1790
|
+
workspaceProfileOverrides: [...this.state.workspaceProfileOverrides.values()].flatMap(
|
|
1791
|
+
(wsMap) => [...wsMap.values()],
|
|
1792
|
+
),
|
|
1793
|
+
} : {}),
|
|
1794
|
+
...(this.state.extensionPreferences.size > 0 ? {
|
|
1795
|
+
extensionPreferences: [...this.state.extensionPreferences.values()],
|
|
1796
|
+
} : {}),
|
|
1797
|
+
...(this.state.teamMembers.size > 0 ? {
|
|
1798
|
+
teamMembers: [...this.state.teamMembers.values()],
|
|
1799
|
+
} : {}),
|
|
1800
|
+
...(this.state.runnerLabels.size > 0 ? {
|
|
1801
|
+
runnerLabels: [...this.state.runnerLabels.values()],
|
|
1802
|
+
} : {}),
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
async compact() {
|
|
1807
|
+
const snapshot = this.createSnapshot()
|
|
1808
|
+
await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
|
|
1809
|
+
await Promise.all([
|
|
1810
|
+
Bun.write(this.projectsLogPath, ""),
|
|
1811
|
+
Bun.write(this.chatsLogPath, ""),
|
|
1812
|
+
Bun.write(this.messagesLogPath, ""),
|
|
1813
|
+
Bun.write(this.turnsLogPath, ""),
|
|
1814
|
+
Bun.write(this.coordinationLogPath, ""),
|
|
1815
|
+
Bun.write(this.agentConfigsLogPath, ""),
|
|
1816
|
+
Bun.write(this.reposLogPath, ""),
|
|
1817
|
+
Bun.write(this.workflowsLogPath, ""),
|
|
1818
|
+
Bun.write(this.sandboxLogPath, ""),
|
|
1819
|
+
Bun.write(this.profilesLogPath, ""),
|
|
1820
|
+
Bun.write(this.extensionPrefsLogPath, ""),
|
|
1821
|
+
Bun.write(this.runnerTeamsLogPath, ""),
|
|
1822
|
+
])
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
async migrateLegacyTranscripts(onProgress?: (message: string) => void) {
|
|
1826
|
+
const stats = await this.getLegacyTranscriptStats()
|
|
1827
|
+
if (!stats.hasLegacyData) return false
|
|
1828
|
+
|
|
1829
|
+
const sourceSummary = stats.sources.map((source) => source === "messages_log" ? "messages.jsonl" : "snapshot.json").join(", ")
|
|
1830
|
+
onProgress?.(`${LOG_PREFIX} transcript migration detected: ${stats.chatCount} chats, ${stats.entryCount} entries from ${sourceSummary}`)
|
|
1831
|
+
|
|
1832
|
+
const messageSets = [...this.legacyMessagesByChatId.entries()]
|
|
1833
|
+
onProgress?.(`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`)
|
|
1834
|
+
|
|
1835
|
+
await mkdir(this.transcriptsDir, { recursive: true })
|
|
1836
|
+
const logEveryChat = messageSets.length <= 10
|
|
1837
|
+
for (let index = 0; index < messageSets.length; index += 1) {
|
|
1838
|
+
const [chatId, entries] = messageSets[index]
|
|
1839
|
+
const transcriptPath = this.transcriptPath(chatId)
|
|
1840
|
+
const tempPath = `${transcriptPath}.tmp`
|
|
1841
|
+
const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
1842
|
+
await writeFile(tempPath, payload ? `${payload}\n` : "", "utf8")
|
|
1843
|
+
await rename(tempPath, transcriptPath)
|
|
1844
|
+
if (logEveryChat || (index + 1) % 25 === 0 || index === messageSets.length - 1) {
|
|
1845
|
+
onProgress?.(`${LOG_PREFIX} transcript migration: ${index + 1}/${messageSets.length} chats`)
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
this.clearLegacyTranscriptState()
|
|
1850
|
+
await this.compact()
|
|
1851
|
+
this.transcriptCache.clear()
|
|
1852
|
+
onProgress?.(`${LOG_PREFIX} transcript migration complete`)
|
|
1853
|
+
return true
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private async shouldCompact() {
|
|
1857
|
+
const sizes = await Promise.all([
|
|
1858
|
+
Bun.file(this.projectsLogPath).size,
|
|
1859
|
+
Bun.file(this.chatsLogPath).size,
|
|
1860
|
+
Bun.file(this.messagesLogPath).size,
|
|
1861
|
+
Bun.file(this.turnsLogPath).size,
|
|
1862
|
+
Bun.file(this.coordinationLogPath).size,
|
|
1863
|
+
Bun.file(this.agentConfigsLogPath).size,
|
|
1864
|
+
Bun.file(this.reposLogPath).size,
|
|
1865
|
+
Bun.file(this.workflowsLogPath).size,
|
|
1866
|
+
Bun.file(this.sandboxLogPath).size,
|
|
1867
|
+
Bun.file(this.profilesLogPath).size,
|
|
1868
|
+
Bun.file(this.extensionPrefsLogPath).size,
|
|
1869
|
+
Bun.file(this.runnerTeamsLogPath).size,
|
|
1870
|
+
])
|
|
1871
|
+
return sizes.reduce((total, size) => total + size, 0) >= COMPACTION_THRESHOLD_BYTES
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// ===== claude-pty additions (in-memory only — PORT-TODO durable persistence) =====
|
|
1875
|
+
|
|
1876
|
+
private readonly _ptySubagentRuns = new Map<string, Map<string, PtySubagentRunSnapshot>>()
|
|
1877
|
+
private readonly _ptyToolRequests = new Map<string, PtyToolRequest>()
|
|
1878
|
+
|
|
1879
|
+
private async replayPtyLogs(): Promise<void> {
|
|
1880
|
+
// Subagent log replay
|
|
1881
|
+
const subagentFile = Bun.file(this.ptySubagentLogPath)
|
|
1882
|
+
if (await subagentFile.exists()) {
|
|
1883
|
+
const text = await subagentFile.text()
|
|
1884
|
+
for (const line of text.split("\n")) {
|
|
1885
|
+
if (!line.trim()) continue
|
|
1886
|
+
try {
|
|
1887
|
+
const event = JSON.parse(line) as PtySubagentRunEvent
|
|
1888
|
+
this.applyPtySubagentEvent(event)
|
|
1889
|
+
} catch {
|
|
1890
|
+
// skip malformed
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
// Session token replay
|
|
1895
|
+
const tokenFile = Bun.file(this.ptySessionTokensLogPath)
|
|
1896
|
+
if (await tokenFile.exists()) {
|
|
1897
|
+
const text = await tokenFile.text()
|
|
1898
|
+
for (const line of text.split("\n")) {
|
|
1899
|
+
if (!line.trim()) continue
|
|
1900
|
+
try {
|
|
1901
|
+
const rec = JSON.parse(line) as { chatId: string; providerId: string; sessionToken: string | null }
|
|
1902
|
+
let map = this._ptySessionTokensByChatId.get(rec.chatId)
|
|
1903
|
+
if (!map) {
|
|
1904
|
+
map = new Map<string, string>()
|
|
1905
|
+
this._ptySessionTokensByChatId.set(rec.chatId, map)
|
|
1906
|
+
}
|
|
1907
|
+
if (rec.sessionToken === null) {
|
|
1908
|
+
map.delete(rec.providerId)
|
|
1909
|
+
} else {
|
|
1910
|
+
map.set(rec.providerId, rec.sessionToken)
|
|
1911
|
+
}
|
|
1912
|
+
} catch {
|
|
1913
|
+
// skip malformed
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
// Tool request log replay
|
|
1918
|
+
const toolFile = Bun.file(this.ptyToolRequestsLogPath)
|
|
1919
|
+
if (await toolFile.exists()) {
|
|
1920
|
+
const text = await toolFile.text()
|
|
1921
|
+
for (const line of text.split("\n")) {
|
|
1922
|
+
if (!line.trim()) continue
|
|
1923
|
+
try {
|
|
1924
|
+
const rec = JSON.parse(line) as { type: "put" | "resolved"; request?: PtyToolRequest; id?: string; status?: import("../shared/permission-policy").ToolRequestStatus; decision?: import("../shared/permission-policy").ToolRequestDecision; resolvedAt?: number; mismatchReason?: string }
|
|
1925
|
+
if (rec.type === "put" && rec.request) {
|
|
1926
|
+
this._ptyToolRequests.set(rec.request.id, { ...rec.request })
|
|
1927
|
+
} else if (rec.type === "resolved" && rec.id) {
|
|
1928
|
+
const existing = this._ptyToolRequests.get(rec.id)
|
|
1929
|
+
if (existing) {
|
|
1930
|
+
this._ptyToolRequests.set(rec.id, {
|
|
1931
|
+
...existing,
|
|
1932
|
+
status: rec.status ?? existing.status,
|
|
1933
|
+
decision: rec.decision ?? existing.decision,
|
|
1934
|
+
resolvedAt: rec.resolvedAt ?? existing.resolvedAt,
|
|
1935
|
+
mismatchReason: rec.mismatchReason,
|
|
1936
|
+
})
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
} catch {
|
|
1940
|
+
// skip malformed
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
private applyPtySubagentEvent(event: PtySubagentRunEvent): void {
|
|
1947
|
+
const chatMap = this._ptySubagentRuns.get(event.chatId) ?? new Map<string, PtySubagentRunSnapshot>()
|
|
1948
|
+
this._ptySubagentRuns.set(event.chatId, chatMap)
|
|
1949
|
+
const existing = chatMap.get(event.runId)
|
|
1950
|
+
switch (event.type) {
|
|
1951
|
+
case "subagent_run_started":
|
|
1952
|
+
chatMap.set(event.runId, {
|
|
1953
|
+
runId: event.runId,
|
|
1954
|
+
chatId: event.chatId,
|
|
1955
|
+
subagentId: event.subagentId,
|
|
1956
|
+
subagentName: event.subagentName,
|
|
1957
|
+
provider: event.provider,
|
|
1958
|
+
model: event.model,
|
|
1959
|
+
status: "running",
|
|
1960
|
+
parentUserMessageId: event.parentUserMessageId,
|
|
1961
|
+
parentRunId: event.parentRunId,
|
|
1962
|
+
depth: event.depth,
|
|
1963
|
+
startedAt: event.timestamp,
|
|
1964
|
+
finishedAt: null,
|
|
1965
|
+
finalText: null,
|
|
1966
|
+
error: null,
|
|
1967
|
+
usage: null,
|
|
1968
|
+
entries: [],
|
|
1969
|
+
pendingTool: null,
|
|
1970
|
+
})
|
|
1971
|
+
return
|
|
1972
|
+
case "subagent_message_delta":
|
|
1973
|
+
if (existing) existing.finalText = (existing.finalText ?? "") + event.content
|
|
1974
|
+
return
|
|
1975
|
+
case "subagent_entry_appended":
|
|
1976
|
+
if (existing) existing.entries.push(event.entry)
|
|
1977
|
+
return
|
|
1978
|
+
case "subagent_run_completed":
|
|
1979
|
+
if (existing) {
|
|
1980
|
+
existing.status = "completed"
|
|
1981
|
+
existing.finishedAt = event.timestamp
|
|
1982
|
+
existing.finalText = event.finalContent
|
|
1983
|
+
existing.usage = event.usage ?? null
|
|
1984
|
+
}
|
|
1985
|
+
return
|
|
1986
|
+
case "subagent_run_failed":
|
|
1987
|
+
if (existing) {
|
|
1988
|
+
existing.status = "failed"
|
|
1989
|
+
existing.finishedAt = event.timestamp
|
|
1990
|
+
existing.error = event.error
|
|
1991
|
+
}
|
|
1992
|
+
return
|
|
1993
|
+
case "subagent_run_cancelled":
|
|
1994
|
+
if (existing) {
|
|
1995
|
+
existing.status = "cancelled"
|
|
1996
|
+
existing.finishedAt = event.timestamp
|
|
1997
|
+
}
|
|
1998
|
+
return
|
|
1999
|
+
case "subagent_tool_pending":
|
|
2000
|
+
if (existing) existing.pendingTool = event.pendingTool
|
|
2001
|
+
return
|
|
2002
|
+
case "subagent_tool_resolved":
|
|
2003
|
+
if (existing) existing.pendingTool = null
|
|
2004
|
+
return
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
async appendSubagentEvent(event: PtySubagentRunEvent): Promise<void> {
|
|
2009
|
+
this.applyPtySubagentEvent(event)
|
|
2010
|
+
const payload = `${JSON.stringify(event)}\n`
|
|
2011
|
+
this.writeChain = this.writeChain.then(() => appendFile(this.ptySubagentLogPath, payload, "utf8"))
|
|
2012
|
+
await this.writeChain
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
getSubagentRuns(chatId: string): Record<string, PtySubagentRunSnapshot> {
|
|
2016
|
+
const map = this._ptySubagentRuns.get(chatId)
|
|
2017
|
+
if (!map) return {}
|
|
2018
|
+
return Object.fromEntries(map.entries())
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
*runningSubagentRuns(): Iterable<PtySubagentRunSnapshot> {
|
|
2022
|
+
for (const map of this._ptySubagentRuns.values()) {
|
|
2023
|
+
for (const run of map.values()) {
|
|
2024
|
+
if (run.status === "running") yield run
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
async putToolRequest(req: PtyToolRequest): Promise<void> {
|
|
2030
|
+
this._ptyToolRequests.set(req.id, { ...req })
|
|
2031
|
+
const payload = `${JSON.stringify({ type: "put", request: req })}\n`
|
|
2032
|
+
this.writeChain = this.writeChain.then(() => appendFile(this.ptyToolRequestsLogPath, payload, "utf8"))
|
|
2033
|
+
await this.writeChain
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
getToolRequest(id: string): PtyToolRequest | null {
|
|
2037
|
+
const req = this._ptyToolRequests.get(id)
|
|
2038
|
+
return req ? { ...req } : null
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
listPendingToolRequests(chatId: string): PtyToolRequest[] {
|
|
2042
|
+
const out: PtyToolRequest[] = []
|
|
2043
|
+
for (const req of this._ptyToolRequests.values()) {
|
|
2044
|
+
if (req.chatId !== chatId) continue
|
|
2045
|
+
if (req.status !== "pending") continue
|
|
2046
|
+
out.push({ ...req })
|
|
2047
|
+
}
|
|
2048
|
+
return out
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
async resolveToolRequest(
|
|
2052
|
+
id: string,
|
|
2053
|
+
args: {
|
|
2054
|
+
status: import("../shared/permission-policy").ToolRequestStatus
|
|
2055
|
+
decision?: import("../shared/permission-policy").ToolRequestDecision
|
|
2056
|
+
resolvedAt: number
|
|
2057
|
+
mismatchReason?: string
|
|
2058
|
+
},
|
|
2059
|
+
): Promise<void> {
|
|
2060
|
+
const existing = this._ptyToolRequests.get(id)
|
|
2061
|
+
if (!existing) throw new Error(`resolveToolRequest: unknown id ${id}`)
|
|
2062
|
+
this._ptyToolRequests.set(id, {
|
|
2063
|
+
...existing,
|
|
2064
|
+
status: args.status,
|
|
2065
|
+
decision: args.decision ?? existing.decision,
|
|
2066
|
+
resolvedAt: args.resolvedAt,
|
|
2067
|
+
mismatchReason: args.mismatchReason,
|
|
2068
|
+
})
|
|
2069
|
+
const payload = `${JSON.stringify({ type: "resolved", id, status: args.status, decision: args.decision, resolvedAt: args.resolvedAt, mismatchReason: args.mismatchReason })}\n`
|
|
2070
|
+
this.writeChain = this.writeChain.then(() => appendFile(this.ptyToolRequestsLogPath, payload, "utf8"))
|
|
2071
|
+
await this.writeChain
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
scanAllToolRequests(): PtyToolRequest[] {
|
|
2075
|
+
return [...this._ptyToolRequests.values()].map((req) => ({ ...req }))
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Generic per-provider session-token setter for claude-pty (and future
|
|
2080
|
+
* non-AgentProvider-union backends). `providerId` is a string so PTY can
|
|
2081
|
+
* use "claude-pty" without forcing the canonical AgentProvider union to
|
|
2082
|
+
* widen.
|
|
2083
|
+
*/
|
|
2084
|
+
async setPtySessionToken(chatId: string, providerId: string, sessionToken: string | null): Promise<void> {
|
|
2085
|
+
let map = this._ptySessionTokensByChatId.get(chatId)
|
|
2086
|
+
if (!map) {
|
|
2087
|
+
map = new Map<string, string>()
|
|
2088
|
+
this._ptySessionTokensByChatId.set(chatId, map)
|
|
2089
|
+
}
|
|
2090
|
+
if (sessionToken === null) {
|
|
2091
|
+
map.delete(providerId)
|
|
2092
|
+
} else {
|
|
2093
|
+
map.set(providerId, sessionToken)
|
|
2094
|
+
}
|
|
2095
|
+
const payload = `${JSON.stringify({ chatId, providerId, sessionToken, timestamp: Date.now() })}\n`
|
|
2096
|
+
this.writeChain = this.writeChain.then(() => appendFile(this.ptySessionTokensLogPath, payload, "utf8"))
|
|
2097
|
+
await this.writeChain
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
getPtySessionToken(chatId: string, providerId: string): string | null {
|
|
2101
|
+
return this._ptySessionTokensByChatId.get(chatId)?.get(providerId) ?? null
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
getPtySessionTokensForChat(chatId: string): Record<string, string> {
|
|
2105
|
+
const map = this._ptySessionTokensByChatId.get(chatId)
|
|
2106
|
+
if (!map) return {}
|
|
2107
|
+
return Object.fromEntries(map.entries())
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// ===== claude-pty shared shapes =====
|
|
2112
|
+
|
|
2113
|
+
export interface PtySubagentRunSnapshot {
|
|
2114
|
+
runId: string
|
|
2115
|
+
chatId: string
|
|
2116
|
+
subagentId: string | null
|
|
2117
|
+
subagentName: string
|
|
2118
|
+
provider: import("../shared/types").AgentProvider
|
|
2119
|
+
model: string
|
|
2120
|
+
status: "running" | "completed" | "failed" | "cancelled"
|
|
2121
|
+
parentUserMessageId: string
|
|
2122
|
+
parentRunId: string | null
|
|
2123
|
+
depth: number
|
|
2124
|
+
startedAt: number
|
|
2125
|
+
finishedAt: number | null
|
|
2126
|
+
finalText: string | null
|
|
2127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2128
|
+
error: { code: any; message: string } | null
|
|
2129
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2130
|
+
usage: any | null
|
|
2131
|
+
entries: import("../shared/types").TranscriptEntry[]
|
|
2132
|
+
pendingTool: import("../shared/types").PendingToolSnapshot | null
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
export type PtySubagentRunEvent =
|
|
2136
|
+
| {
|
|
2137
|
+
v: 3
|
|
2138
|
+
type: "subagent_run_started"
|
|
2139
|
+
timestamp: number
|
|
2140
|
+
chatId: string
|
|
2141
|
+
runId: string
|
|
2142
|
+
subagentId: string | null
|
|
2143
|
+
subagentName: string
|
|
2144
|
+
provider: import("../shared/types").AgentProvider
|
|
2145
|
+
model: string
|
|
2146
|
+
parentUserMessageId: string
|
|
2147
|
+
parentRunId: string | null
|
|
2148
|
+
depth: number
|
|
2149
|
+
}
|
|
2150
|
+
| {
|
|
2151
|
+
v: 3
|
|
2152
|
+
type: "subagent_message_delta"
|
|
2153
|
+
timestamp: number
|
|
2154
|
+
chatId: string
|
|
2155
|
+
runId: string
|
|
2156
|
+
content: string
|
|
2157
|
+
}
|
|
2158
|
+
| {
|
|
2159
|
+
v: 3
|
|
2160
|
+
type: "subagent_run_completed"
|
|
2161
|
+
timestamp: number
|
|
2162
|
+
chatId: string
|
|
2163
|
+
runId: string
|
|
2164
|
+
finalContent: string
|
|
2165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2166
|
+
usage?: any
|
|
2167
|
+
}
|
|
2168
|
+
| {
|
|
2169
|
+
v: 3
|
|
2170
|
+
type: "subagent_run_failed"
|
|
2171
|
+
timestamp: number
|
|
2172
|
+
chatId: string
|
|
2173
|
+
runId: string
|
|
2174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2175
|
+
error: { code: any; message: string }
|
|
2176
|
+
}
|
|
2177
|
+
| {
|
|
2178
|
+
v: 3
|
|
2179
|
+
type: "subagent_run_cancelled"
|
|
2180
|
+
timestamp: number
|
|
2181
|
+
chatId: string
|
|
2182
|
+
runId: string
|
|
2183
|
+
}
|
|
2184
|
+
| {
|
|
2185
|
+
v: 3
|
|
2186
|
+
type: "subagent_entry_appended"
|
|
2187
|
+
timestamp: number
|
|
2188
|
+
chatId: string
|
|
2189
|
+
runId: string
|
|
2190
|
+
entry: import("../shared/types").TranscriptEntry
|
|
2191
|
+
}
|
|
2192
|
+
| {
|
|
2193
|
+
v: 3
|
|
2194
|
+
type: "subagent_tool_pending"
|
|
2195
|
+
timestamp: number
|
|
2196
|
+
chatId: string
|
|
2197
|
+
runId: string
|
|
2198
|
+
pendingTool: import("../shared/types").PendingToolSnapshot
|
|
2199
|
+
}
|
|
2200
|
+
| {
|
|
2201
|
+
v: 3
|
|
2202
|
+
type: "subagent_tool_resolved"
|
|
2203
|
+
timestamp: number
|
|
2204
|
+
chatId: string
|
|
2205
|
+
runId: string
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
export type PtyToolRequest = import("../shared/permission-policy").ToolRequest
|