@iowarp/clio-coder 0.3.1 → 0.3.2
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/CHANGELOG.md +90 -2
- package/CONTRIBUTING.md +23 -23
- package/README.md +284 -613
- package/dist/{acp-FPR54DGL.js → acp-BIYHVZIM.js} +43 -53
- package/dist/{agents-OGPIHPJH.js → agents-YT6SSRIT.js} +43 -26
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-IC3K6NIZ.js → auth-5TWEIYDN.js} +20 -12
- package/dist/{chunk-PV4JUBVJ.js → chunk-2EHAIA3X.js} +40 -21
- package/dist/{chunk-IS3ONKU3.js → chunk-2IR2NMPA.js} +6 -4
- package/dist/chunk-2SFS6XQE.js +122 -0
- package/dist/chunk-2VTFPG5O.js +48 -0
- package/dist/{chunk-MAR7Y6HW.js → chunk-3ZXDFGR5.js} +23 -16
- package/dist/chunk-4BJ5BYCE.js +61 -0
- package/dist/{chunk-474KN5II.js → chunk-4BPJXDWC.js} +111 -181
- package/dist/chunk-4KLWL3UC.js +18 -0
- package/dist/chunk-4VP4KH3K.js +962 -0
- package/dist/chunk-4ZG3XFUR.js +77 -0
- package/dist/chunk-5B2AEOW5.js +5407 -0
- package/dist/{chunk-K2ITRMHZ.js → chunk-5TSRNF4G.js} +6 -138
- package/dist/{chunk-OLBBMFRD.js → chunk-5UUP6MWO.js} +24 -62
- package/dist/chunk-65DEGPJ6.js +52 -0
- package/dist/chunk-6EJMN2Y3.js +17 -0
- package/dist/chunk-6EJV5X2W.js +16405 -0
- package/dist/chunk-6N5PTWMY.js +136 -0
- package/dist/chunk-6XLNIQDB.js +27 -0
- package/dist/{chunk-TEKV33Q5.js → chunk-77VKQEHF.js} +65 -33
- package/dist/chunk-7CR24IG7.js +242 -0
- package/dist/{chunk-M5T5VO65.js → chunk-7EYHLWU7.js} +837 -635
- package/dist/chunk-7MNJORFF.js +22 -0
- package/dist/{chunk-KY56HMHH.js → chunk-A3CYT5EX.js} +125 -31
- package/dist/chunk-AGYYIBLL.js +1069 -0
- package/dist/chunk-AO4RKG4M.js +277 -0
- package/dist/{chunk-GB6QRBXN.js → chunk-APJ265NV.js} +54 -1187
- package/dist/chunk-ARBGF5F7.js +174 -0
- package/dist/{chunk-673JJUWJ.js → chunk-BMEMKKIT.js} +2 -2
- package/dist/chunk-CBCAPZAA.js +229 -0
- package/dist/chunk-CMZWFGD2.js +352 -0
- package/dist/chunk-ECH6PKUQ.js +39 -0
- package/dist/chunk-ED4KHGC3.js +143 -0
- package/dist/chunk-EKMEHE4H.js +340 -0
- package/dist/{chunk-RPTR2H26.js → chunk-EPVUXGXG.js} +21 -15
- package/dist/chunk-FCSXB6T2.js +338 -0
- package/dist/chunk-FJ3H4MN5.js +48 -0
- package/dist/chunk-FQ4SKYE4.js +29 -0
- package/dist/chunk-G2DE3C7R.js +644 -0
- package/dist/chunk-G4BMMOKF.js +182 -0
- package/dist/{chunk-ZPY3JZ5E.js → chunk-GGXXDWE4.js} +183 -1233
- package/dist/chunk-HC4CLZ2Y.js +68 -0
- package/dist/{chunk-LU4TK2PR.js → chunk-HFSBBKSQ.js} +5 -56
- package/dist/{chunk-PIUMUEMV.js → chunk-HKIYEGME.js} +10 -6
- package/dist/chunk-I4HZDVNP.js +73 -0
- package/dist/{chunk-4QKXUHSR.js → chunk-IGLFWIYI.js} +70 -20
- package/dist/chunk-IKCO5N3L.js +162 -0
- package/dist/chunk-IR4CFBFN.js +56 -0
- package/dist/{chunk-PFEFKVGL.js → chunk-J5HN4RYU.js} +13 -11
- package/dist/{chunk-R5KLMSBV.js → chunk-J5Q24KAG.js} +2 -2
- package/dist/{chunk-K5XEMXTI.js → chunk-JVCV3ICN.js} +1 -1
- package/dist/chunk-KJ5LWLOE.js +1077 -0
- package/dist/chunk-LBMZMYH2.js +285 -0
- package/dist/{chunk-G34LV2PF.js → chunk-LBNRH5WM.js} +84 -170
- package/dist/{chunk-H6F6BYOH.js → chunk-LZSJBIVT.js} +7003 -7434
- package/dist/{chunk-HQQID6OA.js → chunk-M6SHUN7Q.js} +5 -5
- package/dist/chunk-MAW544W2.js +1882 -0
- package/dist/chunk-MBS4V7ZP.js +217 -0
- package/dist/{chunk-FST4FYJB.js → chunk-MFFY33HR.js} +99 -140
- package/dist/chunk-MNA4JGU4.js +255 -0
- package/dist/chunk-MQSRRFWA.js +3428 -0
- package/dist/{chunk-BSU2YIWB.js → chunk-MVVUPGPW.js} +131 -136
- package/dist/chunk-OAO4GE4M.js +619 -0
- package/dist/chunk-OHHN2SO4.js +5135 -0
- package/dist/chunk-OKGUZO2U.js +34 -0
- package/dist/{chunk-GAEBEQVI.js → chunk-OOJYHWRB.js} +32 -346
- package/dist/{chunk-Q5WJOSJ7.js → chunk-OQ33BKR3.js} +2 -1
- package/dist/chunk-OQE5J4C6.js +73 -0
- package/dist/{chunk-KKNLWXI6.js → chunk-ORBHGJC5.js} +8 -8
- package/dist/chunk-POHLU5DW.js +1186 -0
- package/dist/chunk-QKMUKYO7.js +4961 -0
- package/dist/{chunk-ASND7OZK.js → chunk-QTYWRVRA.js} +13 -13
- package/dist/{chunk-EYOKLTMF.js → chunk-SRF2PJNW.js} +17 -3
- package/dist/chunk-SST6Z5JA.js +80 -0
- package/dist/chunk-STBPMHSX.js +2456 -0
- package/dist/chunk-T6YILFSB.js +80 -0
- package/dist/chunk-TZTZS7QK.js +227 -0
- package/dist/chunk-UOV2BYIW.js +107 -0
- package/dist/{chunk-Q3RUPKEJ.js → chunk-V4RXGQ5Q.js} +58 -189
- package/dist/chunk-VAKQQHWR.js +434 -0
- package/dist/chunk-VG7TBQIY.js +128 -0
- package/dist/chunk-VJWL6YS5.js +244 -0
- package/dist/chunk-WEH5XRJQ.js +32 -0
- package/dist/chunk-WMSVI4G2.js +2095 -0
- package/dist/chunk-WVO7V2QY.js +797 -0
- package/dist/chunk-X4RCMKVQ.js +641 -0
- package/dist/chunk-X75S7HFS.js +374 -0
- package/dist/chunk-XN3L4EYL.js +46 -0
- package/dist/{chunk-RDLVBZEO.js → chunk-YCWGATWI.js} +6 -4
- package/dist/chunk-YHZX5GEU.js +193 -0
- package/dist/chunk-YXLYO42X.js +91 -0
- package/dist/{chunk-NMOX6HFD.js → chunk-ZDOOVTXZ.js} +29 -77
- package/dist/chunk-ZI647VB5.js +37 -0
- package/dist/{chunk-C4PTHK7P.js → chunk-ZWLZP4ZT.js} +5 -5
- package/dist/cli/index.js +62 -54
- package/dist/clio-4LY5K2AC.js +25 -0
- package/dist/code-nav-7AX6FYE6.js +600 -0
- package/dist/codewiki/build-worker.js +66 -0
- package/dist/compile-cache-CVJMMODC.js +18 -0
- package/dist/{components-DMAOEKFB.js → components-KELWS457.js} +11 -6
- package/dist/{config-IRUQ7SE4.js → config-GTLUW2PR.js} +92 -55
- package/dist/configure-R6A64DHX.js +42 -0
- package/dist/context-5VKGUVJJ.js +866 -0
- package/dist/{context-3KWFLHJG.js → context-JFZEJ7W5.js} +15 -13
- package/dist/{context-5RADCKTR.js → context-RW5HC47S.js} +71 -35
- package/dist/{context-clear-7TSNPAAI.js → context-clear-6ZHBAZZT.js} +54 -28
- package/dist/{context-index-W4RLWOQH.js → context-index-BZ4UYMTC.js} +30 -24
- package/dist/dispatch-runner-VKBRCWQC.js +1997 -0
- package/dist/{docs-5AWSPS37.js → docs-2C2LTVT2.js} +23 -10
- package/dist/{doctor-UC5NAJYQ.js → doctor-KI767GSN.js} +27 -17
- package/dist/{eval-U6TJHRLX.js → eval-XSSNATB4.js} +29 -16
- package/dist/{evidence-YEGUW4L3.js → evidence-UA6AWDQQ.js} +46 -26
- package/dist/{evolve-TXARCTPG.js → evolve-QNTFGV6Z.js} +45 -25
- package/dist/{extensions-OZFJ3A3G.js → extensions-QVDOHDGJ.js} +16 -7
- package/dist/{fleet-6G3DHNYE.js → fleet-Q7UOMUSG.js} +163 -54
- package/dist/{fleet-preflight-DSNT37JK.js → fleet-preflight-DDN536IT.js} +7 -4
- package/dist/{init-KZ5QTF6M.js → init-WBB65ZHQ.js} +69 -32
- package/dist/{memory-73ESV5YC.js → memory-MD3O64RI.js} +48 -27
- package/dist/{models-A4PVNWJK.js → models-BZU34YWD.js} +39 -25
- package/dist/monitor-MEQA5C3I.js +661 -0
- package/dist/{chunk-FCIH3BIZ.js → orchestrator-CGFKEP27.js} +11832 -8687
- package/dist/{paths-C4H6IV77.js → paths-UXLN5YYZ.js} +10 -5
- package/dist/{preload-6WVMHX3A.js → preload-P6DGH2PZ.js} +2 -2
- package/dist/{reset-BGW6OGMV.js → reset-L2FQEE3E.js} +16 -10
- package/dist/{run-YTPEYQOH.js → run-IV4Q6RLN.js} +101 -61
- package/dist/{share-YIFFV4NQ.js → share-S5BZQC5I.js} +15 -8
- package/dist/{skills-2V6RA3OQ.js → skills-LQEKRDTN.js} +34 -14
- package/dist/{skills-eval-S2TVJO4F.js → skills-eval-3DC4HEWS.js} +70 -34
- package/dist/steer-GGWFUJUD.js +77 -0
- package/dist/{targets-TYXLPB23.js → targets-C4SSGQOB.js} +43 -27
- package/dist/terminal-lease-IT5JW2NR.js +395 -0
- package/dist/{trace-GGOJ6Q6Z.js → trace-PNCASAXC.js} +41 -16
- package/dist/{chunk-N6F52NLF.js → tree-sitter-HGKH6LG4.js} +28 -2306
- package/dist/{uninstall-LLLT4F4W.js → uninstall-FZCQCDKC.js} +10 -5
- package/dist/{upgrade-33G2LMM5.js → upgrade-7TT7SQ3G.js} +45 -25
- package/dist/{usage-ZAFSXKKG.js → usage-GV4PKT3M.js} +62 -31
- package/dist/verify-G6V4D2G7.js +716 -0
- package/dist/web-fetch-2YHJ3KTG.js +638 -0
- package/dist/{wiki-generate-NUQCVOQ3.js → wiki-generate-DQF6Z66B.js} +74 -34
- package/dist/worker/entry.js +221 -36
- package/dist/workspace-G4ZWUIPR.js +22 -0
- package/docs/README.md +22 -17
- package/docs/acp.md +168 -16
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +136 -7
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +114 -71
- package/docs/config-knobs-audit.md +1 -3
- package/docs/configuration-and-targets.md +174 -46
- package/docs/context-engine.md +29 -6
- package/docs/development-pipeline.md +26 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +2 -2
- package/docs/documentation-guide.md +1 -1
- package/docs/environment-variables.md +13 -5
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +6 -2
- package/docs/evolution.md +2 -2
- package/docs/exit-codes-and-output.md +15 -9
- package/docs/extensions-and-sharing.md +9 -9
- package/docs/fleet-dispatch.md +7 -5
- package/docs/git-commit-provenance.md +120 -0
- package/docs/glossary.md +1 -1
- package/docs/installation-and-lifecycle.md +34 -27
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +45 -14
- package/docs/observability.md +8 -5
- package/docs/performance-methodology.md +491 -0
- package/docs/pi-boundary.md +72 -0
- package/docs/proactive-memory.md +3 -3
- package/docs/prompt-envelope-and-tools.md +24 -3
- package/docs/provider-adapter-cookbook.md +57 -4
- package/docs/release-cut-checklist.md +129 -115
- package/docs/safety-model.md +9 -5
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +55 -12
- package/docs/skills-marketplace.md +12 -8
- package/docs/time-conventions.md +1 -1
- package/docs/tool-usage.md +3 -3
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +10 -7
- package/docs/tui-design.md +47 -10
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +19 -22
- package/skills/coding/ast-grep/SKILL.md +136 -0
- package/skills/coding/ast-grep/evals.md +56 -0
- package/skills/coding/ast-grep/references/rule_reference.md +297 -0
- package/skills/coding/coding-standards/SKILL.md +113 -0
- package/skills/coding/coding-standards/evals.md +34 -0
- package/skills/coding/prototype/SKILL.md +86 -0
- package/skills/coding/prototype/evals.md +42 -0
- package/skills/coding/prototype/references/LOGIC.md +67 -0
- package/skills/coding/prototype/references/UI.md +112 -0
- package/skills/coding/tdd/SKILL.md +101 -0
- package/skills/coding/tdd/evals.md +41 -0
- package/skills/coding/tdd/references/mocking.md +59 -0
- package/skills/coding/tdd/references/tests.md +77 -0
- package/skills/context/context-handoff/SKILL.md +126 -0
- package/skills/context/context-handoff/evals.md +57 -0
- package/skills/context/context-handoff/scripts/new-handoff.sh +26 -0
- package/skills/context/context-prime/SKILL.md +95 -0
- package/skills/context/context-prime/evals.md +54 -0
- package/skills/meta/clio-dev/SKILL.md +91 -0
- package/skills/meta/clio-dev/evals.md +45 -0
- package/skills/meta/clio-test/SKILL.md +130 -0
- package/skills/meta/clio-test/evals.md +43 -0
- package/skills/meta/clio-test/references/harness.md +97 -0
- package/skills/meta/clio-test/references/test-map.md +59 -0
- package/skills/meta/credentials/SKILL.md +125 -0
- package/skills/meta/credentials/evals.md +104 -0
- package/skills/meta/find-skills/SKILL.md +72 -0
- package/skills/meta/find-skills/evals.md +47 -0
- package/skills/meta/herdr/SKILL.md +127 -0
- package/skills/meta/herdr/evals.md +38 -0
- package/skills/meta/skill-craft/SKILL.md +102 -0
- package/skills/meta/skill-craft/evals.md +41 -0
- package/skills/planning/architecture/SKILL.md +129 -0
- package/skills/planning/architecture/evals.md +36 -0
- package/skills/planning/backlog/SKILL.md +90 -0
- package/skills/planning/backlog/evals.md +43 -0
- package/skills/planning/prd/SKILL.md +82 -0
- package/skills/planning/prd/evals.md +49 -0
- package/skills/planning/product-intent/SKILL.md +112 -0
- package/skills/planning/product-intent/evals.md +36 -0
- package/skills/planning/tech-spec/SKILL.md +115 -0
- package/skills/planning/tech-spec/evals.md +47 -0
- package/skills/registry.yaml +136 -0
- package/skills/research/arxiv-literature/SKILL.md +104 -0
- package/skills/research/arxiv-literature/evals.md +58 -0
- package/skills/research/experiment-protocol/SKILL.md +122 -0
- package/skills/research/experiment-protocol/evals.md +91 -0
- package/skills/research/scientific-debugging/SKILL.md +119 -0
- package/skills/research/scientific-debugging/evals.md +138 -0
- package/skills/research/scientific-modernization/SKILL.md +138 -0
- package/skills/research/scientific-modernization/evals.md +84 -0
- package/skills/workflow/design-council/SKILL.md +139 -0
- package/skills/workflow/design-council/evals.md +97 -0
- package/skills/workflow/grill-me/SKILL.md +186 -0
- package/skills/workflow/grill-me/evals.md +78 -0
- package/skills/workflow/workflow-distiller/SKILL.md +136 -0
- package/skills/workflow/workflow-distiller/evals.md +107 -0
- package/src/cli/acp.ts +31 -4
- package/src/cli/clio.ts +68 -6
- package/src/cli/config-inspect.ts +28 -22
- package/src/cli/configure.ts +47 -9
- package/src/cli/context-clear.ts +2 -2
- package/src/cli/context-index.ts +21 -23
- package/src/cli/context.ts +13 -8
- package/src/cli/default-target.ts +9 -17
- package/src/cli/docs.ts +11 -5
- package/src/cli/evidence.ts +4 -1
- package/src/cli/extensions.ts +10 -1
- package/src/cli/fleet.ts +47 -6
- package/src/cli/index.ts +55 -26
- package/src/cli/memory.ts +3 -1
- package/src/cli/models.ts +1 -1
- package/src/cli/modes/json-stream.ts +37 -1
- package/src/cli/modes/print.ts +24 -9
- package/src/cli/run.ts +2 -2
- package/src/cli/skills-eval.ts +23 -8
- package/src/cli/skills.ts +19 -4
- package/src/cli/targets.ts +4 -0
- package/src/cli/text-layout.ts +15 -5
- package/src/cli/trace.ts +62 -14
- package/src/cli/upgrade.ts +18 -2
- package/src/cli/usage.ts +10 -3
- package/src/cli/wiki-generate.ts +2 -1
- package/src/core/agent-environment.ts +7 -0
- package/src/core/bash-exec.ts +72 -1
- package/src/core/boot-trace.ts +9 -4
- package/src/core/bus-events.ts +20 -4
- package/src/core/commit-attribution.ts +157 -0
- package/src/core/compile-cache.ts +159 -0
- package/src/core/config.ts +131 -2
- package/src/core/defaults.ts +39 -5
- package/src/core/domain-loader.ts +12 -5
- package/src/core/git-commit-attribution.ts +362 -0
- package/src/core/incomplete-installation.ts +45 -0
- package/src/core/response-schema.ts +1 -1
- package/src/core/safe-exec.ts +13 -1
- package/src/core/settings-layers.ts +155 -21
- package/src/core/skill-activation.ts +1 -1
- package/src/core/startup-timer.ts +3 -3
- package/src/core/state-file-lock.ts +13 -1
- package/src/core/termination.ts +78 -5
- package/src/domains/config/classify.ts +15 -3
- package/src/domains/config/extension.ts +19 -13
- package/src/domains/config/index.ts +10 -0
- package/src/domains/config/keybindings.ts +42 -6
- package/src/domains/context/bootstrap-prompt.ts +1 -1
- package/src/domains/context/bootstrap.ts +111 -18
- package/src/domains/context/clear.ts +16 -11
- package/src/domains/context/clio-md.ts +111 -9
- package/src/domains/context/codewiki/artifact.ts +400 -0
- package/src/domains/context/codewiki/build-worker-protocol.ts +24 -0
- package/src/domains/context/codewiki/build-worker.ts +54 -0
- package/src/domains/context/codewiki/coordinator.ts +182 -0
- package/src/domains/context/codewiki/indexer.ts +59 -144
- package/src/domains/context/codewiki/paths.ts +67 -0
- package/src/domains/context/codewiki/schema.ts +80 -0
- package/src/domains/context/codewiki/tree-sitter.ts +1 -1
- package/src/domains/context/contract.ts +11 -5
- package/src/domains/context/extension.ts +94 -143
- package/src/domains/context/fingerprint.ts +3 -1
- package/src/domains/context/index.ts +12 -22
- package/src/domains/context/project-metadata.ts +19 -0
- package/src/domains/context/prompt-context.ts +9 -10
- package/src/domains/context/refresh.ts +29 -21
- package/src/domains/context/runtime.ts +17 -0
- package/src/domains/context/wiki/generate.ts +39 -34
- package/src/domains/context/wiki/plan.ts +1 -1
- package/src/domains/context/wiki/prompts.ts +21 -8
- package/src/domains/dispatch/code-step.ts +20 -1
- package/src/domains/dispatch/extension.ts +158 -21
- package/src/domains/dispatch/failure-classification.ts +6 -0
- package/src/domains/dispatch/fleet-commit-attribution.ts +56 -0
- package/src/domains/dispatch/orphan-recovery.ts +50 -8
- package/src/domains/dispatch/receipt-integrity.ts +5 -0
- package/src/domains/dispatch/state.ts +31 -6
- package/src/domains/dispatch/transport.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +21 -2
- package/src/domains/eval/metrics/context.ts +1 -1
- package/src/domains/eval/types.ts +0 -1
- package/src/domains/evidence/build.ts +41 -1
- package/src/domains/lifecycle/migrations/2026-08-18-lmstudio-runtime-id.ts +52 -0
- package/src/domains/lifecycle/migrations/index.ts +24 -4
- package/src/domains/middleware/hooks-io.ts +12 -0
- package/src/domains/middleware/skills-reminder.ts +30 -15
- package/src/domains/prompts/compiler.ts +142 -84
- package/src/domains/prompts/contract.ts +18 -2
- package/src/domains/prompts/extension.ts +39 -7
- package/src/domains/prompts/fragment-loader.ts +0 -1
- package/src/domains/prompts/fragments/identity/clio.md +2 -4
- package/src/domains/prompts/fragments/identity/docs-routing.md +10 -0
- package/src/domains/prompts/fragments/identity/self-awareness.md +1 -45
- package/src/domains/prompts/fragments/operating/contract.md +4 -50
- package/src/domains/prompts/fragments/operating/delegation.md +42 -0
- package/src/domains/prompts/fragments/operating/skills.md +26 -0
- package/src/domains/prompts/fragments/operating/worker.md +16 -0
- package/src/domains/prompts/fragments/safety/auto-edit.md +5 -5
- package/src/domains/prompts/fragments/safety/full-auto.md +3 -3
- package/src/domains/prompts/fragments/safety/read-only.md +4 -4
- package/src/domains/prompts/fragments/safety/suggest.md +2 -2
- package/src/domains/prompts/fragments/wiki/page.md +10 -0
- package/src/domains/prompts/fragments/wiki/plan.md +10 -0
- package/src/domains/prompts/preload.ts +3 -3
- package/src/domains/providers/auth/api-key.ts +1 -1
- package/src/domains/providers/auth/backend-file.ts +20 -10
- package/src/domains/providers/auth/backend-memory.ts +59 -4
- package/src/domains/providers/auth/boot-status.ts +65 -0
- package/src/domains/providers/auth/oauth.ts +2 -1
- package/src/domains/providers/auth/storage.ts +97 -38
- package/src/domains/providers/capabilities.ts +12 -4
- package/src/domains/providers/contract.ts +15 -4
- package/src/domains/providers/extension.ts +18 -6
- package/src/domains/providers/model-runtime-capabilities.ts +15 -4
- package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +118 -35
- package/src/domains/providers/plugins.ts +5 -3
- package/src/domains/providers/probe/fingerprint.ts +25 -5
- package/src/domains/providers/registry.ts +31 -10
- package/src/domains/providers/runtimes/boot-manifest.ts +55 -0
- package/src/domains/providers/runtimes/builtins.ts +2 -2
- package/src/domains/providers/runtimes/common/lmstudio-http.ts +423 -0
- package/src/domains/providers/runtimes/common/local-synth.ts +6 -7
- package/src/domains/providers/runtimes/local-native/lmstudio.ts +241 -0
- package/src/domains/providers/support.ts +6 -3
- package/src/domains/providers/types/local-model-quirks.ts +7 -9
- package/src/domains/providers/types/runtime-descriptor.ts +12 -1
- package/src/domains/providers/types/target-descriptor.ts +22 -0
- package/src/domains/resources/contract.ts +0 -1
- package/src/domains/resources/extension.ts +1 -3
- package/src/domains/resources/loader.ts +3 -4
- package/src/domains/resources/prompts/loader.ts +16 -2
- package/src/domains/resources/prompts/substitute.ts +1 -65
- package/src/domains/resources/skills/content-hash.ts +2 -0
- package/src/domains/resources/skills/install.ts +17 -0
- package/src/domains/resources/skills/loader.ts +17 -10
- package/src/domains/resources/skills/marketplace.ts +55 -9
- package/src/domains/safety/action-classifier.ts +4 -2
- package/src/domains/safety/audit.ts +8 -2
- package/src/domains/safety/extension.ts +1 -1
- package/src/domains/session/compaction/branch-summary.ts +3 -2
- package/src/domains/session/compaction/cut-point.ts +2 -1
- package/src/domains/session/compaction/tokens.ts +2 -1
- package/src/domains/session/context-ledger.ts +14 -0
- package/src/domains/session/contract.ts +15 -0
- package/src/domains/session/decision-board.ts +190 -0
- package/src/domains/session/entries.ts +66 -3
- package/src/domains/session/extension.ts +93 -12
- package/src/domains/session/retry.ts +10 -18
- package/src/domains/session/session-artifacts.ts +107 -0
- package/src/domains/session/task-board.ts +207 -13
- package/src/domains/session/tree/active-path.ts +44 -5
- package/src/domains/session/tree/fork.ts +26 -27
- package/src/domains/session/tree/preview.ts +2 -2
- package/src/domains/session/workspace/git-probe.ts +17 -11
- package/src/domains/user-tasks/store.ts +297 -0
- package/src/engine/acp/errors.ts +96 -0
- package/src/engine/acp/server.ts +1728 -146
- package/src/engine/acp/transport.ts +135 -14
- package/src/engine/acp/types.ts +26 -0
- package/src/engine/agent.ts +3 -3
- package/src/engine/ai.ts +32 -27
- package/src/engine/alcf-oauth.ts +26 -19
- package/src/engine/api-registry.ts +223 -0
- package/src/engine/apis/index.ts +3 -7
- package/src/engine/apis/llamacpp-residency.ts +49 -9
- package/src/engine/apis/lmstudio-residency.ts +5 -21
- package/src/engine/apis/lmstudio.ts +243 -0
- package/src/engine/apis/ollama-native.ts +24 -3
- package/src/engine/apis/openai-completions.ts +170 -91
- package/src/engine/apis/residency.ts +139 -3
- package/src/engine/apis/types.ts +16 -0
- package/src/engine/env-api-keys.ts +98 -0
- package/src/engine/gemma-channel-filter.ts +223 -0
- package/src/engine/instrumented-tui.ts +192 -0
- package/src/engine/messages.ts +14 -0
- package/src/engine/models.ts +42 -0
- package/src/engine/oauth.ts +16 -12
- package/src/engine/prompt-templates.ts +1 -0
- package/src/engine/provider-payload.ts +16 -59
- package/src/engine/strip-tokenizer-sentinels.ts +1 -1
- package/src/engine/truncate.ts +9 -0
- package/src/engine/tui.ts +17 -9
- package/src/engine/types.ts +3 -6
- package/src/engine/worker-runtime-capabilities.ts +5 -0
- package/src/engine/worker-runtime.ts +1 -1
- package/src/engine/worker-tools.ts +9 -4
- package/src/entry/boot-options.ts +50 -0
- package/src/entry/orchestrator.ts +288 -150
- package/src/interactive/application-controller.ts +89 -2
- package/src/interactive/chat-loop.ts +266 -41
- package/src/interactive/chat-panel.ts +173 -47
- package/src/interactive/chat-renderer.ts +262 -72
- package/src/interactive/clio-editor.ts +3 -8
- package/src/interactive/command-fallbacks.ts +2 -2
- package/src/interactive/context-overlay.ts +27 -1
- package/src/interactive/editor-submit.ts +228 -24
- package/src/interactive/export-html/ansi-to-html.ts +161 -0
- package/src/interactive/export-html/index.ts +51 -0
- package/src/interactive/export-html/template.ts +45 -0
- package/src/interactive/export-html/tool-renderer.ts +54 -0
- package/src/interactive/footer/dashboard.ts +4 -0
- package/src/interactive/footer/notifications.ts +1 -1
- package/src/interactive/footer/widgets.ts +20 -2
- package/src/interactive/footer-panel.ts +2 -2
- package/src/interactive/format-time.ts +14 -2
- package/src/interactive/interactive-application.ts +201 -17
- package/src/interactive/interactive-event-projection.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +50 -4
- package/src/interactive/interactive-presentation.ts +151 -19
- package/src/interactive/interactive-shell.ts +268 -14
- package/src/interactive/interactive-slash-runtime.ts +176 -114
- package/src/interactive/interactive-tickers.ts +38 -7
- package/src/interactive/keybinding-manager.ts +1 -1
- package/src/interactive/layout.ts +40 -3
- package/src/interactive/overlay-frame.ts +1 -1
- package/src/interactive/overlay-general-openers.ts +58 -1
- package/src/interactive/overlay-key-routing.ts +3 -0
- package/src/interactive/overlay-lifecycle.ts +13 -0
- package/src/interactive/overlay-permission-lifecycle.ts +2 -1
- package/src/interactive/overlay-session-lifecycle.ts +69 -12
- package/src/interactive/overlays/decisions.ts +300 -0
- package/src/interactive/overlays/help-reference.ts +15 -10
- package/src/interactive/overlays/model-selector.ts +34 -16
- package/src/interactive/overlays/session-selector.ts +18 -0
- package/src/interactive/overlays/settings.ts +105 -17
- package/src/interactive/overlays/skills-hub.ts +4 -4
- package/src/interactive/overlays/tree-selector.ts +41 -6
- package/src/interactive/render-trace.ts +499 -90
- package/src/interactive/renderers/compaction-summary.ts +2 -2
- package/src/interactive/renderers/diff.ts +115 -104
- package/src/interactive/renderers/mermaid.ts +53 -0
- package/src/interactive/renderers/tool-execution.ts +386 -133
- package/src/interactive/renderers/worker-entry.ts +20 -4
- package/src/interactive/session-switch-settlement.ts +10 -0
- package/src/interactive/slash-autocomplete.ts +6 -114
- package/src/interactive/slash-commands.ts +135 -47
- package/src/interactive/slash-spec.ts +9 -38
- package/src/interactive/status/controller.ts +5 -1
- package/src/interactive/stdout-backpressure.ts +99 -0
- package/src/interactive/stream-pacer.ts +530 -0
- package/src/interactive/stream-pacing-policy.ts +66 -0
- package/src/interactive/tasks-overlay.ts +368 -14
- package/src/interactive/terminal-lease.ts +485 -0
- package/src/interactive/theme/tokens.ts +1 -1
- package/src/interactive/turn-context.ts +4 -3
- package/src/interactive/turn-persistence.ts +30 -13
- package/src/interactive/turn-queues.ts +12 -0
- package/src/interactive/turn-recovery.ts +25 -8
- package/src/interactive/turn-runtime.ts +79 -12
- package/src/interactive/turn-state.ts +10 -0
- package/src/interactive/view/artifacts.ts +114 -4
- package/src/interactive/view/view-overlay.ts +3 -0
- package/src/interactive/welcome-dashboard.ts +17 -16
- package/src/interactive/worker-receipts.ts +52 -3
- package/src/interactive/worker-stream.ts +5 -1
- package/src/tools/agent-tools.ts +23 -3
- package/src/tools/artifact.ts +2 -2
- package/src/tools/ask-user.ts +23 -13
- package/src/tools/bash.ts +30 -2
- package/src/tools/bootstrap.ts +34 -431
- package/src/tools/builtin-tool-catalog.ts +265 -0
- package/src/tools/codewiki/code-nav-surface.ts +29 -0
- package/src/tools/codewiki/code-nav.ts +8 -22
- package/src/tools/codewiki/shared.ts +41 -38
- package/src/tools/context/docs-engine.ts +14 -3
- package/src/tools/context/index.ts +107 -28
- package/src/tools/context/surface.ts +19 -0
- package/src/tools/core-bootstrap.ts +168 -0
- package/src/tools/credential-present.ts +5 -5
- package/src/tools/dispatch-admission.ts +533 -0
- package/src/tools/dispatch-background.ts +54 -0
- package/src/tools/dispatch-event-text.ts +6 -0
- package/src/tools/dispatch-plan.ts +9 -4
- package/src/tools/dispatch-run-events.ts +238 -0
- package/src/tools/dispatch-runner.ts +2370 -0
- package/src/tools/dispatch-scout-admission.ts +295 -0
- package/src/tools/dispatch-types.ts +77 -0
- package/src/tools/dispatch.ts +67 -3161
- package/src/tools/find.ts +4 -2
- package/src/tools/grep.ts +2 -2
- package/src/tools/lazy-tool.ts +60 -0
- package/src/tools/ledger.ts +3 -3
- package/src/tools/monitor-surface.ts +36 -0
- package/src/tools/monitor.ts +2 -32
- package/src/tools/observers.ts +2 -2
- package/src/tools/registry.ts +39 -27
- package/src/tools/safe-exec.ts +2 -2
- package/src/tools/steer-surface.ts +17 -0
- package/src/tools/steer.ts +2 -13
- package/src/tools/tasks.ts +108 -11
- package/src/tools/truncate.ts +25 -184
- package/src/tools/verify/frontend.ts +3 -1
- package/src/tools/verify/index.ts +3 -38
- package/src/tools/verify/surface.ts +46 -0
- package/src/tools/web-fetch-surface.ts +23 -0
- package/src/tools/web-fetch.ts +2 -20
- package/src/tools/write.ts +7 -2
- package/src/worker/entry.ts +39 -2
- package/src/worker/spec-contract.ts +26 -5
- package/dist/chunk-7SS2CTV2.js +0 -61361
- package/dist/chunk-DKGKUHFA.js +0 -924
- package/dist/chunk-GEP36Y4X.js +0 -12796
- package/dist/chunk-XYWBQRDM.js +0 -137
- package/dist/clio-BZVGEUFJ.js +0 -58
- package/dist/configure-S7S6F6CL.js +0 -32
- package/docs/html/agents_blueprint.html +0 -936
- package/docs/html/alcf_blueprint.html +0 -324
- package/docs/html/architecture_blueprint.html +0 -850
- package/docs/html/commands_blueprint.html +0 -939
- package/docs/html/config_knobs_audit_blueprint.html +0 -178
- package/docs/html/configuration_blueprint.html +0 -1080
- package/docs/html/context_blueprint.html +0 -603
- package/docs/html/documentation_blueprint.html +0 -832
- package/docs/html/environment_blueprint.html +0 -404
- package/docs/html/eval_blueprint.html +0 -743
- package/docs/html/evals_internal_blueprint.html +0 -190
- package/docs/html/evolution_blueprint.html +0 -674
- package/docs/html/extensions_blueprint.html +0 -2065
- package/docs/html/fleet_dispatch_blueprint.html +0 -286
- package/docs/html/index.html +0 -919
- package/docs/html/lifecycle_blueprint.html +0 -723
- package/docs/html/memory_blueprint.html +0 -699
- package/docs/html/middleware_blueprint.html +0 -664
- package/docs/html/models_blueprint.html +0 -2366
- package/docs/html/observability_blueprint.html +0 -683
- package/docs/html/provider_adapter_blueprint.html +0 -245
- package/docs/html/safety_blueprint.html +0 -1386
- package/docs/html/shared.css +0 -571
- package/docs/html/shared.js +0 -143
- package/docs/html/skills_blueprint.html +0 -671
- package/docs/html/soak_blueprint.html +0 -182
- package/docs/html/tool_usage_blueprint.html +0 -350
- package/docs/html/tools_blueprint.html +0 -2249
- package/docs/html/trace_blueprint.html +0 -235
- package/docs/html/tui_design_blueprint.html +0 -374
- package/docs/html/validation_blueprint.html +0 -961
- package/docs/html/worker_dispatch_blueprint.html +0 -231
- package/src/core/release.ts +0 -2
- package/src/domains/providers/runtimes/common/lmstudio-logger.ts +0 -32
- package/src/domains/providers/runtimes/local-native/lmstudio-native.ts +0 -491
- package/src/engine/apis/lmstudio-native.ts +0 -1438
- package/src/engine/apis/thinking-replay.ts +0 -11
- package/src/tools/string-enum.ts +0 -15
package/src/tools/dispatch.ts
CHANGED
|
@@ -1,2570 +1,63 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { Type } from "typebox";
|
|
5
|
-
import {
|
|
6
|
-
import type { SafeEventBus } from "../core/event-bus.js";
|
|
2
|
+
import { incompleteInstallationAdvice } from "../core/incomplete-installation.js";
|
|
7
3
|
import { ToolNames } from "../core/tool-names.js";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
DispatchContract,
|
|
15
|
-
DispatchPlanTaskResolution,
|
|
16
|
-
DispatchRequest,
|
|
17
|
-
} from "../domains/dispatch/contract.js";
|
|
18
|
-
import { durableAssistantTextFromEvent } from "../domains/dispatch/event-pump.js";
|
|
19
|
-
import { compileExecutionPlan, type ExecutionPlan, requireAgentSteps } from "../domains/dispatch/execution-plan.js";
|
|
20
|
-
import {
|
|
21
|
-
type AgentRoleFactsResolver,
|
|
22
|
-
gateDeciderAgentId,
|
|
23
|
-
gateRouteCorrelation,
|
|
24
|
-
type RouteCorrelationFacts,
|
|
25
|
-
} from "../domains/dispatch/execution-role.js";
|
|
26
|
-
import {
|
|
27
|
-
decideReviewGate,
|
|
28
|
-
finalizePendingGateDecision,
|
|
29
|
-
type GateDecisionArtifact,
|
|
30
|
-
type GateDecisionCorrelation,
|
|
31
|
-
type GateDecisionDraft,
|
|
32
|
-
materializePendingGateDecision,
|
|
33
|
-
type PendingGateDecisionHandle,
|
|
34
|
-
parseCompeteGateResult,
|
|
35
|
-
preflightPendingGateDecisionMaterialization,
|
|
36
|
-
preparePendingGateDecisionRecovery,
|
|
37
|
-
readGateDecisionArtifacts,
|
|
38
|
-
resolvePendingGateDecision,
|
|
39
|
-
stagePendingGateDecision,
|
|
40
|
-
stagePendingGateOutput,
|
|
41
|
-
} from "../domains/dispatch/gate-decisions.js";
|
|
42
|
-
import {
|
|
43
|
-
COMPETE_STANCES,
|
|
44
|
-
type CompeteStance,
|
|
45
|
-
JUDGE_GATE_PROMPT,
|
|
46
|
-
REVIEWER_GATE_PROMPT,
|
|
47
|
-
} from "../domains/dispatch/gate-role-prompts.js";
|
|
48
|
-
import { UNVERIFIABLE_RECEIPT_VERIFICATION } from "../domains/dispatch/receipt-findings.js";
|
|
49
|
-
import { type ReceiptIntegrityResult, verifyReceiptIntegrity } from "../domains/dispatch/receipt-integrity.js";
|
|
50
|
-
import { approvedRouteCandidates } from "../domains/dispatch/route-approval.js";
|
|
51
|
-
import { defaultRoutingIntent, explainRouteDecision } from "../domains/dispatch/routing-intent.js";
|
|
52
|
-
import type { RunGateProvenance, RunGateSubjectRef, RunPlanProvenance, RunReceipt } from "../domains/dispatch/types.js";
|
|
53
|
-
import { DISPATCH_BRIEFING_MAX_BYTES, type DispatchFailoverMode } from "../domains/dispatch/validation.js";
|
|
54
|
-
import { extractRunProvenance, provenanceCompactSuffix } from "../domains/evidence/provenance.js";
|
|
55
|
-
import type { AutonomyLevel } from "../domains/safety/autonomy.js";
|
|
56
|
-
import {
|
|
57
|
-
type CandidateWorktree,
|
|
58
|
-
type CompeteGroupOwnership,
|
|
59
|
-
candidateDiffStat,
|
|
60
|
-
claimCompeteGroup,
|
|
61
|
-
cleanupCompeteGroup,
|
|
62
|
-
commitCandidateWork,
|
|
63
|
-
createCandidateWorktree,
|
|
64
|
-
isGitRepository,
|
|
65
|
-
loadCompeteGroup,
|
|
66
|
-
markCompeteGroupCleanupReady,
|
|
67
|
-
markCompeteGroupWinnerPreserved,
|
|
68
|
-
mergeWinnerBranch,
|
|
69
|
-
protectedPathsChangedByCompeteBranch,
|
|
70
|
-
recoverCleanupReadyCompeteGroups,
|
|
71
|
-
registerCompeteGroupRun,
|
|
72
|
-
removeCandidateWorktree,
|
|
73
|
-
settleCompeteGroupRun,
|
|
74
|
-
settleRecoveredCompeteDecision,
|
|
75
|
-
} from "./compete-worktrees.js";
|
|
76
|
-
import {
|
|
77
|
-
dispatchRequestsFromArgs,
|
|
78
|
-
maxOutputBytesArg,
|
|
79
|
-
prepareDispatchArguments,
|
|
80
|
-
stringArg,
|
|
81
|
-
timeoutMsArg,
|
|
82
|
-
} from "./dispatch-arguments.js";
|
|
83
|
-
import {
|
|
84
|
-
DISPATCH_PLAN_PREPARATION_ERROR_ARGUMENT,
|
|
85
|
-
describeDispatchPlan,
|
|
86
|
-
RESOLVED_DISPATCH_PLAN_ARGUMENT,
|
|
87
|
-
type ResolvedDispatchPlanArtifact,
|
|
88
|
-
resolvedDispatchPlanFromArgs,
|
|
89
|
-
withResolvedDispatchPlan,
|
|
90
|
-
withResolvedPlanTaskPin,
|
|
91
|
-
} from "./dispatch-plan.js";
|
|
92
|
-
import {
|
|
93
|
-
loadVerifiedScoutSource,
|
|
94
|
-
prepareScoutContinuation,
|
|
95
|
-
runScoutContinuationPlan,
|
|
96
|
-
scoutContinuationRefFromArgs,
|
|
97
|
-
scoutPlanAuthorityGranted,
|
|
98
|
-
scoutTransitionDetail,
|
|
99
|
-
} from "./dispatch-scout.js";
|
|
4
|
+
import { DISPATCH_BRIEFING_MAX_BYTES } from "../domains/dispatch/validation.js";
|
|
5
|
+
import { StringEnum } from "../engine/ai.js";
|
|
6
|
+
import { createDispatchAdmissionController } from "./dispatch-admission.js";
|
|
7
|
+
import { DISPATCH_PLAN_PREPARATION_ERROR_ARGUMENT } from "./dispatch-plan.js";
|
|
8
|
+
import { createDispatchRunEventRegistry } from "./dispatch-run-events.js";
|
|
9
|
+
import type { DispatchToolDeps } from "./dispatch-types.js";
|
|
100
10
|
import { TOOL_PROFILE_NAMES } from "./profiles.js";
|
|
101
|
-
import type {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
11
|
+
import type { ToolSpec } from "./registry.js";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
DispatchBackgroundControl,
|
|
15
|
+
DispatchBackgroundOutcome,
|
|
16
|
+
DispatchBackgroundRegistry,
|
|
17
|
+
} from "./dispatch-background.js";
|
|
18
|
+
export {
|
|
19
|
+
createDispatchRunEventRegistry,
|
|
20
|
+
type DispatchEventSummary,
|
|
21
|
+
type DispatchRunEventRegistry,
|
|
22
|
+
type RunTailEntry,
|
|
23
|
+
} from "./dispatch-run-events.js";
|
|
24
|
+
export type { DispatchToolDeps } from "./dispatch-types.js";
|
|
110
25
|
|
|
111
|
-
const TRUNCATION_MARKER = "\n[agent output truncated]";
|
|
112
26
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
113
27
|
|
|
114
28
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
29
|
+
* Stable, lightweight dispatch surface. Admission remains synchronous so the
|
|
30
|
+
* policy decision and provisional reservation are bound to the exact argument
|
|
31
|
+
* object which the dynamically imported runner later consumes.
|
|
118
32
|
*/
|
|
119
|
-
|
|
33
|
+
type DispatchRunnerModule = Pick<typeof import("./dispatch-runner.js"), "runDispatchTool">;
|
|
120
34
|
|
|
121
|
-
export interface
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
/** Operator-facing name of the call, used in both outcome messages. */
|
|
125
|
-
label: string;
|
|
126
|
-
convert(): DispatchBackgroundOutcome;
|
|
35
|
+
export interface DispatchToolSurfaceOptions {
|
|
36
|
+
/** Test-only loader seam for proving admission failures do not load the implementation graph. */
|
|
37
|
+
loadRunner?: () => Promise<DispatchRunnerModule>;
|
|
127
38
|
}
|
|
128
39
|
|
|
129
|
-
|
|
130
|
-
* Live attached dispatches that the operator may send to the background. An
|
|
131
|
-
* attached `dispatch` call registers one control for as long as it awaits its
|
|
132
|
-
* runs; the TUI keybinding fires the newest one. Absent from a deps bundle
|
|
133
|
-
* (workers, headless) nothing registers and attached dispatch behaves exactly
|
|
134
|
-
* as it did before.
|
|
135
|
-
*/
|
|
136
|
-
export interface DispatchBackgroundRegistry {
|
|
137
|
-
/** Returns the deregistration handle; calling it more than once is harmless. */
|
|
138
|
-
register(control: DispatchBackgroundControl): () => void;
|
|
139
|
-
/** Fire the newest still-registered control. */
|
|
140
|
-
backgroundNewest(): DispatchBackgroundOutcome;
|
|
141
|
-
/** How many attached dispatches are currently registered. */
|
|
142
|
-
size(): number;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function createDispatchBackgroundRegistry(): DispatchBackgroundRegistry {
|
|
146
|
-
// Map insertion order is registration order, and the newest attached dispatch
|
|
147
|
-
// is the one the operator just watched start, so the keypress takes the last
|
|
148
|
-
// entry rather than asking a UI projection which segment is newest.
|
|
149
|
-
const controls = new Map<string, DispatchBackgroundControl>();
|
|
150
|
-
return {
|
|
151
|
-
register(control) {
|
|
152
|
-
controls.set(control.toolCallId, control);
|
|
153
|
-
return () => {
|
|
154
|
-
if (controls.get(control.toolCallId) === control) controls.delete(control.toolCallId);
|
|
155
|
-
};
|
|
156
|
-
},
|
|
157
|
-
backgroundNewest() {
|
|
158
|
-
let newest: DispatchBackgroundControl | undefined;
|
|
159
|
-
for (const control of controls.values()) newest = control;
|
|
160
|
-
if (newest === undefined) {
|
|
161
|
-
return { ok: false, message: "background: no attached dispatch is running" };
|
|
162
|
-
}
|
|
163
|
-
return newest.convert();
|
|
164
|
-
},
|
|
165
|
-
size: () => controls.size,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* One-shot latch an attached executor races against its runs. Firing it never
|
|
171
|
-
* aborts anything: the registry drain that meters the runs was never owned by
|
|
172
|
-
* the awaiting code path, so unwinding the await leaves token metering, run
|
|
173
|
-
* tails, and bus progress flowing.
|
|
174
|
-
*/
|
|
175
|
-
interface BackgroundSwitch {
|
|
176
|
-
/** Resolves when the operator fires the control; never rejects. */
|
|
177
|
-
readonly requested: Promise<void>;
|
|
178
|
-
fire(): void;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function createBackgroundSwitch(): BackgroundSwitch {
|
|
182
|
-
let fired = false;
|
|
183
|
-
let signal = (): void => {};
|
|
184
|
-
const requested = new Promise<void>((resolve) => {
|
|
185
|
-
signal = resolve;
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
requested,
|
|
189
|
-
fire: () => {
|
|
190
|
-
if (fired) return;
|
|
191
|
-
fired = true;
|
|
192
|
-
signal();
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export interface DispatchToolDeps {
|
|
198
|
-
dispatch: DispatchContract;
|
|
199
|
-
bus?: SafeEventBus;
|
|
200
|
-
/** Instance-scoped owner for ordinary tool-owned run streams and monitor tails. */
|
|
201
|
-
runEvents?: DispatchRunEventRegistry;
|
|
202
|
-
/**
|
|
203
|
-
* Operator-initiated backgrounding of an attached call. Wired only by the
|
|
204
|
-
* interactive composition root, because only that process has a keypress to
|
|
205
|
-
* fire the control with.
|
|
206
|
-
*/
|
|
207
|
-
background?: DispatchBackgroundRegistry;
|
|
208
|
-
/** Optional compete storage overrides for alternate backends and deterministic fault tests. */
|
|
209
|
-
competeWorktrees?: {
|
|
210
|
-
createCandidate?: typeof createCandidateWorktree;
|
|
211
|
-
cleanupGroup?: typeof cleanupCompeteGroup;
|
|
212
|
-
mergeWinner?: typeof mergeWinnerBranch;
|
|
213
|
-
};
|
|
214
|
-
getAgentCatalog?: () => string;
|
|
215
|
-
getAgentSpecs: () => ReadonlyArray<AgentSpec>;
|
|
216
|
-
getAgentRoleFacts?: AgentRoleFactsResolver;
|
|
217
|
-
/**
|
|
218
|
-
* Session-effective autonomy level. Plan provenance records how the plan
|
|
219
|
-
* gate resolved (operator approval versus full-auto logging) and compete
|
|
220
|
-
* decides whether to apply the judge's pick or hand the winner to the
|
|
221
|
-
* operator. Absent (minimal test bundles) defaults to auto-edit.
|
|
222
|
-
*/
|
|
223
|
-
getAutonomy?: () => AutonomyLevel;
|
|
224
|
-
/** Scheduling cost ceiling recorded on plan provenance when available. */
|
|
225
|
-
getCostCeilingUsd?: () => number;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
interface EventSummary {
|
|
229
|
-
count: number;
|
|
230
|
-
types: string[];
|
|
231
|
-
lastAssistantText: string;
|
|
232
|
-
/** Immutable run id of the latest attempt represented by this assignment stream. */
|
|
233
|
-
terminalAttemptRunId: string;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
237
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
type ResolvedPlanTask = ResolvedDispatchPlanArtifact["tasks"][number];
|
|
241
|
-
|
|
242
|
-
// In-process rolling tail of recent worker events, per run. Fed by the
|
|
243
|
-
// dispatch tool's event consumption (the same stream the dispatch board
|
|
244
|
-
// renders from the bus); read by monitor(mode="peek"). Bounded both per run
|
|
245
|
-
// and across runs so long fleets cannot grow memory.
|
|
246
|
-
const RUN_TAIL_ENTRY_LIMIT = 100;
|
|
247
|
-
const RUN_TAIL_RUN_LIMIT = 64;
|
|
248
|
-
const RUN_TAIL_TEXT_LIMIT = 600;
|
|
249
|
-
|
|
250
|
-
export interface RunTailEntry {
|
|
251
|
-
at: string;
|
|
252
|
-
type: string;
|
|
253
|
-
detail?: string;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
interface RunTailState {
|
|
257
|
-
agentId: string;
|
|
258
|
-
entries: RunTailEntry[];
|
|
259
|
-
lastSeenAt: number;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function eventDetail(event: unknown): string | undefined {
|
|
263
|
-
const text = assistantTextFromEvent(event);
|
|
264
|
-
if (text.length > 0) return truncateUtf8(text, RUN_TAIL_TEXT_LIMIT, "...");
|
|
265
|
-
if (!isRecord(event)) return undefined;
|
|
266
|
-
if (event.type === "clio_tool_finish" && isRecord(event.payload)) {
|
|
267
|
-
const tool = typeof event.payload.tool === "string" ? event.payload.tool : "tool";
|
|
268
|
-
const outcome = typeof event.payload.outcome === "string" ? event.payload.outcome : "";
|
|
269
|
-
return `${tool} ${outcome}`.trim();
|
|
270
|
-
}
|
|
271
|
-
if (event.type === "attempt_start") {
|
|
272
|
-
const attempt = typeof event.attempt === "number" ? event.attempt : "?";
|
|
273
|
-
const runId = typeof event.runId === "string" ? event.runId : "?";
|
|
274
|
-
const reason = typeof event.reason === "string" ? event.reason : "retry";
|
|
275
|
-
return truncateUtf8(`attempt ${attempt} -> ${runId}: ${reason}`, RUN_TAIL_TEXT_LIMIT, "...");
|
|
276
|
-
}
|
|
277
|
-
return undefined;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
interface RegisteredSingleDispatch {
|
|
281
|
-
runId: string;
|
|
282
|
-
completion: Promise<{ receipt: RunReceipt; summary: EventSummary }>;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
interface RegisteredBatchDispatch {
|
|
286
|
-
batchId: string;
|
|
287
|
-
assignmentIds: ReadonlyArray<string>;
|
|
288
|
-
completion: Promise<{ receipts: ReadonlyArray<RunReceipt>; summaries: Map<string, EventSummary> }>;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Instance-scoped owner of ordinary model-facing dispatch iterators and their
|
|
293
|
-
* monitor tails. Registering a handle starts its sole drain immediately; sync
|
|
294
|
-
* callers await `completion` while detached callers retain that same drain.
|
|
295
|
-
*
|
|
296
|
-
* Review and compete coordinators are intentional exceptions: they drain each
|
|
297
|
-
* gate-sensitive iterator directly, stage reviewer/judge output, and only then
|
|
298
|
-
* await the receipt. They use `recordEvent` to retain monitor-tail projection
|
|
299
|
-
* without surrendering iterator ownership to this registry.
|
|
300
|
-
*/
|
|
301
|
-
export interface DispatchRunEventRegistry {
|
|
302
|
-
registerSingle(
|
|
303
|
-
handle: Awaited<ReturnType<DispatchContract["dispatch"]>>,
|
|
304
|
-
agentId: string,
|
|
305
|
-
bus?: SafeEventBus,
|
|
306
|
-
): RegisteredSingleDispatch;
|
|
307
|
-
registerBatch(
|
|
308
|
-
handle: Awaited<ReturnType<DispatchContract["dispatchBatch"]>>,
|
|
309
|
-
agentIds: ReadonlyArray<string>,
|
|
310
|
-
bus?: SafeEventBus,
|
|
311
|
-
): RegisteredBatchDispatch;
|
|
312
|
-
recordEvent(runId: string, agentId: string, event: unknown): void;
|
|
313
|
-
eventTail(runId: string): { agentId: string; entries: ReadonlyArray<RunTailEntry> } | null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function createDispatchRunEventRegistry(): DispatchRunEventRegistry {
|
|
317
|
-
const runTails = new Map<string, RunTailState>();
|
|
318
|
-
const activeRuns = new Set<string>();
|
|
319
|
-
const activeBatches = new Set<string>();
|
|
320
|
-
|
|
321
|
-
const pruneRunTails = (): void => {
|
|
322
|
-
while (runTails.size > RUN_TAIL_RUN_LIMIT) {
|
|
323
|
-
let oldestKey: string | null = null;
|
|
324
|
-
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
325
|
-
for (const [key, state] of runTails) {
|
|
326
|
-
if (activeRuns.has(key) || state.lastSeenAt >= oldestSeen) continue;
|
|
327
|
-
oldestKey = key;
|
|
328
|
-
oldestSeen = state.lastSeenAt;
|
|
329
|
-
}
|
|
330
|
-
if (oldestKey === null) break;
|
|
331
|
-
runTails.delete(oldestKey);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const recordRunEvent = (runId: string, agentId: string, event: unknown): void => {
|
|
336
|
-
const type = isRecord(event) && typeof event.type === "string" ? event.type : "unknown";
|
|
337
|
-
if (type === "heartbeat" || type === "message_update") return;
|
|
338
|
-
const state = runTails.get(runId) ?? { agentId, entries: [], lastSeenAt: Date.now() };
|
|
339
|
-
state.lastSeenAt = Date.now();
|
|
340
|
-
const entry: RunTailEntry = { at: new Date().toISOString(), type };
|
|
341
|
-
const detail = eventDetail(event);
|
|
342
|
-
if (detail !== undefined) entry.detail = detail;
|
|
343
|
-
state.entries.push(entry);
|
|
344
|
-
if (state.entries.length > RUN_TAIL_ENTRY_LIMIT) {
|
|
345
|
-
state.entries.splice(0, state.entries.length - RUN_TAIL_ENTRY_LIMIT);
|
|
346
|
-
}
|
|
347
|
-
runTails.set(runId, state);
|
|
348
|
-
pruneRunTails();
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const drainSingle = async (
|
|
352
|
-
runId: string,
|
|
353
|
-
agentId: string,
|
|
354
|
-
events: AsyncIterableIterator<unknown>,
|
|
355
|
-
bus: SafeEventBus | undefined,
|
|
356
|
-
): Promise<EventSummary> => {
|
|
357
|
-
const summary: EventSummary = { count: 0, types: [], lastAssistantText: "", terminalAttemptRunId: runId };
|
|
358
|
-
for await (const event of events) {
|
|
359
|
-
summary.count += 1;
|
|
360
|
-
const type = isRecord(event) && typeof event.type === "string" ? event.type : "unknown";
|
|
361
|
-
summary.types.push(type);
|
|
362
|
-
// A superseded attempt's answer is not the assignment's answer: the
|
|
363
|
-
// failover marker discards it before the next attempt speaks.
|
|
364
|
-
if (type === "attempt_start") summary.lastAssistantText = "";
|
|
365
|
-
const text = rawAssistantTextFromEvent(event);
|
|
366
|
-
if (text.trim().length > 0) summary.lastAssistantText = text;
|
|
367
|
-
recordRunEvent(runId, agentId, event);
|
|
368
|
-
if (type !== "heartbeat") bus?.emit(BusChannels.DispatchProgress, { runId, agentId, event });
|
|
369
|
-
}
|
|
370
|
-
return summary;
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const drainBatch = async (
|
|
374
|
-
batchId: string,
|
|
375
|
-
events: AsyncIterableIterator<unknown>,
|
|
376
|
-
bus: SafeEventBus | undefined,
|
|
377
|
-
): Promise<Map<string, EventSummary>> => {
|
|
378
|
-
const summaries = new Map<string, EventSummary>();
|
|
379
|
-
for await (const event of events) {
|
|
380
|
-
if (!isRecord(event) || event.type !== "batch_run_event") continue;
|
|
381
|
-
const runId = typeof event.runId === "string" ? event.runId : batchId;
|
|
382
|
-
const agentId = typeof event.agentId === "string" ? event.agentId : "batch";
|
|
383
|
-
const inner = event.event;
|
|
384
|
-
const summary = summaries.get(runId) ?? {
|
|
385
|
-
count: 0,
|
|
386
|
-
types: [],
|
|
387
|
-
lastAssistantText: "",
|
|
388
|
-
terminalAttemptRunId: runId,
|
|
389
|
-
};
|
|
390
|
-
summary.count += 1;
|
|
391
|
-
const type = isRecord(inner) && typeof inner.type === "string" ? inner.type : "unknown";
|
|
392
|
-
summary.types.push(type);
|
|
393
|
-
const text = rawAssistantTextFromEvent(inner);
|
|
394
|
-
if (text.trim().length > 0) summary.lastAssistantText = text;
|
|
395
|
-
summaries.set(runId, summary);
|
|
396
|
-
recordRunEvent(runId, agentId, inner);
|
|
397
|
-
if (type !== "heartbeat") bus?.emit(BusChannels.DispatchProgress, { runId, agentId, event: inner });
|
|
398
|
-
}
|
|
399
|
-
return summaries;
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
return {
|
|
403
|
-
registerSingle(handle, agentId, bus) {
|
|
404
|
-
if (activeRuns.has(handle.runId)) {
|
|
405
|
-
throw new Error(`dispatch event registry: run '${handle.runId}' is already registered`);
|
|
406
|
-
}
|
|
407
|
-
activeRuns.add(handle.runId);
|
|
408
|
-
const summaryPromise = drainSingle(handle.runId, agentId, handle.events, bus);
|
|
409
|
-
const completion = Promise.allSettled([summaryPromise, handle.finalPromise]).then(
|
|
410
|
-
([summaryResult, receiptResult]) => {
|
|
411
|
-
if (summaryResult.status === "rejected") throw summaryResult.reason;
|
|
412
|
-
if (receiptResult.status === "rejected") throw receiptResult.reason;
|
|
413
|
-
return { receipt: receiptResult.value, summary: summaryResult.value };
|
|
414
|
-
},
|
|
415
|
-
);
|
|
416
|
-
void completion
|
|
417
|
-
.finally(() => {
|
|
418
|
-
activeRuns.delete(handle.runId);
|
|
419
|
-
pruneRunTails();
|
|
420
|
-
})
|
|
421
|
-
.catch(() => {});
|
|
422
|
-
return { runId: handle.runId, completion };
|
|
423
|
-
},
|
|
424
|
-
registerBatch(handle, agentIds, bus) {
|
|
425
|
-
if (activeBatches.has(handle.batchId)) {
|
|
426
|
-
throw new Error(`dispatch event registry: batch '${handle.batchId}' is already registered`);
|
|
427
|
-
}
|
|
428
|
-
const duplicateRunId = handle.assignmentIds.find((runId, index) =>
|
|
429
|
-
handle.assignmentIds.slice(0, index).includes(runId),
|
|
430
|
-
);
|
|
431
|
-
if (duplicateRunId !== undefined) {
|
|
432
|
-
throw new Error(`dispatch event registry: batch '${handle.batchId}' repeats run '${duplicateRunId}'`);
|
|
433
|
-
}
|
|
434
|
-
const activeRunId = handle.assignmentIds.find((runId) => activeRuns.has(runId));
|
|
435
|
-
if (activeRunId !== undefined) {
|
|
436
|
-
throw new Error(`dispatch event registry: run '${activeRunId}' is already registered`);
|
|
437
|
-
}
|
|
438
|
-
activeBatches.add(handle.batchId);
|
|
439
|
-
for (const runId of handle.assignmentIds) activeRuns.add(runId);
|
|
440
|
-
const completion = Promise.allSettled([drainBatch(handle.batchId, handle.events, bus), handle.finalPromise]).then(
|
|
441
|
-
([summariesResult, receiptsResult]) => {
|
|
442
|
-
if (summariesResult.status === "rejected") throw summariesResult.reason;
|
|
443
|
-
if (receiptsResult.status === "rejected") throw receiptsResult.reason;
|
|
444
|
-
return { receipts: receiptsResult.value, summaries: summariesResult.value };
|
|
445
|
-
},
|
|
446
|
-
);
|
|
447
|
-
void completion
|
|
448
|
-
.finally(() => {
|
|
449
|
-
activeBatches.delete(handle.batchId);
|
|
450
|
-
for (const runId of handle.assignmentIds) activeRuns.delete(runId);
|
|
451
|
-
pruneRunTails();
|
|
452
|
-
})
|
|
453
|
-
.catch(() => {});
|
|
454
|
-
// Seed agent routing even when a run completes before its first event.
|
|
455
|
-
for (const [index, runId] of handle.assignmentIds.entries()) {
|
|
456
|
-
if (!runTails.has(runId)) {
|
|
457
|
-
runTails.set(runId, { agentId: agentIds[index] ?? "unknown", entries: [], lastSeenAt: Date.now() });
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return { batchId: handle.batchId, assignmentIds: handle.assignmentIds, completion };
|
|
461
|
-
},
|
|
462
|
-
recordEvent: recordRunEvent,
|
|
463
|
-
eventTail(runId) {
|
|
464
|
-
const state = runTails.get(runId);
|
|
465
|
-
if (!state) return null;
|
|
466
|
-
return { agentId: state.agentId, entries: [...state.entries] };
|
|
467
|
-
},
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function rawAssistantTextFromEvent(event: unknown): string {
|
|
472
|
-
return durableAssistantTextFromEvent(event);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export function assistantTextFromEvent(event: unknown): string {
|
|
476
|
-
return rawAssistantTextFromEvent(event).trim();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function normalizedAssistantText(summary: EventSummary): string {
|
|
480
|
-
return summary.lastAssistantText.trim();
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
type RegisteredDispatchToolDeps = DispatchToolDeps & { runEvents: DispatchRunEventRegistry };
|
|
484
|
-
|
|
485
|
-
function fallbackProgressBus(deps: DispatchToolDeps): SafeEventBus | undefined {
|
|
486
|
-
return deps.dispatch.ownsProgressBus?.(deps.bus) === true ? undefined : deps.bus;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Gate-sensitive coordinator drain. Review and compete must finish this drain
|
|
491
|
-
* and stage pending decision output before awaiting the receipt-facing final
|
|
492
|
-
* promise; the ordinary registry's concurrent join cannot provide that edge.
|
|
493
|
-
*/
|
|
494
|
-
async function consumeGateSensitiveDispatchEvents(
|
|
495
|
-
deps: RegisteredDispatchToolDeps,
|
|
496
|
-
runId: string,
|
|
497
|
-
agentId: string,
|
|
498
|
-
events: AsyncIterableIterator<unknown>,
|
|
499
|
-
): Promise<EventSummary> {
|
|
500
|
-
const summary: EventSummary = { count: 0, types: [], lastAssistantText: "", terminalAttemptRunId: runId };
|
|
501
|
-
const bus = fallbackProgressBus(deps);
|
|
502
|
-
for await (const event of events) {
|
|
503
|
-
summary.count += 1;
|
|
504
|
-
const type = isRecord(event) && typeof event.type === "string" ? event.type : "unknown";
|
|
505
|
-
summary.types.push(type);
|
|
506
|
-
if (type === "attempt_start" && isRecord(event) && typeof event.runId === "string" && event.runId.length > 0) {
|
|
507
|
-
summary.terminalAttemptRunId = event.runId;
|
|
508
|
-
}
|
|
509
|
-
const text = rawAssistantTextFromEvent(event);
|
|
510
|
-
if (text.trim().length > 0) summary.lastAssistantText = text;
|
|
511
|
-
deps.runEvents.recordEvent(runId, agentId, event);
|
|
512
|
-
if (type !== "heartbeat") bus?.emit(BusChannels.DispatchProgress, { runId, agentId, event });
|
|
513
|
-
}
|
|
514
|
-
return summary;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Fan out without waiting: admit and spawn every task, persist the durable
|
|
519
|
-
* batch record, then hand the merged event stream to a background drain so
|
|
520
|
-
* token metering, run tails (monitor peek), and bus progress keep flowing
|
|
521
|
-
* while the orchestrator's turn continues. The tool result carries only the
|
|
522
|
-
* batch id and run ids; results are gathered later through monitor
|
|
523
|
-
* mode="wait"/"collect", in this session or after a resume.
|
|
524
|
-
*/
|
|
525
|
-
async function runDetached(
|
|
526
|
-
deps: RegisteredDispatchToolDeps,
|
|
527
|
-
requests: ReadonlyArray<DispatchRequest>,
|
|
528
|
-
sessionId: string | null,
|
|
529
|
-
): Promise<ToolResult> {
|
|
530
|
-
const detached = deps.dispatch.detached;
|
|
531
|
-
if (!detached) {
|
|
532
|
-
return { kind: "error", message: "dispatch: detach is not supported in this context" };
|
|
533
|
-
}
|
|
534
|
-
// Detached runs are concurrent peers too, so they get a board whenever there
|
|
535
|
-
// is more than one of them. Collection closes it, which can happen in a later
|
|
536
|
-
// process, so the batch record carries the id.
|
|
537
|
-
const ledgerId = requests.length >= 2 ? newGateGroupId("ledger") : null;
|
|
538
|
-
if (ledgerId !== null) await openAgentLedger(ledgerId);
|
|
539
|
-
const ledgered =
|
|
540
|
-
ledgerId === null ? requests : requests.map((request) => ({ ...request, ledger: { id: ledgerId, sequence: 0 } }));
|
|
541
|
-
const handle = await deps.dispatch.dispatchBatch(ledgered);
|
|
542
|
-
const registered = deps.runEvents.registerBatch(
|
|
543
|
-
handle,
|
|
544
|
-
ledgered.map((request) => request.agentId),
|
|
545
|
-
fallbackProgressBus(deps),
|
|
546
|
-
);
|
|
547
|
-
const assignmentIds = handle.assignmentIds;
|
|
548
|
-
const runs = handle.assignmentIds.map((runId, index) => ({
|
|
549
|
-
runId,
|
|
550
|
-
assignmentId: assignmentIds[index] ?? runId,
|
|
551
|
-
agentId: requests[index]?.agentId ?? "unknown",
|
|
552
|
-
}));
|
|
553
|
-
registered.completion.catch(() => {});
|
|
554
|
-
// The runs are live; make their assignment records durable before returning
|
|
555
|
-
// so an immediate collect resolves the logical assignment (and any queued
|
|
556
|
-
// retry) instead of falling back to a bare, possibly-failed first attempt.
|
|
557
|
-
await deps.dispatch.assignments?.flushWrites?.();
|
|
558
|
-
try {
|
|
559
|
-
await detached.register({
|
|
560
|
-
batchId: handle.batchId,
|
|
561
|
-
runs,
|
|
562
|
-
sessionId,
|
|
563
|
-
...(ledgerId !== null ? { ledgerId } : {}),
|
|
564
|
-
});
|
|
565
|
-
} catch (err) {
|
|
566
|
-
// The runs are already live; report the durability gap instead of
|
|
567
|
-
// pretending the batch does not exist.
|
|
568
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
569
|
-
return {
|
|
570
|
-
kind: "error",
|
|
571
|
-
message: `dispatch: detached assignments started (batch=${handle.batchId}, assignments=${handle.assignmentIds.join(", ")}) but the durable batch record failed: ${message}`,
|
|
572
|
-
details: { mode: "detached", batchId: handle.batchId, assignmentIds: [...handle.assignmentIds] },
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
const lines = [
|
|
576
|
-
`dispatch (detached) batch=${handle.batchId} started ${runs.length} run(s)`,
|
|
577
|
-
...runs.map((run) => `- ${run.assignmentId} agent=${run.agentId}`),
|
|
578
|
-
"",
|
|
579
|
-
`Runs continue in the background. Collect results with monitor(mode="collect", batch_id="${handle.batchId}"); block on one run with monitor(mode="wait", run_id=<id>).`,
|
|
580
|
-
];
|
|
581
|
-
return {
|
|
582
|
-
kind: "ok",
|
|
583
|
-
output: lines.join("\n"),
|
|
584
|
-
details: {
|
|
585
|
-
mode: "detached",
|
|
586
|
-
batchId: handle.batchId,
|
|
587
|
-
assignmentIds: [...handle.assignmentIds],
|
|
588
|
-
runs: runs.map((run) => ({ runId: run.runId, assignmentId: run.assignmentId, agentId: run.agentId })),
|
|
589
|
-
},
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Publish this call's conversion control for as long as its topology branch
|
|
595
|
-
* runs. A refusing control still registers: the operator who pressed the key
|
|
596
|
-
* needs the topology's reason, and an unregistered call would answer with the
|
|
597
|
-
* "nothing running" no-op instead.
|
|
598
|
-
*/
|
|
599
|
-
function registerBackgroundControl(
|
|
600
|
-
deps: DispatchToolDeps,
|
|
601
|
-
toolCallId: string | undefined,
|
|
602
|
-
plan: { label: string; refusal: string | null; fire: () => void },
|
|
603
|
-
): () => void {
|
|
604
|
-
const registry = deps.background;
|
|
605
|
-
if (registry === undefined || toolCallId === undefined || toolCallId.length === 0) return () => {};
|
|
606
|
-
let release = (): void => {};
|
|
607
|
-
release = registry.register({
|
|
608
|
-
toolCallId,
|
|
609
|
-
label: plan.label,
|
|
610
|
-
convert: () => {
|
|
611
|
-
if (plan.refusal !== null) {
|
|
612
|
-
return { ok: false, message: `${plan.label} cannot be backgrounded: ${plan.refusal}` };
|
|
613
|
-
}
|
|
614
|
-
// Drop the control before firing so a second keypress reaches the next
|
|
615
|
-
// attached dispatch instead of re-firing a converted one.
|
|
616
|
-
release();
|
|
617
|
-
plan.fire();
|
|
618
|
-
return { ok: true, message: `${plan.label} is moving to the background as a detached batch` };
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
return () => release();
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Thrown out of an attached executor when the operator converts the call to a
|
|
626
|
-
* detached batch. The runs keep going; only the await unwinds. `settled` names
|
|
627
|
-
* sequential steps that already finished, and `undispatched` names later steps
|
|
628
|
-
* that were never started, because a mid-sequence conversion really does leave
|
|
629
|
-
* them unstarted and reporting a completed sequence would be a lie.
|
|
630
|
-
*/
|
|
631
|
-
class DispatchBackgroundedError extends Error {
|
|
632
|
-
constructor(
|
|
633
|
-
readonly batchId: string,
|
|
634
|
-
readonly live: ReadonlyArray<DetachedBatchRun>,
|
|
635
|
-
readonly settled: ReadonlyArray<CompletedRun>,
|
|
636
|
-
readonly undispatched: ReadonlyArray<string>,
|
|
637
|
-
/**
|
|
638
|
-
* The agent ledger the live runs still share. It stays open across the
|
|
639
|
-
* conversion and rides on the detached record so collect closes it, the
|
|
640
|
-
* same as a batch that was detached from the start.
|
|
641
|
-
*/
|
|
642
|
-
readonly ledgerId: string | null = null,
|
|
643
|
-
) {
|
|
644
|
-
super("dispatch: backgrounded by operator");
|
|
645
|
-
this.name = "DispatchBackgroundedError";
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Finish an operator-initiated conversion: persist the same durable batch
|
|
651
|
-
* record a `detach: true` call would have written, so monitor collect, the
|
|
652
|
-
* completion nudge, and resume all attach to it exactly as they do for a
|
|
653
|
-
* model-initiated detach, then answer the model in the detached shape.
|
|
654
|
-
*/
|
|
655
|
-
async function backgroundedDispatchResult(
|
|
656
|
-
deps: RegisteredDispatchToolDeps,
|
|
657
|
-
converted: DispatchBackgroundedError,
|
|
658
|
-
mode: string,
|
|
659
|
-
sessionId: string | null,
|
|
660
|
-
): Promise<ToolResult> {
|
|
661
|
-
const liveIds = converted.live.map((run) => run.assignmentId);
|
|
662
|
-
const detached = deps.dispatch.detached;
|
|
663
|
-
if (!detached) {
|
|
664
|
-
return {
|
|
665
|
-
kind: "error",
|
|
666
|
-
message: `dispatch: the operator moved this call to the background, but detached batch records are unavailable in this context; ${liveIds.join(", ")} keep running and can only be reached with monitor(mode="wait", run_id=<id>)`,
|
|
667
|
-
details: { mode: "detached", assignmentIds: liveIds },
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
// Same ordering as runDetached: the runs are already live, so their
|
|
671
|
-
// assignment records must land before the batch record an immediate collect
|
|
672
|
-
// would resolve through.
|
|
673
|
-
await deps.dispatch.assignments?.flushWrites?.();
|
|
674
|
-
try {
|
|
675
|
-
await detached.register({
|
|
676
|
-
batchId: converted.batchId,
|
|
677
|
-
runs: converted.live,
|
|
678
|
-
sessionId,
|
|
679
|
-
...(converted.ledgerId !== null ? { ledgerId: converted.ledgerId } : {}),
|
|
680
|
-
});
|
|
681
|
-
} catch (err) {
|
|
682
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
683
|
-
return {
|
|
684
|
-
kind: "error",
|
|
685
|
-
message: `dispatch: the operator moved this call to the background (batch=${converted.batchId}, assignments=${liveIds.join(", ")}) but the durable batch record failed: ${message}`,
|
|
686
|
-
details: { mode: "detached", batchId: converted.batchId, assignmentIds: liveIds },
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
const lines = [
|
|
690
|
-
`dispatch (${mode}) was moved to the background by the operator: batch=${converted.batchId} holds ${converted.live.length} still-running assignment(s)`,
|
|
691
|
-
...converted.live.map((run) => `- ${run.assignmentId} agent=${run.agentId}`),
|
|
692
|
-
];
|
|
693
|
-
if (converted.settled.length > 0) {
|
|
694
|
-
lines.push(
|
|
695
|
-
`${converted.settled.length} earlier ${mode} step(s) had already finished and are not in this batch: ${converted.settled.map((run) => run.receipt.runId).join(", ")}. Read them with monitor(mode="receipt", run_id=<id>).`,
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
if (converted.undispatched.length > 0) {
|
|
699
|
-
lines.push(
|
|
700
|
-
`${converted.undispatched.length} later ${mode} step(s) were never dispatched (agents: ${converted.undispatched.join(", ")}); re-issue them if they are still wanted.`,
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
lines.push(
|
|
704
|
-
"",
|
|
705
|
-
`Runs continue in the background. Collect results with monitor(mode="collect", batch_id="${converted.batchId}"); block on one run with monitor(mode="wait", run_id=<id>).`,
|
|
706
|
-
);
|
|
707
|
-
return {
|
|
708
|
-
kind: "ok",
|
|
709
|
-
output: lines.join("\n"),
|
|
710
|
-
details: {
|
|
711
|
-
mode: "detached",
|
|
712
|
-
conversion: "operator-backgrounded",
|
|
713
|
-
batchId: converted.batchId,
|
|
714
|
-
assignmentIds: liveIds,
|
|
715
|
-
runs: converted.live.map((run) => ({ runId: run.runId, assignmentId: run.assignmentId, agentId: run.agentId })),
|
|
716
|
-
settledRunIds: converted.settled.map((run) => run.receipt.runId),
|
|
717
|
-
undispatchedAgentIds: [...converted.undispatched],
|
|
718
|
-
},
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* Surfaces a succeeded run's outcomeDetail to the calling model. Today that
|
|
724
|
-
* detail is only set for runs that finished without a successful tool call;
|
|
725
|
-
* the dispatch summary must not flatter such a run as plainly "completed".
|
|
726
|
-
*/
|
|
727
|
-
function successNote(receipt: RunReceipt): string | null {
|
|
728
|
-
if (receipt.outcome !== undefined && receipt.outcome !== "succeeded") return null;
|
|
729
|
-
if (receipt.exitCode !== 0) return null;
|
|
730
|
-
return receipt.outcomeDetail ?? null;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
interface CompletedRun {
|
|
734
|
-
receipt: RunReceipt;
|
|
735
|
-
receiptPath: string | null;
|
|
736
|
-
summary: EventSummary;
|
|
737
|
-
/**
|
|
738
|
-
* Sealed-receipt integrity verified against the ledger envelope at
|
|
739
|
-
* completion. Fails closed: a missing envelope reads as FAILED, never as
|
|
740
|
-
* verified. Rendering only; it never feeds retry, reroute, or outcome.
|
|
741
|
-
*/
|
|
742
|
-
integrity: ReceiptIntegrityResult;
|
|
743
|
-
/** Reviewer/judge output staged before its receipt settlement boundary. */
|
|
744
|
-
pendingGate?: PendingGateDecisionHandle;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Assemble the parent-facing completion record for a settled run: resolve the
|
|
749
|
-
* ledger envelope once for both the receipt path and the integrity check.
|
|
750
|
-
*/
|
|
751
|
-
function completeRun(
|
|
752
|
-
deps: DispatchToolDeps,
|
|
753
|
-
receipt: RunReceipt,
|
|
754
|
-
summary: EventSummary,
|
|
755
|
-
pendingGate?: PendingGateDecisionHandle,
|
|
756
|
-
): CompletedRun {
|
|
757
|
-
const envelope = deps.dispatch.getRun(receipt.runId);
|
|
758
|
-
return {
|
|
759
|
-
receipt,
|
|
760
|
-
receiptPath: envelope?.receiptPath ?? null,
|
|
761
|
-
summary,
|
|
762
|
-
integrity:
|
|
763
|
-
envelope === null
|
|
764
|
-
? { ok: false, reason: "run ledger envelope unavailable" }
|
|
765
|
-
: verifyReceiptIntegrity(receipt, envelope),
|
|
766
|
-
...(pendingGate !== undefined ? { pendingGate } : {}),
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
class PipelineHaltError extends Error {
|
|
771
|
-
constructor(
|
|
772
|
-
message: string,
|
|
773
|
-
readonly runs: ReadonlyArray<CompletedRun>,
|
|
774
|
-
) {
|
|
775
|
-
super(message);
|
|
776
|
-
this.name = "PipelineHaltError";
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function integrityFailureBanner(run: CompletedRun): string | null {
|
|
781
|
-
if (run.integrity.ok) return null;
|
|
782
|
-
return `RECEIPT INTEGRITY FAILED for ${run.receipt.runId} (${run.integrity.reason}); treat this run's receipt fields and worker text as untrusted.`;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* The settled board, appended after the per-run lines. Its budget is reserved
|
|
787
|
-
* out of the output ceiling rather than taken from it, so the board a topology
|
|
788
|
-
* produced is never the part the truncation drops. A topology with no board
|
|
789
|
-
* passes null and the output is byte-identical to what it was before boards
|
|
790
|
-
* reached the main model at all.
|
|
791
|
-
*/
|
|
792
|
-
function withAgentLedgerBoard(body: string, board: string | null): string {
|
|
793
|
-
return board === null ? body : `${body}\n\n${board}`;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function formatDispatchOutput(
|
|
797
|
-
mode: string,
|
|
798
|
-
runs: ReadonlyArray<CompletedRun>,
|
|
799
|
-
maxOutputBytes: number,
|
|
800
|
-
board: string | null = null,
|
|
801
|
-
): string {
|
|
802
|
-
const failed = runs.filter((run) => run.receipt.exitCode !== 0);
|
|
803
|
-
const perRunOutputBytes = Math.max(1024, Math.floor(maxOutputBytes / Math.max(1, runs.length)));
|
|
804
|
-
// Integrity failures and the spot-check reminder lead the summary: the
|
|
805
|
-
// truncation below keeps the head and drops the tail, and neither warning
|
|
806
|
-
// may be hidden behind a successful process outcome or a long answer.
|
|
807
|
-
const integrityBanners = runs
|
|
808
|
-
.map((run) => integrityFailureBanner(run))
|
|
809
|
-
.filter((banner): banner is string => banner !== null);
|
|
810
|
-
const needsSpotCheck = runs.some((run) => {
|
|
811
|
-
const state = run.receipt.verification.state;
|
|
812
|
-
return state === "unverified" || state === "unknown";
|
|
813
|
-
});
|
|
814
|
-
const lines = [
|
|
815
|
-
`dispatch (${mode}) total=${runs.length} failed=${failed.length}`,
|
|
816
|
-
`runs=${runs.map((run) => run.receipt.runId).join(", ")}`,
|
|
817
|
-
...integrityBanners,
|
|
818
|
-
...(needsSpotCheck ? [SPOT_CHECK_GUIDANCE] : []),
|
|
819
|
-
"",
|
|
820
|
-
...runs.flatMap((run, index) => {
|
|
821
|
-
const { receipt, receiptPath } = run;
|
|
822
|
-
const note = successNote(receipt);
|
|
823
|
-
const noteSuffix = note !== null ? ` note=${note}` : "";
|
|
824
|
-
// A non-success outcome is load-bearing evidence (a timeout has no
|
|
825
|
-
// failureMessage at all); render it and its detail on the run line.
|
|
826
|
-
const outcomeSuffix =
|
|
827
|
-
receipt.outcome !== undefined && receipt.outcome !== "succeeded"
|
|
828
|
-
? ` outcome=${receipt.outcome}${receipt.outcomeDetail ? ` detail=${receipt.outcomeDetail}` : ""}`
|
|
829
|
-
: "";
|
|
830
|
-
const failure = receipt.failureMessage ? ` failure=${receipt.failureMessage}` : "";
|
|
831
|
-
// Pipeline runs are an ordered chain, so each line names its step.
|
|
832
|
-
const stepLabel = mode === "pipeline" ? `step ${index + 1}/${runs.length} ` : "";
|
|
833
|
-
// Provenance suffix is empty when a run carries no
|
|
834
|
-
// pipeline/persona/escalation fields, so the line format is unchanged.
|
|
835
|
-
const provenance = provenanceCompactSuffix(extractRunProvenance(receipt));
|
|
836
|
-
// Evidence confidence comes from the sealed receipt. A receipt that fails
|
|
837
|
-
// integrity cannot be read as evidence at all.
|
|
838
|
-
const verification = run.integrity.ok ? receipt.verification : UNVERIFIABLE_RECEIPT_VERIFICATION;
|
|
839
|
-
const evidenceSuffix = ` ${receiptEvidenceLabels(receipt, verification, run.integrity).join(" ")}${
|
|
840
|
-
run.integrity.ok ? "" : " evidence_verification=unknown/receipt-integrity-failed"
|
|
841
|
-
}`;
|
|
842
|
-
const routingSuffix =
|
|
843
|
-
run.integrity.ok && receipt.routeDecision !== undefined && receipt.routingIntent !== undefined
|
|
844
|
-
? ` route_decision=${receipt.routeDecision.decisionHash} route_mode=${receipt.routeDecision.mode}`
|
|
845
|
-
: "";
|
|
846
|
-
// The sealed receipt is authoritative. Live summaries remain useful for
|
|
847
|
-
// monitoring but can contain transient tool-use prose and never override
|
|
848
|
-
// a missing, partial, or integrity-invalid durable answer.
|
|
849
|
-
const answerText =
|
|
850
|
-
run.integrity.ok &&
|
|
851
|
-
(receipt.output?.state === "final" || (receipt.outcome !== undefined && receipt.outcome !== "succeeded"))
|
|
852
|
-
? (receipt.output?.text ?? "")
|
|
853
|
-
: "";
|
|
854
|
-
const output =
|
|
855
|
-
answerText.length > 0
|
|
856
|
-
? truncateUtf8(answerText, perRunOutputBytes, TRUNCATION_MARKER)
|
|
857
|
-
: run.integrity.ok
|
|
858
|
-
? "(no receipt-sealed assistant text captured)"
|
|
859
|
-
: "(worker text withheld because receipt integrity failed)";
|
|
860
|
-
return [
|
|
861
|
-
`- ${stepLabel}${receipt.runId} agent=${receipt.agentId} exit=${receipt.exitCode} target=${receipt.targetId} model=${receipt.wireModelId} tokens=${receipt.tokenCount} receipt=${receiptPath ?? "n/a"}${evidenceSuffix}${outcomeSuffix}${noteSuffix}${failure}${provenance}${routingSuffix}`,
|
|
862
|
-
` ${workerTextLabel(verification)}`,
|
|
863
|
-
...output.split("\n").map((line) => ` ${line}`),
|
|
864
|
-
...workerTextNonEvidenceNotices(receipt, verification, answerText).map((notice) => ` ${notice}`),
|
|
865
|
-
];
|
|
866
|
-
}),
|
|
867
|
-
];
|
|
868
|
-
if (board === null) return truncateUtf8(lines.join("\n"), maxOutputBytes, TRUNCATION_MARKER);
|
|
869
|
-
const runBudget = Math.max(1024, maxOutputBytes - Buffer.byteLength(board, "utf8") - 2);
|
|
870
|
-
return withAgentLedgerBoard(truncateUtf8(lines.join("\n"), runBudget, TRUNCATION_MARKER), board);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function dispatchDetails(
|
|
874
|
-
deps: DispatchToolDeps,
|
|
875
|
-
mode: string,
|
|
876
|
-
runs: ReadonlyArray<CompletedRun>,
|
|
877
|
-
board: string | null = null,
|
|
878
|
-
): ToolResultDetails {
|
|
879
|
-
const failed = runs.filter((run) => run.receipt.exitCode !== 0);
|
|
880
|
-
let transition: ReturnType<typeof scoutTransitionDetail> = null;
|
|
881
|
-
for (const run of runs) {
|
|
882
|
-
const envelope = deps.dispatch.getRun(run.receipt.runId);
|
|
883
|
-
const candidate =
|
|
884
|
-
run.integrity.ok && envelope !== null
|
|
885
|
-
? scoutTransitionDetail({ receipt: run.receipt, envelope, agentSpecs: deps.getAgentSpecs() })
|
|
886
|
-
: null;
|
|
887
|
-
if (candidate === null) continue;
|
|
888
|
-
if (transition !== null) {
|
|
889
|
-
transition = null;
|
|
890
|
-
break;
|
|
891
|
-
}
|
|
892
|
-
transition = candidate;
|
|
893
|
-
}
|
|
894
|
-
return {
|
|
895
|
-
mode,
|
|
896
|
-
assignmentIds: runs.map((run) => run.receipt.lineage?.rootRunId ?? run.receipt.runId),
|
|
897
|
-
terminalRunIds: runs.map((run) => run.receipt.runId),
|
|
898
|
-
receiptCount: runs.length,
|
|
899
|
-
failedCount: failed.length,
|
|
900
|
-
// Same text the output carries, under a stable key, so a surface that
|
|
901
|
-
// renders details can show the board without re-reading the store.
|
|
902
|
-
...(board !== null ? { agentLedgerBoard: board } : {}),
|
|
903
|
-
...(transition !== null ? { scoutTransition: transition } : {}),
|
|
904
|
-
runs: runs.map(({ receipt, receiptPath, summary, integrity }) => {
|
|
905
|
-
// Additive provenance keys only; folded in when the receipt carries the
|
|
906
|
-
// field so a run entry without them keeps its exact shape.
|
|
907
|
-
const provenance = extractRunProvenance(receipt);
|
|
908
|
-
return {
|
|
909
|
-
runId: receipt.runId,
|
|
910
|
-
agentId: receipt.agentId,
|
|
911
|
-
exitCode: receipt.exitCode,
|
|
912
|
-
receiptPath,
|
|
913
|
-
eventCount: summary.count,
|
|
914
|
-
// Structured evidence state for downstream consumers: mirrors the
|
|
915
|
-
// sealed receipt read-only, plus the integrity check so a tampered
|
|
916
|
-
// receipt is machine-visible here too.
|
|
917
|
-
verification: integrity.ok ? receipt.verification : UNVERIFIABLE_RECEIPT_VERIFICATION,
|
|
918
|
-
receiptIntegrity: integrity,
|
|
919
|
-
...(receipt.outcome !== undefined && receipt.outcome !== "succeeded"
|
|
920
|
-
? { outcome: receipt.outcome, outcomeDetail: receipt.outcomeDetail ?? null }
|
|
921
|
-
: {}),
|
|
922
|
-
...(provenance.pipeline !== undefined ? { pipeline: provenance.pipeline } : {}),
|
|
923
|
-
...(provenance.personaOverride !== undefined ? { personaOverride: provenance.personaOverride } : {}),
|
|
924
|
-
...(provenance.escalation !== undefined ? { escalation: provenance.escalation } : {}),
|
|
925
|
-
...(provenance.autonomyEnforcement !== undefined ? { autonomyEnforcement: provenance.autonomyEnforcement } : {}),
|
|
926
|
-
...(integrity.ok && receipt.routeDecision !== undefined && receipt.routingIntent !== undefined
|
|
927
|
-
? { routing: explainRouteDecision(receipt.routeDecision, receipt.routingIntent) }
|
|
928
|
-
: {}),
|
|
929
|
-
};
|
|
930
|
-
}),
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function newGateGroupId(prefix: string): string {
|
|
935
|
-
return `${prefix}-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function subjectRef(receipt: RunReceipt): RunGateSubjectRef {
|
|
939
|
-
return { runId: receipt.runId, digest: receipt.integrity?.digest ?? null };
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Sealed correlation between a decider route and the subject route it graded.
|
|
944
|
-
* Reported on every decision, including when no independent route exists.
|
|
945
|
-
*/
|
|
946
|
-
function gateCorrelationOf(subject: RunReceipt, decider: RunReceipt): GateDecisionCorrelation {
|
|
947
|
-
const facts = (receipt: RunReceipt): RouteCorrelationFacts => ({
|
|
948
|
-
agentId: receipt.agentId,
|
|
949
|
-
targetId: receipt.targetId,
|
|
950
|
-
wireModelId: receipt.wireModelId,
|
|
951
|
-
runtimeId: receipt.runtimeId,
|
|
952
|
-
nodeId: receipt.node?.id ?? "local",
|
|
953
|
-
});
|
|
954
|
-
const { agent, target, modelFamily, runtime, node, independent } = gateRouteCorrelation(
|
|
955
|
-
facts(subject),
|
|
956
|
-
facts(decider),
|
|
957
|
-
);
|
|
958
|
-
return { agent, target, modelFamily, runtime, node, independent };
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/** Compete candidates share the base request's agent and model, so the first is representative. */
|
|
962
|
-
function competeCorrelation(
|
|
963
|
-
candidates: ReadonlyArray<{ receipt: RunReceipt }>,
|
|
964
|
-
decider: RunReceipt,
|
|
965
|
-
): GateDecisionCorrelation {
|
|
966
|
-
const subject = candidates[0]?.receipt;
|
|
967
|
-
if (subject === undefined) throw new Error("compete decision requires at least one candidate receipt");
|
|
968
|
-
return gateCorrelationOf(subject, decider);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function reviewerTask(originalTask: string, builderRunId: string, cycle: number): string {
|
|
972
|
-
return [
|
|
973
|
-
`Review the work of builder run ${builderRunId} (review cycle ${cycle}).`,
|
|
974
|
-
"The builder's final answer is provided as input data; verify it against the workspace, do not trust it blindly.",
|
|
975
|
-
"Original task the builder was given:",
|
|
976
|
-
originalTask,
|
|
977
|
-
].join("\n\n");
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
export interface ReviewGateSettings {
|
|
981
|
-
reviewer?: string;
|
|
982
|
-
maxCycles: number;
|
|
983
|
-
node?: string;
|
|
984
|
-
model?: string;
|
|
985
|
-
target?: string;
|
|
986
|
-
/** Immutable per-cycle builder/reviewer pins expanded by plan admission. */
|
|
987
|
-
resolvedTasks?: ReadonlyArray<ResolvedPlanTask>;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const REVIEW_MAX_CYCLES_DEFAULT = 2;
|
|
991
|
-
const REVIEW_MAX_CYCLES_LIMIT = 4;
|
|
992
|
-
|
|
993
|
-
const REVIEW_SINGLE_TASK_MESSAGE =
|
|
994
|
-
"dispatch: review supports exactly one task; run the fan-out without review, then dispatch one integration task with review to gate the combined result";
|
|
995
|
-
const COMPETE_SINGLE_TASK_MESSAGE = "dispatch: compete requires exactly one task";
|
|
996
|
-
const COMPETE_NO_REVIEW_MESSAGE = "dispatch: compete has its own judge and cannot combine with review";
|
|
997
|
-
|
|
998
|
-
function reviewSettingsFromArgs(
|
|
999
|
-
args: Record<string, unknown>,
|
|
1000
|
-
): { ok: true; review: ReviewGateSettings | undefined } | { ok: false; message: string } {
|
|
1001
|
-
const raw = args.review;
|
|
1002
|
-
if (raw === undefined || raw === false) return { ok: true, review: undefined };
|
|
1003
|
-
const record = raw === true ? {} : isRecord(raw) ? raw : null;
|
|
1004
|
-
if (record === null) return { ok: false, message: "dispatch: review must be true or an options object" };
|
|
1005
|
-
const review: ReviewGateSettings = { maxCycles: REVIEW_MAX_CYCLES_DEFAULT };
|
|
1006
|
-
const reviewer = stringArg(record, "reviewer", "agent");
|
|
1007
|
-
if (reviewer) review.reviewer = reviewer;
|
|
1008
|
-
if (record.max_cycles !== undefined) {
|
|
1009
|
-
const cycles = record.max_cycles;
|
|
1010
|
-
if (typeof cycles !== "number" || !Number.isInteger(cycles) || cycles < 1 || cycles > REVIEW_MAX_CYCLES_LIMIT) {
|
|
1011
|
-
return { ok: false, message: `dispatch: review.max_cycles must be an integer 1..${REVIEW_MAX_CYCLES_LIMIT}` };
|
|
1012
|
-
}
|
|
1013
|
-
review.maxCycles = cycles;
|
|
1014
|
-
}
|
|
1015
|
-
const node = stringArg(record, "node");
|
|
1016
|
-
if (node) review.node = node;
|
|
1017
|
-
const model = stringArg(record, "model");
|
|
1018
|
-
if (model) review.model = model;
|
|
1019
|
-
const target = stringArg(record, "target");
|
|
1020
|
-
if (target) review.target = target;
|
|
1021
|
-
return { ok: true, review };
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
interface GateRunOutcome {
|
|
1025
|
-
runs: CompletedRun[];
|
|
1026
|
-
decisions: Array<{ artifact: GateDecisionArtifact; path: string }>;
|
|
1027
|
-
verdict: "pass" | "fail" | null;
|
|
1028
|
-
cycles: number;
|
|
1029
|
-
/** Set when the gate ended without a pass/fail verdict and needs the operator. */
|
|
1030
|
-
needsDecision?: string;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Reviewer-gated dispatch: the builder runs, a read-only reviewer evaluates
|
|
1035
|
-
* the workspace against the task, and a revise verdict re-runs the builder
|
|
1036
|
-
* with the findings threaded as input data. Bounded by maxCycles; exhaustion
|
|
1037
|
-
* (or an unparseable reviewer answer at the last cycle) surfaces as a
|
|
1038
|
-
* needs-decision outcome, never a silent failure. Receipts chain backward:
|
|
1039
|
-
* each reviewer references the builder it reviewed, each revise builder
|
|
1040
|
-
* references the reviewer whose findings it received.
|
|
1041
|
-
*/
|
|
1042
|
-
async function runReviewGated(
|
|
1043
|
-
deps: RegisteredDispatchToolDeps,
|
|
1044
|
-
base: DispatchRequest,
|
|
1045
|
-
review: ReviewGateSettings,
|
|
1046
|
-
timeoutMs: number | undefined,
|
|
1047
|
-
signal: AbortSignal | undefined,
|
|
1048
|
-
): Promise<GateRunOutcome> {
|
|
1049
|
-
const group = newGateGroupId("review");
|
|
1050
|
-
const runs: CompletedRun[] = [];
|
|
1051
|
-
const decisions: Array<{ artifact: GateDecisionArtifact; path: string }> = [];
|
|
1052
|
-
const recordDecision = (
|
|
1053
|
-
draft: GateDecisionDraft,
|
|
1054
|
-
pending?: PendingGateDecisionHandle,
|
|
1055
|
-
): { artifact: GateDecisionArtifact; path: string } => {
|
|
1056
|
-
const decision = pending
|
|
1057
|
-
? finalizePendingGateDecision(pending, draft)
|
|
1058
|
-
: materializePendingGateDecision(stagePendingGateDecision(draft));
|
|
1059
|
-
decisions.push(decision);
|
|
1060
|
-
return decision;
|
|
1061
|
-
};
|
|
1062
|
-
let expired = false;
|
|
1063
|
-
let activeRunId: string | null = null;
|
|
1064
|
-
const abortActive = (bySignal: boolean): void => {
|
|
1065
|
-
expired = true;
|
|
1066
|
-
if (activeRunId !== null) {
|
|
1067
|
-
deps.dispatch.abort(
|
|
1068
|
-
activeRunId,
|
|
1069
|
-
bySignal ? undefined : { cause: "timeout", detail: `timed out after ${timeoutMs}ms` },
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
const onSignalAbort = (): void => abortActive(true);
|
|
1074
|
-
const timer = timeoutMs !== undefined ? setTimeout(() => abortActive(false), timeoutMs) : null;
|
|
1075
|
-
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
1076
|
-
|
|
1077
|
-
const runOne = async (request: DispatchRequest): Promise<CompletedRun> => {
|
|
1078
|
-
if (expired || signal?.aborted) {
|
|
1079
|
-
throw new Error(
|
|
1080
|
-
`review dispatch stopped after ${runs.length} run(s): ${signal?.aborted ? "aborted" : `timed out after ${timeoutMs}ms`}`,
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
const handle = await deps.dispatch.dispatch(request);
|
|
1084
|
-
activeRunId = handle.runId;
|
|
1085
|
-
const summary = await consumeGateSensitiveDispatchEvents(deps, handle.runId, request.agentId, handle.events);
|
|
1086
|
-
let pendingGate: PendingGateDecisionHandle | undefined;
|
|
1087
|
-
if (request.gate?.role === "reviewer") {
|
|
1088
|
-
try {
|
|
1089
|
-
pendingGate = stagePendingGateOutput({
|
|
1090
|
-
group: request.gate.group,
|
|
1091
|
-
topology: "review",
|
|
1092
|
-
cycle: request.gate.cycle,
|
|
1093
|
-
subjects: request.gate.subjects ?? [],
|
|
1094
|
-
deciderRunId: summary.terminalAttemptRunId,
|
|
1095
|
-
finalOutput: normalizedAssistantText(summary),
|
|
1096
|
-
terminalCycle: request.gate.cycle === review.maxCycles,
|
|
1097
|
-
});
|
|
1098
|
-
} catch (error) {
|
|
1099
|
-
try {
|
|
1100
|
-
deps.dispatch.abort(handle.runId);
|
|
1101
|
-
} catch {
|
|
1102
|
-
// The receipt barrier below still observes the admitted process.
|
|
1103
|
-
}
|
|
1104
|
-
await Promise.allSettled([handle.finalPromise]);
|
|
1105
|
-
activeRunId = null;
|
|
1106
|
-
throw error;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
let receipt: RunReceipt;
|
|
1110
|
-
try {
|
|
1111
|
-
receipt = await handle.finalPromise;
|
|
1112
|
-
} finally {
|
|
1113
|
-
activeRunId = null;
|
|
1114
|
-
}
|
|
1115
|
-
const completed = completeRun(deps, receipt, summary, pendingGate);
|
|
1116
|
-
runs.push(completed);
|
|
1117
|
-
return completed;
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
try {
|
|
1121
|
-
let findings: { reviewer: CompletedRun; text: string } | null = null;
|
|
1122
|
-
for (let cycle = 1; cycle <= review.maxCycles; cycle += 1) {
|
|
1123
|
-
const builderGate: RunGateProvenance = {
|
|
1124
|
-
role: "builder",
|
|
1125
|
-
group,
|
|
1126
|
-
cycle,
|
|
1127
|
-
...(findings !== null ? { subjects: [subjectRef(findings.reviewer.receipt)], verdict: "revise" } : {}),
|
|
1128
|
-
};
|
|
1129
|
-
const builderRequest: DispatchRequest = {
|
|
1130
|
-
...base,
|
|
1131
|
-
executionRole: "builder",
|
|
1132
|
-
gate: builderGate,
|
|
1133
|
-
...(findings !== null
|
|
1134
|
-
? { pipelineInput: { fromRunId: findings.reviewer.receipt.runId, position: cycle, text: findings.text } }
|
|
1135
|
-
: {}),
|
|
1136
|
-
};
|
|
1137
|
-
const builder = await runOne(
|
|
1138
|
-
withResolvedPlanTaskPin(
|
|
1139
|
-
builderRequest,
|
|
1140
|
-
review.resolvedTasks?.find((task) => task.role === "builder" && task.position === cycle),
|
|
1141
|
-
),
|
|
1142
|
-
);
|
|
1143
|
-
if (isPipelineStepFailure(builder.receipt)) {
|
|
1144
|
-
recordDecision({
|
|
1145
|
-
group,
|
|
1146
|
-
topology: "review",
|
|
1147
|
-
cycle,
|
|
1148
|
-
outcome: "exhausted",
|
|
1149
|
-
subjects: [subjectRef(builder.receipt)],
|
|
1150
|
-
detail: `builder ended ${pipelineFailureReason(builder.receipt)}`,
|
|
1151
|
-
});
|
|
1152
|
-
return {
|
|
1153
|
-
runs,
|
|
1154
|
-
decisions,
|
|
1155
|
-
verdict: null,
|
|
1156
|
-
cycles: cycle,
|
|
1157
|
-
needsDecision: `builder run ${builder.receipt.runId} ended ${pipelineFailureReason(builder.receipt)} in cycle ${cycle}`,
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
const reviewerRequest: DispatchRequest = {
|
|
1161
|
-
agentId: gateDeciderAgentId(review.reviewer),
|
|
1162
|
-
executionRole: "reviewer",
|
|
1163
|
-
task: reviewerTask(base.task, builder.receipt.runId, cycle),
|
|
1164
|
-
systemPrompt: REVIEWER_GATE_PROMPT,
|
|
1165
|
-
autonomy: "read-only",
|
|
1166
|
-
gate: { role: "reviewer", group, cycle, subjects: [subjectRef(builder.receipt)] },
|
|
1167
|
-
pipelineInput: {
|
|
1168
|
-
fromRunId: builder.receipt.runId,
|
|
1169
|
-
position: cycle,
|
|
1170
|
-
text: normalizedAssistantText(builder.summary),
|
|
1171
|
-
},
|
|
1172
|
-
...(base.cwd !== undefined ? { cwd: base.cwd } : {}),
|
|
1173
|
-
...(review.node !== undefined ? { node: review.node } : {}),
|
|
1174
|
-
...(review.model !== undefined ? { model: review.model } : {}),
|
|
1175
|
-
...(review.target !== undefined ? { target: review.target } : {}),
|
|
1176
|
-
...(base.plan !== undefined ? { plan: base.plan } : {}),
|
|
1177
|
-
...(base.parentToolCallId !== undefined ? { parentToolCallId: base.parentToolCallId } : {}),
|
|
1178
|
-
...(base.reservation !== undefined
|
|
1179
|
-
? { reservation: { ownerId: base.reservation.ownerId, memberId: base.reservation.memberId } }
|
|
1180
|
-
: {}),
|
|
1181
|
-
};
|
|
1182
|
-
const reviewer = await runOne(
|
|
1183
|
-
withResolvedPlanTaskPin(
|
|
1184
|
-
reviewerRequest,
|
|
1185
|
-
review.resolvedTasks?.find((task) => task.role === "reviewer" && task.position === cycle),
|
|
1186
|
-
{ pinTask: false },
|
|
1187
|
-
),
|
|
1188
|
-
);
|
|
1189
|
-
if (isPipelineStepFailure(reviewer.receipt)) {
|
|
1190
|
-
recordDecision(
|
|
1191
|
-
{
|
|
1192
|
-
group,
|
|
1193
|
-
topology: "review",
|
|
1194
|
-
cycle,
|
|
1195
|
-
outcome: "exhausted",
|
|
1196
|
-
subjects: [subjectRef(builder.receipt)],
|
|
1197
|
-
decider: subjectRef(reviewer.receipt),
|
|
1198
|
-
correlation: gateCorrelationOf(builder.receipt, reviewer.receipt),
|
|
1199
|
-
detail: `reviewer ended ${pipelineFailureReason(reviewer.receipt)}`,
|
|
1200
|
-
},
|
|
1201
|
-
reviewer.pendingGate,
|
|
1202
|
-
);
|
|
1203
|
-
return {
|
|
1204
|
-
runs,
|
|
1205
|
-
decisions,
|
|
1206
|
-
verdict: null,
|
|
1207
|
-
cycles: cycle,
|
|
1208
|
-
needsDecision: `reviewer run ${reviewer.receipt.runId} ended ${pipelineFailureReason(reviewer.receipt)} in cycle ${cycle}`,
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
|
-
const decided = decideReviewGate({
|
|
1212
|
-
group,
|
|
1213
|
-
cycle,
|
|
1214
|
-
terminalCycle: cycle === review.maxCycles,
|
|
1215
|
-
subjects: [subjectRef(builder.receipt)],
|
|
1216
|
-
decider: subjectRef(reviewer.receipt),
|
|
1217
|
-
correlation: gateCorrelationOf(builder.receipt, reviewer.receipt),
|
|
1218
|
-
output: normalizedAssistantText(reviewer.summary),
|
|
1219
|
-
});
|
|
1220
|
-
recordDecision(decided.draft, reviewer.pendingGate);
|
|
1221
|
-
if (decided.findings === null) {
|
|
1222
|
-
return {
|
|
1223
|
-
runs,
|
|
1224
|
-
decisions,
|
|
1225
|
-
verdict: decided.verdict,
|
|
1226
|
-
cycles: cycle,
|
|
1227
|
-
...(decided.needsDecision !== null ? { needsDecision: decided.needsDecision } : {}),
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
findings = { reviewer, text: decided.findings };
|
|
1231
|
-
}
|
|
1232
|
-
throw new Error("review gate exhausted without terminal decision evidence");
|
|
1233
|
-
} finally {
|
|
1234
|
-
if (timer) clearTimeout(timer);
|
|
1235
|
-
signal?.removeEventListener("abort", onSignalAbort);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
function judgeTask(originalTask: string, candidates: ReadonlyArray<CandidateWorktree>, stats: string[]): string {
|
|
1240
|
-
const lines = [
|
|
1241
|
-
`Rank ${candidates.length} candidate implementations of this task and pick the best one.`,
|
|
1242
|
-
"Original task:",
|
|
1243
|
-
originalTask,
|
|
1244
|
-
"Candidates:",
|
|
1245
|
-
...candidates.map(
|
|
1246
|
-
(candidate, index) =>
|
|
1247
|
-
` ${candidate.index}. branch=${candidate.branch} worktree=${candidate.path} (${stats[index] ?? "?"})`,
|
|
1248
|
-
),
|
|
1249
|
-
];
|
|
1250
|
-
return lines.join("\n\n");
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function readVerifiedGateReceipt(deps: DispatchToolDeps, runId: string): RunReceipt | null {
|
|
1254
|
-
const envelope = deps.dispatch.getRun(runId);
|
|
1255
|
-
if (envelope === null) return null;
|
|
1256
|
-
const path = envelope.receiptPath ?? join(clioStateDir(), "receipts", `${runId}.json`);
|
|
1257
|
-
if (!existsSync(path)) return null;
|
|
1258
|
-
let receipt: RunReceipt;
|
|
1259
|
-
try {
|
|
1260
|
-
receipt = JSON.parse(readFileSync(path, "utf8")) as RunReceipt;
|
|
1261
|
-
} catch (error) {
|
|
1262
|
-
throw new Error(`pending gate receipt ${runId} is unreadable: ${competeErrorMessage(error)}`);
|
|
1263
|
-
}
|
|
1264
|
-
if (receipt.runId !== runId) throw new Error(`pending gate receipt path for ${runId} contains ${receipt.runId}`);
|
|
1265
|
-
const verification = verifyReceiptIntegrity(receipt, envelope);
|
|
1266
|
-
if (!verification.ok) throw new Error(`pending gate receipt ${runId} failed integrity: ${verification.reason}`);
|
|
1267
|
-
if (receipt.integrity?.digest === undefined) throw new Error(`pending gate receipt ${runId} has no integrity digest`);
|
|
1268
|
-
return receipt;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function recoveredCorrelation(
|
|
1272
|
-
deps: DispatchToolDeps,
|
|
1273
|
-
subjects: ReadonlyArray<RunGateSubjectRef>,
|
|
1274
|
-
decider: RunReceipt,
|
|
1275
|
-
): GateDecisionCorrelation {
|
|
1276
|
-
const first = subjects[0];
|
|
1277
|
-
const subject = first === undefined ? null : readVerifiedGateReceipt(deps, first.runId);
|
|
1278
|
-
if (subject === null) throw new Error("recovered gate decision has no verified subject receipt for correlation");
|
|
1279
|
-
return gateCorrelationOf(subject, decider);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
function settlePendingCompeteResource(handle: PendingGateDecisionHandle, draft: GateDecisionDraft): void {
|
|
1283
|
-
if (draft.topology !== "compete" || (draft.outcome !== "winner" && draft.outcome !== "no-winner")) return;
|
|
1284
|
-
const root = handle.record.resourceRoot ?? process.cwd();
|
|
1285
|
-
settleRecoveredCompeteDecision(root, draft.group, draft.outcome === "winner" ? (draft.winner?.index ?? null) : null);
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
/**
|
|
1289
|
-
* Rebuild decisions whose reviewer/judge output crossed the WAL boundary but
|
|
1290
|
-
* whose coordinator died before parsing or materialization. Receipt integrity
|
|
1291
|
-
* is verified before the output protocol is trusted. Every resolved record is
|
|
1292
|
-
* collision-checked before compete worktrees move to their recovered state.
|
|
1293
|
-
*/
|
|
1294
|
-
function recoverPendingGateEvidence(deps: DispatchToolDeps): void {
|
|
1295
|
-
const recovery = preparePendingGateDecisionRecovery();
|
|
1296
|
-
const resolved: PendingGateDecisionHandle[] = [];
|
|
1297
|
-
for (const handle of recovery.unresolved) {
|
|
1298
|
-
if (handle.record.kind !== "output") continue;
|
|
1299
|
-
const record = handle.record;
|
|
1300
|
-
const deciderReceipt = readVerifiedGateReceipt(deps, record.deciderRunId);
|
|
1301
|
-
if (deciderReceipt === null) {
|
|
1302
|
-
throw new Error(
|
|
1303
|
-
`pending ${record.topology} decision ${record.id} has no verified decider receipt for ${record.deciderRunId}`,
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
const decider = subjectRef(deciderReceipt);
|
|
1307
|
-
let draft: GateDecisionDraft;
|
|
1308
|
-
if (record.topology === "review") {
|
|
1309
|
-
if (isPipelineStepFailure(deciderReceipt)) {
|
|
1310
|
-
draft = {
|
|
1311
|
-
group: record.group,
|
|
1312
|
-
topology: "review",
|
|
1313
|
-
cycle: record.cycle,
|
|
1314
|
-
outcome: "exhausted",
|
|
1315
|
-
subjects: record.subjects,
|
|
1316
|
-
decider,
|
|
1317
|
-
correlation: recoveredCorrelation(deps, record.subjects, deciderReceipt),
|
|
1318
|
-
detail: `reviewer ended ${pipelineFailureReason(deciderReceipt)}`,
|
|
1319
|
-
};
|
|
1320
|
-
} else {
|
|
1321
|
-
draft = decideReviewGate({
|
|
1322
|
-
group: record.group,
|
|
1323
|
-
cycle: record.cycle,
|
|
1324
|
-
terminalCycle: record.terminalCycle === true,
|
|
1325
|
-
subjects: record.subjects,
|
|
1326
|
-
decider,
|
|
1327
|
-
correlation: recoveredCorrelation(deps, record.subjects, deciderReceipt),
|
|
1328
|
-
output: record.finalOutput,
|
|
1329
|
-
}).draft;
|
|
1330
|
-
}
|
|
1331
|
-
resolved.push(resolvePendingGateDecision(handle, draft));
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const judged = isPipelineStepFailure(deciderReceipt)
|
|
1336
|
-
? null
|
|
1337
|
-
: parseCompeteGateResult(record.finalOutput, record.subjects.length);
|
|
1338
|
-
const pick = judged?.ok ? judged.result.winner : null;
|
|
1339
|
-
const pickedSubject = pick === null ? undefined : record.subjects[pick - 1];
|
|
1340
|
-
const candidateReceipt = pickedSubject === undefined ? null : readVerifiedGateReceipt(deps, pickedSubject.runId);
|
|
1341
|
-
if (pickedSubject !== undefined && candidateReceipt === null) {
|
|
1342
|
-
throw new Error(
|
|
1343
|
-
`pending compete decision ${record.id} has no verified candidate receipt for ${pickedSubject.runId}`,
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
let blockedProtected: string[] = [];
|
|
1347
|
-
if (pick !== null && pickedSubject !== undefined && candidateReceipt !== null) {
|
|
1348
|
-
const root = record.resourceRoot ?? process.cwd();
|
|
1349
|
-
const protectedPaths = deps.dispatch.protectedArtifactState?.().artifacts.map((artifact) => artifact.path) ?? [];
|
|
1350
|
-
blockedProtected = protectedPathsChangedByCompeteBranch(
|
|
1351
|
-
root,
|
|
1352
|
-
`clio/compete/${record.group}/${pick}`,
|
|
1353
|
-
protectedPaths,
|
|
1354
|
-
);
|
|
1355
|
-
}
|
|
1356
|
-
const validWinner =
|
|
1357
|
-
pick !== null &&
|
|
1358
|
-
pickedSubject !== undefined &&
|
|
1359
|
-
candidateReceipt !== null &&
|
|
1360
|
-
!isPipelineStepFailure(candidateReceipt) &&
|
|
1361
|
-
blockedProtected.length === 0;
|
|
1362
|
-
if (validWinner && pick !== null && pickedSubject !== undefined) {
|
|
1363
|
-
draft = {
|
|
1364
|
-
group: record.group,
|
|
1365
|
-
topology: "compete",
|
|
1366
|
-
cycle: record.cycle,
|
|
1367
|
-
outcome: "winner",
|
|
1368
|
-
subjects: record.subjects,
|
|
1369
|
-
decider,
|
|
1370
|
-
correlation: recoveredCorrelation(deps, record.subjects, deciderReceipt),
|
|
1371
|
-
winner: {
|
|
1372
|
-
index: pick,
|
|
1373
|
-
subject: pickedSubject,
|
|
1374
|
-
branch: `clio/compete/${record.group}/${pick}`,
|
|
1375
|
-
},
|
|
1376
|
-
};
|
|
1377
|
-
} else {
|
|
1378
|
-
draft = {
|
|
1379
|
-
group: record.group,
|
|
1380
|
-
topology: "compete",
|
|
1381
|
-
cycle: record.cycle,
|
|
1382
|
-
outcome: "no-winner",
|
|
1383
|
-
subjects: record.subjects,
|
|
1384
|
-
decider,
|
|
1385
|
-
correlation: recoveredCorrelation(deps, record.subjects, deciderReceipt),
|
|
1386
|
-
detail: isPipelineStepFailure(deciderReceipt)
|
|
1387
|
-
? `judge ended ${pipelineFailureReason(deciderReceipt)}`
|
|
1388
|
-
: judged !== null && !judged.ok
|
|
1389
|
-
? judged.reason
|
|
1390
|
-
: blockedProtected.length > 0
|
|
1391
|
-
? `judge-selected candidate ${pick} changes protected artifact(s): ${blockedProtected.join(", ")}`
|
|
1392
|
-
: `judge picked failed or missing candidate ${pick}`,
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
resolved.push(resolvePendingGateDecision(handle, draft));
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const ready = [...recovery.ready, ...resolved];
|
|
1399
|
-
preflightPendingGateDecisionMaterialization(ready);
|
|
1400
|
-
for (const handle of ready) {
|
|
1401
|
-
if (handle.record.kind !== "decision") continue;
|
|
1402
|
-
settlePendingCompeteResource(handle, handle.record.decision);
|
|
1403
|
-
materializePendingGateDecision(handle);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// A process may have died after the final winner artifact was written but
|
|
1407
|
-
// before the worktree manifest moved to winner-preserved. Restore that safe
|
|
1408
|
-
// decision point even though no pending WAL remains.
|
|
1409
|
-
const durable = readGateDecisionArtifacts();
|
|
1410
|
-
const confirmedGroups = new Set(
|
|
1411
|
-
durable
|
|
1412
|
-
.filter(
|
|
1413
|
-
({ artifact }) =>
|
|
1414
|
-
artifact.topology === "compete" &&
|
|
1415
|
-
(artifact.outcome === "operator-confirmed" || artifact.outcome === "full-auto-applied"),
|
|
1416
|
-
)
|
|
1417
|
-
.map(({ artifact }) => artifact.group),
|
|
1418
|
-
);
|
|
1419
|
-
for (const { artifact } of durable) {
|
|
1420
|
-
if (
|
|
1421
|
-
artifact.topology !== "compete" ||
|
|
1422
|
-
artifact.outcome !== "winner" ||
|
|
1423
|
-
artifact.winner === undefined ||
|
|
1424
|
-
confirmedGroups.has(artifact.group)
|
|
1425
|
-
) {
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
settleRecoveredCompeteDecision(process.cwd(), artifact.group, artifact.winner.index);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
export interface CompeteSettings {
|
|
1433
|
-
candidates: number;
|
|
1434
|
-
judge?: { agent?: string; model?: string; target?: string; node?: string };
|
|
1435
|
-
/** Immutable per-candidate and judge pins expanded by plan admission. */
|
|
1436
|
-
resolvedTasks?: ReadonlyArray<ResolvedPlanTask>;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
const COMPETE_MIN_CANDIDATES = 2;
|
|
1440
|
-
const COMPETE_MAX_CANDIDATES = 4;
|
|
1441
|
-
|
|
1442
|
-
function competeSettingsFromArgs(
|
|
1443
|
-
args: Record<string, unknown>,
|
|
1444
|
-
): { ok: true; compete: CompeteSettings } | { ok: false; message: string } {
|
|
1445
|
-
const raw = args.candidates;
|
|
1446
|
-
let candidates = COMPETE_MIN_CANDIDATES;
|
|
1447
|
-
if (raw !== undefined) {
|
|
1448
|
-
if (
|
|
1449
|
-
typeof raw !== "number" ||
|
|
1450
|
-
!Number.isInteger(raw) ||
|
|
1451
|
-
raw < COMPETE_MIN_CANDIDATES ||
|
|
1452
|
-
raw > COMPETE_MAX_CANDIDATES
|
|
1453
|
-
) {
|
|
1454
|
-
return {
|
|
1455
|
-
ok: false,
|
|
1456
|
-
message: `dispatch: candidates must be an integer ${COMPETE_MIN_CANDIDATES}..${COMPETE_MAX_CANDIDATES}`,
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
candidates = raw;
|
|
1460
|
-
}
|
|
1461
|
-
const judgeRaw = args.judge;
|
|
1462
|
-
if (judgeRaw !== undefined && !isRecord(judgeRaw)) {
|
|
1463
|
-
return { ok: false, message: "dispatch: judge must be an options object" };
|
|
1464
|
-
}
|
|
1465
|
-
let judge: CompeteSettings["judge"];
|
|
1466
|
-
if (isRecord(judgeRaw)) {
|
|
1467
|
-
judge = {};
|
|
1468
|
-
const agent = stringArg(judgeRaw, "agent");
|
|
1469
|
-
if (agent !== undefined) judge.agent = agent;
|
|
1470
|
-
const model = stringArg(judgeRaw, "model");
|
|
1471
|
-
if (model !== undefined) judge.model = model;
|
|
1472
|
-
const target = stringArg(judgeRaw, "target");
|
|
1473
|
-
if (target !== undefined) judge.target = target;
|
|
1474
|
-
const node = stringArg(judgeRaw, "node");
|
|
1475
|
-
if (node !== undefined) judge.node = node;
|
|
1476
|
-
}
|
|
1477
|
-
return { ok: true, compete: { candidates, ...(judge !== undefined ? { judge } : {}) } };
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
interface CompeteOutcome {
|
|
1481
|
-
runs: CompletedRun[];
|
|
1482
|
-
decisions: Array<{ artifact: GateDecisionArtifact; path: string }>;
|
|
1483
|
-
group: string;
|
|
1484
|
-
winner: { index: number; branch: string; applied: boolean } | null;
|
|
1485
|
-
needsDecision?: string;
|
|
1486
|
-
/** Rendered board the candidates and the judge shared, null when empty. */
|
|
1487
|
-
board?: string | null;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
interface OwnedCompeteRun {
|
|
1491
|
-
runId: string;
|
|
1492
|
-
settled: boolean;
|
|
1493
|
-
abortRequested: boolean;
|
|
1494
|
-
settlement: Promise<CompletedRun>;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
interface CompeteStop {
|
|
1498
|
-
message: string;
|
|
1499
|
-
reason?: AbortReason;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function competeErrorMessage(err: unknown): string {
|
|
1503
|
-
return err instanceof Error ? err.message : String(err);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
/**
|
|
1507
|
-
* Prefix the tool name once. Errors raised inside the dispatch domain already
|
|
1508
|
-
* name themselves, and blind prefixing turned those into `dispatch: dispatch:
|
|
1509
|
-
* ...`, which spends the first eleven bytes of a short error saying the same
|
|
1510
|
-
* thing twice.
|
|
1511
|
-
*/
|
|
1512
|
-
function dispatchErrorMessage(err: unknown): string {
|
|
1513
|
-
const message = competeErrorMessage(err);
|
|
1514
|
-
return message.startsWith("dispatch: ") ? message : `dispatch: ${message}`;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function rejectedReasons(results: ReadonlyArray<PromiseSettledResult<unknown>>): unknown[] {
|
|
1518
|
-
return results
|
|
1519
|
-
.filter((result): result is PromiseRejectedResult => result.status === "rejected")
|
|
1520
|
-
.map((result) => result.reason as unknown);
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
/**
|
|
1524
|
-
* Best-of-N compete: N candidate builders run the same task in isolated
|
|
1525
|
-
* scratch git worktrees, each candidate's work is committed on its branch, a
|
|
1526
|
-
* read-only judge ranks them, and the winner is applied (full-auto) or handed
|
|
1527
|
-
* to the operator (supervised: the winner's branch and worktree survive; the
|
|
1528
|
-
* apply_winner action routes through plan approval). Losers are always
|
|
1529
|
-
* cleaned, including on abort and on any thrown error.
|
|
1530
|
-
*/
|
|
1531
|
-
async function runCompete(
|
|
1532
|
-
deps: RegisteredDispatchToolDeps,
|
|
1533
|
-
base: DispatchRequest,
|
|
1534
|
-
compete: CompeteSettings,
|
|
1535
|
-
autonomy: AutonomyLevel,
|
|
1536
|
-
timeoutMs: number | undefined,
|
|
1537
|
-
signal: AbortSignal | undefined,
|
|
1538
|
-
): Promise<CompeteOutcome> {
|
|
1539
|
-
const requestedRoot = base.cwd !== undefined && base.cwd.length > 0 ? base.cwd : process.cwd();
|
|
1540
|
-
if (!isGitRepository(requestedRoot)) {
|
|
1541
|
-
throw new Error(`compete requires a git repository at ${requestedRoot}; scratch worktrees isolate the candidates`);
|
|
1542
|
-
}
|
|
1543
|
-
const protectedPaths = deps.dispatch.protectedArtifactState?.().artifacts.map((artifact) => artifact.path) ?? [];
|
|
1544
|
-
// Startup recovery intentionally sweeps only groups that another lifecycle
|
|
1545
|
-
// durably marked cleanup-ready after all of its workers settled. Active,
|
|
1546
|
-
// preserved, and malformed crash leftovers remain untouched.
|
|
1547
|
-
recoverCleanupReadyCompeteGroups(requestedRoot);
|
|
1548
|
-
const group = newGateGroupId("compete");
|
|
1549
|
-
// Candidates run concurrently and the judge reads what they contributed, so
|
|
1550
|
-
// the board reaches the judge with corroboration and dispute labels instead
|
|
1551
|
-
// of only per-candidate output.
|
|
1552
|
-
const ledgerId = newGateGroupId("ledger");
|
|
1553
|
-
const ledger = { id: ledgerId, sequence: 0 };
|
|
1554
|
-
await openAgentLedger(ledgerId);
|
|
1555
|
-
let ownership: CompeteGroupOwnership | null = null;
|
|
1556
|
-
const worktrees: CandidateWorktree[] = [];
|
|
1557
|
-
const runs: CompletedRun[] = [];
|
|
1558
|
-
const decisions: Array<{ artifact: GateDecisionArtifact; path: string }> = [];
|
|
1559
|
-
const recordDecision = (draft: GateDecisionDraft, pending?: PendingGateDecisionHandle) => {
|
|
1560
|
-
const decision = pending
|
|
1561
|
-
? finalizePendingGateDecision(pending, draft)
|
|
1562
|
-
: materializePendingGateDecision(stagePendingGateDecision(draft, { resourceRoot: requestedRoot }));
|
|
1563
|
-
decisions.push(decision);
|
|
1564
|
-
return decision;
|
|
1565
|
-
};
|
|
1566
|
-
const ownedRuns: OwnedCompeteRun[] = [];
|
|
1567
|
-
const abortErrors: unknown[] = [];
|
|
1568
|
-
let stop: CompeteStop | null = null;
|
|
1569
|
-
let primaryError: unknown = null;
|
|
1570
|
-
let winner: CompeteOutcome["winner"] = null;
|
|
1571
|
-
const currentWinner = (): CompeteOutcome["winner"] => winner;
|
|
1572
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1573
|
-
let signalListenerInstalled = false;
|
|
1574
|
-
|
|
1575
|
-
const abortOwnedRun = (run: OwnedCompeteRun): void => {
|
|
1576
|
-
if (run.settled || run.abortRequested || stop === null) return;
|
|
1577
|
-
run.abortRequested = true;
|
|
1578
|
-
try {
|
|
1579
|
-
deps.dispatch.abort(run.runId, stop.reason);
|
|
1580
|
-
} catch (err) {
|
|
1581
|
-
abortErrors.push(err);
|
|
1582
|
-
}
|
|
1583
|
-
};
|
|
1584
|
-
const requestStop = (next: CompeteStop): void => {
|
|
1585
|
-
if (stop === null) stop = next;
|
|
1586
|
-
for (const run of ownedRuns) abortOwnedRun(run);
|
|
1587
|
-
};
|
|
1588
|
-
const onSignalAbort = (): void => requestStop({ message: "compete dispatch aborted" });
|
|
1589
|
-
const throwIfStopped = (): void => {
|
|
1590
|
-
if (stop !== null) throw new Error(stop.message);
|
|
1591
|
-
};
|
|
1592
|
-
const settleRun = async (
|
|
1593
|
-
handle: Awaited<ReturnType<DispatchContract["dispatch"]>>,
|
|
1594
|
-
request: DispatchRequest,
|
|
1595
|
-
): Promise<CompletedRun> => {
|
|
1596
|
-
const summaryPromise = consumeGateSensitiveDispatchEvents(deps, handle.runId, request.agentId, handle.events).then(
|
|
1597
|
-
(summary) => {
|
|
1598
|
-
const pendingGate =
|
|
1599
|
-
request.gate?.role === "judge"
|
|
1600
|
-
? stagePendingGateOutput({
|
|
1601
|
-
group: request.gate.group,
|
|
1602
|
-
topology: "compete",
|
|
1603
|
-
cycle: request.gate.cycle,
|
|
1604
|
-
subjects: request.gate.subjects ?? [],
|
|
1605
|
-
deciderRunId: summary.terminalAttemptRunId,
|
|
1606
|
-
finalOutput: normalizedAssistantText(summary),
|
|
1607
|
-
...(request.cwd !== undefined ? { resourceRoot: request.cwd } : {}),
|
|
1608
|
-
})
|
|
1609
|
-
: undefined;
|
|
1610
|
-
return { summary, pendingGate };
|
|
1611
|
-
},
|
|
1612
|
-
);
|
|
1613
|
-
const [summaryResult, receiptResult] = await Promise.allSettled([summaryPromise, handle.finalPromise]);
|
|
1614
|
-
const failures = rejectedReasons([summaryResult, receiptResult]);
|
|
1615
|
-
if (failures.length > 0) {
|
|
1616
|
-
throw new Error(`run ${handle.runId} failed to settle: ${failures.map(competeErrorMessage).join("; ")}`);
|
|
1617
|
-
}
|
|
1618
|
-
const summaryWithGate = summaryResult.status === "fulfilled" ? summaryResult.value : null;
|
|
1619
|
-
const receipt = receiptResult.status === "fulfilled" ? receiptResult.value : null;
|
|
1620
|
-
if (summaryWithGate === null || receipt === null) throw new Error(`run ${handle.runId} produced no settled result`);
|
|
1621
|
-
return completeRun(deps, receipt, summaryWithGate.summary, summaryWithGate.pendingGate);
|
|
1622
|
-
};
|
|
1623
|
-
const admitOwnedRun = async (request: DispatchRequest, label: string): Promise<OwnedCompeteRun> => {
|
|
1624
|
-
try {
|
|
1625
|
-
const handle = await deps.dispatch.dispatch(request, {
|
|
1626
|
-
onAdmitted(admission) {
|
|
1627
|
-
if (ownership === null) throw new Error(`compete ${group} has no active ownership claim`);
|
|
1628
|
-
registerCompeteGroupRun(ownership, admission);
|
|
1629
|
-
},
|
|
1630
|
-
});
|
|
1631
|
-
const owned: OwnedCompeteRun = {
|
|
1632
|
-
runId: handle.runId,
|
|
1633
|
-
settled: false,
|
|
1634
|
-
abortRequested: false,
|
|
1635
|
-
settlement: Promise.resolve(null as never),
|
|
1636
|
-
};
|
|
1637
|
-
ownedRuns.push(owned);
|
|
1638
|
-
owned.settlement = (async () => {
|
|
1639
|
-
try {
|
|
1640
|
-
return await settleRun(handle, request);
|
|
1641
|
-
} catch (err) {
|
|
1642
|
-
requestStop({ message: `${label} failed to settle: ${competeErrorMessage(err)}` });
|
|
1643
|
-
throw err;
|
|
1644
|
-
} finally {
|
|
1645
|
-
owned.settled = true;
|
|
1646
|
-
if (ownership !== null) settleCompeteGroupRun(ownership, owned.runId);
|
|
1647
|
-
}
|
|
1648
|
-
})();
|
|
1649
|
-
// A concurrent admission can fail before this settlement is awaited.
|
|
1650
|
-
// Attach a sink immediately while preserving the original promise for
|
|
1651
|
-
// the transaction's mandatory all-settled barrier.
|
|
1652
|
-
owned.settlement.catch(() => {});
|
|
1653
|
-
if (stop !== null) abortOwnedRun(owned);
|
|
1654
|
-
return owned;
|
|
1655
|
-
} catch (err) {
|
|
1656
|
-
requestStop({ message: `${label} admission failed: ${competeErrorMessage(err)}` });
|
|
1657
|
-
throw err;
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
const settledCompletedRuns = async (owned: ReadonlyArray<OwnedCompeteRun>, phase: string): Promise<CompletedRun[]> => {
|
|
1661
|
-
const results = await Promise.allSettled(owned.map((run) => run.settlement));
|
|
1662
|
-
const failures = rejectedReasons(results);
|
|
1663
|
-
if (failures.length > 0) {
|
|
1664
|
-
throw new Error(`${phase} failed: ${failures.map(competeErrorMessage).join("; ")}`);
|
|
1665
|
-
}
|
|
1666
|
-
return results.map((result) => {
|
|
1667
|
-
if (result.status !== "fulfilled") throw new Error(`${phase} produced no completed run`);
|
|
1668
|
-
return result.value;
|
|
1669
|
-
});
|
|
1670
|
-
};
|
|
1671
|
-
|
|
1672
|
-
let outcome: CompeteOutcome | null = null;
|
|
1673
|
-
try {
|
|
1674
|
-
outcome = await (async (): Promise<CompeteOutcome> => {
|
|
1675
|
-
// The owner manifest is the transaction's first group-specific mutation;
|
|
1676
|
-
// every branch and worktree created below is covered by finalization.
|
|
1677
|
-
ownership = claimCompeteGroup(requestedRoot, group);
|
|
1678
|
-
const root = ownership.root;
|
|
1679
|
-
const createCandidate = deps.competeWorktrees?.createCandidate ?? createCandidateWorktree;
|
|
1680
|
-
for (let index = 1; index <= compete.candidates; index += 1) {
|
|
1681
|
-
throwIfStopped();
|
|
1682
|
-
worktrees.push(createCandidate(ownership, index, "HEAD"));
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
if (timeoutMs !== undefined) {
|
|
1686
|
-
timer = setTimeout(
|
|
1687
|
-
() =>
|
|
1688
|
-
requestStop({
|
|
1689
|
-
message: `compete dispatch timed out after ${timeoutMs}ms`,
|
|
1690
|
-
reason: { cause: "timeout", detail: `timed out after ${timeoutMs}ms` },
|
|
1691
|
-
}),
|
|
1692
|
-
timeoutMs,
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
1696
|
-
signalListenerInstalled = signal !== undefined;
|
|
1697
|
-
if (signal?.aborted) onSignalAbort();
|
|
1698
|
-
throwIfStopped();
|
|
1699
|
-
|
|
1700
|
-
// Candidates run concurrently through the normal per-run admission path;
|
|
1701
|
-
// every admission promise is observed to settlement. The first rejection
|
|
1702
|
-
// aborts known siblings, and any sibling accepted later sees `stop` and is
|
|
1703
|
-
// aborted before it can escape the transaction.
|
|
1704
|
-
const admissionResults = await Promise.allSettled(
|
|
1705
|
-
worktrees.map((worktree) => {
|
|
1706
|
-
const request: DispatchRequest = {
|
|
1707
|
-
...base,
|
|
1708
|
-
executionRole: "builder",
|
|
1709
|
-
cwd: worktree.path,
|
|
1710
|
-
protectedArtifactRemap: { sourceRoot: root, workerRoot: worktree.path },
|
|
1711
|
-
gate: { role: "candidate", group, cycle: worktree.index },
|
|
1712
|
-
ledger,
|
|
1713
|
-
competeStance: COMPETE_STANCES[(worktree.index - 1) % COMPETE_STANCES.length] as CompeteStance,
|
|
1714
|
-
};
|
|
1715
|
-
return admitOwnedRun(
|
|
1716
|
-
withResolvedPlanTaskPin(
|
|
1717
|
-
request,
|
|
1718
|
-
compete.resolvedTasks?.find((task) => task.role === "candidate" && task.position === worktree.index),
|
|
1719
|
-
),
|
|
1720
|
-
`candidate ${worktree.index}`,
|
|
1721
|
-
);
|
|
1722
|
-
}),
|
|
1723
|
-
);
|
|
1724
|
-
const admissionFailures = rejectedReasons(admissionResults);
|
|
1725
|
-
if (admissionFailures.length > 0) {
|
|
1726
|
-
const admissionMessage = `compete candidate admission failed: ${admissionFailures
|
|
1727
|
-
.map(competeErrorMessage)
|
|
1728
|
-
.join("; ")}`;
|
|
1729
|
-
requestStop({ message: admissionMessage });
|
|
1730
|
-
// All admission promises have now settled, including late accepts. Wait
|
|
1731
|
-
// for every accepted run before finalization removes any path.
|
|
1732
|
-
await Promise.allSettled(ownedRuns.map((run) => run.settlement));
|
|
1733
|
-
throw new Error(admissionMessage);
|
|
1734
|
-
}
|
|
1735
|
-
const candidateOwned = admissionResults.map((result) => {
|
|
1736
|
-
if (result.status !== "fulfilled") throw new Error("compete candidate admission produced no handle");
|
|
1737
|
-
return result.value;
|
|
1738
|
-
});
|
|
1739
|
-
const candidateRuns = await settledCompletedRuns(candidateOwned, "candidate settlement");
|
|
1740
|
-
runs.push(...candidateRuns);
|
|
1741
|
-
throwIfStopped();
|
|
1742
|
-
const stats = worktrees.map((worktree, index) => {
|
|
1743
|
-
const receipt = candidateRuns[index]?.receipt;
|
|
1744
|
-
const failed = receipt !== undefined && isPipelineStepFailure(receipt);
|
|
1745
|
-
if (failed) return "builder failed";
|
|
1746
|
-
commitCandidateWork(worktree, `clio-coder compete ${group} candidate ${worktree.index}`);
|
|
1747
|
-
return candidateDiffStat(root, worktree.branch);
|
|
1748
|
-
});
|
|
1749
|
-
if (candidateRuns.every((run) => isPipelineStepFailure(run.receipt))) {
|
|
1750
|
-
recordDecision({
|
|
1751
|
-
group,
|
|
1752
|
-
topology: "compete",
|
|
1753
|
-
cycle: 1,
|
|
1754
|
-
outcome: "no-winner",
|
|
1755
|
-
subjects: candidateRuns.map((run) => subjectRef(run.receipt)),
|
|
1756
|
-
detail: "every candidate builder failed; nothing to judge",
|
|
1757
|
-
});
|
|
1758
|
-
return { runs, decisions, group, winner: null, needsDecision: "every candidate builder failed; nothing to judge" };
|
|
1759
|
-
}
|
|
1760
|
-
const judgeRequest: DispatchRequest = {
|
|
1761
|
-
agentId: gateDeciderAgentId(compete.judge?.agent),
|
|
1762
|
-
executionRole: "judge",
|
|
1763
|
-
task: judgeTask(base.task, worktrees, stats),
|
|
1764
|
-
systemPrompt: JUDGE_GATE_PROMPT,
|
|
1765
|
-
autonomy: "read-only",
|
|
1766
|
-
cwd: root,
|
|
1767
|
-
gate: {
|
|
1768
|
-
role: "judge",
|
|
1769
|
-
group,
|
|
1770
|
-
cycle: 1,
|
|
1771
|
-
subjects: candidateRuns.map((run) => subjectRef(run.receipt)),
|
|
1772
|
-
},
|
|
1773
|
-
ledger,
|
|
1774
|
-
...(compete.judge?.model !== undefined ? { model: compete.judge.model } : {}),
|
|
1775
|
-
...(compete.judge?.target !== undefined ? { target: compete.judge.target } : {}),
|
|
1776
|
-
...(compete.judge?.node !== undefined ? { node: compete.judge.node } : {}),
|
|
1777
|
-
...(base.plan !== undefined ? { plan: base.plan } : {}),
|
|
1778
|
-
...(base.parentToolCallId !== undefined ? { parentToolCallId: base.parentToolCallId } : {}),
|
|
1779
|
-
...(base.reservation !== undefined
|
|
1780
|
-
? { reservation: { ownerId: base.reservation.ownerId, memberId: base.reservation.memberId } }
|
|
1781
|
-
: {}),
|
|
1782
|
-
};
|
|
1783
|
-
throwIfStopped();
|
|
1784
|
-
const judgeOwned = await admitOwnedRun(
|
|
1785
|
-
withResolvedPlanTaskPin(
|
|
1786
|
-
judgeRequest,
|
|
1787
|
-
compete.resolvedTasks?.find((task) => task.role === "judge"),
|
|
1788
|
-
{ pinTask: false },
|
|
1789
|
-
),
|
|
1790
|
-
"judge",
|
|
1791
|
-
);
|
|
1792
|
-
const judgeRun = (await settledCompletedRuns([judgeOwned], "judge settlement"))[0];
|
|
1793
|
-
if (judgeRun === undefined) throw new Error("judge produced no completed run");
|
|
1794
|
-
runs.push(judgeRun);
|
|
1795
|
-
throwIfStopped();
|
|
1796
|
-
const judgeReceipt = judgeRun.receipt;
|
|
1797
|
-
const judgeSummary = judgeRun.summary;
|
|
1798
|
-
// Every judged outcome names the same subjects, decider, and correlation;
|
|
1799
|
-
// only the reason the gate produced no applicable winner differs.
|
|
1800
|
-
const noWinner = (detail: string): GateDecisionDraft => ({
|
|
1801
|
-
group,
|
|
1802
|
-
topology: "compete",
|
|
1803
|
-
cycle: 1,
|
|
1804
|
-
outcome: "no-winner",
|
|
1805
|
-
subjects: candidateRuns.map((run) => subjectRef(run.receipt)),
|
|
1806
|
-
decider: subjectRef(judgeReceipt),
|
|
1807
|
-
correlation: competeCorrelation(candidateRuns, judgeReceipt),
|
|
1808
|
-
detail,
|
|
1809
|
-
});
|
|
1810
|
-
if (isPipelineStepFailure(judgeReceipt)) {
|
|
1811
|
-
recordDecision(noWinner(`judge ended ${pipelineFailureReason(judgeReceipt)}`), judgeRun.pendingGate);
|
|
1812
|
-
return {
|
|
1813
|
-
runs,
|
|
1814
|
-
decisions,
|
|
1815
|
-
group,
|
|
1816
|
-
winner: null,
|
|
1817
|
-
needsDecision: `judge run ${judgeReceipt.runId} ended ${pipelineFailureReason(judgeReceipt)}; candidate worktrees were cleaned, their receipts remain; re-run compete or build directly`,
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
const judged = parseCompeteGateResult(normalizedAssistantText(judgeSummary), compete.candidates);
|
|
1821
|
-
if (!judged.ok) {
|
|
1822
|
-
recordDecision(noWinner(judged.reason), judgeRun.pendingGate);
|
|
1823
|
-
return {
|
|
1824
|
-
runs,
|
|
1825
|
-
decisions,
|
|
1826
|
-
group,
|
|
1827
|
-
winner: null,
|
|
1828
|
-
needsDecision: `${judged.reason}; candidate worktrees were cleaned, their receipts remain; re-run compete or build directly`,
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
const pick = judged.result.winner;
|
|
1832
|
-
const pickedWorktree = worktrees.find((worktree) => worktree.index === pick);
|
|
1833
|
-
const pickedRun = candidateRuns[pick - 1];
|
|
1834
|
-
if (!pickedWorktree || pickedRun === undefined || isPipelineStepFailure(pickedRun.receipt)) {
|
|
1835
|
-
recordDecision(noWinner(`judge picked failed or missing candidate ${pick}`), judgeRun.pendingGate);
|
|
1836
|
-
return {
|
|
1837
|
-
runs,
|
|
1838
|
-
decisions,
|
|
1839
|
-
group,
|
|
1840
|
-
winner: null,
|
|
1841
|
-
needsDecision: `judge picked candidate ${pick}, whose builder failed; the operator decides`,
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
const protectedChanges = protectedPathsChangedByCompeteBranch(root, pickedWorktree.branch, protectedPaths);
|
|
1845
|
-
if (protectedChanges.length > 0) {
|
|
1846
|
-
recordDecision(
|
|
1847
|
-
noWinner(`judge-selected candidate ${pick} changes protected artifact(s): ${protectedChanges.join(", ")}`),
|
|
1848
|
-
judgeRun.pendingGate,
|
|
1849
|
-
);
|
|
1850
|
-
return {
|
|
1851
|
-
runs,
|
|
1852
|
-
decisions,
|
|
1853
|
-
group,
|
|
1854
|
-
winner: null,
|
|
1855
|
-
needsDecision: `candidate ${pick} changes protected artifact(s) and cannot be applied: ${protectedChanges.join(", ")}`,
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
const winnerRef = {
|
|
1859
|
-
index: pick,
|
|
1860
|
-
subject: subjectRef(pickedRun.receipt),
|
|
1861
|
-
branch: pickedWorktree.branch,
|
|
1862
|
-
};
|
|
1863
|
-
const winnerDecision = recordDecision(
|
|
1864
|
-
{
|
|
1865
|
-
group,
|
|
1866
|
-
topology: "compete",
|
|
1867
|
-
cycle: 1,
|
|
1868
|
-
outcome: "winner",
|
|
1869
|
-
subjects: candidateRuns.map((run) => subjectRef(run.receipt)),
|
|
1870
|
-
decider: subjectRef(judgeReceipt),
|
|
1871
|
-
correlation: competeCorrelation(candidateRuns, judgeReceipt),
|
|
1872
|
-
winner: winnerRef,
|
|
1873
|
-
},
|
|
1874
|
-
judgeRun.pendingGate,
|
|
1875
|
-
);
|
|
1876
|
-
if (autonomy === "full-auto") {
|
|
1877
|
-
const merge = (deps.competeWorktrees?.mergeWinner ?? mergeWinnerBranch)(root, pickedWorktree.branch);
|
|
1878
|
-
if (!merge.ok) {
|
|
1879
|
-
winner = { index: pick, branch: pickedWorktree.branch, applied: false };
|
|
1880
|
-
return {
|
|
1881
|
-
runs,
|
|
1882
|
-
decisions,
|
|
1883
|
-
group,
|
|
1884
|
-
winner,
|
|
1885
|
-
needsDecision: `winner candidate ${pick} could not be merged (${merge.reason}); its branch ${pickedWorktree.branch} is preserved`,
|
|
1886
|
-
};
|
|
1887
|
-
}
|
|
1888
|
-
try {
|
|
1889
|
-
recordDecision({
|
|
1890
|
-
group,
|
|
1891
|
-
topology: "compete",
|
|
1892
|
-
cycle: 1,
|
|
1893
|
-
outcome: "full-auto-applied",
|
|
1894
|
-
subjects: [winnerRef.subject],
|
|
1895
|
-
winner: winnerRef,
|
|
1896
|
-
confirmation: {
|
|
1897
|
-
id: winnerDecision.artifact.id,
|
|
1898
|
-
digest: winnerDecision.artifact.integrity.digest,
|
|
1899
|
-
},
|
|
1900
|
-
detail: `full-auto applied ${pickedWorktree.branch} under dispatch plan ${base.plan?.hash ?? "unavailable"}`,
|
|
1901
|
-
});
|
|
1902
|
-
} catch (error) {
|
|
1903
|
-
winner = { index: pick, branch: pickedWorktree.branch, applied: false };
|
|
1904
|
-
throw new Error(
|
|
1905
|
-
`winner ${pickedWorktree.branch} merged but full-auto application evidence failed: ${competeErrorMessage(error)}; branch preserved for recovery`,
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
winner = { index: pick, branch: pickedWorktree.branch, applied: true };
|
|
1909
|
-
return { runs, decisions, group, winner };
|
|
1910
|
-
}
|
|
1911
|
-
winner = { index: pick, branch: pickedWorktree.branch, applied: false };
|
|
1912
|
-
return { runs, decisions, group, winner };
|
|
1913
|
-
})();
|
|
1914
|
-
} catch (err) {
|
|
1915
|
-
primaryError = err;
|
|
1916
|
-
}
|
|
1917
|
-
if (timer) clearTimeout(timer);
|
|
1918
|
-
if (signalListenerInstalled) signal?.removeEventListener("abort", onSignalAbort);
|
|
1919
|
-
|
|
1920
|
-
// No path may be removed while a worker can still write through it. On
|
|
1921
|
-
// every exceptional exit, abort all accepted runs (including late
|
|
1922
|
-
// admissions) and await the settlement promise that drains both events
|
|
1923
|
-
// and final receipt completion.
|
|
1924
|
-
if (ownedRuns.some((run) => !run.settled)) {
|
|
1925
|
-
requestStop({ message: "compete lifecycle exited before every worker settled" });
|
|
1926
|
-
}
|
|
1927
|
-
const finalizationErrors: unknown[] = [];
|
|
1928
|
-
await Promise.allSettled(ownedRuns.map((run) => run.settlement));
|
|
1929
|
-
// Every worker has settled, so no further post can be admitted. The board is
|
|
1930
|
-
// read here, on the way past, for the model that started the compete.
|
|
1931
|
-
const board = renderAgentLedgerBoard(ledgerId);
|
|
1932
|
-
await closeAgentLedger(ledgerId);
|
|
1933
|
-
finalizationErrors.push(...abortErrors);
|
|
1934
|
-
|
|
1935
|
-
// Losers are always cleaned; the winner's worktree and branch survive
|
|
1936
|
-
// only while an operator decision is pending (supervised pick or a
|
|
1937
|
-
// failed merge). The durable state transition happens after the worker
|
|
1938
|
-
// barrier and before deletion, defining the safe restart boundary.
|
|
1939
|
-
const winnerAtFinalization = currentWinner();
|
|
1940
|
-
if (ownership !== null) {
|
|
1941
|
-
if (winnerAtFinalization !== null && !winnerAtFinalization.applied) {
|
|
1942
|
-
try {
|
|
1943
|
-
ownership = markCompeteGroupWinnerPreserved(ownership, winnerAtFinalization.index);
|
|
1944
|
-
for (const worktree of worktrees) {
|
|
1945
|
-
if (worktree.index === winnerAtFinalization.index) continue;
|
|
1946
|
-
try {
|
|
1947
|
-
removeCandidateWorktree(ownership, worktree, true);
|
|
1948
|
-
} catch (err) {
|
|
1949
|
-
finalizationErrors.push(err);
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
} catch (err) {
|
|
1953
|
-
// Without the preserved-state record, retain every candidate; a
|
|
1954
|
-
// later process must not guess which path is safe to remove.
|
|
1955
|
-
finalizationErrors.push(err);
|
|
1956
|
-
}
|
|
1957
|
-
} else {
|
|
1958
|
-
try {
|
|
1959
|
-
ownership = markCompeteGroupCleanupReady(ownership);
|
|
1960
|
-
} catch (err) {
|
|
1961
|
-
finalizationErrors.push(err);
|
|
1962
|
-
}
|
|
1963
|
-
try {
|
|
1964
|
-
const cleanupGroup = deps.competeWorktrees?.cleanupGroup ?? cleanupCompeteGroup;
|
|
1965
|
-
cleanupGroup(ownership);
|
|
1966
|
-
} catch (err) {
|
|
1967
|
-
finalizationErrors.push(err);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
if (finalizationErrors.length > 0) {
|
|
1973
|
-
const unique = [...new Set(finalizationErrors)];
|
|
1974
|
-
const suffix = unique.map(competeErrorMessage).join("; ");
|
|
1975
|
-
if (primaryError !== null) {
|
|
1976
|
-
throw new Error(`${competeErrorMessage(primaryError)}; compete finalization also failed: ${suffix}`);
|
|
1977
|
-
}
|
|
1978
|
-
throw new Error(`compete finalization failed: ${suffix}`);
|
|
1979
|
-
}
|
|
1980
|
-
if (primaryError !== null) throw primaryError;
|
|
1981
|
-
if (outcome === null) throw new Error("compete lifecycle produced no outcome");
|
|
1982
|
-
return { ...outcome, board };
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
/**
|
|
1986
|
-
* Apply a preserved compete winner. Plan-scale by definition, so supervised
|
|
1987
|
-
* autonomy levels park this call for operator confirmation; the approval IS
|
|
1988
|
-
* the winner confirmation. After a successful merge the whole compete group
|
|
1989
|
-
* is cleaned up.
|
|
1990
|
-
*/
|
|
1991
|
-
function runApplyWinner(
|
|
1992
|
-
args: Record<string, unknown>,
|
|
1993
|
-
authority:
|
|
1994
|
-
| { outcome: "operator-confirmed"; requestId: string; requestedBy: string }
|
|
1995
|
-
| { outcome: "full-auto-applied" }
|
|
1996
|
-
| null,
|
|
1997
|
-
mergeWinner: typeof mergeWinnerBranch = mergeWinnerBranch,
|
|
1998
|
-
protectedPaths: ReadonlyArray<string> = [],
|
|
1999
|
-
): ToolResult {
|
|
2000
|
-
if (authority === null) {
|
|
2001
|
-
return {
|
|
2002
|
-
kind: "error",
|
|
2003
|
-
message: "dispatch: apply_winner requires a registry-authenticated operator approval or full-auto authority",
|
|
2004
|
-
};
|
|
2005
|
-
}
|
|
2006
|
-
const raw = args.apply_winner;
|
|
2007
|
-
if (!isRecord(raw)) return { kind: "error", message: "dispatch: apply_winner must be an options object" };
|
|
2008
|
-
const branch = stringArg(raw, "branch");
|
|
2009
|
-
if (!branch) return { kind: "error", message: "dispatch: apply_winner.branch is required" };
|
|
2010
|
-
const match = /^clio\/compete\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/([1-9]\d*)$/.exec(branch);
|
|
2011
|
-
if (!match) {
|
|
2012
|
-
return { kind: "error", message: "dispatch: apply_winner.branch must be a clio/compete/<group>/<n> branch" };
|
|
2013
|
-
}
|
|
2014
|
-
const group = match[1] ?? "";
|
|
2015
|
-
const winnerIndex = Number.parseInt(match[2] ?? "", 10);
|
|
2016
|
-
const root = stringArg(raw, "cwd") ?? process.cwd();
|
|
2017
|
-
if (!isGitRepository(root)) {
|
|
2018
|
-
return { kind: "error", message: `dispatch: apply_winner requires a git repository at ${root}` };
|
|
2019
|
-
}
|
|
2020
|
-
recoverCleanupReadyCompeteGroups(root);
|
|
2021
|
-
const ownership = loadCompeteGroup(root, group);
|
|
2022
|
-
if (ownership === null) {
|
|
2023
|
-
return {
|
|
2024
|
-
kind: "error",
|
|
2025
|
-
message: `dispatch: winner branch ${branch} has no matching compete ownership manifest; refusing to merge or delete unproven state`,
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
if (ownership.state !== "winner-preserved" || ownership.winnerIndex !== winnerIndex) {
|
|
2029
|
-
return {
|
|
2030
|
-
kind: "error",
|
|
2031
|
-
message: `dispatch: winner branch ${branch} does not match the preserved winner recorded for compete group ${group}`,
|
|
2032
|
-
};
|
|
2033
|
-
}
|
|
2034
|
-
const winnerDecision = readGateDecisionArtifacts(group)
|
|
2035
|
-
.filter(
|
|
2036
|
-
(entry) =>
|
|
2037
|
-
entry.artifact.outcome === "winner" &&
|
|
2038
|
-
entry.artifact.winner?.index === winnerIndex &&
|
|
2039
|
-
entry.artifact.winner.branch === branch,
|
|
2040
|
-
)
|
|
2041
|
-
.sort(
|
|
2042
|
-
(left, right) =>
|
|
2043
|
-
left.artifact.createdAt.localeCompare(right.artifact.createdAt) ||
|
|
2044
|
-
left.artifact.id.localeCompare(right.artifact.id),
|
|
2045
|
-
)
|
|
2046
|
-
.at(-1);
|
|
2047
|
-
if (winnerDecision === undefined || winnerDecision.artifact.winner === undefined) {
|
|
2048
|
-
return {
|
|
2049
|
-
kind: "error",
|
|
2050
|
-
message: `dispatch: winner branch ${branch} has no integrity-valid judge decision; refusing an unauditable confirmation`,
|
|
2051
|
-
};
|
|
2052
|
-
}
|
|
2053
|
-
const winnerRef = winnerDecision.artifact.winner;
|
|
2054
|
-
const protectedChanges = protectedPathsChangedByCompeteBranch(root, branch, protectedPaths);
|
|
2055
|
-
if (protectedChanges.length > 0) {
|
|
2056
|
-
return {
|
|
2057
|
-
kind: "error",
|
|
2058
|
-
message: `dispatch: winner branch ${branch} changes protected artifact(s) and cannot be merged: ${protectedChanges.join(", ")}`,
|
|
2059
|
-
};
|
|
2060
|
-
}
|
|
2061
|
-
const writeConfirmation = (): { artifact: GateDecisionArtifact; path: string } =>
|
|
2062
|
-
materializePendingGateDecision(
|
|
2063
|
-
stagePendingGateDecision({
|
|
2064
|
-
group,
|
|
2065
|
-
topology: "compete",
|
|
2066
|
-
cycle: 1,
|
|
2067
|
-
outcome: authority.outcome,
|
|
2068
|
-
subjects: [winnerRef.subject],
|
|
2069
|
-
winner: winnerRef,
|
|
2070
|
-
confirmation: {
|
|
2071
|
-
id: winnerDecision.artifact.id,
|
|
2072
|
-
digest: winnerDecision.artifact.integrity.digest,
|
|
2073
|
-
},
|
|
2074
|
-
detail:
|
|
2075
|
-
authority.outcome === "operator-confirmed"
|
|
2076
|
-
? `operator confirmation ${authority.requestId} (${authority.requestedBy}) approved ${branch} under dispatch plan ${describeDispatchPlan(args).hash}`
|
|
2077
|
-
: `full-auto applied ${branch} under dispatch plan ${describeDispatchPlan(args).hash}`,
|
|
2078
|
-
}),
|
|
2079
|
-
);
|
|
2080
|
-
let confirmation: { artifact: GateDecisionArtifact; path: string } | null = null;
|
|
2081
|
-
if (authority.outcome === "operator-confirmed") {
|
|
2082
|
-
try {
|
|
2083
|
-
confirmation = writeConfirmation();
|
|
2084
|
-
} catch (err) {
|
|
2085
|
-
return {
|
|
2086
|
-
kind: "error",
|
|
2087
|
-
message: `dispatch: could not persist operator confirmation for ${branch}: ${competeErrorMessage(err)}`,
|
|
2088
|
-
};
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
const merge = mergeWinner(root, branch);
|
|
2092
|
-
if (!merge.ok) {
|
|
2093
|
-
return {
|
|
2094
|
-
kind: "error",
|
|
2095
|
-
message: `dispatch: winner branch ${branch} could not be merged: ${merge.reason}; the branch is preserved`,
|
|
2096
|
-
...(confirmation !== null
|
|
2097
|
-
? { details: { confirmationPath: confirmation.path, confirmationId: confirmation.artifact.id } }
|
|
2098
|
-
: {}),
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
if (authority.outcome === "full-auto-applied") {
|
|
2102
|
-
try {
|
|
2103
|
-
confirmation = writeConfirmation();
|
|
2104
|
-
} catch (err) {
|
|
2105
|
-
return {
|
|
2106
|
-
kind: "error",
|
|
2107
|
-
message: `dispatch: winner branch ${branch} was merged, but full-auto application evidence failed: ${competeErrorMessage(err)}; branch is preserved for recovery`,
|
|
2108
|
-
details: { mode: "apply_winner", branch, group, applied: true, evidencePending: true },
|
|
2109
|
-
};
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
if (confirmation === null) {
|
|
2113
|
-
return { kind: "error", message: `dispatch: winner branch ${branch} was merged without confirmation evidence` };
|
|
2114
|
-
}
|
|
2115
|
-
try {
|
|
2116
|
-
const cleanupOwnership = markCompeteGroupCleanupReady(ownership);
|
|
2117
|
-
cleanupCompeteGroup(cleanupOwnership);
|
|
2118
|
-
} catch (err) {
|
|
2119
|
-
return {
|
|
2120
|
-
kind: "error",
|
|
2121
|
-
message: `dispatch: winner branch ${branch} was merged, but compete cleanup failed: ${competeErrorMessage(err)}`,
|
|
2122
|
-
details: {
|
|
2123
|
-
mode: "apply_winner",
|
|
2124
|
-
branch,
|
|
2125
|
-
group,
|
|
2126
|
-
applied: true,
|
|
2127
|
-
cleanupPending: true,
|
|
2128
|
-
confirmationPath: confirmation.path,
|
|
2129
|
-
confirmationId: confirmation.artifact.id,
|
|
2130
|
-
},
|
|
2131
|
-
};
|
|
2132
|
-
}
|
|
40
|
+
function admissionFailureResult(message: string) {
|
|
2133
41
|
return {
|
|
2134
|
-
kind: "
|
|
2135
|
-
|
|
2136
|
-
details: {
|
|
2137
|
-
mode: "apply_winner",
|
|
2138
|
-
branch,
|
|
2139
|
-
group,
|
|
2140
|
-
confirmationPath: confirmation.path,
|
|
2141
|
-
confirmationId: confirmation.artifact.id,
|
|
2142
|
-
},
|
|
42
|
+
kind: "error" as const,
|
|
43
|
+
message: message.startsWith("dispatch: ") ? message : `dispatch: plan admission failed: ${message}`,
|
|
2143
44
|
};
|
|
2144
45
|
}
|
|
2145
46
|
|
|
2146
|
-
function
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
verdict: outcome.verdict,
|
|
2152
|
-
cycles: outcome.cycles,
|
|
2153
|
-
decisions: outcome.decisions.map(({ artifact, path }) => ({
|
|
2154
|
-
id: artifact.id,
|
|
2155
|
-
outcome: artifact.outcome,
|
|
2156
|
-
path,
|
|
2157
|
-
digest: artifact.integrity.digest,
|
|
2158
|
-
})),
|
|
2159
|
-
...(outcome.needsDecision !== undefined ? { needsDecision: outcome.needsDecision } : {}),
|
|
2160
|
-
},
|
|
2161
|
-
};
|
|
2162
|
-
if (outcome.verdict === "pass") {
|
|
2163
|
-
return { kind: "ok", output: `review gate passed after ${outcome.cycles} cycle(s)\n\n${body}`, details };
|
|
2164
|
-
}
|
|
2165
|
-
if (outcome.verdict === "fail") {
|
|
2166
|
-
return { kind: "error", message: `dispatch: review gate failed (reviewer verdict fail)\n\n${body}`, details };
|
|
2167
|
-
}
|
|
2168
|
-
return {
|
|
2169
|
-
kind: "error",
|
|
2170
|
-
message: `dispatch: review gate needs an operator decision: ${outcome.needsDecision ?? "no verdict"}\n\n${body}`,
|
|
2171
|
-
details,
|
|
2172
|
-
};
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
function competeResult(
|
|
2176
|
-
deps: DispatchToolDeps,
|
|
2177
|
-
outcome: CompeteOutcome,
|
|
2178
|
-
autonomy: AutonomyLevel,
|
|
2179
|
-
maxOutputBytes: number,
|
|
2180
|
-
): ToolResult {
|
|
2181
|
-
const board = outcome.board ?? null;
|
|
2182
|
-
const body = formatDispatchOutput("compete", outcome.runs, maxOutputBytes, board);
|
|
2183
|
-
const details: ToolResultDetails = {
|
|
2184
|
-
...dispatchDetails(deps, "compete", outcome.runs, board),
|
|
2185
|
-
compete: {
|
|
2186
|
-
group: outcome.group,
|
|
2187
|
-
winner: outcome.winner,
|
|
2188
|
-
decisions: outcome.decisions.map(({ artifact, path }) => ({
|
|
2189
|
-
id: artifact.id,
|
|
2190
|
-
outcome: artifact.outcome,
|
|
2191
|
-
path,
|
|
2192
|
-
digest: artifact.integrity.digest,
|
|
2193
|
-
})),
|
|
2194
|
-
...(outcome.needsDecision !== undefined ? { needsDecision: outcome.needsDecision } : {}),
|
|
2195
|
-
},
|
|
2196
|
-
};
|
|
2197
|
-
if (outcome.winner?.applied === true) {
|
|
2198
|
-
return {
|
|
2199
|
-
kind: "ok",
|
|
2200
|
-
output: `compete winner candidate ${outcome.winner.index} applied (branch ${outcome.winner.branch} merged)\n\n${body}`,
|
|
2201
|
-
details,
|
|
2202
|
-
};
|
|
2203
|
-
}
|
|
2204
|
-
if (outcome.winner !== null && outcome.needsDecision === undefined) {
|
|
2205
|
-
const lines = [
|
|
2206
|
-
`compete winner: candidate ${outcome.winner.index} (branch ${outcome.winner.branch}), preserved for confirmation at autonomy ${autonomy}`,
|
|
2207
|
-
`Apply it with dispatch apply_winner={branch: "${outcome.winner.branch}"}; the approval prompt is the winner confirmation. Losing candidates were cleaned up.`,
|
|
2208
|
-
];
|
|
2209
|
-
return { kind: "ok", output: `${lines.join("\n")}\n\n${body}`, details };
|
|
2210
|
-
}
|
|
2211
|
-
return {
|
|
2212
|
-
kind: "error",
|
|
2213
|
-
message: `dispatch: compete needs an operator decision: ${outcome.needsDecision ?? "no winner"}\n\n${body}`,
|
|
2214
|
-
details,
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
2219
|
-
const deps: RegisteredDispatchToolDeps = {
|
|
47
|
+
export function createDispatchTool(
|
|
48
|
+
inputDeps: DispatchToolDeps,
|
|
49
|
+
surfaceOptions: DispatchToolSurfaceOptions = {},
|
|
50
|
+
): ToolSpec {
|
|
51
|
+
const deps: DispatchToolDeps = {
|
|
2220
52
|
...inputDeps,
|
|
2221
53
|
runEvents: inputDeps.runEvents ?? createDispatchRunEventRegistry(),
|
|
2222
54
|
};
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
const preparedAdmissionArgs = new WeakSet<Record<string, unknown>>();
|
|
2229
|
-
const trustedResolvedPlans = new WeakMap<Record<string, unknown>, ResolvedDispatchPlanArtifact>();
|
|
2230
|
-
const trustedReservationOwners = new WeakMap<Record<string, unknown>, string>();
|
|
2231
|
-
const trustedParsedRequests = new WeakMap<Record<string, unknown>, ReadonlyArray<DispatchRequest>>();
|
|
2232
|
-
const trustedExecutionPlans = new WeakMap<Record<string, unknown>, ExecutionPlan>();
|
|
2233
|
-
const taskResolutions = new WeakMap<object, DispatchPlanTaskResolution>();
|
|
2234
|
-
const deepFreeze = <T>(value: T): T => {
|
|
2235
|
-
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
2236
|
-
for (const child of Object.values(value)) deepFreeze(child);
|
|
2237
|
-
return Object.freeze(value);
|
|
2238
|
-
};
|
|
2239
|
-
const parseRequests = (args: Record<string, unknown>) =>
|
|
2240
|
-
dispatchRequestsFromArgs(args, {
|
|
2241
|
-
...(deps.getAgentRoleFacts ? { resolveFacts: deps.getAgentRoleFacts } : {}),
|
|
2242
|
-
hasAgent: (id) => deps.getAgentSpecs().some((spec) => spec.id === id),
|
|
2243
|
-
auto: {
|
|
2244
|
-
approvedAuthorities: ["read-only", "verification", "artifact-write", "workspace-edit"],
|
|
2245
|
-
authorityBasis: deps.getAutonomy?.() === "full-auto" ? "full-auto-policy" : "operator-plan-approval",
|
|
2246
|
-
},
|
|
2247
|
-
});
|
|
2248
|
-
const trustRequests = (args: Record<string, unknown>, requests: ReadonlyArray<DispatchRequest>) => {
|
|
2249
|
-
trustedParsedRequests.set(args, deepFreeze(structuredClone(requests)));
|
|
2250
|
-
return args;
|
|
55
|
+
const admission = createDispatchAdmissionController(deps);
|
|
56
|
+
let runnerPromise: Promise<DispatchRunnerModule> | null = null;
|
|
57
|
+
const loadRunner = (): Promise<DispatchRunnerModule> => {
|
|
58
|
+
runnerPromise ??= (surfaceOptions.loadRunner ?? (() => import("./dispatch-runner.js")))();
|
|
59
|
+
return runnerPromise;
|
|
2251
60
|
};
|
|
2252
|
-
const markPrepared = (args: Record<string, unknown>): Record<string, unknown> => {
|
|
2253
|
-
const exposed = resolvedDispatchPlanFromArgs(args);
|
|
2254
|
-
if (exposed !== null) {
|
|
2255
|
-
// The WeakMap snapshot is the execution authority. The separately
|
|
2256
|
-
// cloned/frozen hidden value remains available to generic registry policy
|
|
2257
|
-
// rendering, but cannot be mutated or replaced after preparation.
|
|
2258
|
-
const trusted = deepFreeze(structuredClone(exposed));
|
|
2259
|
-
const policyView = deepFreeze(structuredClone(exposed));
|
|
2260
|
-
trustedResolvedPlans.set(args, trusted);
|
|
2261
|
-
Object.defineProperty(args, RESOLVED_DISPATCH_PLAN_ARGUMENT, {
|
|
2262
|
-
value: policyView,
|
|
2263
|
-
enumerable: true,
|
|
2264
|
-
configurable: false,
|
|
2265
|
-
writable: false,
|
|
2266
|
-
});
|
|
2267
|
-
}
|
|
2268
|
-
preparedAdmissionArgs.add(args);
|
|
2269
|
-
return args;
|
|
2270
|
-
};
|
|
2271
|
-
const stripUntrustedPlanFields = (rawArgs: Record<string, unknown>): Record<string, unknown> => {
|
|
2272
|
-
const normalized = prepareDispatchArguments(rawArgs);
|
|
2273
|
-
const clean = { ...normalized };
|
|
2274
|
-
Reflect.deleteProperty(clean, RESOLVED_DISPATCH_PLAN_ARGUMENT);
|
|
2275
|
-
Reflect.deleteProperty(clean, DISPATCH_PLAN_PREPARATION_ERROR_ARGUMENT);
|
|
2276
|
-
return clean;
|
|
2277
|
-
};
|
|
2278
|
-
const preparationFailure = (args: Record<string, unknown>, err: unknown): Record<string, unknown> =>
|
|
2279
|
-
markPrepared({
|
|
2280
|
-
...args,
|
|
2281
|
-
[DISPATCH_PLAN_PREPARATION_ERROR_ARGUMENT]: err instanceof Error ? err.message : String(err),
|
|
2282
|
-
});
|
|
2283
|
-
// A call whose shape the executor will refuse must not reach the operator
|
|
2284
|
-
// as an approvable plan. Marking it prepared without a plan would render a
|
|
2285
|
-
// plan-scale card from the raw arguments, and the approved call would then
|
|
2286
|
-
// die on "resolved plan is missing after admission" instead of the real
|
|
2287
|
-
// shape error. Recording the executor's own message as the preparation
|
|
2288
|
-
// failure keeps the card away and hands the model the actionable text.
|
|
2289
|
-
const shapeRejection = (args: Record<string, unknown>, message: string): Record<string, unknown> =>
|
|
2290
|
-
describeDispatchPlan(args).planScale ? preparationFailure(args, message) : markPrepared(args);
|
|
2291
|
-
const resolveTask = (
|
|
2292
|
-
request: DispatchRequest,
|
|
2293
|
-
role: NonNullable<ResolvedDispatchPlanArtifact["tasks"][number]["role"]>,
|
|
2294
|
-
position: number,
|
|
2295
|
-
): ResolvedDispatchPlanArtifact["tasks"][number] => {
|
|
2296
|
-
const resolution = deps.dispatch.preview?.(request);
|
|
2297
|
-
if (resolution === undefined) throw new Error("dispatch preview is unavailable");
|
|
2298
|
-
// An approved exact route and automatic failover are semantically opposed,
|
|
2299
|
-
// so a planned task is never "automatic". An explicit pin seals that exact
|
|
2300
|
-
// tuple with no failover; anything else seals a bounded envelope the
|
|
2301
|
-
// operator saw. The plan hash covers the rendered candidate list, so
|
|
2302
|
-
// distinct envelopes get distinct hashes.
|
|
2303
|
-
const pinned = request.node !== undefined || request.target !== undefined || request.model !== undefined;
|
|
2304
|
-
const approvedCandidates =
|
|
2305
|
-
pinned || request.failover !== "approved"
|
|
2306
|
-
? []
|
|
2307
|
-
: resolution.routeApproval === null
|
|
2308
|
-
? (deps.dispatch.routeCandidates?.(request) ?? [])
|
|
2309
|
-
: approvedRouteCandidates(resolution.routeApproval);
|
|
2310
|
-
const failover: DispatchFailoverMode = pinned || approvedCandidates.length === 0 ? "none" : "approved";
|
|
2311
|
-
const task: ResolvedDispatchPlanArtifact["tasks"][number] = {
|
|
2312
|
-
agent: resolution.agentId,
|
|
2313
|
-
task: request.task,
|
|
2314
|
-
...(request.briefing !== undefined ? { briefing: request.briefing } : {}),
|
|
2315
|
-
target: resolution.targetId,
|
|
2316
|
-
model: resolution.wireModelId,
|
|
2317
|
-
node: resolution.node.id,
|
|
2318
|
-
nodeKind: resolution.node.kind,
|
|
2319
|
-
...(resolution.node.host !== undefined ? { nodeHost: resolution.node.host } : {}),
|
|
2320
|
-
routingIntent: request.routingIntent ?? defaultRoutingIntent(request),
|
|
2321
|
-
failover,
|
|
2322
|
-
routeApproval: resolution.routeApproval,
|
|
2323
|
-
agentSelection: request.agentSelection === undefined ? null : structuredClone(request.agentSelection),
|
|
2324
|
-
stepId: null,
|
|
2325
|
-
dependencies: [],
|
|
2326
|
-
executionRole: request.executionRole,
|
|
2327
|
-
expectedResultContract: null,
|
|
2328
|
-
authorityGrant: null,
|
|
2329
|
-
agentDecision: null,
|
|
2330
|
-
wave: null,
|
|
2331
|
-
...(failover === "approved" ? { allowedCandidates: approvedCandidates.map((candidate) => ({ ...candidate })) } : {}),
|
|
2332
|
-
role,
|
|
2333
|
-
position,
|
|
2334
|
-
};
|
|
2335
|
-
taskResolutions.set(task, resolution);
|
|
2336
|
-
return task;
|
|
2337
|
-
};
|
|
2338
|
-
const resolvedCostCeiling = (): number => {
|
|
2339
|
-
const injected = deps.getCostCeilingUsd?.();
|
|
2340
|
-
if (injected !== undefined && Number.isFinite(injected) && injected > 0) return injected;
|
|
2341
|
-
const ceiling = deps.dispatch.costCeilingUsd?.();
|
|
2342
|
-
if (ceiling === undefined || !Number.isFinite(ceiling) || ceiling <= 0) {
|
|
2343
|
-
throw new Error("dispatch scheduling cost ceiling is unavailable");
|
|
2344
|
-
}
|
|
2345
|
-
return ceiling;
|
|
2346
|
-
};
|
|
2347
|
-
const reservationWave = (
|
|
2348
|
-
topology: ResolvedDispatchPlanArtifact["topology"],
|
|
2349
|
-
task: ResolvedDispatchPlanArtifact["tasks"][number],
|
|
2350
|
-
index: number,
|
|
2351
|
-
): number => {
|
|
2352
|
-
if (topology === "fleet") return task.wave ?? index;
|
|
2353
|
-
if (topology === "parallel" || topology === "detached") return 0;
|
|
2354
|
-
if (topology === "compete") return task.role === "judge" ? 1 : 0;
|
|
2355
|
-
return index;
|
|
2356
|
-
};
|
|
2357
|
-
const markReservedPlan = (
|
|
2358
|
-
args: Record<string, unknown>,
|
|
2359
|
-
artifact: ResolvedDispatchPlanArtifact,
|
|
2360
|
-
): Record<string, unknown> => {
|
|
2361
|
-
const prepared = withResolvedDispatchPlan(args, artifact);
|
|
2362
|
-
if (deps.dispatch.reservations === undefined) return markPrepared(prepared);
|
|
2363
|
-
const tasks = artifact.tasks.map((task, index) => {
|
|
2364
|
-
const resolution = taskResolutions.get(task);
|
|
2365
|
-
if (resolution === undefined || (task.stepId === null && (task.role === undefined || task.position === undefined))) {
|
|
2366
|
-
throw new Error("dispatch reservation resolution is incomplete");
|
|
2367
|
-
}
|
|
2368
|
-
return {
|
|
2369
|
-
memberId: task.stepId ?? `${task.role}-${task.position}`,
|
|
2370
|
-
wave: reservationWave(artifact.topology, task, index),
|
|
2371
|
-
resolution,
|
|
2372
|
-
};
|
|
2373
|
-
});
|
|
2374
|
-
const reservation = deps.dispatch.reservations.prepare({
|
|
2375
|
-
topology: artifact.topology === "fleet" ? "parallel" : artifact.topology,
|
|
2376
|
-
tasks,
|
|
2377
|
-
});
|
|
2378
|
-
try {
|
|
2379
|
-
const marked = markPrepared(prepared);
|
|
2380
|
-
trustedReservationOwners.set(marked, reservation.ownerId);
|
|
2381
|
-
return marked;
|
|
2382
|
-
} catch (error) {
|
|
2383
|
-
deps.dispatch.reservations.rollback(reservation.ownerId);
|
|
2384
|
-
throw error;
|
|
2385
|
-
}
|
|
2386
|
-
};
|
|
2387
|
-
const prepareAdmissionArguments = (rawArgs: Record<string, unknown>): Record<string, unknown> => {
|
|
2388
|
-
const args = stripUntrustedPlanFields(rawArgs);
|
|
2389
|
-
if (args.list === true) return markPrepared(args);
|
|
2390
|
-
const continuation = scoutContinuationRefFromArgs(args);
|
|
2391
|
-
if (!continuation.ok) return preparationFailure(args, continuation.message);
|
|
2392
|
-
if (continuation.ref !== null) {
|
|
2393
|
-
try {
|
|
2394
|
-
const source = loadVerifiedScoutSource({
|
|
2395
|
-
ref: continuation.ref,
|
|
2396
|
-
dispatch: deps.dispatch,
|
|
2397
|
-
agentSpecs: deps.getAgentSpecs(),
|
|
2398
|
-
});
|
|
2399
|
-
const prepared = prepareScoutContinuation({
|
|
2400
|
-
source,
|
|
2401
|
-
authorization: deps.getAutonomy?.() === "full-auto" ? "full-auto-policy" : "operator-plan-approval",
|
|
2402
|
-
planAgentSelection: deps.dispatch.planAgentSelection,
|
|
2403
|
-
costCeilingUsd: resolvedCostCeiling(),
|
|
2404
|
-
});
|
|
2405
|
-
for (const [index, task] of prepared.artifact.tasks.entries()) {
|
|
2406
|
-
const resolution = prepared.resolutions[index];
|
|
2407
|
-
if (resolution === undefined) throw new Error("dispatch: Scout route resolution is incomplete");
|
|
2408
|
-
taskResolutions.set(task, resolution);
|
|
2409
|
-
}
|
|
2410
|
-
const marked = trustRequests(markReservedPlan(args, prepared.artifact), prepared.requests);
|
|
2411
|
-
trustedExecutionPlans.set(marked, deepFreeze(structuredClone(prepared.executionPlan)));
|
|
2412
|
-
return marked;
|
|
2413
|
-
} catch (error) {
|
|
2414
|
-
return preparationFailure(args, error);
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
if (args.apply_winner !== undefined) {
|
|
2418
|
-
if (!isRecord(args.apply_winner)) return markPrepared(args);
|
|
2419
|
-
const branch = stringArg(args.apply_winner, "branch");
|
|
2420
|
-
const match = branch ? /^clio\/compete\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/([1-9]\d*)$/.exec(branch) : null;
|
|
2421
|
-
if (branch === undefined || match === null) return markPrepared(args);
|
|
2422
|
-
if (deps.getCostCeilingUsd === undefined && deps.dispatch.costCeilingUsd === undefined) return markPrepared(args);
|
|
2423
|
-
try {
|
|
2424
|
-
return markPrepared(
|
|
2425
|
-
withResolvedDispatchPlan(args, {
|
|
2426
|
-
version: 3,
|
|
2427
|
-
topology: "compete",
|
|
2428
|
-
source: null,
|
|
2429
|
-
maxWorkers: null,
|
|
2430
|
-
onFailure: null,
|
|
2431
|
-
tasks: [],
|
|
2432
|
-
costCeilingUsd: resolvedCostCeiling(),
|
|
2433
|
-
deadlineMs: null,
|
|
2434
|
-
confirmation: {
|
|
2435
|
-
branch,
|
|
2436
|
-
group: match[1] ?? "",
|
|
2437
|
-
index: Number.parseInt(match[2] ?? "", 10),
|
|
2438
|
-
},
|
|
2439
|
-
}),
|
|
2440
|
-
);
|
|
2441
|
-
} catch (err) {
|
|
2442
|
-
return preparationFailure(args, err);
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
if (deps.dispatch.preview === undefined) return markPrepared(args);
|
|
2446
|
-
const parsed = parseRequests(args);
|
|
2447
|
-
if (!parsed.ok) return shapeRejection(args, parsed.message);
|
|
2448
|
-
if (args.mode !== undefined && !["parallel", "sequential", "pipeline", "compete"].includes(String(args.mode))) {
|
|
2449
|
-
return shapeRejection(
|
|
2450
|
-
args,
|
|
2451
|
-
`dispatch: mode must be parallel, sequential, pipeline, or compete; got '${String(args.mode)}'`,
|
|
2452
|
-
);
|
|
2453
|
-
}
|
|
2454
|
-
const mode =
|
|
2455
|
-
args.mode === "sequential"
|
|
2456
|
-
? "sequential"
|
|
2457
|
-
: args.mode === "pipeline"
|
|
2458
|
-
? "pipeline"
|
|
2459
|
-
: args.mode === "compete"
|
|
2460
|
-
? "compete"
|
|
2461
|
-
: "parallel";
|
|
2462
|
-
try {
|
|
2463
|
-
const tasks: ResolvedDispatchPlanArtifact["tasks"] = [];
|
|
2464
|
-
const reviewResult = reviewSettingsFromArgs(args);
|
|
2465
|
-
if (!reviewResult.ok) return shapeRejection(args, reviewResult.message);
|
|
2466
|
-
if (reviewResult.review !== undefined) {
|
|
2467
|
-
if (mode === "compete") return shapeRejection(args, COMPETE_NO_REVIEW_MESSAGE);
|
|
2468
|
-
if (mode !== "parallel") return shapeRejection(args, `dispatch: review does not combine with mode=${mode}`);
|
|
2469
|
-
if (parsed.requests.length !== 1 || parsed.requests[0] === undefined) {
|
|
2470
|
-
return shapeRejection(args, REVIEW_SINGLE_TASK_MESSAGE);
|
|
2471
|
-
}
|
|
2472
|
-
const base = parsed.requests[0];
|
|
2473
|
-
for (let cycle = 1; cycle <= reviewResult.review.maxCycles; cycle += 1) {
|
|
2474
|
-
const builderSubject: RunGateSubjectRef = { runId: `plan-builder-${cycle}`, digest: null };
|
|
2475
|
-
tasks.push(
|
|
2476
|
-
resolveTask(
|
|
2477
|
-
{ ...base, executionRole: "builder", gate: { role: "builder", group: "plan-preview", cycle } },
|
|
2478
|
-
"builder",
|
|
2479
|
-
cycle,
|
|
2480
|
-
),
|
|
2481
|
-
);
|
|
2482
|
-
tasks.push(
|
|
2483
|
-
resolveTask(
|
|
2484
|
-
{
|
|
2485
|
-
agentId: gateDeciderAgentId(reviewResult.review.reviewer),
|
|
2486
|
-
executionRole: "reviewer",
|
|
2487
|
-
task: reviewerTask(base.task, builderSubject.runId, cycle),
|
|
2488
|
-
systemPrompt: REVIEWER_GATE_PROMPT,
|
|
2489
|
-
autonomy: "read-only",
|
|
2490
|
-
gate: { role: "reviewer", group: "plan-preview", cycle, subjects: [builderSubject] },
|
|
2491
|
-
...(base.cwd !== undefined ? { cwd: base.cwd } : {}),
|
|
2492
|
-
...(reviewResult.review.node !== undefined ? { node: reviewResult.review.node } : {}),
|
|
2493
|
-
...(reviewResult.review.model !== undefined ? { model: reviewResult.review.model } : {}),
|
|
2494
|
-
...(reviewResult.review.target !== undefined ? { target: reviewResult.review.target } : {}),
|
|
2495
|
-
},
|
|
2496
|
-
"reviewer",
|
|
2497
|
-
cycle,
|
|
2498
|
-
),
|
|
2499
|
-
);
|
|
2500
|
-
}
|
|
2501
|
-
} else if (mode === "compete") {
|
|
2502
|
-
if (parsed.requests.length !== 1 || parsed.requests[0] === undefined) {
|
|
2503
|
-
return shapeRejection(args, COMPETE_SINGLE_TASK_MESSAGE);
|
|
2504
|
-
}
|
|
2505
|
-
const competeResult = competeSettingsFromArgs(args);
|
|
2506
|
-
if (!competeResult.ok) return shapeRejection(args, competeResult.message);
|
|
2507
|
-
const base = parsed.requests[0];
|
|
2508
|
-
const subjects: RunGateSubjectRef[] = [];
|
|
2509
|
-
for (let candidate = 1; candidate <= competeResult.compete.candidates; candidate += 1) {
|
|
2510
|
-
subjects.push({ runId: `plan-candidate-${candidate}`, digest: null });
|
|
2511
|
-
tasks.push(
|
|
2512
|
-
resolveTask(
|
|
2513
|
-
{ ...base, executionRole: "builder", gate: { role: "candidate", group: "plan-preview", cycle: candidate } },
|
|
2514
|
-
"candidate",
|
|
2515
|
-
candidate,
|
|
2516
|
-
),
|
|
2517
|
-
);
|
|
2518
|
-
}
|
|
2519
|
-
tasks.push(
|
|
2520
|
-
resolveTask(
|
|
2521
|
-
{
|
|
2522
|
-
agentId: gateDeciderAgentId(competeResult.compete.judge?.agent),
|
|
2523
|
-
executionRole: "judge",
|
|
2524
|
-
task: `Plan-time capability check for the ${competeResult.compete.candidates}-candidate judge.`,
|
|
2525
|
-
systemPrompt: JUDGE_GATE_PROMPT,
|
|
2526
|
-
autonomy: "read-only",
|
|
2527
|
-
gate: { role: "judge", group: "plan-preview", cycle: 1, subjects },
|
|
2528
|
-
...(base.cwd !== undefined ? { cwd: base.cwd } : {}),
|
|
2529
|
-
...(competeResult.compete.judge?.node !== undefined ? { node: competeResult.compete.judge.node } : {}),
|
|
2530
|
-
...(competeResult.compete.judge?.model !== undefined ? { model: competeResult.compete.judge.model } : {}),
|
|
2531
|
-
...(competeResult.compete.judge?.target !== undefined ? { target: competeResult.compete.judge.target } : {}),
|
|
2532
|
-
},
|
|
2533
|
-
"judge",
|
|
2534
|
-
1,
|
|
2535
|
-
),
|
|
2536
|
-
);
|
|
2537
|
-
} else {
|
|
2538
|
-
for (const [index, request] of parsed.requests.entries()) {
|
|
2539
|
-
tasks.push(resolveTask(request, "task", index + 1));
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
const topology = describeDispatchPlan(args).topology;
|
|
2544
|
-
const planScale =
|
|
2545
|
-
tasks.length > 1 ||
|
|
2546
|
-
topology === "compete" ||
|
|
2547
|
-
tasks.some((task) => task.node !== "local" || task.failover === "approved");
|
|
2548
|
-
if (!planScale) return trustRequests(markPrepared(args), parsed.requests);
|
|
2549
|
-
return trustRequests(
|
|
2550
|
-
markReservedPlan(args, {
|
|
2551
|
-
version: 3,
|
|
2552
|
-
topology,
|
|
2553
|
-
source: null,
|
|
2554
|
-
maxWorkers: null,
|
|
2555
|
-
onFailure: null,
|
|
2556
|
-
tasks,
|
|
2557
|
-
costCeilingUsd: resolvedCostCeiling(),
|
|
2558
|
-
deadlineMs: null,
|
|
2559
|
-
}),
|
|
2560
|
-
parsed.requests,
|
|
2561
|
-
);
|
|
2562
|
-
} catch (err) {
|
|
2563
|
-
return preparationFailure(args, err);
|
|
2564
|
-
}
|
|
2565
|
-
};
|
|
2566
|
-
const prepareExecutionArguments = (args: Record<string, unknown>): Record<string, unknown> =>
|
|
2567
|
-
preparedAdmissionArgs.has(args) ? args : prepareAdmissionArguments(args);
|
|
2568
61
|
|
|
2569
62
|
return {
|
|
2570
63
|
name: ToolNames.Dispatch,
|
|
@@ -2609,7 +102,9 @@ export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
|
2609
102
|
"Ad-hoc specialist persona to substitute for the recipe body inside the stable worker shell, max 8000 chars.",
|
|
2610
103
|
}),
|
|
2611
104
|
),
|
|
2612
|
-
tool_profile: Type.Optional(
|
|
105
|
+
tool_profile: Type.Optional(
|
|
106
|
+
StringEnum(TOOL_PROFILE_NAMES, { description: "Narrow this worker's available tools." }),
|
|
107
|
+
),
|
|
2613
108
|
target: Type.Optional(Type.String()),
|
|
2614
109
|
model: Type.Optional(Type.String()),
|
|
2615
110
|
node: Type.Optional(Type.String({ description: "Fleet node pin: local or a fleet.nodes id." })),
|
|
@@ -2620,10 +115,10 @@ export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
|
2620
115
|
),
|
|
2621
116
|
),
|
|
2622
117
|
mode: Type.Optional(
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
),
|
|
118
|
+
StringEnum(["parallel", "sequential", "pipeline", "compete"], {
|
|
119
|
+
description:
|
|
120
|
+
"Run tasks concurrently (default), one at a time, as a pipeline where each task receives the previous task's output as input data, or as a compete where N candidates build the same single task in scratch worktrees and a judge picks the winner.",
|
|
121
|
+
}),
|
|
2627
122
|
),
|
|
2628
123
|
detach: Type.Optional(
|
|
2629
124
|
Type.Boolean({
|
|
@@ -2692,11 +187,9 @@ export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
|
2692
187
|
}),
|
|
2693
188
|
),
|
|
2694
189
|
persona: Type.Optional(
|
|
2695
|
-
Type.String({
|
|
2696
|
-
description: "Default ad-hoc specialist persona for dispatched tasks, max 8000 chars.",
|
|
2697
|
-
}),
|
|
190
|
+
Type.String({ description: "Default ad-hoc specialist persona for dispatched tasks, max 8000 chars." }),
|
|
2698
191
|
),
|
|
2699
|
-
tool_profile: Type.Optional(
|
|
192
|
+
tool_profile: Type.Optional(StringEnum(TOOL_PROFILE_NAMES, { description: "Default worker tool profile." })),
|
|
2700
193
|
target: Type.Optional(Type.String({ description: "Default configured target id (omit for fleet default)." })),
|
|
2701
194
|
model: Type.Optional(Type.String({ description: "Default model override." })),
|
|
2702
195
|
node: Type.Optional(
|
|
@@ -2705,13 +198,13 @@ export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
|
2705
198
|
routing: Type.Optional(
|
|
2706
199
|
Type.Object(
|
|
2707
200
|
{
|
|
2708
|
-
posture: Type.Optional(
|
|
201
|
+
posture: Type.Optional(StringEnum(["manual", "quality", "balanced", "latency", "economy"] as const)),
|
|
2709
202
|
maxCostUsd: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
2710
203
|
deadlineMs: Type.Optional(Type.Integer({ exclusiveMinimum: 0 })),
|
|
2711
204
|
minimumQuality: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
2712
205
|
requiredCapabilities: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2713
|
-
locality: Type.Optional(
|
|
2714
|
-
failover: Type.Optional(
|
|
206
|
+
locality: Type.Optional(StringEnum(["local-only", "prefer-local", "any"] as const)),
|
|
207
|
+
failover: Type.Optional(StringEnum(["none", "approved"] as const)),
|
|
2715
208
|
},
|
|
2716
209
|
{
|
|
2717
210
|
additionalProperties: false,
|
|
@@ -2719,620 +212,33 @@ export function createDispatchTool(inputDeps: DispatchToolDeps): ToolSpec {
|
|
|
2719
212
|
},
|
|
2720
213
|
),
|
|
2721
214
|
),
|
|
2722
|
-
thinking_level: Type.Optional(
|
|
215
|
+
thinking_level: Type.Optional(StringEnum(THINKING_LEVELS)),
|
|
2723
216
|
cwd: Type.Optional(Type.String({ description: "Default agent working directory." })),
|
|
2724
217
|
timeout_ms: Type.Optional(Type.Number({ description: "Abort the dispatch after this many ms." })),
|
|
2725
218
|
max_output_bytes: Type.Optional(Type.Number({ description: "Max summary bytes returned." })),
|
|
2726
219
|
}),
|
|
2727
220
|
baseActionClass: "dispatch",
|
|
2728
221
|
executionMode: "sequential",
|
|
2729
|
-
prepareAdmissionArguments,
|
|
2730
|
-
disposeAdmissionArguments:
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
describeDispatchPlan: (args) => {
|
|
2736
|
-
const trusted = trustedResolvedPlans.get(args);
|
|
2737
|
-
return describeDispatchPlan(trusted === undefined ? args : { ...args, [RESOLVED_DISPATCH_PLAN_ARGUMENT]: trusted });
|
|
2738
|
-
},
|
|
2739
|
-
async run(rawArgs, options): Promise<ToolResult> {
|
|
2740
|
-
const args = prepareExecutionArguments(rawArgs);
|
|
222
|
+
prepareAdmissionArguments: admission.prepareAdmissionArguments,
|
|
223
|
+
disposeAdmissionArguments: admission.disposeAdmissionArguments,
|
|
224
|
+
prepareArguments: admission.prepareArguments,
|
|
225
|
+
describeDispatchPlan: admission.describeDispatchPlan,
|
|
226
|
+
async run(rawArgs, options) {
|
|
227
|
+
const args = admission.prepareArguments(rawArgs);
|
|
2741
228
|
const preparationError = args[DISPATCH_PLAN_PREPARATION_ERROR_ARGUMENT];
|
|
2742
|
-
if (typeof preparationError === "string")
|
|
2743
|
-
|
|
2744
|
-
kind: "error",
|
|
2745
|
-
message: preparationError.startsWith("dispatch: ")
|
|
2746
|
-
? preparationError
|
|
2747
|
-
: `dispatch: plan admission failed: ${preparationError}`,
|
|
2748
|
-
};
|
|
2749
|
-
}
|
|
2750
|
-
if (args.list === true) {
|
|
2751
|
-
const catalog = deps.getAgentCatalog?.().trim() ?? "";
|
|
2752
|
-
if (catalog.length === 0) {
|
|
2753
|
-
return { kind: "error", message: "dispatch: no agent catalog is available in this context" };
|
|
2754
|
-
}
|
|
2755
|
-
return { kind: "ok", output: catalog };
|
|
2756
|
-
}
|
|
229
|
+
if (typeof preparationError === "string") return admissionFailureResult(preparationError);
|
|
230
|
+
let runner: DispatchRunnerModule;
|
|
2757
231
|
try {
|
|
2758
|
-
|
|
232
|
+
runner = await loadRunner();
|
|
2759
233
|
} catch (error) {
|
|
2760
234
|
return {
|
|
2761
235
|
kind: "error",
|
|
2762
|
-
message:
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if (args.apply_winner !== undefined) {
|
|
2766
|
-
const confirmation = resolvedDispatchPlanFromArgs(args)?.confirmation;
|
|
2767
|
-
if (confirmation !== undefined) {
|
|
2768
|
-
const branch = isRecord(args.apply_winner) ? stringArg(args.apply_winner, "branch") : undefined;
|
|
2769
|
-
if (branch !== confirmation.branch) {
|
|
2770
|
-
return { kind: "error", message: "dispatch: winner confirmation differs from the approved plan" };
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
const approval = options?.approval;
|
|
2774
|
-
const authority =
|
|
2775
|
-
approval?.actionClass === "dispatch"
|
|
2776
|
-
? {
|
|
2777
|
-
outcome: "operator-confirmed" as const,
|
|
2778
|
-
requestId: approval.requestId,
|
|
2779
|
-
requestedBy: approval.requestedBy,
|
|
2780
|
-
}
|
|
2781
|
-
: deps.getAutonomy?.() === "full-auto"
|
|
2782
|
-
? { outcome: "full-auto-applied" as const }
|
|
2783
|
-
: null;
|
|
2784
|
-
let protectedPaths: string[];
|
|
2785
|
-
try {
|
|
2786
|
-
protectedPaths = deps.dispatch.protectedArtifactState?.().artifacts.map((artifact) => artifact.path) ?? [];
|
|
2787
|
-
} catch (error) {
|
|
2788
|
-
return {
|
|
2789
|
-
kind: "error",
|
|
2790
|
-
message: `dispatch: protected artifact state is unavailable: ${competeErrorMessage(error)}`,
|
|
2791
|
-
};
|
|
2792
|
-
}
|
|
2793
|
-
return runApplyWinner(args, authority, deps.competeWorktrees?.mergeWinner, protectedPaths);
|
|
2794
|
-
}
|
|
2795
|
-
const trustedRequests = trustedParsedRequests.get(args);
|
|
2796
|
-
const parsed =
|
|
2797
|
-
trustedRequests === undefined
|
|
2798
|
-
? parseRequests(args)
|
|
2799
|
-
: ({ ok: true, requests: structuredClone(trustedRequests) } as const);
|
|
2800
|
-
if (!parsed.ok) return { kind: "error", message: parsed.message };
|
|
2801
|
-
const validModes = ["parallel", "sequential", "pipeline", "compete"];
|
|
2802
|
-
if (args.mode !== undefined && !validModes.includes(String(args.mode))) {
|
|
2803
|
-
return {
|
|
2804
|
-
kind: "error",
|
|
2805
|
-
message: `dispatch: mode must be parallel, sequential, pipeline, or compete; got '${String(args.mode)}'`,
|
|
2806
|
-
};
|
|
2807
|
-
}
|
|
2808
|
-
const mode =
|
|
2809
|
-
args.mode === "sequential"
|
|
2810
|
-
? "sequential"
|
|
2811
|
-
: args.mode === "pipeline"
|
|
2812
|
-
? "pipeline"
|
|
2813
|
-
: args.mode === "compete"
|
|
2814
|
-
? "compete"
|
|
2815
|
-
: "parallel";
|
|
2816
|
-
if (options?.signal?.aborted) return { kind: "error", message: "dispatch: aborted" };
|
|
2817
|
-
const maxOutputBytes = maxOutputBytesArg(args);
|
|
2818
|
-
const timeoutMs = timeoutMsArg(args);
|
|
2819
|
-
const reviewParsed = reviewSettingsFromArgs(args);
|
|
2820
|
-
if (!reviewParsed.ok) return { kind: "error", message: reviewParsed.message };
|
|
2821
|
-
let review = reviewParsed.review;
|
|
2822
|
-
|
|
2823
|
-
// Plan-scale calls are either approved at supervised admission or run
|
|
2824
|
-
// unstopped at full-auto; every run seals the same plan hash.
|
|
2825
|
-
const autonomy = deps.getAutonomy?.() ?? "auto-edit";
|
|
2826
|
-
const authenticatedApproval = options?.approval?.actionClass === "dispatch" ? options.approval : undefined;
|
|
2827
|
-
const trustedResolvedPlan = trustedResolvedPlans.get(args) ?? null;
|
|
2828
|
-
const planArgs =
|
|
2829
|
-
trustedResolvedPlan === null ? args : { ...args, [RESOLVED_DISPATCH_PLAN_ARGUMENT]: trustedResolvedPlan };
|
|
2830
|
-
const planView = describeDispatchPlan(planArgs);
|
|
2831
|
-
// Production preparation always registers the authoritative snapshot.
|
|
2832
|
-
// The parser remains available in dispatch-plan.ts for direct utility
|
|
2833
|
-
// tests/tools, but a hidden model-supplied field is never execution trust.
|
|
2834
|
-
const resolvedPlan = trustedResolvedPlan;
|
|
2835
|
-
const reservationOwnerId = trustedReservationOwners.get(args);
|
|
2836
|
-
// Stamp this call's own id on every request before the mode branches
|
|
2837
|
-
// split them. Derived sub-requests that spread a base request inherit
|
|
2838
|
-
// it; the ones built fresh (reviewer, judge) copy it explicitly. A
|
|
2839
|
-
// surface that cannot find the parent still renders the run, just not
|
|
2840
|
-
// nested, so an absent id is never an error.
|
|
2841
|
-
const parentToolCallId = options?.toolCallId;
|
|
2842
|
-
let requests: ReadonlyArray<DispatchRequest> =
|
|
2843
|
-
parentToolCallId === undefined || parentToolCallId.length === 0
|
|
2844
|
-
? parsed.requests
|
|
2845
|
-
: parsed.requests.map((request) => ({ ...request, parentToolCallId }));
|
|
2846
|
-
if (planView.planScale && deps.dispatch.preview !== undefined && resolvedPlan === null) {
|
|
2847
|
-
return { kind: "error", message: "dispatch: resolved plan is missing after admission" };
|
|
2848
|
-
}
|
|
2849
|
-
if (planView.planScale && autonomy !== "full-auto" && authenticatedApproval === undefined) {
|
|
2850
|
-
return {
|
|
2851
|
-
kind: "error",
|
|
2852
|
-
message: "dispatch: resolved plan requires a registry-authenticated operator approval",
|
|
2853
|
-
};
|
|
2854
|
-
}
|
|
2855
|
-
if (resolvedPlan !== null) {
|
|
2856
|
-
const primaryRole = review !== undefined ? "builder" : mode === "compete" ? "candidate" : "task";
|
|
2857
|
-
const primaryTasks = resolvedPlan.tasks.filter((task) => task.role === primaryRole);
|
|
2858
|
-
const executionTasks = primaryRole === "task" ? primaryTasks : primaryTasks.slice(0, 1);
|
|
2859
|
-
if (executionTasks.length !== requests.length) {
|
|
2860
|
-
return {
|
|
2861
|
-
kind: "error",
|
|
2862
|
-
message: `dispatch: resolved plan has ${executionTasks.length} primary task(s), expected ${requests.length}`,
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
requests = requests.map((request, index) => {
|
|
2866
|
-
const task = executionTasks[index];
|
|
2867
|
-
const pinned = withResolvedPlanTaskPin(request, task);
|
|
2868
|
-
return reservationOwnerId !== undefined && task?.role !== undefined && task.position !== undefined
|
|
2869
|
-
? {
|
|
2870
|
-
...pinned,
|
|
2871
|
-
reservation: {
|
|
2872
|
-
ownerId: reservationOwnerId,
|
|
2873
|
-
memberId: task.stepId ?? `${task.role}-${task.position}`,
|
|
2874
|
-
},
|
|
2875
|
-
}
|
|
2876
|
-
: pinned;
|
|
2877
|
-
});
|
|
2878
|
-
if (review !== undefined) {
|
|
2879
|
-
const reviewer = resolvedPlan.tasks.find((task) => task.role === "reviewer");
|
|
2880
|
-
if (reviewer === undefined) {
|
|
2881
|
-
return { kind: "error", message: "dispatch: resolved review plan has no reviewer task" };
|
|
2882
|
-
}
|
|
2883
|
-
review = {
|
|
2884
|
-
...review,
|
|
2885
|
-
reviewer: reviewer.agent,
|
|
2886
|
-
target: reviewer.target,
|
|
2887
|
-
model: reviewer.model,
|
|
2888
|
-
node: reviewer.node,
|
|
2889
|
-
resolvedTasks: resolvedPlan.tasks,
|
|
2890
|
-
};
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
if (planView.planScale) {
|
|
2894
|
-
const plan: RunPlanProvenance = {
|
|
2895
|
-
hash: planView.hash,
|
|
2896
|
-
topology: planView.topology,
|
|
2897
|
-
taskCount: planView.taskCount,
|
|
2898
|
-
approval: authenticatedApproval !== undefined ? "operator" : "full-auto",
|
|
2899
|
-
source:
|
|
2900
|
-
resolvedPlan?.source === null || resolvedPlan?.source === undefined
|
|
2901
|
-
? null
|
|
2902
|
-
: {
|
|
2903
|
-
kind: "scout-transition",
|
|
2904
|
-
runId: resolvedPlan.source.runId,
|
|
2905
|
-
receiptDigest: resolvedPlan.source.receiptDigest,
|
|
2906
|
-
executionPlanHash: resolvedPlan.source.executionPlanHash,
|
|
2907
|
-
},
|
|
2908
|
-
...(authenticatedApproval !== undefined
|
|
2909
|
-
? {
|
|
2910
|
-
approvalRequestId: authenticatedApproval.requestId,
|
|
2911
|
-
approvalRequestedBy: authenticatedApproval.requestedBy,
|
|
2912
|
-
}
|
|
2913
|
-
: {}),
|
|
2914
|
-
...(planView.costCeilingUsd !== undefined ? { costCeilingUsd: planView.costCeilingUsd } : {}),
|
|
236
|
+
message:
|
|
237
|
+
incompleteInstallationAdvice(error) ??
|
|
238
|
+
`dispatch: implementation unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2915
239
|
};
|
|
2916
|
-
requests = requests.map((request) => ({ ...request, plan }));
|
|
2917
|
-
}
|
|
2918
|
-
const backgroundRefusal =
|
|
2919
|
-
resolvedPlan?.topology === "fleet"
|
|
2920
|
-
? "a Scout dependency plan drives its stages from this turn"
|
|
2921
|
-
: mode === "compete"
|
|
2922
|
-
? "compete holds its judge gate in this turn"
|
|
2923
|
-
: review !== undefined
|
|
2924
|
-
? "a review gate holds its cycle state in this turn"
|
|
2925
|
-
: mode === "pipeline" && requests.length > 1
|
|
2926
|
-
? "a pipeline threads each step's output through this turn"
|
|
2927
|
-
: timeoutMs !== undefined
|
|
2928
|
-
? `this call set timeout_ms=${timeoutMs} and nobody would be left to enforce the deadline`
|
|
2929
|
-
: deps.dispatch.detached === undefined
|
|
2930
|
-
? "detached batch records are unavailable in this context"
|
|
2931
|
-
: null;
|
|
2932
|
-
const background = createBackgroundSwitch();
|
|
2933
|
-
// Review and Scout both execute under mode=parallel, so the operator-facing
|
|
2934
|
-
// label names the topology they actually asked for.
|
|
2935
|
-
const backgroundLabel = resolvedPlan?.topology === "fleet" ? "fleet" : review !== undefined ? "review" : mode;
|
|
2936
|
-
const releaseBackground = registerBackgroundControl(deps, options?.toolCallId, {
|
|
2937
|
-
label: `dispatch (${backgroundLabel})`,
|
|
2938
|
-
refusal: backgroundRefusal,
|
|
2939
|
-
fire: () => background.fire(),
|
|
2940
|
-
});
|
|
2941
|
-
try {
|
|
2942
|
-
if (resolvedPlan?.topology === "fleet") {
|
|
2943
|
-
const executionPlan = trustedExecutionPlans.get(args);
|
|
2944
|
-
if (
|
|
2945
|
-
executionPlan === undefined ||
|
|
2946
|
-
resolvedPlan.source === null ||
|
|
2947
|
-
resolvedPlan.deadlineMs === null ||
|
|
2948
|
-
reservationOwnerId === undefined
|
|
2949
|
-
) {
|
|
2950
|
-
return { kind: "error", message: "dispatch: trusted Scout dependency plan is unavailable" };
|
|
2951
|
-
}
|
|
2952
|
-
if (executionPlan.hash !== resolvedPlan.source.executionPlanHash) {
|
|
2953
|
-
return { kind: "error", message: "dispatch: Scout dependency plan hash drifted after admission" };
|
|
2954
|
-
}
|
|
2955
|
-
if (!scoutPlanAuthorityGranted(resolvedPlan, authenticatedApproval !== undefined, autonomy === "full-auto")) {
|
|
2956
|
-
return { kind: "error", message: "dispatch: Scout dependency plan lacks authenticated authority grants" };
|
|
2957
|
-
}
|
|
2958
|
-
const executable = compileExecutionPlan({
|
|
2959
|
-
topology: executionPlan.topology,
|
|
2960
|
-
rootTask: executionPlan.rootTask,
|
|
2961
|
-
maxWorkers: executionPlan.maxWorkers,
|
|
2962
|
-
onFailure: executionPlan.onFailure,
|
|
2963
|
-
steps: requireAgentSteps(executionPlan.steps).map((step) => ({
|
|
2964
|
-
...step,
|
|
2965
|
-
approvedAuthority: step.requestedAuthority,
|
|
2966
|
-
})),
|
|
2967
|
-
});
|
|
2968
|
-
try {
|
|
2969
|
-
const outcome = await runScoutContinuationPlan({
|
|
2970
|
-
dispatch: deps.dispatch,
|
|
2971
|
-
plan: executable,
|
|
2972
|
-
artifact: resolvedPlan,
|
|
2973
|
-
requests,
|
|
2974
|
-
reservationOwnerId,
|
|
2975
|
-
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
2976
|
-
register: (handle, agentId) =>
|
|
2977
|
-
deps.runEvents.registerSingle(handle, agentId, fallbackProgressBus(deps)).completion,
|
|
2978
|
-
complete: (receipt, summary) => {
|
|
2979
|
-
const completed = completeRun(deps, receipt, summary);
|
|
2980
|
-
return { value: completed, integrityValid: completed.integrity.ok };
|
|
2981
|
-
},
|
|
2982
|
-
});
|
|
2983
|
-
const output = formatDispatchOutput("fleet", outcome.runs, maxOutputBytes);
|
|
2984
|
-
const details = {
|
|
2985
|
-
...dispatchDetails(deps, "fleet", outcome.runs),
|
|
2986
|
-
executionPlanHash: executable.hash,
|
|
2987
|
-
skippedSteps: outcome.skipped,
|
|
2988
|
-
};
|
|
2989
|
-
const failed = outcome.runs.some((run) => run.receipt.exitCode !== 0) || outcome.skipped.length > 0;
|
|
2990
|
-
return failed ? { kind: "error", message: output, details } : { kind: "ok", output, details };
|
|
2991
|
-
} catch (error) {
|
|
2992
|
-
return {
|
|
2993
|
-
kind: "error",
|
|
2994
|
-
message: `dispatch: Scout dependency plan failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
if (args.detach === true) {
|
|
3000
|
-
if (mode !== "parallel") {
|
|
3001
|
-
return { kind: "error", message: `dispatch: detach only supports parallel mode; got '${mode}'` };
|
|
3002
|
-
}
|
|
3003
|
-
if (review !== undefined) {
|
|
3004
|
-
return {
|
|
3005
|
-
kind: "error",
|
|
3006
|
-
message: "dispatch: detach cannot combine with a review gate; run gated dispatch attached",
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
if (timeoutMs !== undefined) {
|
|
3010
|
-
return {
|
|
3011
|
-
kind: "error",
|
|
3012
|
-
message:
|
|
3013
|
-
'dispatch: detach cannot enforce timeout_ms because no caller waits on the runs; monitor(mode="wait") only observes for a bounded time, and steer(action="cancel") stops a run',
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
try {
|
|
3017
|
-
return await runDetached(deps, requests, options?.sessionId ?? null);
|
|
3018
|
-
} catch (err) {
|
|
3019
|
-
return { kind: "error", message: dispatchErrorMessage(err) };
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
if (mode === "compete") {
|
|
3024
|
-
if (requests.length !== 1 || requests[0] === undefined) {
|
|
3025
|
-
return { kind: "error", message: COMPETE_SINGLE_TASK_MESSAGE };
|
|
3026
|
-
}
|
|
3027
|
-
if (review !== undefined) {
|
|
3028
|
-
return { kind: "error", message: COMPETE_NO_REVIEW_MESSAGE };
|
|
3029
|
-
}
|
|
3030
|
-
const competeParsed = competeSettingsFromArgs(args);
|
|
3031
|
-
if (!competeParsed.ok) return { kind: "error", message: competeParsed.message };
|
|
3032
|
-
let compete = competeParsed.compete;
|
|
3033
|
-
if (resolvedPlan !== null) {
|
|
3034
|
-
const judge = resolvedPlan.tasks.find((task) => task.role === "judge");
|
|
3035
|
-
if (judge === undefined) {
|
|
3036
|
-
return { kind: "error", message: "dispatch: resolved compete plan has no judge task" };
|
|
3037
|
-
}
|
|
3038
|
-
compete = {
|
|
3039
|
-
...compete,
|
|
3040
|
-
judge: { agent: judge.agent, target: judge.target, model: judge.model, node: judge.node },
|
|
3041
|
-
resolvedTasks: resolvedPlan.tasks,
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
try {
|
|
3045
|
-
const outcome = await runCompete(deps, requests[0], compete, autonomy, timeoutMs, options?.signal);
|
|
3046
|
-
return competeResult(deps, outcome, autonomy, maxOutputBytes);
|
|
3047
|
-
} catch (err) {
|
|
3048
|
-
return { kind: "error", message: dispatchErrorMessage(err) };
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
if (review !== undefined) {
|
|
3053
|
-
if (requests.length !== 1 || requests[0] === undefined) {
|
|
3054
|
-
return { kind: "error", message: REVIEW_SINGLE_TASK_MESSAGE };
|
|
3055
|
-
}
|
|
3056
|
-
if (mode !== "parallel") {
|
|
3057
|
-
return { kind: "error", message: `dispatch: review does not combine with mode=${mode}` };
|
|
3058
|
-
}
|
|
3059
|
-
try {
|
|
3060
|
-
const outcome = await runReviewGated(deps, requests[0], review, timeoutMs, options?.signal);
|
|
3061
|
-
return reviewGateResult(deps, outcome, maxOutputBytes);
|
|
3062
|
-
} catch (err) {
|
|
3063
|
-
return { kind: "error", message: dispatchErrorMessage(err) };
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
try {
|
|
3068
|
-
let runs: CompletedRun[];
|
|
3069
|
-
// Only the concurrent fan-out has peers, so only it has a board.
|
|
3070
|
-
let board: string | null = null;
|
|
3071
|
-
if (mode === "pipeline" && requests.length > 1) {
|
|
3072
|
-
runs = await runPipeline(deps, requests, timeoutMs, options?.signal);
|
|
3073
|
-
} else if (mode === "sequential" || mode === "pipeline" || requests.length === 1) {
|
|
3074
|
-
// A single-task pipeline has nothing to thread, so it degrades to
|
|
3075
|
-
// plain sequential and no pipeline-input message is sent.
|
|
3076
|
-
runs = await runSequential(deps, requests, mode, timeoutMs, options?.signal, background);
|
|
3077
|
-
} else {
|
|
3078
|
-
const batch = await runBatch(deps, requests, timeoutMs, options?.signal, background);
|
|
3079
|
-
runs = batch.runs;
|
|
3080
|
-
board = batch.board;
|
|
3081
|
-
}
|
|
3082
|
-
const output = formatDispatchOutput(mode, runs, maxOutputBytes, board);
|
|
3083
|
-
const details = dispatchDetails(deps, mode, runs, board);
|
|
3084
|
-
const failed = runs.filter((run) => run.receipt.exitCode !== 0);
|
|
3085
|
-
if (failed.length > 0) return { kind: "error", message: output, details };
|
|
3086
|
-
return { kind: "ok", output, details };
|
|
3087
|
-
} catch (err) {
|
|
3088
|
-
if (err instanceof DispatchBackgroundedError) {
|
|
3089
|
-
return await backgroundedDispatchResult(deps, err, mode, options?.sessionId ?? null);
|
|
3090
|
-
}
|
|
3091
|
-
if (err instanceof PipelineHaltError) {
|
|
3092
|
-
const haltMessage = `dispatch: ${err.message}`;
|
|
3093
|
-
const output = formatDispatchOutput("pipeline", err.runs, maxOutputBytes);
|
|
3094
|
-
return {
|
|
3095
|
-
kind: "error",
|
|
3096
|
-
message: `${haltMessage}\n\n${output}`,
|
|
3097
|
-
details: dispatchDetails(deps, "pipeline", err.runs),
|
|
3098
|
-
};
|
|
3099
|
-
}
|
|
3100
|
-
return { kind: "error", message: dispatchErrorMessage(err) };
|
|
3101
|
-
}
|
|
3102
|
-
} finally {
|
|
3103
|
-
releaseBackground();
|
|
3104
240
|
}
|
|
241
|
+
return runner.runDispatchTool(deps, admission.state, args, options);
|
|
3105
242
|
},
|
|
3106
243
|
};
|
|
3107
244
|
}
|
|
3108
|
-
|
|
3109
|
-
/**
|
|
3110
|
-
* One at a time: each run completes before the next dispatches. Also serves
|
|
3111
|
-
* single-task parallel calls, where batching adds nothing. The timeout and
|
|
3112
|
-
* abort signal cover the whole sequence; remaining tasks are skipped once
|
|
3113
|
-
* either fires and the skip is reported through the thrown error.
|
|
3114
|
-
*/
|
|
3115
|
-
async function runSequential(
|
|
3116
|
-
deps: RegisteredDispatchToolDeps,
|
|
3117
|
-
requests: ReadonlyArray<DispatchRequest>,
|
|
3118
|
-
mode: string,
|
|
3119
|
-
timeoutMs: number | undefined,
|
|
3120
|
-
signal: AbortSignal | undefined,
|
|
3121
|
-
background: BackgroundSwitch,
|
|
3122
|
-
): Promise<CompletedRun[]> {
|
|
3123
|
-
const runs: CompletedRun[] = [];
|
|
3124
|
-
let expired = false;
|
|
3125
|
-
let activeRunId: string | null = null;
|
|
3126
|
-
// The operator signal is a cancel; the timer is a timeout. Both stop the
|
|
3127
|
-
// sequence, but the timeout carries a cause so the receipt names it.
|
|
3128
|
-
const abortActive = (bySignal: boolean): void => {
|
|
3129
|
-
expired = true;
|
|
3130
|
-
if (activeRunId !== null) {
|
|
3131
|
-
deps.dispatch.abort(
|
|
3132
|
-
activeRunId,
|
|
3133
|
-
bySignal ? undefined : { cause: "timeout", detail: `timed out after ${timeoutMs}ms` },
|
|
3134
|
-
);
|
|
3135
|
-
}
|
|
3136
|
-
};
|
|
3137
|
-
const onSignalAbort = (): void => abortActive(true);
|
|
3138
|
-
const timer = timeoutMs !== undefined ? setTimeout(() => abortActive(false), timeoutMs) : null;
|
|
3139
|
-
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
3140
|
-
try {
|
|
3141
|
-
for (const [index, request] of requests.entries()) {
|
|
3142
|
-
if (expired || signal?.aborted) {
|
|
3143
|
-
throw new Error(
|
|
3144
|
-
`${mode} dispatch stopped after ${runs.length}/${requests.length} task(s): ${signal?.aborted ? "aborted" : `timed out after ${timeoutMs}ms`}`,
|
|
3145
|
-
);
|
|
3146
|
-
}
|
|
3147
|
-
const handle = await deps.dispatch.dispatch(request);
|
|
3148
|
-
activeRunId = handle.runId;
|
|
3149
|
-
const registered = deps.runEvents.registerSingle(handle, request.agentId, fallbackProgressBus(deps));
|
|
3150
|
-
const completion = registered.completion.then((value) => ({ kind: "completed" as const, value }));
|
|
3151
|
-
const settled = await Promise.race([completion, background.requested.then(() => ({ kind: "background" as const }))]);
|
|
3152
|
-
if (settled.kind === "background") {
|
|
3153
|
-
// activeRunId stays set on purpose: the finally below only clears the
|
|
3154
|
-
// timer and the abort listener, and the live step must survive both.
|
|
3155
|
-
completion.catch(() => {});
|
|
3156
|
-
throw new DispatchBackgroundedError(
|
|
3157
|
-
newGateGroupId("batch"),
|
|
3158
|
-
[{ runId: handle.runId, assignmentId: handle.runId, agentId: request.agentId }],
|
|
3159
|
-
[...runs],
|
|
3160
|
-
requests.slice(index + 1).map((later) => later.agentId),
|
|
3161
|
-
);
|
|
3162
|
-
}
|
|
3163
|
-
activeRunId = null;
|
|
3164
|
-
runs.push(completeRun(deps, settled.value.receipt, settled.value.summary));
|
|
3165
|
-
}
|
|
3166
|
-
return runs;
|
|
3167
|
-
} finally {
|
|
3168
|
-
if (timer) clearTimeout(timer);
|
|
3169
|
-
signal?.removeEventListener("abort", onSignalAbort);
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
/** A pipeline step failed when its worker exited nonzero or its outcome is not success. */
|
|
3174
|
-
function isPipelineStepFailure(receipt: RunReceipt): boolean {
|
|
3175
|
-
if (receipt.exitCode !== 0) return true;
|
|
3176
|
-
return receipt.outcome !== undefined && receipt.outcome !== "succeeded";
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
function pipelineFailureReason(receipt: RunReceipt): string {
|
|
3180
|
-
if (receipt.outcome !== undefined && receipt.outcome !== "succeeded") return `outcome=${receipt.outcome}`;
|
|
3181
|
-
return `exit=${receipt.exitCode}`;
|
|
3182
|
-
}
|
|
3183
|
-
|
|
3184
|
-
/**
|
|
3185
|
-
* Chain worker outputs: each step runs to completion, then its final assistant
|
|
3186
|
-
* text is threaded to the next step as `pipelineInput` (data, not instruction
|
|
3187
|
-
* text). Step 1 receives none. A failed step halts the chain and the thrown
|
|
3188
|
-
* error names the step and how many later steps were skipped, mirroring
|
|
3189
|
-
* runSequential's "stopped after N/M" phrasing. Whole-sequence timeout and
|
|
3190
|
-
* abort handling match runSequential.
|
|
3191
|
-
*/
|
|
3192
|
-
async function runPipeline(
|
|
3193
|
-
deps: RegisteredDispatchToolDeps,
|
|
3194
|
-
requests: ReadonlyArray<DispatchRequest>,
|
|
3195
|
-
timeoutMs: number | undefined,
|
|
3196
|
-
signal: AbortSignal | undefined,
|
|
3197
|
-
): Promise<CompletedRun[]> {
|
|
3198
|
-
const runs: CompletedRun[] = [];
|
|
3199
|
-
let expired = false;
|
|
3200
|
-
let activeRunId: string | null = null;
|
|
3201
|
-
// The operator signal is a cancel; the timer is a timeout. Both stop the
|
|
3202
|
-
// chain, but the timeout carries a cause so the receipt names it.
|
|
3203
|
-
const abortActive = (bySignal: boolean): void => {
|
|
3204
|
-
expired = true;
|
|
3205
|
-
if (activeRunId !== null) {
|
|
3206
|
-
deps.dispatch.abort(
|
|
3207
|
-
activeRunId,
|
|
3208
|
-
bySignal ? undefined : { cause: "timeout", detail: `timed out after ${timeoutMs}ms` },
|
|
3209
|
-
);
|
|
3210
|
-
}
|
|
3211
|
-
};
|
|
3212
|
-
const onSignalAbort = (): void => abortActive(true);
|
|
3213
|
-
const timer = timeoutMs !== undefined ? setTimeout(() => abortActive(false), timeoutMs) : null;
|
|
3214
|
-
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
3215
|
-
try {
|
|
3216
|
-
let previous: { runId: string; text: string } | null = null;
|
|
3217
|
-
for (const [index, base] of requests.entries()) {
|
|
3218
|
-
if (expired || signal?.aborted) {
|
|
3219
|
-
throw new Error(
|
|
3220
|
-
`pipeline dispatch stopped after ${runs.length}/${requests.length} task(s): ${signal?.aborted ? "aborted" : `timed out after ${timeoutMs}ms`}`,
|
|
3221
|
-
);
|
|
3222
|
-
}
|
|
3223
|
-
// Thread the previous step's output as data; the task string the
|
|
3224
|
-
// orchestrator authored is sent verbatim. Step 1 (previous === null)
|
|
3225
|
-
// carries no pipeline input.
|
|
3226
|
-
const request: DispatchRequest =
|
|
3227
|
-
previous === null
|
|
3228
|
-
? base
|
|
3229
|
-
: { ...base, pipelineInput: { fromRunId: previous.runId, position: index + 1, text: previous.text } };
|
|
3230
|
-
const handle = await deps.dispatch.dispatch(request);
|
|
3231
|
-
activeRunId = handle.runId;
|
|
3232
|
-
const registered = deps.runEvents.registerSingle(handle, request.agentId, fallbackProgressBus(deps));
|
|
3233
|
-
const { receipt, summary } = await registered.completion;
|
|
3234
|
-
activeRunId = null;
|
|
3235
|
-
runs.push(completeRun(deps, receipt, summary));
|
|
3236
|
-
if (isPipelineStepFailure(receipt)) {
|
|
3237
|
-
const skipped = requests.length - (index + 1);
|
|
3238
|
-
throw new PipelineHaltError(
|
|
3239
|
-
`pipeline dispatch halted at step ${index + 1}/${requests.length} (run ${receipt.runId}, ${pipelineFailureReason(receipt)}); skipped ${skipped} later step(s)`,
|
|
3240
|
-
[...runs],
|
|
3241
|
-
);
|
|
3242
|
-
}
|
|
3243
|
-
previous = {
|
|
3244
|
-
runId: receipt.runId,
|
|
3245
|
-
text: receipt.output?.state === "final" ? receipt.output.text : "",
|
|
3246
|
-
};
|
|
3247
|
-
}
|
|
3248
|
-
return runs;
|
|
3249
|
-
} finally {
|
|
3250
|
-
if (timer) clearTimeout(timer);
|
|
3251
|
-
signal?.removeEventListener("abort", onSignalAbort);
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
interface BatchOutcome {
|
|
3256
|
-
runs: CompletedRun[];
|
|
3257
|
-
/** Rendered board of the batch's shared ledger, null when nothing was posted. */
|
|
3258
|
-
board: string | null;
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
async function runBatch(
|
|
3262
|
-
deps: RegisteredDispatchToolDeps,
|
|
3263
|
-
requests: ReadonlyArray<DispatchRequest>,
|
|
3264
|
-
timeoutMs: number | undefined,
|
|
3265
|
-
signal: AbortSignal | undefined,
|
|
3266
|
-
background: BackgroundSwitch,
|
|
3267
|
-
): Promise<BatchOutcome> {
|
|
3268
|
-
// This arm is the concurrent parallel fan-out, so its peers can see each
|
|
3269
|
-
// other. The ledger opens before admission and every request carries it, so
|
|
3270
|
-
// no worker spawns into a dispatch whose board it cannot reach.
|
|
3271
|
-
const ledgerId = newGateGroupId("ledger");
|
|
3272
|
-
await openAgentLedger(ledgerId);
|
|
3273
|
-
const ledgered = requests.map((request) => ({ ...request, ledger: { id: ledgerId, sequence: 0 } }));
|
|
3274
|
-
const handle = await deps.dispatch.dispatchBatch(ledgered);
|
|
3275
|
-
const registered = deps.runEvents.registerBatch(
|
|
3276
|
-
handle,
|
|
3277
|
-
ledgered.map((request) => request.agentId),
|
|
3278
|
-
fallbackProgressBus(deps),
|
|
3279
|
-
);
|
|
3280
|
-
// The operator signal is a cancel; the timer is a timeout. The timeout
|
|
3281
|
-
// carries a cause so each killed run's receipt names it.
|
|
3282
|
-
const abort = (bySignal: boolean): void => {
|
|
3283
|
-
const reason = bySignal ? undefined : ({ cause: "timeout", detail: `timed out after ${timeoutMs}ms` } as const);
|
|
3284
|
-
for (const runId of handle.assignmentIds) deps.dispatch.abort(runId, reason);
|
|
3285
|
-
};
|
|
3286
|
-
const onSignalAbort = (): void => abort(true);
|
|
3287
|
-
const timer = timeoutMs !== undefined ? setTimeout(() => abort(false), timeoutMs) : null;
|
|
3288
|
-
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
3289
|
-
// The board closes when the batch settles in this call. A backgrounded
|
|
3290
|
-
// batch settles elsewhere: its runs are still live and still posting, so
|
|
3291
|
-
// the ledger rides on the converted record and closes at collect instead.
|
|
3292
|
-
let backgrounded = false;
|
|
3293
|
-
try {
|
|
3294
|
-
const completion = registered.completion.then((value) => ({ kind: "completed" as const, value }));
|
|
3295
|
-
const settled = await Promise.race([completion, background.requested.then(() => ({ kind: "background" as const }))]);
|
|
3296
|
-
if (settled.kind === "background") {
|
|
3297
|
-
// The batch drain keeps metering; only this await unwinds. Every run in
|
|
3298
|
-
// a parallel batch is already live, so the whole batch converts.
|
|
3299
|
-
completion.catch(() => {});
|
|
3300
|
-
backgrounded = true;
|
|
3301
|
-
throw new DispatchBackgroundedError(
|
|
3302
|
-
handle.batchId,
|
|
3303
|
-
handle.assignmentIds.map((runId, index) => ({
|
|
3304
|
-
runId,
|
|
3305
|
-
assignmentId: runId,
|
|
3306
|
-
agentId: ledgered[index]?.agentId ?? "unknown",
|
|
3307
|
-
})),
|
|
3308
|
-
[],
|
|
3309
|
-
[],
|
|
3310
|
-
ledgerId,
|
|
3311
|
-
);
|
|
3312
|
-
}
|
|
3313
|
-
const { summaries, receipts } = settled.value;
|
|
3314
|
-
// Every worker has settled and the board has not closed yet, so this is
|
|
3315
|
-
// the whole board the peers built, read once for the model that started
|
|
3316
|
-
// them.
|
|
3317
|
-
const board = renderAgentLedgerBoard(ledgerId);
|
|
3318
|
-
return {
|
|
3319
|
-
runs: receipts.map((receipt) =>
|
|
3320
|
-
completeRun(
|
|
3321
|
-
deps,
|
|
3322
|
-
receipt,
|
|
3323
|
-
summaries.get(receipt.runId) ?? {
|
|
3324
|
-
count: 0,
|
|
3325
|
-
types: [],
|
|
3326
|
-
lastAssistantText: "",
|
|
3327
|
-
terminalAttemptRunId: receipt.runId,
|
|
3328
|
-
},
|
|
3329
|
-
),
|
|
3330
|
-
),
|
|
3331
|
-
board,
|
|
3332
|
-
};
|
|
3333
|
-
} finally {
|
|
3334
|
-
if (timer) clearTimeout(timer);
|
|
3335
|
-
signal?.removeEventListener("abort", onSignalAbort);
|
|
3336
|
-
if (!backgrounded) await closeAgentLedger(ledgerId);
|
|
3337
|
-
}
|
|
3338
|
-
}
|