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,887 @@
|
|
|
1
|
+
import crypto from "node:crypto"
|
|
2
|
+
import { LOG_PREFIX } from "../../shared/branding"
|
|
3
|
+
import type {
|
|
4
|
+
AgentProvider,
|
|
5
|
+
Subagent,
|
|
6
|
+
TranscriptEntry,
|
|
7
|
+
} from "../../shared/types"
|
|
8
|
+
import type { EventStore } from "../event-store"
|
|
9
|
+
// PORT-TODO: kanna's ProviderUsage / SubagentErrorCode not yet ported as concrete shapes.
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type ProviderUsage = any
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
type SubagentErrorCode = any
|
|
14
|
+
import { buildHistoryPrimer, extractPreviousAssistantReply } from "../claude-pty-mcp/history-primer"
|
|
15
|
+
import { parseMentions, type ParsedMention } from "../claude-pty-mcp/mention-parser"
|
|
16
|
+
|
|
17
|
+
class PausableTimeout {
|
|
18
|
+
private remainingMs: number
|
|
19
|
+
private deadline: number | null = null
|
|
20
|
+
private handle: ReturnType<typeof setTimeout> | null = null
|
|
21
|
+
private onFire: () => void
|
|
22
|
+
|
|
23
|
+
constructor(totalMs: number, onFire: () => void) {
|
|
24
|
+
this.remainingMs = totalMs
|
|
25
|
+
this.onFire = onFire
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(now: number = Date.now()): void {
|
|
29
|
+
this.deadline = now + this.remainingMs
|
|
30
|
+
this.handle = setTimeout(this.onFire, this.remainingMs)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pause(now: number = Date.now()): void {
|
|
34
|
+
if (this.handle == null || this.deadline == null) return
|
|
35
|
+
clearTimeout(this.handle)
|
|
36
|
+
this.handle = null
|
|
37
|
+
this.remainingMs = Math.max(0, this.deadline - now)
|
|
38
|
+
this.deadline = null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resume(now: number = Date.now()): void {
|
|
42
|
+
if (this.handle != null) return
|
|
43
|
+
this.start(now)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clear(): void {
|
|
47
|
+
if (this.handle != null) clearTimeout(this.handle)
|
|
48
|
+
this.handle = null
|
|
49
|
+
this.deadline = null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Deferred<T> {
|
|
54
|
+
promise: Promise<T>
|
|
55
|
+
resolve: (value: T) => void
|
|
56
|
+
reject: (err: Error) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createDeferred<T>(): Deferred<T> {
|
|
60
|
+
let resolve!: (value: T) => void
|
|
61
|
+
let reject!: (err: Error) => void
|
|
62
|
+
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
|
|
63
|
+
return { promise, resolve, reject }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ProviderRunStart {
|
|
67
|
+
provider: AgentProvider
|
|
68
|
+
model: string
|
|
69
|
+
systemPrompt: string
|
|
70
|
+
preamble: string | null
|
|
71
|
+
/**
|
|
72
|
+
* Run the subagent against its provider.
|
|
73
|
+
* - `onChunk(text)`: every assistant_text fragment, in order. Used to
|
|
74
|
+
* persist `subagent_message_delta` events for streaming UI.
|
|
75
|
+
* - `onEntry(entry)`: every TranscriptEntry — including the assistant_text
|
|
76
|
+
* entries forwarded to onChunk, plus tool_call / tool_result / result.
|
|
77
|
+
* Used to persist `subagent_entry_appended` events.
|
|
78
|
+
* Returns the final accumulated text + usage for the run_completed event.
|
|
79
|
+
*/
|
|
80
|
+
start: (
|
|
81
|
+
onChunk: (chunk: string) => void,
|
|
82
|
+
onEntry: (entry: TranscriptEntry) => void,
|
|
83
|
+
) => Promise<{ text: string; usage?: ProviderUsage }>
|
|
84
|
+
authReady: () => Promise<boolean>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface OrchestratorAppSettings {
|
|
88
|
+
getSnapshot(): { subagents: Subagent[] }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SubagentOrchestratorDeps {
|
|
92
|
+
store: EventStore
|
|
93
|
+
appSettings: OrchestratorAppSettings
|
|
94
|
+
startProviderRun: (args: {
|
|
95
|
+
subagent: Subagent
|
|
96
|
+
chatId: string
|
|
97
|
+
primer: string | null
|
|
98
|
+
/**
|
|
99
|
+
* Instruction text shown to the subagent above the primer — user's own
|
|
100
|
+
* message for direct mentions, parent agent's reply for chained mentions,
|
|
101
|
+
* or null when unavailable. Used by composeInitialPrompt to ensure the
|
|
102
|
+
* subagent sees the request, not only the prior context.
|
|
103
|
+
*/
|
|
104
|
+
userInstruction: string | null
|
|
105
|
+
runId: string
|
|
106
|
+
abortSignal: AbortSignal
|
|
107
|
+
/** Depth of THIS run in the chain (top-level user delegation = 0). */
|
|
108
|
+
depth: number
|
|
109
|
+
/** Ancestor chain of subagent ids leading to this run, oldest first. */
|
|
110
|
+
ancestorSubagentIds: string[]
|
|
111
|
+
/** User message id the originating chat turn is responding to. */
|
|
112
|
+
parentUserMessageId: string
|
|
113
|
+
}) => ProviderRunStart
|
|
114
|
+
/**
|
|
115
|
+
* Called when a subagent run enters a terminal state (failed / completed /
|
|
116
|
+
* interrupted) so external resources keyed on (chatId, runId) — e.g. the
|
|
117
|
+
* `subagentPendingResolvers` map on AgentCoordinator — can be released.
|
|
118
|
+
* The SDK's `canUseTool` Promise must be rejected when the run dies, or it
|
|
119
|
+
* hangs forever and leaks. Optional for tests.
|
|
120
|
+
*/
|
|
121
|
+
onRunTerminal?: (chatId: string, runId: string, reason: "failed" | "completed") => void
|
|
122
|
+
/**
|
|
123
|
+
* Called on every non-terminal subagent state change — run start and each
|
|
124
|
+
* persisted transcript entry. Wired by AgentCoordinator to
|
|
125
|
+
* `emitStateChange(chatId)` so the ws-router pushes a fresh chat snapshot
|
|
126
|
+
* (carrying `subagentRuns`) to connected clients WHILE the run is in
|
|
127
|
+
* flight. Without this the client only ever sees the run at terminal
|
|
128
|
+
* (the sole `onRunTerminal` broadcast), so a delegated run renders as
|
|
129
|
+
* blank/absent until it finishes. ws-router coalesces these at 16ms and
|
|
130
|
+
* signature-dedups, so high-frequency entry fan-out is cheap. Optional
|
|
131
|
+
* for tests.
|
|
132
|
+
*/
|
|
133
|
+
onRunProgress?: (chatId: string, runId: string) => void
|
|
134
|
+
now?: () => number
|
|
135
|
+
maxParallel?: number
|
|
136
|
+
maxChainDepth?: number
|
|
137
|
+
runTimeoutMs?: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const DEFAULT_MAX_PARALLEL = 4
|
|
141
|
+
const DEFAULT_MAX_CHAIN_DEPTH = 1
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Terminal outcome of a single subagent run, surfaced to callers that
|
|
145
|
+
* need the final reply text — e.g. `mcp__kanna__delegate_subagent` so
|
|
146
|
+
* the main agent can synthesize the subagent's answer into its own reply.
|
|
147
|
+
*/
|
|
148
|
+
export type DelegationOutcome =
|
|
149
|
+
| { status: "completed"; runId: string; text: string }
|
|
150
|
+
| { status: "failed"; runId: string; errorCode: SubagentErrorCode; errorMessage: string }
|
|
151
|
+
// Subagents now run with full toolset (Bash, Read, etc) so single turns may
|
|
152
|
+
// take minutes. 600s matches the default Bash tool wall-clock cap. Tests still
|
|
153
|
+
// override via SubagentOrchestratorDeps.runTimeoutMs.
|
|
154
|
+
const DEFAULT_RUN_TIMEOUT_MS = 600_000
|
|
155
|
+
|
|
156
|
+
interface RunState {
|
|
157
|
+
chatId: string
|
|
158
|
+
parentRunId: string | null
|
|
159
|
+
childRunIds: Set<string>
|
|
160
|
+
abortController: AbortController
|
|
161
|
+
timeout: PausableTimeout | null
|
|
162
|
+
cancelled: boolean
|
|
163
|
+
pendingAcquire: boolean
|
|
164
|
+
permitWaiter: { resolve: () => void; reject: (e: Error) => void } | null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class SubagentOrchestrator {
|
|
168
|
+
private permits: number
|
|
169
|
+
private readonly waiters: Array<{ chatId: string; resolve: () => void; reject: (err: Error) => void }> = []
|
|
170
|
+
private readonly cancelledChats = new Set<string>()
|
|
171
|
+
private readonly runStateByRunId = new Map<string, RunState>()
|
|
172
|
+
|
|
173
|
+
private readonly recoveryPromise: Promise<void>
|
|
174
|
+
|
|
175
|
+
constructor(private readonly deps: SubagentOrchestratorDeps) {
|
|
176
|
+
this.permits = this.maxParallel()
|
|
177
|
+
this.recoveryPromise = this.recoverInterruptedRuns()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Caller must `await` this before spawning new runs to ensure orphan
|
|
182
|
+
* `running` runs from a previous server lifetime have been failed first.
|
|
183
|
+
*/
|
|
184
|
+
whenRecovered(): Promise<void> {
|
|
185
|
+
return this.recoveryPromise
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async recoverInterruptedRuns(): Promise<void> {
|
|
189
|
+
// Recover ALL `running` runs from the previous server lifetime, not just
|
|
190
|
+
// those mid-tool. A subagent crashed mid-bash (or mid-streaming) leaves
|
|
191
|
+
// its run in `running` forever otherwise, blocking the UI and leaking a
|
|
192
|
+
// permit until the server is restarted again with a fix.
|
|
193
|
+
for (const run of this.deps.store.runningSubagentRuns()) {
|
|
194
|
+
try {
|
|
195
|
+
await this.deps.store.appendSubagentEvent({
|
|
196
|
+
v: 3,
|
|
197
|
+
type: "subagent_run_failed",
|
|
198
|
+
timestamp: this.now(),
|
|
199
|
+
chatId: run.chatId,
|
|
200
|
+
runId: run.runId,
|
|
201
|
+
error: {
|
|
202
|
+
code: "INTERRUPTED",
|
|
203
|
+
message: run.pendingTool
|
|
204
|
+
? "Server restart while subagent awaited tool response"
|
|
205
|
+
: "Server restart while subagent run was in progress",
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.warn(`${LOG_PREFIX} interrupted-run recovery failed`, {
|
|
210
|
+
chatId: run.chatId, runId: run.runId, err,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private maxParallel() { return this.deps.maxParallel ?? DEFAULT_MAX_PARALLEL }
|
|
217
|
+
private maxDepth() { return this.deps.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH }
|
|
218
|
+
private timeoutMs() { return this.deps.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS }
|
|
219
|
+
private now() { return this.deps.now?.() ?? Date.now() }
|
|
220
|
+
|
|
221
|
+
activePermitCount() {
|
|
222
|
+
return this.maxParallel() - this.permits
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
notifySubagentToolPending(runId: string): void {
|
|
226
|
+
this.runStateByRunId.get(runId)?.timeout?.pause()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
notifySubagentToolResolved(runId: string): void {
|
|
230
|
+
this.runStateByRunId.get(runId)?.timeout?.resume()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async acquire(chatId: string, runId: string): Promise<void> {
|
|
234
|
+
if (this.cancelledChats.has(chatId)) {
|
|
235
|
+
throw new Error("CHAT_CANCELLED")
|
|
236
|
+
}
|
|
237
|
+
if (this.permits > 0) {
|
|
238
|
+
this.permits -= 1
|
|
239
|
+
const state = this.runStateByRunId.get(runId)
|
|
240
|
+
if (state) state.pendingAcquire = false
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>()
|
|
244
|
+
const state = this.runStateByRunId.get(runId)
|
|
245
|
+
if (state) {
|
|
246
|
+
state.permitWaiter = { resolve, reject }
|
|
247
|
+
}
|
|
248
|
+
this.waiters.push({ chatId, resolve, reject })
|
|
249
|
+
try {
|
|
250
|
+
// `release()` hands a permit to the next waiter by resolving its
|
|
251
|
+
// promise without incrementing this.permits — the permit transfers
|
|
252
|
+
// in-place. Decrementing here would double-charge the handoff and
|
|
253
|
+
// permanently leak one parallel slot per waiter (B1).
|
|
254
|
+
await promise
|
|
255
|
+
} finally {
|
|
256
|
+
if (state) {
|
|
257
|
+
state.permitWaiter = null
|
|
258
|
+
state.pendingAcquire = false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private release(): void {
|
|
264
|
+
const next = this.waiters.shift()
|
|
265
|
+
if (next) {
|
|
266
|
+
next.resolve()
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
this.permits += 1
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Clear the sticky cancel marker for a chat. Call this at the start of
|
|
274
|
+
* each new user turn so a previous cancelChat() does not poison every
|
|
275
|
+
* subsequent `delegateRun` with "Chat cancelled before run started"
|
|
276
|
+
* forever (B3 + delegate_subagent path).
|
|
277
|
+
*/
|
|
278
|
+
clearChatCancellation(chatId: string): void {
|
|
279
|
+
this.cancelledChats.delete(chatId)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
cancelChat(chatId: string): void {
|
|
283
|
+
this.cancelledChats.add(chatId)
|
|
284
|
+
for (let i = this.waiters.length - 1; i >= 0; i -= 1) {
|
|
285
|
+
const w = this.waiters[i]
|
|
286
|
+
if (w.chatId !== chatId) continue
|
|
287
|
+
this.waiters.splice(i, 1)
|
|
288
|
+
w.reject(new Error("CHAT_CANCELLED"))
|
|
289
|
+
}
|
|
290
|
+
// Cancel every acquired/queued run in this chat. cancelRun is idempotent
|
|
291
|
+
// (re-cancellation no-op) and handles both queued and running states.
|
|
292
|
+
// Snapshot runIds first because cancelRun may mutate the map.
|
|
293
|
+
const runIds: string[] = []
|
|
294
|
+
for (const [runId, state] of this.runStateByRunId) {
|
|
295
|
+
if (state.chatId === chatId) runIds.push(runId)
|
|
296
|
+
}
|
|
297
|
+
for (const runId of runIds) this.cancelRun(chatId, runId)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
cancelRun(chatId: string, runId: string): void {
|
|
301
|
+
const state = this.runStateByRunId.get(runId)
|
|
302
|
+
if (!state) return
|
|
303
|
+
if (state.cancelled) return
|
|
304
|
+
if (state.chatId !== chatId) return
|
|
305
|
+
state.cancelled = true
|
|
306
|
+
// Cascade to running descendants. With current DEFAULT_MAX_CHAIN_DEPTH=1
|
|
307
|
+
// this is a no-op in practice (children spawn only after parent
|
|
308
|
+
// completes) but guards forward-compat with higher chain depths.
|
|
309
|
+
for (const childRunId of [...state.childRunIds]) {
|
|
310
|
+
this.cancelRun(chatId, childRunId)
|
|
311
|
+
}
|
|
312
|
+
if (state.pendingAcquire && state.permitWaiter) {
|
|
313
|
+
// Queued: splice waiter out of this.waiters FIRST so release() cannot
|
|
314
|
+
// grant us a permit we will never use, then reject the Promise.
|
|
315
|
+
const idx = this.waiters.findIndex((w) => w.resolve === state.permitWaiter!.resolve)
|
|
316
|
+
if (idx >= 0) this.waiters.splice(idx, 1)
|
|
317
|
+
const reject = state.permitWaiter.reject
|
|
318
|
+
state.permitWaiter = null
|
|
319
|
+
reject(new Error("USER_CANCELLED"))
|
|
320
|
+
} else {
|
|
321
|
+
state.abortController.abort()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async runMentionsForUserMessage(args: {
|
|
326
|
+
chatId: string
|
|
327
|
+
userMessageId: string
|
|
328
|
+
mentions: ParsedMention[]
|
|
329
|
+
/**
|
|
330
|
+
* The text accompanying the @agent mention. For user-triggered runs this
|
|
331
|
+
* is the user's typed message. For main-Claude-triggered runs this is the
|
|
332
|
+
* assistant's reply text. Passed through to the subagent's initial prompt
|
|
333
|
+
* so the run sees the request, not just the prior context primer. Default
|
|
334
|
+
* "" preserves prior call-site semantics (primer-only) in tests that
|
|
335
|
+
* haven't been migrated.
|
|
336
|
+
*/
|
|
337
|
+
userContent?: string
|
|
338
|
+
}): Promise<void> {
|
|
339
|
+
const userContent = args.userContent ?? ""
|
|
340
|
+
// A new mention batch from this chat means the user is asking for fresh
|
|
341
|
+
// work — clear any "cancelled" marker left over from a prior cancelChat
|
|
342
|
+
// call (B3). Without this, every subagent in a chat that has ever been
|
|
343
|
+
// cancelled would fail before start until process restart.
|
|
344
|
+
this.cancelledChats.delete(args.chatId)
|
|
345
|
+
await this.recoveryPromise
|
|
346
|
+
const subagents = this.deps.appSettings.getSnapshot().subagents
|
|
347
|
+
const resolved: { mention: Extract<ParsedMention, { kind: "subagent" }>; subagent: Subagent }[] = []
|
|
348
|
+
|
|
349
|
+
for (const mention of args.mentions) {
|
|
350
|
+
if (mention.kind === "unknown-subagent") {
|
|
351
|
+
const runId = crypto.randomUUID()
|
|
352
|
+
await this.deps.store.appendSubagentEvent({
|
|
353
|
+
v: 3,
|
|
354
|
+
type: "subagent_run_started",
|
|
355
|
+
timestamp: this.now(),
|
|
356
|
+
chatId: args.chatId,
|
|
357
|
+
runId,
|
|
358
|
+
subagentId: null,
|
|
359
|
+
subagentName: mention.name,
|
|
360
|
+
provider: "claude",
|
|
361
|
+
model: "",
|
|
362
|
+
parentUserMessageId: args.userMessageId,
|
|
363
|
+
parentRunId: null,
|
|
364
|
+
depth: 0,
|
|
365
|
+
})
|
|
366
|
+
await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Unknown subagent '${mention.name}'`)
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const subagent = subagents.find((s) => s.id === mention.subagentId)
|
|
370
|
+
if (!subagent) {
|
|
371
|
+
const runId = crypto.randomUUID()
|
|
372
|
+
await this.deps.store.appendSubagentEvent({
|
|
373
|
+
v: 3,
|
|
374
|
+
type: "subagent_run_started",
|
|
375
|
+
timestamp: this.now(),
|
|
376
|
+
chatId: args.chatId,
|
|
377
|
+
runId,
|
|
378
|
+
subagentId: mention.subagentId,
|
|
379
|
+
subagentName: mention.subagentId,
|
|
380
|
+
provider: "claude",
|
|
381
|
+
model: "",
|
|
382
|
+
parentUserMessageId: args.userMessageId,
|
|
383
|
+
parentRunId: null,
|
|
384
|
+
depth: 0,
|
|
385
|
+
})
|
|
386
|
+
await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Subagent ${mention.subagentId} was deleted`)
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
resolved.push({ mention, subagent })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await Promise.all(resolved.map(({ subagent }) =>
|
|
393
|
+
this.spawnRun({
|
|
394
|
+
subagent,
|
|
395
|
+
chatId: args.chatId,
|
|
396
|
+
parentUserMessageId: args.userMessageId,
|
|
397
|
+
parentRunId: null,
|
|
398
|
+
depth: 0,
|
|
399
|
+
ancestorSubagentIds: [],
|
|
400
|
+
userInstruction: userContent,
|
|
401
|
+
})
|
|
402
|
+
))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Public entry point for `mcp__kanna__delegate_subagent`. The main agent
|
|
407
|
+
* (or a parent subagent, when sub-spawning-sub is enabled) calls this with
|
|
408
|
+
* a subagent id and a prompt; the orchestrator runs the subagent and the
|
|
409
|
+
* caller awaits the terminal {@link DelegationOutcome}.
|
|
410
|
+
*
|
|
411
|
+
* Cycle / depth guards mirror the chained-mention path in `spawnRun`: a
|
|
412
|
+
* parent cannot delegate to a subagent already in its ancestor chain, and
|
|
413
|
+
* `depth > maxChainDepth` fails fast with `DEPTH_EXCEEDED`.
|
|
414
|
+
*/
|
|
415
|
+
async delegateRun(args: {
|
|
416
|
+
chatId: string
|
|
417
|
+
parentUserMessageId: string
|
|
418
|
+
parentRunId: string | null
|
|
419
|
+
parentSubagentId: string | null
|
|
420
|
+
ancestorSubagentIds: string[]
|
|
421
|
+
depth: number
|
|
422
|
+
subagentId: string
|
|
423
|
+
prompt: string
|
|
424
|
+
/**
|
|
425
|
+
* Optional per-entry sink. Called once for each persisted
|
|
426
|
+
* `subagent_entry_appended` event while the run is in flight. Used by
|
|
427
|
+
* the MCP `delegate_subagent` tool to emit `notifications/progress`
|
|
428
|
+
* so the CLI's transport-error watchdog cannot declare the call lost
|
|
429
|
+
* on long-running subagent runs.
|
|
430
|
+
*/
|
|
431
|
+
onEntry?: (entry: TranscriptEntry) => void
|
|
432
|
+
}): Promise<DelegationOutcome> {
|
|
433
|
+
await this.recoveryPromise
|
|
434
|
+
const subagent = this.deps.appSettings
|
|
435
|
+
.getSnapshot()
|
|
436
|
+
.subagents.find((s) => s.id === args.subagentId)
|
|
437
|
+
if (!subagent) {
|
|
438
|
+
const runId = crypto.randomUUID()
|
|
439
|
+
await this.deps.store.appendSubagentEvent({
|
|
440
|
+
v: 3,
|
|
441
|
+
type: "subagent_run_started",
|
|
442
|
+
timestamp: this.now(),
|
|
443
|
+
chatId: args.chatId,
|
|
444
|
+
runId,
|
|
445
|
+
subagentId: args.subagentId,
|
|
446
|
+
subagentName: args.subagentId,
|
|
447
|
+
provider: "claude",
|
|
448
|
+
model: "",
|
|
449
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
450
|
+
parentRunId: args.parentRunId,
|
|
451
|
+
depth: args.depth,
|
|
452
|
+
})
|
|
453
|
+
return await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Subagent ${args.subagentId} not found`)
|
|
454
|
+
}
|
|
455
|
+
if (args.depth > this.maxDepth()) {
|
|
456
|
+
const runId = crypto.randomUUID()
|
|
457
|
+
await this.deps.store.appendSubagentEvent({
|
|
458
|
+
v: 3,
|
|
459
|
+
type: "subagent_run_started",
|
|
460
|
+
timestamp: this.now(),
|
|
461
|
+
chatId: args.chatId,
|
|
462
|
+
runId,
|
|
463
|
+
subagentId: subagent.id,
|
|
464
|
+
subagentName: subagent.name,
|
|
465
|
+
provider: subagent.provider,
|
|
466
|
+
model: subagent.model,
|
|
467
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
468
|
+
parentRunId: args.parentRunId,
|
|
469
|
+
depth: args.depth,
|
|
470
|
+
})
|
|
471
|
+
return await this.failRun(
|
|
472
|
+
args.chatId,
|
|
473
|
+
runId,
|
|
474
|
+
"DEPTH_EXCEEDED",
|
|
475
|
+
`Chain depth ${args.depth} exceeds limit ${this.maxDepth()}`,
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (args.ancestorSubagentIds.includes(subagent.id)) {
|
|
479
|
+
const runId = crypto.randomUUID()
|
|
480
|
+
await this.deps.store.appendSubagentEvent({
|
|
481
|
+
v: 3,
|
|
482
|
+
type: "subagent_run_started",
|
|
483
|
+
timestamp: this.now(),
|
|
484
|
+
chatId: args.chatId,
|
|
485
|
+
runId,
|
|
486
|
+
subagentId: subagent.id,
|
|
487
|
+
subagentName: subagent.name,
|
|
488
|
+
provider: subagent.provider,
|
|
489
|
+
model: subagent.model,
|
|
490
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
491
|
+
parentRunId: args.parentRunId,
|
|
492
|
+
depth: args.depth,
|
|
493
|
+
})
|
|
494
|
+
return await this.failRun(
|
|
495
|
+
args.chatId,
|
|
496
|
+
runId,
|
|
497
|
+
"LOOP_DETECTED",
|
|
498
|
+
`Subagent ${subagent.name} already in ancestor chain`,
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
const outcome = await this.spawnRun({
|
|
502
|
+
subagent,
|
|
503
|
+
chatId: args.chatId,
|
|
504
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
505
|
+
parentRunId: args.parentRunId,
|
|
506
|
+
depth: args.depth,
|
|
507
|
+
ancestorSubagentIds: args.ancestorSubagentIds,
|
|
508
|
+
userInstruction: args.prompt,
|
|
509
|
+
onEntry: args.onEntry,
|
|
510
|
+
})
|
|
511
|
+
// Trace point: this is the return that flows back through the MCP
|
|
512
|
+
// `delegate_subagent` tool to the parent claude as its tool_result.
|
|
513
|
+
// If the parent appears to hang after this point, the bug is on the
|
|
514
|
+
// MCP shim or the parent PTY side, not in the orchestrator.
|
|
515
|
+
console.log("[kanna/subagent] delegateRun outcome", {
|
|
516
|
+
chatId: args.chatId,
|
|
517
|
+
subagentId: args.subagentId,
|
|
518
|
+
parentRunId: args.parentRunId,
|
|
519
|
+
depth: args.depth,
|
|
520
|
+
status: outcome.status,
|
|
521
|
+
errorCode: outcome.status === "failed" ? outcome.errorCode : undefined,
|
|
522
|
+
textChars: outcome.status === "completed" ? outcome.text.length : undefined,
|
|
523
|
+
})
|
|
524
|
+
return outcome
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private async spawnRun(args: {
|
|
528
|
+
subagent: Subagent
|
|
529
|
+
chatId: string
|
|
530
|
+
parentUserMessageId: string
|
|
531
|
+
parentRunId: string | null
|
|
532
|
+
depth: number
|
|
533
|
+
ancestorSubagentIds: string[]
|
|
534
|
+
/**
|
|
535
|
+
* Instruction the spawn was triggered by — user's typed text for top-level
|
|
536
|
+
* runs, parent agent's full reply for chained runs. Forwarded to the
|
|
537
|
+
* provider run so composeInitialPrompt can render it above the primer.
|
|
538
|
+
*/
|
|
539
|
+
userInstruction: string
|
|
540
|
+
/** External per-entry sink (see {@link delegateRun}). */
|
|
541
|
+
onEntry?: (entry: TranscriptEntry) => void
|
|
542
|
+
}): Promise<DelegationOutcome> {
|
|
543
|
+
const runId = crypto.randomUUID()
|
|
544
|
+
await this.deps.store.appendSubagentEvent({
|
|
545
|
+
v: 3,
|
|
546
|
+
type: "subagent_run_started",
|
|
547
|
+
timestamp: this.now(),
|
|
548
|
+
chatId: args.chatId,
|
|
549
|
+
runId,
|
|
550
|
+
subagentId: args.subagent.id,
|
|
551
|
+
subagentName: args.subagent.name,
|
|
552
|
+
provider: args.subagent.provider,
|
|
553
|
+
model: args.subagent.model,
|
|
554
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
555
|
+
parentRunId: args.parentRunId,
|
|
556
|
+
depth: args.depth,
|
|
557
|
+
})
|
|
558
|
+
this.deps.onRunProgress?.(args.chatId, runId)
|
|
559
|
+
|
|
560
|
+
// Register RunState BEFORE acquire so cancelRun can find a queued run.
|
|
561
|
+
// The reducer marks the run as `status: "running"` from this event on,
|
|
562
|
+
// which is what the UI uses to show the X button.
|
|
563
|
+
const runState: RunState = {
|
|
564
|
+
chatId: args.chatId,
|
|
565
|
+
parentRunId: args.parentRunId,
|
|
566
|
+
childRunIds: new Set(),
|
|
567
|
+
abortController: new AbortController(),
|
|
568
|
+
timeout: null,
|
|
569
|
+
cancelled: false,
|
|
570
|
+
pendingAcquire: true,
|
|
571
|
+
permitWaiter: null,
|
|
572
|
+
}
|
|
573
|
+
this.runStateByRunId.set(runId, runState)
|
|
574
|
+
if (args.parentRunId != null) {
|
|
575
|
+
this.runStateByRunId.get(args.parentRunId)?.childRunIds.add(runId)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
await this.acquire(args.chatId, runId)
|
|
580
|
+
} catch (err) {
|
|
581
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
582
|
+
const code: SubagentErrorCode = msg === "USER_CANCELLED" ? "USER_CANCELLED" : "PROVIDER_ERROR"
|
|
583
|
+
const message = msg === "USER_CANCELLED"
|
|
584
|
+
? "Cancelled before run started"
|
|
585
|
+
: "Chat cancelled before run started"
|
|
586
|
+
const outcome = await this.failRun(args.chatId, runId, code, message)
|
|
587
|
+
this.cleanupRunState(runId)
|
|
588
|
+
return outcome
|
|
589
|
+
}
|
|
590
|
+
let released = false
|
|
591
|
+
const releaseSlot = () => {
|
|
592
|
+
if (released) return
|
|
593
|
+
released = true
|
|
594
|
+
this.release()
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (this.cancelledChats.has(args.chatId)) {
|
|
598
|
+
releaseSlot()
|
|
599
|
+
const outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", "Chat cancelled before run started")
|
|
600
|
+
this.cleanupRunState(runId)
|
|
601
|
+
return outcome
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
const transcript = (await this.deps.store.getMessages(args.chatId)) as TranscriptEntry[]
|
|
606
|
+
let primer: string | null
|
|
607
|
+
if (args.subagent.contextScope === "full-transcript") {
|
|
608
|
+
primer = buildHistoryPrimer(transcript, args.subagent.provider, "")
|
|
609
|
+
} else {
|
|
610
|
+
const reply = extractPreviousAssistantReply(transcript)
|
|
611
|
+
primer = reply == null ? null : `Previous assistant reply:\n${reply}`
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let runStart: ProviderRunStart
|
|
615
|
+
try {
|
|
616
|
+
runStart = this.deps.startProviderRun({
|
|
617
|
+
subagent: args.subagent,
|
|
618
|
+
chatId: args.chatId,
|
|
619
|
+
primer,
|
|
620
|
+
userInstruction: args.userInstruction.length > 0 ? args.userInstruction : null,
|
|
621
|
+
runId,
|
|
622
|
+
abortSignal: runState.abortController.signal,
|
|
623
|
+
depth: args.depth,
|
|
624
|
+
ancestorSubagentIds: args.ancestorSubagentIds,
|
|
625
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
626
|
+
})
|
|
627
|
+
} catch (err) {
|
|
628
|
+
// Defensive: startProviderRun is a synchronous factory but a real impl
|
|
629
|
+
// (buildSubagentProviderRunForChat in agent.ts) can throw if e.g. the
|
|
630
|
+
// chat's project lookup fails. Without this guard the run would leak
|
|
631
|
+
// as `running` forever (no failed/completed event ever appended).
|
|
632
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
633
|
+
const outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", msg)
|
|
634
|
+
// releaseSlot — outer `finally` would re-release if we called raw
|
|
635
|
+
// `this.release()` here (B2). releaseSlot is idempotent via the
|
|
636
|
+
// `released` flag so the finally is a no-op.
|
|
637
|
+
releaseSlot()
|
|
638
|
+
this.cleanupRunState(runId)
|
|
639
|
+
return outcome
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!(await runStart.authReady())) {
|
|
643
|
+
const outcome = await this.failRun(args.chatId, runId, "AUTH_REQUIRED", `Authentication required for ${args.subagent.provider}`)
|
|
644
|
+
releaseSlot()
|
|
645
|
+
this.cleanupRunState(runId)
|
|
646
|
+
return outcome
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
let finalText = ""
|
|
650
|
+
let usage: ProviderUsage | undefined
|
|
651
|
+
// Trailing-edge throttle handle for chunk-driven progress broadcasts.
|
|
652
|
+
let chunkProgressTimer: ReturnType<typeof setTimeout> | null = null
|
|
653
|
+
const CHUNK_PROGRESS_THROTTLE_MS = 100
|
|
654
|
+
|
|
655
|
+
const onChunk = (chunk: string) => {
|
|
656
|
+
if (!chunk) return
|
|
657
|
+
this.deps.store
|
|
658
|
+
.appendSubagentEvent({
|
|
659
|
+
v: 3,
|
|
660
|
+
type: "subagent_message_delta",
|
|
661
|
+
timestamp: this.now(),
|
|
662
|
+
chatId: args.chatId,
|
|
663
|
+
runId,
|
|
664
|
+
content: chunk,
|
|
665
|
+
})
|
|
666
|
+
.catch((err: unknown) => {
|
|
667
|
+
console.warn(`${LOG_PREFIX} subagent delta append failed`, { chatId: args.chatId, runId, err })
|
|
668
|
+
})
|
|
669
|
+
// Trailing-edge throttle: fire progress after a quiet window so
|
|
670
|
+
// streamed assistant text becomes visible incrementally in the UI.
|
|
671
|
+
if (chunkProgressTimer !== null) clearTimeout(chunkProgressTimer)
|
|
672
|
+
chunkProgressTimer = setTimeout(() => {
|
|
673
|
+
chunkProgressTimer = null
|
|
674
|
+
this.deps.onRunProgress?.(args.chatId, runId)
|
|
675
|
+
}, CHUNK_PROGRESS_THROTTLE_MS)
|
|
676
|
+
}
|
|
677
|
+
const externalOnEntry = args.onEntry
|
|
678
|
+
const onEntry = (entry: TranscriptEntry) => {
|
|
679
|
+
this.deps.store
|
|
680
|
+
.appendSubagentEvent({
|
|
681
|
+
v: 3,
|
|
682
|
+
type: "subagent_entry_appended",
|
|
683
|
+
timestamp: this.now(),
|
|
684
|
+
chatId: args.chatId,
|
|
685
|
+
runId,
|
|
686
|
+
entry,
|
|
687
|
+
})
|
|
688
|
+
.catch((err: unknown) => {
|
|
689
|
+
console.warn(`${LOG_PREFIX} subagent entry append failed`, { chatId: args.chatId, runId, err })
|
|
690
|
+
})
|
|
691
|
+
// Fire immediately — applyEvent now runs synchronously in appendSubagentEvent
|
|
692
|
+
// so the broadcast snapshot already includes this entry.
|
|
693
|
+
this.deps.onRunProgress?.(args.chatId, runId)
|
|
694
|
+
if (externalOnEntry) {
|
|
695
|
+
try {
|
|
696
|
+
externalOnEntry(entry)
|
|
697
|
+
} catch (err) {
|
|
698
|
+
console.warn(`${LOG_PREFIX} external onEntry threw`, { chatId: args.chatId, runId, err })
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const timeoutRejection = createDeferred<never>()
|
|
703
|
+
const pausable = new PausableTimeout(this.timeoutMs(), () => {
|
|
704
|
+
// Race-rejection ORDER MATTERS. Reject TIMEOUT before aborting so
|
|
705
|
+
// `Promise.race` resolves with the TIMEOUT error and the catch
|
|
706
|
+
// branch records `failRun TIMEOUT` instead of `USER_CANCELLED`.
|
|
707
|
+
// Then abort the controller to tear down the underlying provider
|
|
708
|
+
// session — `buildSubagentProviderRun` wires
|
|
709
|
+
// `session.interrupt()` / `codexManager.stopSession()` to this
|
|
710
|
+
// signal, without which a timed-out run keeps streaming and
|
|
711
|
+
// pollutes the event log (B5).
|
|
712
|
+
timeoutRejection.reject(new Error("TIMEOUT"))
|
|
713
|
+
runState.abortController.abort()
|
|
714
|
+
})
|
|
715
|
+
runState.timeout = pausable
|
|
716
|
+
pausable.start()
|
|
717
|
+
try {
|
|
718
|
+
const abortRejection = createDeferred<never>()
|
|
719
|
+
const abortListener = () => abortRejection.reject(new Error("USER_CANCELLED"))
|
|
720
|
+
runState.abortController.signal.addEventListener("abort", abortListener, { once: true })
|
|
721
|
+
let result: { text: string; usage?: ProviderUsage }
|
|
722
|
+
try {
|
|
723
|
+
// Fast-path: if already aborted, fire listener synchronously so the
|
|
724
|
+
// race rejects on the next microtask. Doing this AFTER abortRejection.promise
|
|
725
|
+
// is passed to Promise.race ensures the rejection always has a handler.
|
|
726
|
+
if (runState.abortController.signal.aborted) {
|
|
727
|
+
abortListener()
|
|
728
|
+
}
|
|
729
|
+
result = await Promise.race([
|
|
730
|
+
runStart.start(onChunk, onEntry),
|
|
731
|
+
timeoutRejection.promise,
|
|
732
|
+
abortRejection.promise,
|
|
733
|
+
])
|
|
734
|
+
} finally {
|
|
735
|
+
runState.abortController.signal.removeEventListener("abort", abortListener)
|
|
736
|
+
}
|
|
737
|
+
finalText = result.text
|
|
738
|
+
usage = result.usage
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
741
|
+
let outcome: DelegationOutcome
|
|
742
|
+
if (message === "TIMEOUT") {
|
|
743
|
+
outcome = await this.failRun(args.chatId, runId, "TIMEOUT", `Run exceeded ${this.timeoutMs()}ms`)
|
|
744
|
+
} else if (message === "USER_CANCELLED" || runState.cancelled) {
|
|
745
|
+
outcome = await this.failRun(args.chatId, runId, "USER_CANCELLED", "Cancelled by user")
|
|
746
|
+
} else {
|
|
747
|
+
outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", message)
|
|
748
|
+
}
|
|
749
|
+
return outcome
|
|
750
|
+
} finally {
|
|
751
|
+
pausable.clear()
|
|
752
|
+
runState.timeout = null
|
|
753
|
+
if (chunkProgressTimer !== null) {
|
|
754
|
+
// Flush any pending chunk-driven progress immediately at run end
|
|
755
|
+
// so the final streamed text is always broadcast before completion.
|
|
756
|
+
clearTimeout(chunkProgressTimer)
|
|
757
|
+
chunkProgressTimer = null
|
|
758
|
+
this.deps.onRunProgress?.(args.chatId, runId)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Codex `stopSession` finishes the pending stream queue rather than
|
|
763
|
+
// rejecting — without this guard, a cancelled run can reach the
|
|
764
|
+
// success path.
|
|
765
|
+
if (runState.cancelled) {
|
|
766
|
+
return await this.failRun(args.chatId, runId, "USER_CANCELLED", "Cancelled by user")
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
await this.deps.store.appendSubagentEvent({
|
|
770
|
+
v: 3,
|
|
771
|
+
type: "subagent_run_completed",
|
|
772
|
+
timestamp: this.now(),
|
|
773
|
+
chatId: args.chatId,
|
|
774
|
+
runId,
|
|
775
|
+
finalContent: finalText,
|
|
776
|
+
usage,
|
|
777
|
+
})
|
|
778
|
+
try {
|
|
779
|
+
this.deps.onRunTerminal?.(args.chatId, runId, "completed")
|
|
780
|
+
} catch (err) {
|
|
781
|
+
console.warn(`${LOG_PREFIX} onRunTerminal(completed) threw`, { chatId: args.chatId, runId, err })
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
releaseSlot()
|
|
785
|
+
|
|
786
|
+
const chainedMentions = parseMentions(finalText, this.deps.appSettings.getSnapshot().subagents)
|
|
787
|
+
for (const mention of chainedMentions) {
|
|
788
|
+
if (mention.kind !== "subagent") continue
|
|
789
|
+
const chainSubagent = this.deps.appSettings.getSnapshot().subagents.find((s) => s.id === mention.subagentId)
|
|
790
|
+
if (!chainSubagent) continue
|
|
791
|
+
const childDepth = args.depth + 1
|
|
792
|
+
if (childDepth > this.maxDepth()) {
|
|
793
|
+
const childRunId = crypto.randomUUID()
|
|
794
|
+
await this.deps.store.appendSubagentEvent({
|
|
795
|
+
v: 3,
|
|
796
|
+
type: "subagent_run_started",
|
|
797
|
+
timestamp: this.now(),
|
|
798
|
+
chatId: args.chatId,
|
|
799
|
+
runId: childRunId,
|
|
800
|
+
subagentId: chainSubagent.id,
|
|
801
|
+
subagentName: chainSubagent.name,
|
|
802
|
+
provider: chainSubagent.provider,
|
|
803
|
+
model: chainSubagent.model,
|
|
804
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
805
|
+
parentRunId: runId,
|
|
806
|
+
depth: childDepth,
|
|
807
|
+
})
|
|
808
|
+
await this.failRun(args.chatId, childRunId, "DEPTH_EXCEEDED", `Chain depth ${childDepth} exceeds limit ${this.maxDepth()}`)
|
|
809
|
+
continue
|
|
810
|
+
}
|
|
811
|
+
if ([...args.ancestorSubagentIds, args.subagent.id].includes(chainSubagent.id)) {
|
|
812
|
+
const childRunId = crypto.randomUUID()
|
|
813
|
+
await this.deps.store.appendSubagentEvent({
|
|
814
|
+
v: 3,
|
|
815
|
+
type: "subagent_run_started",
|
|
816
|
+
timestamp: this.now(),
|
|
817
|
+
chatId: args.chatId,
|
|
818
|
+
runId: childRunId,
|
|
819
|
+
subagentId: chainSubagent.id,
|
|
820
|
+
subagentName: chainSubagent.name,
|
|
821
|
+
provider: chainSubagent.provider,
|
|
822
|
+
model: chainSubagent.model,
|
|
823
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
824
|
+
parentRunId: runId,
|
|
825
|
+
depth: childDepth,
|
|
826
|
+
})
|
|
827
|
+
await this.failRun(args.chatId, childRunId, "LOOP_DETECTED", `Subagent ${chainSubagent.name} already in ancestor chain`)
|
|
828
|
+
continue
|
|
829
|
+
}
|
|
830
|
+
await this.spawnRun({
|
|
831
|
+
subagent: chainSubagent,
|
|
832
|
+
chatId: args.chatId,
|
|
833
|
+
parentUserMessageId: args.parentUserMessageId,
|
|
834
|
+
parentRunId: runId,
|
|
835
|
+
depth: childDepth,
|
|
836
|
+
ancestorSubagentIds: [...args.ancestorSubagentIds, args.subagent.id],
|
|
837
|
+
userInstruction: finalText,
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
return { status: "completed", runId, text: finalText }
|
|
841
|
+
} finally {
|
|
842
|
+
releaseSlot()
|
|
843
|
+
this.cleanupRunState(runId)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private cleanupRunState(runId: string) {
|
|
848
|
+
const state = this.runStateByRunId.get(runId)
|
|
849
|
+
if (!state) return
|
|
850
|
+
state.timeout?.clear()
|
|
851
|
+
if (state.parentRunId != null) {
|
|
852
|
+
this.runStateByRunId.get(state.parentRunId)?.childRunIds.delete(runId)
|
|
853
|
+
}
|
|
854
|
+
this.runStateByRunId.delete(runId)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private async failRun(
|
|
858
|
+
chatId: string,
|
|
859
|
+
runId: string,
|
|
860
|
+
code: SubagentErrorCode,
|
|
861
|
+
message: string,
|
|
862
|
+
): Promise<DelegationOutcome> {
|
|
863
|
+
console.warn(`${LOG_PREFIX} subagent run failed`, { chatId, runId, code, message })
|
|
864
|
+
try {
|
|
865
|
+
await this.deps.store.appendSubagentEvent({
|
|
866
|
+
v: 3,
|
|
867
|
+
type: "subagent_run_failed",
|
|
868
|
+
timestamp: this.now(),
|
|
869
|
+
chatId,
|
|
870
|
+
runId,
|
|
871
|
+
error: { code, message },
|
|
872
|
+
})
|
|
873
|
+
} catch (err) {
|
|
874
|
+
// Persisting the failure event must never throw out of failRun — it's
|
|
875
|
+
// called from `catch` and `finally` blocks where an unhandled rejection
|
|
876
|
+
// would leak the permit. Log and continue; the orchestrator will still
|
|
877
|
+
// notify the terminal callback below so the resolver map is cleaned up.
|
|
878
|
+
console.warn(`${LOG_PREFIX} failRun appendSubagentEvent threw`, { chatId, runId, code, err })
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
this.deps.onRunTerminal?.(chatId, runId, "failed")
|
|
882
|
+
} catch (err) {
|
|
883
|
+
console.warn(`${LOG_PREFIX} onRunTerminal(failed) threw`, { chatId, runId, err })
|
|
884
|
+
}
|
|
885
|
+
return { status: "failed", runId, errorCode: code, errorMessage: message }
|
|
886
|
+
}
|
|
887
|
+
}
|