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,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for RunnerRouter Stage 1.
|
|
3
|
+
*
|
|
4
|
+
* No NATS server — all tests exercise the pure functions `buildDescriptors`
|
|
5
|
+
* and `selectFrom` directly. The KV-reading class methods (`list`, `get`,
|
|
6
|
+
* `select`) are thin wrappers that compose these two pure functions; they are
|
|
7
|
+
* covered by integration tests in later stages.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test"
|
|
11
|
+
import { buildDescriptors, eligibleFor, selectFrom } from "./runner-router"
|
|
12
|
+
import type { RunnerDescriptor } from "./runner-router"
|
|
13
|
+
import type { RunnerRegistration } from "../shared/runner-protocol"
|
|
14
|
+
import {
|
|
15
|
+
LIVENESS_DEGRADED_MS,
|
|
16
|
+
LIVENESS_OFFLINE_MS,
|
|
17
|
+
SUPPORTED_RANGE,
|
|
18
|
+
} from "../shared/runner-protocol"
|
|
19
|
+
import type { AgentProvider } from "../shared/types"
|
|
20
|
+
|
|
21
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const NOW = 1_000_000_000_000 // arbitrary fixed "now" ms
|
|
24
|
+
|
|
25
|
+
/** A fresh online registration (lastSeenAt = NOW - 1s, well within DEGRADED threshold). */
|
|
26
|
+
function makeReg(overrides: Partial<RunnerRegistration> = {}): RunnerRegistration {
|
|
27
|
+
return {
|
|
28
|
+
runnerId: "runner-a",
|
|
29
|
+
pid: 1234,
|
|
30
|
+
startedAt: NOW - 60_000,
|
|
31
|
+
providers: ["claude"],
|
|
32
|
+
protocolVersion: SUPPORTED_RANGE.min,
|
|
33
|
+
lastSeenAt: NOW - 1_000, // 1 s ago → online
|
|
34
|
+
...overrides,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeEntry(key: string, overrides: Partial<RunnerRegistration> = {}) {
|
|
39
|
+
return { key, reg: makeReg({ runnerId: key, ...overrides }) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── buildDescriptors ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("buildDescriptors", () => {
|
|
45
|
+
test("annotates state=online for a fresh runner", () => {
|
|
46
|
+
const [d] = buildDescriptors([makeEntry("r1")], null, NOW)
|
|
47
|
+
expect(d.state).toBe("online")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("annotates state=degraded when lastSeenAt is between thresholds", () => {
|
|
51
|
+
const age = LIVENESS_DEGRADED_MS + 1_000 // just past degraded threshold
|
|
52
|
+
const [d] = buildDescriptors(
|
|
53
|
+
[makeEntry("r1", { lastSeenAt: NOW - age })],
|
|
54
|
+
null,
|
|
55
|
+
NOW,
|
|
56
|
+
)
|
|
57
|
+
expect(d.state).toBe("degraded")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("annotates state=offline when lastSeenAt is too old", () => {
|
|
61
|
+
const age = LIVENESS_OFFLINE_MS + 1_000
|
|
62
|
+
const [d] = buildDescriptors(
|
|
63
|
+
[makeEntry("r1", { lastSeenAt: NOW - age })],
|
|
64
|
+
null,
|
|
65
|
+
NOW,
|
|
66
|
+
)
|
|
67
|
+
expect(d.state).toBe("offline")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("annotates state=offline when lastSeenAt is null", () => {
|
|
71
|
+
// lastSeenAt missing → treated as null → offline
|
|
72
|
+
const entry = { key: "r1", reg: { ...makeReg(), lastSeenAt: undefined } as RunnerRegistration }
|
|
73
|
+
const [d] = buildDescriptors([entry], null, NOW)
|
|
74
|
+
expect(d.state).toBe("offline")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("sets incompatible=false for a supported protocolVersion", () => {
|
|
78
|
+
const [d] = buildDescriptors([makeEntry("r1", { protocolVersion: SUPPORTED_RANGE.min })], null, NOW)
|
|
79
|
+
expect(d.incompatible).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("sets incompatible=true when protocolVersion is outside range", () => {
|
|
83
|
+
const [d] = buildDescriptors([makeEntry("r1", { protocolVersion: 999 })], null, NOW)
|
|
84
|
+
expect(d.incompatible).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("sets incompatible=true when protocolVersion is missing (defensive)", () => {
|
|
88
|
+
const entry = {
|
|
89
|
+
key: "r1",
|
|
90
|
+
reg: { ...makeReg(), protocolVersion: undefined } as unknown as RunnerRegistration,
|
|
91
|
+
}
|
|
92
|
+
const [d] = buildDescriptors([entry], null, NOW)
|
|
93
|
+
expect(d.incompatible).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("sets capabilities from reg.capabilities when present", () => {
|
|
97
|
+
const caps = { providers: ["claude"] as AgentProvider[] }
|
|
98
|
+
const [d] = buildDescriptors(
|
|
99
|
+
[makeEntry("r1", { capabilities: { providers: ["claude"] } })],
|
|
100
|
+
null,
|
|
101
|
+
NOW,
|
|
102
|
+
)
|
|
103
|
+
expect(d.capabilities).toEqual(caps)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("sets capabilities=null when reg.capabilities is absent", () => {
|
|
107
|
+
const entry = { key: "r1", reg: { ...makeReg(), capabilities: undefined } }
|
|
108
|
+
const [d] = buildDescriptors([entry], null, NOW)
|
|
109
|
+
expect(d.capabilities).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("marks isShared=true when key matches sharedId", () => {
|
|
113
|
+
const [d] = buildDescriptors([makeEntry("shared-runner")], "shared-runner", NOW)
|
|
114
|
+
expect(d.isShared).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("marks isShared=false when key does not match sharedId", () => {
|
|
118
|
+
const [d] = buildDescriptors([makeEntry("r1")], "shared-runner", NOW)
|
|
119
|
+
expect(d.isShared).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("marks isShared=false when sharedId is null", () => {
|
|
123
|
+
const [d] = buildDescriptors([makeEntry("r1")], null, NOW)
|
|
124
|
+
expect(d.isShared).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("reads ownerId defensively from reg", () => {
|
|
128
|
+
const reg = { ...makeReg(), ownerId: "user-42" } as RunnerRegistration & { ownerId?: string }
|
|
129
|
+
const [d] = buildDescriptors([{ key: "r1", reg }], null, NOW)
|
|
130
|
+
expect(d.ownerId).toBe("user-42")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("ownerId is null when absent", () => {
|
|
134
|
+
const [d] = buildDescriptors([makeEntry("r1")], null, NOW)
|
|
135
|
+
expect(d.ownerId).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("returns empty array for empty entries", () => {
|
|
139
|
+
expect(buildDescriptors([], null, NOW)).toEqual([])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("builds multiple descriptors in entry order", () => {
|
|
143
|
+
const entries = [makeEntry("r1"), makeEntry("r2"), makeEntry("r3")]
|
|
144
|
+
const ds = buildDescriptors(entries, null, NOW)
|
|
145
|
+
expect(ds.map((d) => d.runnerId)).toEqual(["r1", "r2", "r3"])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ── eligibleFor ──────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("eligibleFor", () => {
|
|
152
|
+
function makeDescriptor(overrides: Partial<RunnerDescriptor> = {}): RunnerDescriptor {
|
|
153
|
+
return {
|
|
154
|
+
runnerId: "r1",
|
|
155
|
+
state: "online",
|
|
156
|
+
capabilities: null,
|
|
157
|
+
protocolVersion: SUPPORTED_RANGE.min,
|
|
158
|
+
incompatible: false,
|
|
159
|
+
lastSeenAt: NOW - 1_000,
|
|
160
|
+
pid: 1234,
|
|
161
|
+
ownerId: null,
|
|
162
|
+
isShared: false,
|
|
163
|
+
...overrides,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test("online + compatible + null capabilities → eligible", () => {
|
|
168
|
+
expect(eligibleFor("claude")(makeDescriptor())).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test("offline → not eligible", () => {
|
|
172
|
+
expect(eligibleFor("claude")(makeDescriptor({ state: "offline" }))).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("degraded → eligible (not offline)", () => {
|
|
176
|
+
expect(eligibleFor("claude")(makeDescriptor({ state: "degraded" }))).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("incompatible → not eligible", () => {
|
|
180
|
+
expect(eligibleFor("claude")(makeDescriptor({ incompatible: true }))).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test("capabilities.providers includes provider → eligible", () => {
|
|
184
|
+
const d = makeDescriptor({ capabilities: { providers: ["claude", "codex"] } })
|
|
185
|
+
expect(eligibleFor("claude")(d)).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("capabilities.providers excludes provider → not eligible", () => {
|
|
189
|
+
const d = makeDescriptor({ capabilities: { providers: ["codex"] } })
|
|
190
|
+
expect(eligibleFor("claude")(d)).toBe(false)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test("capabilities=null treated as capable (fail-open) for any provider", () => {
|
|
194
|
+
expect(eligibleFor("codex")(makeDescriptor({ capabilities: null }))).toBe(true)
|
|
195
|
+
expect(eligibleFor("claude")(makeDescriptor({ capabilities: null }))).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// ── selectFrom ───────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
describe("selectFrom", () => {
|
|
202
|
+
/** Online, compatible, no-capabilities descriptor */
|
|
203
|
+
function desc(
|
|
204
|
+
id: string,
|
|
205
|
+
overrides: Partial<RunnerDescriptor> = {},
|
|
206
|
+
): RunnerDescriptor {
|
|
207
|
+
return {
|
|
208
|
+
runnerId: id,
|
|
209
|
+
state: "online",
|
|
210
|
+
capabilities: null,
|
|
211
|
+
protocolVersion: SUPPORTED_RANGE.min,
|
|
212
|
+
incompatible: false,
|
|
213
|
+
lastSeenAt: NOW - 1_000,
|
|
214
|
+
pid: 1,
|
|
215
|
+
ownerId: null,
|
|
216
|
+
isShared: false,
|
|
217
|
+
...overrides,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── sticky-hit ────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
test("sticky-hit: eligible preferred runner → selected with sticky=true", () => {
|
|
224
|
+
const descriptors = [desc("r1"), desc("r2")]
|
|
225
|
+
const result = selectFrom(descriptors, {
|
|
226
|
+
provider: "claude",
|
|
227
|
+
preferredRunnerId: "r1",
|
|
228
|
+
})
|
|
229
|
+
expect(result).toEqual({ kind: "selected", runnerId: "r1", sticky: true })
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// ── sticky-offline ────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
test("sticky-offline: preferred runner offline → needs_pick/sticky_offline", () => {
|
|
235
|
+
const descriptors = [
|
|
236
|
+
desc("r1", { state: "offline" }), // preferred but offline
|
|
237
|
+
desc("r2"), // eligible alternative
|
|
238
|
+
]
|
|
239
|
+
const result = selectFrom(descriptors, {
|
|
240
|
+
provider: "claude",
|
|
241
|
+
preferredRunnerId: "r1",
|
|
242
|
+
})
|
|
243
|
+
expect(result.kind).toBe("needs_pick")
|
|
244
|
+
if (result.kind === "needs_pick") {
|
|
245
|
+
expect(result.reason).toBe("sticky_offline")
|
|
246
|
+
expect(result.candidates.map((c) => c.runnerId)).toContain("r2")
|
|
247
|
+
expect(result.candidates.map((c) => c.runnerId)).not.toContain("r1")
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test("sticky-offline: preferred runner incompatible → needs_pick/sticky_offline", () => {
|
|
252
|
+
const descriptors = [
|
|
253
|
+
desc("r1", { incompatible: true }),
|
|
254
|
+
desc("r2"),
|
|
255
|
+
]
|
|
256
|
+
const result = selectFrom(descriptors, {
|
|
257
|
+
provider: "claude",
|
|
258
|
+
preferredRunnerId: "r1",
|
|
259
|
+
})
|
|
260
|
+
expect(result.kind).toBe("needs_pick")
|
|
261
|
+
if (result.kind === "needs_pick") expect(result.reason).toBe("sticky_offline")
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test("sticky-offline: preferred runner gone entirely → needs_pick/sticky_offline", () => {
|
|
265
|
+
// "r-gone" is not in descriptors at all
|
|
266
|
+
const descriptors = [desc("r2")]
|
|
267
|
+
const result = selectFrom(descriptors, {
|
|
268
|
+
provider: "claude",
|
|
269
|
+
preferredRunnerId: "r-gone",
|
|
270
|
+
})
|
|
271
|
+
expect(result.kind).toBe("needs_pick")
|
|
272
|
+
if (result.kind === "needs_pick") {
|
|
273
|
+
expect(result.reason).toBe("sticky_offline")
|
|
274
|
+
expect(result.candidates.map((c) => c.runnerId)).toEqual(["r2"])
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// ── sole-eligible ─────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
test("sole-eligible: single eligible runner → selected with sticky=false", () => {
|
|
281
|
+
const descriptors = [desc("r1")]
|
|
282
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
283
|
+
expect(result).toEqual({ kind: "selected", runnerId: "r1", sticky: false })
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test("sole-eligible: other runners offline/incompatible, one eligible → selected/!sticky", () => {
|
|
287
|
+
const descriptors = [
|
|
288
|
+
desc("r-offline", { state: "offline" }),
|
|
289
|
+
desc("r-incompat", { incompatible: true }),
|
|
290
|
+
desc("r-ok"),
|
|
291
|
+
]
|
|
292
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
293
|
+
expect(result).toEqual({ kind: "selected", runnerId: "r-ok", sticky: false })
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ── shared-runner auto-select ─────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
test("shared-only: sole shared runner is auto-selected when no personal runner exists", () => {
|
|
299
|
+
const descriptors = [desc("shared", { isShared: true })]
|
|
300
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
301
|
+
expect(result).toEqual({ kind: "selected", runnerId: "shared", sticky: false })
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// ── ambiguous ─────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
test("ambiguous: two eligible runners, no preference → needs_pick/ambiguous", () => {
|
|
307
|
+
const descriptors = [desc("r1"), desc("r2")]
|
|
308
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
309
|
+
expect(result.kind).toBe("needs_pick")
|
|
310
|
+
if (result.kind === "needs_pick") {
|
|
311
|
+
expect(result.reason).toBe("ambiguous")
|
|
312
|
+
expect(result.candidates.length).toBe(2)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("ambiguous: three eligible runners → needs_pick/ambiguous with all candidates", () => {
|
|
317
|
+
const descriptors = [desc("r1"), desc("r2"), desc("r3")]
|
|
318
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
319
|
+
expect(result.kind).toBe("needs_pick")
|
|
320
|
+
if (result.kind === "needs_pick") {
|
|
321
|
+
expect(result.reason).toBe("ambiguous")
|
|
322
|
+
expect(result.candidates.length).toBe(3)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// ── unavailable ───────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
test("none-eligible → unavailable with reason mentioning provider", () => {
|
|
329
|
+
const result = selectFrom([], { provider: "claude" })
|
|
330
|
+
expect(result.kind).toBe("unavailable")
|
|
331
|
+
if (result.kind === "unavailable") {
|
|
332
|
+
expect(result.reason).toContain("claude")
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test("all offline → unavailable", () => {
|
|
337
|
+
const descriptors = [
|
|
338
|
+
desc("r1", { state: "offline" }),
|
|
339
|
+
desc("r2", { state: "offline" }),
|
|
340
|
+
]
|
|
341
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
342
|
+
expect(result.kind).toBe("unavailable")
|
|
343
|
+
if (result.kind === "unavailable") expect(result.reason).toContain("claude")
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test("all incompatible → unavailable", () => {
|
|
347
|
+
const descriptors = [
|
|
348
|
+
desc("r1", { incompatible: true }),
|
|
349
|
+
desc("r2", { incompatible: true }),
|
|
350
|
+
]
|
|
351
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
352
|
+
expect(result.kind).toBe("unavailable")
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// ── capability filtering ──────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
test("runner without claude capability is excluded for claude provider", () => {
|
|
358
|
+
const descriptors = [
|
|
359
|
+
desc("r-codex-only", { capabilities: { providers: ["codex"] } }),
|
|
360
|
+
desc("r-claude", { capabilities: { providers: ["claude"] } }),
|
|
361
|
+
]
|
|
362
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
363
|
+
expect(result).toEqual({ kind: "selected", runnerId: "r-claude", sticky: false })
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test("capabilities=null treated capable for any provider (fail-open)", () => {
|
|
367
|
+
const descriptors = [desc("r1", { capabilities: null })]
|
|
368
|
+
const result = selectFrom(descriptors, { provider: "codex" })
|
|
369
|
+
expect(result).toEqual({ kind: "selected", runnerId: "r1", sticky: false })
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// ── candidate ordering ────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
test("candidates: non-shared runners come before shared", () => {
|
|
375
|
+
const descriptors = [
|
|
376
|
+
desc("shared", { isShared: true, lastSeenAt: NOW - 500 }),
|
|
377
|
+
desc("personal", { isShared: false, lastSeenAt: NOW - 1_000 }),
|
|
378
|
+
]
|
|
379
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
380
|
+
// Two eligible → ambiguous
|
|
381
|
+
expect(result.kind).toBe("needs_pick")
|
|
382
|
+
if (result.kind === "needs_pick") {
|
|
383
|
+
expect(result.candidates[0].runnerId).toBe("personal")
|
|
384
|
+
expect(result.candidates[1].runnerId).toBe("shared")
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test("candidates: within non-shared group, sorted by lastSeenAt desc", () => {
|
|
389
|
+
const descriptors = [
|
|
390
|
+
desc("r-old", { lastSeenAt: NOW - 10_000 }),
|
|
391
|
+
desc("r-new", { lastSeenAt: NOW - 500 }),
|
|
392
|
+
desc("r-mid", { lastSeenAt: NOW - 5_000 }),
|
|
393
|
+
]
|
|
394
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
395
|
+
expect(result.kind).toBe("needs_pick")
|
|
396
|
+
if (result.kind === "needs_pick") {
|
|
397
|
+
expect(result.candidates.map((c) => c.runnerId)).toEqual(["r-new", "r-mid", "r-old"])
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test("candidates: shared runner comes after all non-shared, even if more recently seen", () => {
|
|
402
|
+
const descriptors = [
|
|
403
|
+
desc("shared", { isShared: true, lastSeenAt: NOW - 100 }), // most recent
|
|
404
|
+
desc("personal-a", { lastSeenAt: NOW - 5_000 }),
|
|
405
|
+
desc("personal-b", { lastSeenAt: NOW - 10_000 }),
|
|
406
|
+
]
|
|
407
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
408
|
+
expect(result.kind).toBe("needs_pick")
|
|
409
|
+
if (result.kind === "needs_pick") {
|
|
410
|
+
const ids = result.candidates.map((c) => c.runnerId)
|
|
411
|
+
expect(ids[ids.length - 1]).toBe("shared")
|
|
412
|
+
expect(ids[0]).toBe("personal-a")
|
|
413
|
+
expect(ids[1]).toBe("personal-b")
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test("candidates: nulls in lastSeenAt go last within their group", () => {
|
|
418
|
+
const descriptors = [
|
|
419
|
+
desc("r-null", { lastSeenAt: null }),
|
|
420
|
+
desc("r-ts", { lastSeenAt: NOW - 1_000 }),
|
|
421
|
+
]
|
|
422
|
+
const result = selectFrom(descriptors, { provider: "claude" })
|
|
423
|
+
expect(result.kind).toBe("needs_pick")
|
|
424
|
+
if (result.kind === "needs_pick") {
|
|
425
|
+
expect(result.candidates[0].runnerId).toBe("r-ts")
|
|
426
|
+
expect(result.candidates[1].runnerId).toBe("r-null")
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { NatsConnection } from "@nats-io/transport-node"
|
|
2
|
+
import { Kvm } from "@nats-io/kv"
|
|
3
|
+
import {
|
|
4
|
+
RUNNER_REGISTRY_BUCKET,
|
|
5
|
+
isProtocolSupported,
|
|
6
|
+
runnerLivenessState,
|
|
7
|
+
type RunnerCapabilities,
|
|
8
|
+
type RunnerLivenessState,
|
|
9
|
+
type RunnerRegistration,
|
|
10
|
+
} from "../shared/runner-protocol"
|
|
11
|
+
import type { AgentProvider } from "../shared/types"
|
|
12
|
+
|
|
13
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type RunnerDescriptor = {
|
|
16
|
+
runnerId: string
|
|
17
|
+
state: RunnerLivenessState
|
|
18
|
+
capabilities: RunnerCapabilities | null
|
|
19
|
+
protocolVersion: number | null
|
|
20
|
+
/** True when protocolVersion is null or outside SUPPORTED_RANGE. Fail-closed. */
|
|
21
|
+
incompatible: boolean
|
|
22
|
+
lastSeenAt: number | null
|
|
23
|
+
pid: number | null
|
|
24
|
+
/** Carried from reg.ownerId if present; not yet enforced (deferred to post-PR2). */
|
|
25
|
+
ownerId: string | null
|
|
26
|
+
/** True when runnerId matches the server-spawned shared runner. */
|
|
27
|
+
isShared: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RunnerSelection =
|
|
31
|
+
| { kind: "selected"; runnerId: string; sticky: boolean }
|
|
32
|
+
| { kind: "needs_pick"; candidates: RunnerDescriptor[]; reason: "ambiguous" | "sticky_offline" }
|
|
33
|
+
| { kind: "unavailable"; reason: string }
|
|
34
|
+
|
|
35
|
+
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a descriptor from a raw KV entry. Exported for unit tests (no NATS needed).
|
|
39
|
+
*/
|
|
40
|
+
export function buildDescriptors(
|
|
41
|
+
entries: { key: string; reg: RunnerRegistration }[],
|
|
42
|
+
sharedId: string | null,
|
|
43
|
+
now: number,
|
|
44
|
+
): RunnerDescriptor[] {
|
|
45
|
+
return entries.map(({ key, reg }) => {
|
|
46
|
+
const lastSeenAt = reg.lastSeenAt ?? null
|
|
47
|
+
const state = runnerLivenessState(lastSeenAt, now)
|
|
48
|
+
const capabilities = reg.capabilities ?? null
|
|
49
|
+
// Defensive: missing protocolVersion treated as incompatible (fail-closed).
|
|
50
|
+
const protocolVersion = (reg as { protocolVersion?: number }).protocolVersion ?? null
|
|
51
|
+
const incompatible =
|
|
52
|
+
protocolVersion === null ? true : !isProtocolSupported(protocolVersion)
|
|
53
|
+
return {
|
|
54
|
+
runnerId: key,
|
|
55
|
+
state,
|
|
56
|
+
capabilities,
|
|
57
|
+
protocolVersion,
|
|
58
|
+
incompatible,
|
|
59
|
+
lastSeenAt,
|
|
60
|
+
pid: reg.pid ?? null,
|
|
61
|
+
ownerId: (reg as { ownerId?: string }).ownerId ?? null,
|
|
62
|
+
isShared: key === sharedId,
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns true when a descriptor is eligible to handle `provider`.
|
|
69
|
+
*
|
|
70
|
+
* Eligibility rules:
|
|
71
|
+
* - state !== "offline" (liveness, fail-closed)
|
|
72
|
+
* - !incompatible (protocol compat, fail-closed)
|
|
73
|
+
* - capabilities === null OR capabilities.providers.includes(provider)
|
|
74
|
+
* (capability, fail-open: pre-PR4 runners without capabilities are assumed capable)
|
|
75
|
+
*
|
|
76
|
+
* Exported for unit tests.
|
|
77
|
+
*/
|
|
78
|
+
export function eligibleFor(provider: AgentProvider): (d: RunnerDescriptor) => boolean {
|
|
79
|
+
return (d) =>
|
|
80
|
+
d.state !== "offline" &&
|
|
81
|
+
!d.incompatible &&
|
|
82
|
+
(d.capabilities === null || d.capabilities.providers.includes(provider))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sort candidates for display: non-shared first, then shared; within each group
|
|
87
|
+
* by lastSeenAt descending (nulls last).
|
|
88
|
+
*/
|
|
89
|
+
function sortCandidates(candidates: RunnerDescriptor[]): RunnerDescriptor[] {
|
|
90
|
+
return [...candidates].sort((a, b) => {
|
|
91
|
+
// Shared runners go last
|
|
92
|
+
if (a.isShared !== b.isShared) return a.isShared ? 1 : -1
|
|
93
|
+
// Within group: most-recently-seen first; nulls go to the end
|
|
94
|
+
const aTs = a.lastSeenAt ?? -Infinity
|
|
95
|
+
const bTs = b.lastSeenAt ?? -Infinity
|
|
96
|
+
return bTs - aTs
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Pure selection logic. Exported for unit tests.
|
|
102
|
+
*
|
|
103
|
+
* Policy (deterministic, in order):
|
|
104
|
+
* 1. eligible = descriptors.filter(eligibleFor(provider))
|
|
105
|
+
* 2. preferredRunnerId present:
|
|
106
|
+
* a. eligible contains preferred → { selected, sticky: true }
|
|
107
|
+
* b. otherwise (preferred exists in descriptors OR is gone) → { needs_pick, sticky_offline }
|
|
108
|
+
* 3. eligible.length === 0 → { unavailable }
|
|
109
|
+
* 4. eligible.length === 1 → { selected, sticky: false }
|
|
110
|
+
* 5. else → { needs_pick, ambiguous }
|
|
111
|
+
*/
|
|
112
|
+
export function selectFrom(
|
|
113
|
+
descriptors: RunnerDescriptor[],
|
|
114
|
+
req: {
|
|
115
|
+
provider: AgentProvider
|
|
116
|
+
preferredRunnerId?: string | null
|
|
117
|
+
now?: number
|
|
118
|
+
},
|
|
119
|
+
): RunnerSelection {
|
|
120
|
+
const { provider, preferredRunnerId } = req
|
|
121
|
+
const eligible = descriptors.filter(eligibleFor(provider))
|
|
122
|
+
const orderedCandidates = sortCandidates(eligible)
|
|
123
|
+
|
|
124
|
+
if (preferredRunnerId) {
|
|
125
|
+
const hit = eligible.find((d) => d.runnerId === preferredRunnerId)
|
|
126
|
+
if (hit) {
|
|
127
|
+
return { kind: "selected", runnerId: hit.runnerId, sticky: true }
|
|
128
|
+
}
|
|
129
|
+
// Preferred runner exists in any state OR is gone entirely → needs a new pick.
|
|
130
|
+
return {
|
|
131
|
+
kind: "needs_pick",
|
|
132
|
+
candidates: orderedCandidates,
|
|
133
|
+
reason: "sticky_offline",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (eligible.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
kind: "unavailable",
|
|
140
|
+
reason: `No online runner for "${provider}" — pair or start a runner.`,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (eligible.length === 1) {
|
|
145
|
+
return { kind: "selected", runnerId: eligible[0].runnerId, sticky: false }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { kind: "needs_pick", candidates: orderedCandidates, reason: "ambiguous" }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── RunnerRouter ─────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
const decoder = new TextDecoder()
|
|
154
|
+
|
|
155
|
+
export class RunnerRouter {
|
|
156
|
+
private readonly nc: NatsConnection
|
|
157
|
+
private readonly sharedRunnerId: () => string | null
|
|
158
|
+
|
|
159
|
+
constructor(opts: { nc: NatsConnection; sharedRunnerId: () => string | null }) {
|
|
160
|
+
this.nc = opts.nc
|
|
161
|
+
this.sharedRunnerId = opts.sharedRunnerId
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Enumerate all registered runners from the KV bucket, annotated with
|
|
166
|
+
* liveness, compat, and isShared. Returns [] on missing/empty bucket.
|
|
167
|
+
*/
|
|
168
|
+
async list(now = Date.now()): Promise<RunnerDescriptor[]> {
|
|
169
|
+
const entries: { key: string; reg: RunnerRegistration }[] = []
|
|
170
|
+
try {
|
|
171
|
+
const kvm = new Kvm(this.nc)
|
|
172
|
+
const kvStore = await kvm.open(RUNNER_REGISTRY_BUCKET)
|
|
173
|
+
const keys = await kvStore.keys()
|
|
174
|
+
for await (const key of keys) {
|
|
175
|
+
const entry = await kvStore.get(key)
|
|
176
|
+
if (!entry) continue
|
|
177
|
+
try {
|
|
178
|
+
const reg = JSON.parse(decoder.decode(entry.value)) as RunnerRegistration
|
|
179
|
+
entries.push({ key, reg })
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.warn(`[RunnerRouter] skipping corrupt KV entry "${key}":`, e instanceof Error ? e.message : String(e))
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// Bucket missing or NATS not ready — return what we have (possibly empty).
|
|
187
|
+
}
|
|
188
|
+
return buildDescriptors(entries, this.sharedRunnerId(), now)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fetch a single runner descriptor by id. Returns null if not found.
|
|
193
|
+
*/
|
|
194
|
+
async get(runnerId: string, now = Date.now()): Promise<RunnerDescriptor | null> {
|
|
195
|
+
const all = await this.list(now)
|
|
196
|
+
return all.find((d) => d.runnerId === runnerId) ?? null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Select a runner for `provider`, honoring sticky preference when eligible.
|
|
201
|
+
* See `selectFrom` for the full deterministic policy.
|
|
202
|
+
*/
|
|
203
|
+
async select(req: {
|
|
204
|
+
provider: AgentProvider
|
|
205
|
+
preferredRunnerId?: string | null
|
|
206
|
+
now?: number
|
|
207
|
+
}): Promise<RunnerSelection> {
|
|
208
|
+
const now = req.now ?? Date.now()
|
|
209
|
+
const descriptors = await this.list(now)
|
|
210
|
+
return selectFrom(descriptors, req)
|
|
211
|
+
}
|
|
212
|
+
}
|