smol-symphony 0.2.0 → 0.3.0
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/AGENTS.md +41 -22
- package/DESIGN.md +494 -273
- package/README.md +109 -57
- package/SPEC.md +33 -24
- package/WORKFLOW.minimal.yaml +34 -0
- package/{WORKFLOW.template.md → WORKFLOW.template.yaml} +409 -256
- package/WORKFLOW.yaml +487 -0
- package/assets/skills/symphony-issues/SKILL.md +136 -0
- package/assets/symphony-mise.system.toml +68 -0
- package/dist/src/bin/symphony.js +30 -0
- package/dist/src/bin/symphony.js.map +1 -0
- package/dist/src/core/actions/context.js +109 -0
- package/dist/src/core/actions/context.js.map +1 -0
- package/dist/{actions/parsing.js → src/core/actions/parse.js} +33 -114
- package/dist/src/core/actions/parse.js.map +1 -0
- package/dist/src/core/actions/plan.js +197 -0
- package/dist/src/core/actions/plan.js.map +1 -0
- package/dist/src/core/actions/predicates.js +111 -0
- package/dist/src/core/actions/predicates.js.map +1 -0
- package/dist/src/core/actions/run-fold.js +248 -0
- package/dist/src/core/actions/run-fold.js.map +1 -0
- package/dist/src/core/actions/template.js +118 -0
- package/dist/src/core/actions/template.js.map +1 -0
- package/dist/src/core/cli/args.js +116 -0
- package/dist/src/core/cli/args.js.map +1 -0
- package/dist/src/core/coerce.js +75 -0
- package/dist/src/core/coerce.js.map +1 -0
- package/dist/src/core/credential/account-id.js +20 -0
- package/dist/src/core/credential/account-id.js.map +1 -0
- package/dist/src/core/credential/adapter-config.js +136 -0
- package/dist/src/core/credential/adapter-config.js.map +1 -0
- package/dist/src/core/credential/availability.js +98 -0
- package/dist/src/core/credential/availability.js.map +1 -0
- package/dist/src/core/credential/extract.js +228 -0
- package/dist/src/core/credential/extract.js.map +1 -0
- package/dist/src/core/credential/fake-creds.js +171 -0
- package/dist/src/core/credential/fake-creds.js.map +1 -0
- package/dist/src/core/credential/identity.js +125 -0
- package/dist/src/core/credential/identity.js.map +1 -0
- package/dist/src/core/credential/shape.js +230 -0
- package/dist/src/core/credential/shape.js.map +1 -0
- package/dist/src/core/credential/strings.js +15 -0
- package/dist/src/core/credential/strings.js.map +1 -0
- package/dist/src/core/doctor/checks.js +303 -0
- package/dist/src/core/doctor/checks.js.map +1 -0
- package/dist/src/core/git/result.js +107 -0
- package/dist/src/core/git/result.js.map +1 -0
- package/dist/src/core/http/decisions.js +225 -0
- package/dist/src/core/http/decisions.js.map +1 -0
- package/dist/{http.js → src/core/http/render.js} +472 -738
- package/dist/src/core/http/render.js.map +1 -0
- package/dist/{http-handlers.js → src/core/http/routes.js} +52 -87
- package/dist/src/core/http/routes.js.map +1 -0
- package/dist/src/core/http/views.js +181 -0
- package/dist/src/core/http/views.js.map +1 -0
- package/dist/src/core/image/managed-image.js +95 -0
- package/dist/src/core/image/managed-image.js.map +1 -0
- package/dist/src/core/issue/file.js +149 -0
- package/dist/src/core/issue/file.js.map +1 -0
- package/dist/src/core/issue/parse.js +210 -0
- package/dist/src/core/issue/parse.js.map +1 -0
- package/dist/src/core/mcp/dispatch.js +239 -0
- package/dist/src/core/mcp/dispatch.js.map +1 -0
- package/dist/src/core/mcp/post-move.js +92 -0
- package/dist/src/core/mcp/post-move.js.map +1 -0
- package/dist/src/core/mcp/protocol.js +293 -0
- package/dist/src/core/mcp/protocol.js.map +1 -0
- package/dist/src/core/mcp/url.js +162 -0
- package/dist/src/core/mcp/url.js.map +1 -0
- package/dist/src/core/path.js +63 -0
- package/dist/src/core/path.js.map +1 -0
- package/dist/src/core/reconcile/image-decide.js +48 -0
- package/dist/src/core/reconcile/image-decide.js.map +1 -0
- package/dist/src/core/reconcile/ledger.js +142 -0
- package/dist/src/core/reconcile/ledger.js.map +1 -0
- package/dist/src/core/reconcile/pr-classify.js +62 -0
- package/dist/src/core/reconcile/pr-classify.js.map +1 -0
- package/dist/{reconciler → src/core/reconcile}/pr-decide.js +25 -12
- package/dist/src/core/reconcile/pr-decide.js.map +1 -0
- package/dist/src/core/reconcile/pr-loop.js +161 -0
- package/dist/src/core/reconcile/pr-loop.js.map +1 -0
- package/dist/src/core/reconcile/pr-notes.js +35 -0
- package/dist/src/core/reconcile/pr-notes.js.map +1 -0
- package/dist/src/core/reconcile/vm-decide.js +70 -0
- package/dist/src/core/reconcile/vm-decide.js.map +1 -0
- package/dist/src/core/reconcile/vm-reap.js +207 -0
- package/dist/src/core/reconcile/vm-reap.js.map +1 -0
- package/dist/src/core/reconcile/workspace-decide.js +162 -0
- package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/core/runlog/summary.js +231 -0
- package/dist/src/core/runlog/summary.js.map +1 -0
- package/dist/src/core/runner/dispatch-config.js +95 -0
- package/dist/src/core/runner/dispatch-config.js.map +1 -0
- package/dist/src/core/runner/injection.js +61 -0
- package/dist/src/core/runner/injection.js.map +1 -0
- package/dist/src/core/runner/mise.js +210 -0
- package/dist/src/core/runner/mise.js.map +1 -0
- package/dist/src/core/runner/prompt.js +720 -0
- package/dist/src/core/runner/prompt.js.map +1 -0
- package/dist/src/core/runner/turn.js +242 -0
- package/dist/src/core/runner/turn.js.map +1 -0
- package/dist/src/core/runner/vm-plan.js +390 -0
- package/dist/src/core/runner/vm-plan.js.map +1 -0
- package/dist/src/core/schedule/admission.js +123 -0
- package/dist/src/core/schedule/admission.js.map +1 -0
- package/dist/src/core/schedule/circuit-breaker.js +111 -0
- package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/core/schedule/eligibility.js +83 -0
- package/dist/src/core/schedule/eligibility.js.map +1 -0
- package/dist/src/core/schedule/reconcile-issue.js +82 -0
- package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
- package/dist/src/core/schedule/retry.js +96 -0
- package/dist/src/core/schedule/retry.js.map +1 -0
- package/dist/src/core/schedule/sleep-cycle.js +133 -0
- package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/core/schedule/slots.js +124 -0
- package/dist/src/core/schedule/slots.js.map +1 -0
- package/dist/src/core/schedule/tick.js +553 -0
- package/dist/src/core/schedule/tick.js.map +1 -0
- package/dist/src/core/schedule/token-fold.js +181 -0
- package/dist/src/core/schedule/token-fold.js.map +1 -0
- package/dist/src/core/state-resolve.js +86 -0
- package/dist/src/core/state-resolve.js.map +1 -0
- package/dist/src/core/vm-guards.js +278 -0
- package/dist/src/core/vm-guards.js.map +1 -0
- package/dist/src/core/workflow/derive.js +107 -0
- package/dist/src/core/workflow/derive.js.map +1 -0
- package/dist/src/core/workflow/parse.js +687 -0
- package/dist/src/core/workflow/parse.js.map +1 -0
- package/dist/src/core/workflow/prompt-probe.js +78 -0
- package/dist/src/core/workflow/prompt-probe.js.map +1 -0
- package/dist/src/core/workflow/validate.js +189 -0
- package/dist/src/core/workflow/validate.js.map +1 -0
- package/dist/src/core/workspace-key.js +19 -0
- package/dist/src/core/workspace-key.js.map +1 -0
- package/dist/src/shell/actions-runner.js +356 -0
- package/dist/src/shell/actions-runner.js.map +1 -0
- package/dist/src/shell/adapter/adapter-registry.js +45 -0
- package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
- package/dist/src/shell/adapter/clock-random.js +96 -0
- package/dist/src/shell/adapter/clock-random.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/shell/adapter/launcher-asset.js +57 -0
- package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
- package/dist/src/shell/adapter/mise-config-asset.js +65 -0
- package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
- package/dist/src/shell/adapter/workflow-loader.js +304 -0
- package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
- package/dist/src/shell/cli/doctor.js +268 -0
- package/dist/src/shell/cli/doctor.js.map +1 -0
- package/dist/src/shell/effect-interpreter-families.js +314 -0
- package/dist/src/shell/effect-interpreter-families.js.map +1 -0
- package/dist/src/shell/effect-interpreter.js +29 -0
- package/dist/src/shell/effect-interpreter.js.map +1 -0
- package/dist/src/shell/interp/acp-frame.js +137 -0
- package/dist/src/shell/interp/acp-frame.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-conn.js +320 -0
- package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-frames.js +159 -0
- package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
- package/dist/src/shell/interp/acp-ws.js +197 -0
- package/dist/src/shell/interp/acp-ws.js.map +1 -0
- package/dist/src/shell/interp/acp.js +319 -0
- package/dist/src/shell/interp/acp.js.map +1 -0
- package/dist/src/shell/interp/credential-defaults.js +128 -0
- package/dist/src/shell/interp/credential-defaults.js.map +1 -0
- package/dist/src/shell/interp/credential-hooks.js +149 -0
- package/dist/src/shell/interp/credential-hooks.js.map +1 -0
- package/dist/src/shell/interp/credential-registry.js +226 -0
- package/dist/src/shell/interp/credential-registry.js.map +1 -0
- package/dist/src/shell/interp/credential.js +103 -0
- package/dist/src/shell/interp/credential.js.map +1 -0
- package/dist/src/shell/interp/gh.js +163 -0
- package/dist/src/shell/interp/gh.js.map +1 -0
- package/dist/src/shell/interp/git.js +28 -0
- package/dist/src/shell/interp/git.js.map +1 -0
- package/dist/src/shell/interp/log.js +213 -0
- package/dist/src/shell/interp/log.js.map +1 -0
- package/dist/src/shell/interp/process.js +178 -0
- package/dist/src/shell/interp/process.js.map +1 -0
- package/dist/src/shell/interp/runlog.js +193 -0
- package/dist/src/shell/interp/runlog.js.map +1 -0
- package/dist/src/shell/interp/timer.js +64 -0
- package/dist/src/shell/interp/timer.js.map +1 -0
- package/dist/src/shell/interp/tracker-disk.js +99 -0
- package/dist/src/shell/interp/tracker-disk.js.map +1 -0
- package/dist/src/shell/interp/tracker-parse.js +71 -0
- package/dist/src/shell/interp/tracker-parse.js.map +1 -0
- package/dist/src/shell/interp/tracker-scan.js +238 -0
- package/dist/src/shell/interp/tracker-scan.js.map +1 -0
- package/dist/src/shell/interp/tracker-write.js +91 -0
- package/dist/src/shell/interp/tracker-write.js.map +1 -0
- package/dist/src/shell/interp/tracker.js +41 -0
- package/dist/src/shell/interp/tracker.js.map +1 -0
- package/dist/src/shell/interp/tty.js +48 -0
- package/dist/src/shell/interp/tty.js.map +1 -0
- package/dist/src/shell/interp/vm.js +199 -0
- package/dist/src/shell/interp/vm.js.map +1 -0
- package/dist/src/shell/interp/workspace.js +310 -0
- package/dist/src/shell/interp/workspace.js.map +1 -0
- package/dist/src/shell/main-acp.js +78 -0
- package/dist/src/shell/main-acp.js.map +1 -0
- package/dist/src/shell/main-adapters.js +222 -0
- package/dist/src/shell/main-adapters.js.map +1 -0
- package/dist/src/shell/main-credential.js +122 -0
- package/dist/src/shell/main-credential.js.map +1 -0
- package/dist/src/shell/main-doctor.js +22 -0
- package/dist/src/shell/main-doctor.js.map +1 -0
- package/dist/src/shell/main-entry.js +46 -0
- package/dist/src/shell/main-entry.js.map +1 -0
- package/dist/src/shell/main-http-csrf.js +45 -0
- package/dist/src/shell/main-http-csrf.js.map +1 -0
- package/dist/src/shell/main-http-handler.js +389 -0
- package/dist/src/shell/main-http-handler.js.map +1 -0
- package/dist/src/shell/main-http-mcp.js +122 -0
- package/dist/src/shell/main-http-mcp.js.map +1 -0
- package/dist/src/shell/main-http-views.js +253 -0
- package/dist/src/shell/main-http-views.js.map +1 -0
- package/dist/src/shell/main-http.js +76 -0
- package/dist/src/shell/main-http.js.map +1 -0
- package/dist/src/shell/main-loops.js +130 -0
- package/dist/src/shell/main-loops.js.map +1 -0
- package/dist/src/shell/main-mcp.js +129 -0
- package/dist/src/shell/main-mcp.js.map +1 -0
- package/dist/src/shell/main-orchestrator.js +120 -0
- package/dist/src/shell/main-orchestrator.js.map +1 -0
- package/dist/src/shell/main-preflight.js +43 -0
- package/dist/src/shell/main-preflight.js.map +1 -0
- package/dist/src/shell/main-reconcilers-helpers.js +244 -0
- package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
- package/dist/src/shell/main-reconcilers-pr.js +148 -0
- package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
- package/dist/src/shell/main-reconcilers.js +225 -0
- package/dist/src/shell/main-reconcilers.js.map +1 -0
- package/dist/src/shell/main-runner.js +355 -0
- package/dist/src/shell/main-runner.js.map +1 -0
- package/dist/src/shell/main-scaffold.js +116 -0
- package/dist/src/shell/main-scaffold.js.map +1 -0
- package/dist/src/shell/main-shutdown.js +115 -0
- package/dist/src/shell/main-shutdown.js.map +1 -0
- package/dist/src/shell/main-startup.js +48 -0
- package/dist/src/shell/main-startup.js.map +1 -0
- package/dist/src/shell/main-substrates.js +43 -0
- package/dist/src/shell/main-substrates.js.map +1 -0
- package/dist/src/shell/main.js +385 -0
- package/dist/src/shell/main.js.map +1 -0
- package/dist/src/shell/orchestrator-feedback.js +69 -0
- package/dist/src/shell/orchestrator-feedback.js.map +1 -0
- package/dist/src/shell/orchestrator-image.js +167 -0
- package/dist/src/shell/orchestrator-image.js.map +1 -0
- package/dist/src/shell/orchestrator-loop.js +468 -0
- package/dist/src/shell/orchestrator-loop.js.map +1 -0
- package/dist/src/shell/orchestrator-reconcile.js +36 -0
- package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
- package/dist/src/shell/reconciler-loop.js +228 -0
- package/dist/src/shell/reconciler-loop.js.map +1 -0
- package/dist/src/shell/runner-loop-turn.js +301 -0
- package/dist/src/shell/runner-loop-turn.js.map +1 -0
- package/dist/src/shell/runner-loop.js +338 -0
- package/dist/src/shell/runner-loop.js.map +1 -0
- package/dist/src/shell/server/http.js +208 -0
- package/dist/src/shell/server/http.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
- package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime.js +99 -0
- package/dist/src/shell/server/mcp-runtime.js.map +1 -0
- package/dist/src/shell/workspace-key.js +14 -0
- package/dist/src/shell/workspace-key.js.map +1 -0
- package/dist/src/types/acp.js +8 -0
- package/dist/src/types/acp.js.map +1 -0
- package/dist/src/types/actions/plan.js +6 -0
- package/dist/src/types/actions/plan.js.map +1 -0
- package/dist/src/types/actions/predicates.js +6 -0
- package/dist/src/types/actions/predicates.js.map +1 -0
- package/dist/src/types/actions/run-fold.js +8 -0
- package/dist/src/types/actions/run-fold.js.map +1 -0
- package/dist/src/types/actions.js +7 -0
- package/dist/src/types/actions.js.map +1 -0
- package/dist/src/types/adapter/clock-random.js +4 -0
- package/dist/src/types/adapter/clock-random.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
- package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/types/adapter/workflow-loader.js +4 -0
- package/dist/src/types/adapter/workflow-loader.js.map +1 -0
- package/dist/src/types/cli/args.js +8 -0
- package/dist/src/types/cli/args.js.map +1 -0
- package/dist/src/types/config.js +8 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/credential-interp.js +6 -0
- package/dist/src/types/credential-interp.js.map +1 -0
- package/dist/src/types/credentials.js +10 -0
- package/dist/src/types/credentials.js.map +1 -0
- package/dist/src/types/doctor.js +7 -0
- package/dist/src/types/doctor.js.map +1 -0
- package/dist/src/types/domain.js +7 -0
- package/dist/src/types/domain.js.map +1 -0
- package/dist/src/types/effect.js +15 -0
- package/dist/src/types/effect.js.map +1 -0
- package/dist/src/types/errors.js +39 -0
- package/dist/src/types/errors.js.map +1 -0
- package/dist/src/types/http/decisions.js +6 -0
- package/dist/src/types/http/decisions.js.map +1 -0
- package/dist/src/types/http/render.js +10 -0
- package/dist/src/types/http/render.js.map +1 -0
- package/dist/src/types/http/views.js +6 -0
- package/dist/src/types/http/views.js.map +1 -0
- package/dist/src/types/http.js +9 -0
- package/dist/src/types/http.js.map +1 -0
- package/dist/src/types/image/managed-image.js +7 -0
- package/dist/src/types/image/managed-image.js.map +1 -0
- package/dist/src/types/interp/effect-interpreter.js +8 -0
- package/dist/src/types/interp/effect-interpreter.js.map +1 -0
- package/dist/src/types/interp/tracker.js +7 -0
- package/dist/src/types/interp/tracker.js.map +1 -0
- package/dist/src/types/issue/file.js +6 -0
- package/dist/src/types/issue/file.js.map +1 -0
- package/dist/src/types/issue/parse.js +8 -0
- package/dist/src/types/issue/parse.js.map +1 -0
- package/dist/src/types/main-acp.js +13 -0
- package/dist/src/types/main-acp.js.map +1 -0
- package/dist/src/types/main-adapters.js +5 -0
- package/dist/src/types/main-adapters.js.map +1 -0
- package/dist/src/types/main-credential.js +21 -0
- package/dist/src/types/main-credential.js.map +1 -0
- package/dist/src/types/main-doctor.js +6 -0
- package/dist/src/types/main-doctor.js.map +1 -0
- package/dist/src/types/main-http-handler.js +12 -0
- package/dist/src/types/main-http-handler.js.map +1 -0
- package/dist/src/types/main-http.js +5 -0
- package/dist/src/types/main-http.js.map +1 -0
- package/dist/src/types/main-loops.js +5 -0
- package/dist/src/types/main-loops.js.map +1 -0
- package/dist/src/types/main-mcp.js +12 -0
- package/dist/src/types/main-mcp.js.map +1 -0
- package/dist/src/types/main-orchestrator.js +5 -0
- package/dist/src/types/main-orchestrator.js.map +1 -0
- package/dist/src/types/main-reconcilers.js +11 -0
- package/dist/src/types/main-reconcilers.js.map +1 -0
- package/dist/src/types/main-runner.js +13 -0
- package/dist/src/types/main-runner.js.map +1 -0
- package/dist/src/types/main-startup.js +5 -0
- package/dist/src/types/main-startup.js.map +1 -0
- package/dist/src/types/main-substrates.js +5 -0
- package/dist/src/types/main-substrates.js.map +1 -0
- package/dist/src/types/mcp/dispatch.js +4 -0
- package/dist/src/types/mcp/dispatch.js.map +1 -0
- package/dist/src/types/mcp/post-move.js +7 -0
- package/dist/src/types/mcp/post-move.js.map +1 -0
- package/dist/src/types/mcp.js +9 -0
- package/dist/src/types/mcp.js.map +1 -0
- package/dist/src/types/ports.js +12 -0
- package/dist/src/types/ports.js.map +1 -0
- package/dist/src/types/reconcile/image-decide.js +5 -0
- package/dist/src/types/reconcile/image-decide.js.map +1 -0
- package/dist/src/types/reconcile/ledger.js +7 -0
- package/dist/src/types/reconcile/ledger.js.map +1 -0
- package/dist/src/types/reconcile/pr-loop.js +8 -0
- package/dist/src/types/reconcile/pr-loop.js.map +1 -0
- package/dist/src/types/reconcile/vm-reap.js +8 -0
- package/dist/src/types/reconcile/vm-reap.js.map +1 -0
- package/dist/src/types/reconcile/workspace-decide.js +7 -0
- package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/types/reconcile.js +9 -0
- package/dist/src/types/reconcile.js.map +1 -0
- package/dist/src/types/runlog.js +7 -0
- package/dist/src/types/runlog.js.map +1 -0
- package/dist/src/types/runner/actions-runner.js +12 -0
- package/dist/src/types/runner/actions-runner.js.map +1 -0
- package/dist/src/types/runner/gondolin-dispatch.js +5 -0
- package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
- package/dist/src/types/runner/injection.js +6 -0
- package/dist/src/types/runner/injection.js.map +1 -0
- package/dist/src/types/runner/runner-loop.js +5 -0
- package/dist/src/types/runner/runner-loop.js.map +1 -0
- package/dist/src/types/runner/turn.js +4 -0
- package/dist/src/types/runner/turn.js.map +1 -0
- package/dist/src/types/runner/vm-plan.js +4 -0
- package/dist/src/types/runner/vm-plan.js.map +1 -0
- package/dist/src/types/runtime.js +9 -0
- package/dist/src/types/runtime.js.map +1 -0
- package/dist/src/types/schedule/admission.js +7 -0
- package/dist/src/types/schedule/admission.js.map +1 -0
- package/dist/src/types/schedule/circuit-breaker.js +2 -0
- package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/types/schedule/eligibility.js +9 -0
- package/dist/src/types/schedule/eligibility.js.map +1 -0
- package/dist/src/types/schedule/orchestrator-loop.js +10 -0
- package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
- package/dist/src/types/schedule/sleep-cycle.js +4 -0
- package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/types/schedule/slots.js +8 -0
- package/dist/src/types/schedule/slots.js.map +1 -0
- package/dist/src/types/schedule/tick.js +9 -0
- package/dist/src/types/schedule/tick.js.map +1 -0
- package/dist/src/types/server/mcp-runtime.js +8 -0
- package/dist/src/types/server/mcp-runtime.js.map +1 -0
- package/dist/src/types/workflow/parse.js +4 -0
- package/dist/src/types/workflow/parse.js.map +1 -0
- package/dist/tests/core/account-id.test.js +35 -0
- package/dist/tests/core/account-id.test.js.map +1 -0
- package/dist/tests/core/actions-parse.test.js +176 -0
- package/dist/tests/core/actions-parse.test.js.map +1 -0
- package/dist/tests/core/adapter-config.test.js +133 -0
- package/dist/tests/core/adapter-config.test.js.map +1 -0
- package/dist/tests/core/admission.test.js +215 -0
- package/dist/tests/core/admission.test.js.map +1 -0
- package/dist/tests/core/args.test.js +132 -0
- package/dist/tests/core/args.test.js.map +1 -0
- package/dist/tests/core/availability.test.js +62 -0
- package/dist/tests/core/availability.test.js.map +1 -0
- package/dist/tests/core/checks.test.js +395 -0
- package/dist/tests/core/checks.test.js.map +1 -0
- package/dist/tests/core/circuit-breaker.test.js +172 -0
- package/dist/tests/core/circuit-breaker.test.js.map +1 -0
- package/dist/tests/core/coerce.test.js +87 -0
- package/dist/tests/core/coerce.test.js.map +1 -0
- package/dist/tests/core/context.test.js +228 -0
- package/dist/tests/core/context.test.js.map +1 -0
- package/dist/tests/core/decisions.test.js +310 -0
- package/dist/tests/core/decisions.test.js.map +1 -0
- package/dist/tests/core/derive.test.js +205 -0
- package/dist/tests/core/derive.test.js.map +1 -0
- package/dist/tests/core/dispatch-config.test.js +164 -0
- package/dist/tests/core/dispatch-config.test.js.map +1 -0
- package/dist/tests/core/dispatch.test.js +302 -0
- package/dist/tests/core/dispatch.test.js.map +1 -0
- package/dist/tests/core/eligibility.test.js +163 -0
- package/dist/tests/core/eligibility.test.js.map +1 -0
- package/dist/tests/core/extract.test.js +139 -0
- package/dist/tests/core/extract.test.js.map +1 -0
- package/dist/tests/core/fake-creds.test.js +134 -0
- package/dist/tests/core/fake-creds.test.js.map +1 -0
- package/dist/tests/core/file.test.js +197 -0
- package/dist/tests/core/file.test.js.map +1 -0
- package/dist/tests/core/git-result.test.js +113 -0
- package/dist/tests/core/git-result.test.js.map +1 -0
- package/dist/tests/core/identity.test.js +180 -0
- package/dist/tests/core/identity.test.js.map +1 -0
- package/dist/tests/core/image-decide.test.js +59 -0
- package/dist/tests/core/image-decide.test.js.map +1 -0
- package/dist/tests/core/injection.test.js +163 -0
- package/dist/tests/core/injection.test.js.map +1 -0
- package/dist/tests/core/ledger.test.js +218 -0
- package/dist/tests/core/ledger.test.js.map +1 -0
- package/dist/tests/core/managed-image.test.js +68 -0
- package/dist/tests/core/managed-image.test.js.map +1 -0
- package/dist/tests/core/mise.test.js +138 -0
- package/dist/tests/core/mise.test.js.map +1 -0
- package/dist/tests/core/parse.test.js +174 -0
- package/dist/tests/core/parse.test.js.map +1 -0
- package/dist/tests/core/path.test.js +50 -0
- package/dist/tests/core/path.test.js.map +1 -0
- package/dist/tests/core/plan.test.js +218 -0
- package/dist/tests/core/plan.test.js.map +1 -0
- package/dist/tests/core/post-move.test.js +162 -0
- package/dist/tests/core/post-move.test.js.map +1 -0
- package/dist/tests/core/pr-classify.test.js +117 -0
- package/dist/tests/core/pr-classify.test.js.map +1 -0
- package/dist/tests/core/pr-decide.test.js +298 -0
- package/dist/tests/core/pr-decide.test.js.map +1 -0
- package/dist/tests/core/pr-loop.test.js +301 -0
- package/dist/tests/core/pr-loop.test.js.map +1 -0
- package/dist/tests/core/pr-notes.test.js +165 -0
- package/dist/tests/core/pr-notes.test.js.map +1 -0
- package/dist/tests/core/predicates.test.js +154 -0
- package/dist/tests/core/predicates.test.js.map +1 -0
- package/dist/tests/core/prompt.test.js +189 -0
- package/dist/tests/core/prompt.test.js.map +1 -0
- package/dist/tests/core/protocol.test.js +195 -0
- package/dist/tests/core/protocol.test.js.map +1 -0
- package/dist/tests/core/reconcile-issue.test.js +116 -0
- package/dist/tests/core/reconcile-issue.test.js.map +1 -0
- package/dist/tests/core/render.test.js +549 -0
- package/dist/tests/core/render.test.js.map +1 -0
- package/dist/tests/core/retry.test.js +186 -0
- package/dist/tests/core/retry.test.js.map +1 -0
- package/dist/tests/core/routes.test.js +247 -0
- package/dist/tests/core/routes.test.js.map +1 -0
- package/dist/tests/core/run-fold.test.js +299 -0
- package/dist/tests/core/run-fold.test.js.map +1 -0
- package/dist/tests/core/shape.test.js +185 -0
- package/dist/tests/core/shape.test.js.map +1 -0
- package/dist/tests/core/sleep-cycle.test.js +150 -0
- package/dist/tests/core/sleep-cycle.test.js.map +1 -0
- package/dist/tests/core/slots.test.js +201 -0
- package/dist/tests/core/slots.test.js.map +1 -0
- package/dist/tests/core/state-resolve.test.js +80 -0
- package/dist/tests/core/state-resolve.test.js.map +1 -0
- package/dist/tests/core/summary.test.js +200 -0
- package/dist/tests/core/summary.test.js.map +1 -0
- package/dist/tests/core/template.test.js +116 -0
- package/dist/tests/core/template.test.js.map +1 -0
- package/dist/tests/core/tick.test.js +558 -0
- package/dist/tests/core/tick.test.js.map +1 -0
- package/dist/tests/core/token-fold.test.js +176 -0
- package/dist/tests/core/token-fold.test.js.map +1 -0
- package/dist/tests/core/turn.test.js +388 -0
- package/dist/tests/core/turn.test.js.map +1 -0
- package/dist/tests/core/url.test.js +118 -0
- package/dist/tests/core/url.test.js.map +1 -0
- package/dist/tests/core/validate.test.js +247 -0
- package/dist/tests/core/validate.test.js.map +1 -0
- package/dist/tests/core/views.test.js +252 -0
- package/dist/tests/core/views.test.js.map +1 -0
- package/dist/tests/core/vm-decide.test.js +110 -0
- package/dist/tests/core/vm-decide.test.js.map +1 -0
- package/dist/tests/core/vm-guards.test.js +153 -0
- package/dist/tests/core/vm-guards.test.js.map +1 -0
- package/dist/tests/core/vm-plan.test.js +332 -0
- package/dist/tests/core/vm-plan.test.js.map +1 -0
- package/dist/tests/core/vm-reap.test.js +196 -0
- package/dist/tests/core/vm-reap.test.js.map +1 -0
- package/dist/tests/core/workflow-parse.test.js +493 -0
- package/dist/tests/core/workflow-parse.test.js.map +1 -0
- package/dist/tests/core/workspace-decide.test.js +236 -0
- package/dist/tests/core/workspace-decide.test.js.map +1 -0
- package/dist/tests/helpers/fixtures.js +167 -0
- package/dist/tests/helpers/fixtures.js.map +1 -0
- package/dist/tests/shell/acp-substrate.test.js +101 -0
- package/dist/tests/shell/acp-substrate.test.js.map +1 -0
- package/dist/tests/shell/actions-runner-push.test.js +203 -0
- package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
- package/dist/tests/shell/credential-hooks.test.js +36 -0
- package/dist/tests/shell/credential-hooks.test.js.map +1 -0
- package/dist/tests/shell/credential-registry.test.js +165 -0
- package/dist/tests/shell/credential-registry.test.js.map +1 -0
- package/dist/tests/shell/credential-substrate.test.js +179 -0
- package/dist/tests/shell/credential-substrate.test.js.map +1 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
- package/dist/tests/shell/doctor.test.js +101 -0
- package/dist/tests/shell/doctor.test.js.map +1 -0
- package/dist/tests/shell/effect-vm-create.test.js +52 -0
- package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
- package/dist/tests/shell/gh-port.test.js +63 -0
- package/dist/tests/shell/gh-port.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
- package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
- package/dist/tests/shell/http-handler.test.js +608 -0
- package/dist/tests/shell/http-handler.test.js.map +1 -0
- package/dist/tests/shell/http-server.test.js +53 -0
- package/dist/tests/shell/http-server.test.js.map +1 -0
- package/dist/tests/shell/mcp-runtime.test.js +366 -0
- package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
- package/dist/tests/shell/mise-config-asset.test.js +87 -0
- package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
- package/dist/tests/shell/orchestrator-loop.test.js +583 -0
- package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
- package/dist/tests/shell/reconciler-passes.test.js +314 -0
- package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
- package/dist/tests/shell/runner-loop-turn.test.js +97 -0
- package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
- package/dist/tests/shell/runner-slice.test.js +536 -0
- package/dist/tests/shell/runner-slice.test.js.map +1 -0
- package/dist/tests/shell/scaffold.test.js +65 -0
- package/dist/tests/shell/scaffold.test.js.map +1 -0
- package/dist/tests/shell/tick-config.test.js +83 -0
- package/dist/tests/shell/tick-config.test.js.map +1 -0
- package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
- package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
- package/dist/tests/shell/tracker-write-issue.test.js +154 -0
- package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
- package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
- package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
- package/dist/tests/shell/workspace-live-config.test.js +140 -0
- package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
- package/package.json +21 -9
- package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
- package/prompts/Reflect.md +91 -0
- package/prompts/Review.md +97 -0
- package/prompts/Todo.md +96 -0
- package/prompts/_footer.md +41 -0
- package/prompts/_preamble.md +42 -0
- package/prompts-minimal/Todo.md +26 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/vm-agent.mjs +312 -90
- package/WORKFLOW.md +0 -744
- package/dist/acp-bridge.js +0 -324
- package/dist/acp-bridge.js.map +0 -1
- package/dist/actions/cache.js +0 -191
- package/dist/actions/cache.js.map +0 -1
- package/dist/actions/effects.js +0 -41
- package/dist/actions/effects.js.map +0 -1
- package/dist/actions/executor.js +0 -570
- package/dist/actions/executor.js.map +0 -1
- package/dist/actions/index.js +0 -13
- package/dist/actions/index.js.map +0 -1
- package/dist/actions/parsing.js.map +0 -1
- package/dist/actions/predicate-env.js +0 -27
- package/dist/actions/predicate-env.js.map +0 -1
- package/dist/actions/predicates.js +0 -49
- package/dist/actions/predicates.js.map +0 -1
- package/dist/actions/templating.js +0 -66
- package/dist/actions/templating.js.map +0 -1
- package/dist/actions/types.js +0 -15
- package/dist/actions/types.js.map +0 -1
- package/dist/agent/acp.js +0 -473
- package/dist/agent/acp.js.map +0 -1
- package/dist/agent/adapter-names.js +0 -159
- package/dist/agent/adapter-names.js.map +0 -1
- package/dist/agent/adapters.js +0 -511
- package/dist/agent/adapters.js.map +0 -1
- package/dist/agent/credential-extractors.js +0 -342
- package/dist/agent/credential-extractors.js.map +0 -1
- package/dist/agent/credential-secrets.js +0 -628
- package/dist/agent/credential-secrets.js.map +0 -1
- package/dist/agent/credential-ticker.js +0 -57
- package/dist/agent/credential-ticker.js.map +0 -1
- package/dist/agent/gondolin-creds-staging.js +0 -356
- package/dist/agent/gondolin-creds-staging.js.map +0 -1
- package/dist/agent/gondolin-dispatch.js +0 -375
- package/dist/agent/gondolin-dispatch.js.map +0 -1
- package/dist/agent/gondolin.js +0 -124
- package/dist/agent/gondolin.js.map +0 -1
- package/dist/agent/runner-decisions.js +0 -134
- package/dist/agent/runner-decisions.js.map +0 -1
- package/dist/agent/runner.js +0 -1456
- package/dist/agent/runner.js.map +0 -1
- package/dist/agent/tool-call-summary.js +0 -102
- package/dist/agent/tool-call-summary.js.map +0 -1
- package/dist/agent/vm-acp-mapping.js +0 -73
- package/dist/agent/vm-acp-mapping.js.map +0 -1
- package/dist/agent/vm-guards.js +0 -262
- package/dist/agent/vm-guards.js.map +0 -1
- package/dist/agent/vm-port.js +0 -22
- package/dist/agent/vm-port.js.map +0 -1
- package/dist/agent/vm-process-registry.js +0 -79
- package/dist/agent/vm-process-registry.js.map +0 -1
- package/dist/bin/cli-args.js +0 -105
- package/dist/bin/cli-args.js.map +0 -1
- package/dist/bin/symphony.js +0 -794
- package/dist/bin/symphony.js.map +0 -1
- package/dist/errors.js +0 -15
- package/dist/errors.js.map +0 -1
- package/dist/http-disk.js +0 -135
- package/dist/http-disk.js.map +0 -1
- package/dist/http-handlers.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/issues.js +0 -178
- package/dist/issues.js.map +0 -1
- package/dist/logging.js +0 -203
- package/dist/logging.js.map +0 -1
- package/dist/mcp.js +0 -706
- package/dist/mcp.js.map +0 -1
- package/dist/memory.js +0 -85
- package/dist/memory.js.map +0 -1
- package/dist/orchestrator-decisions.js +0 -331
- package/dist/orchestrator-decisions.js.map +0 -1
- package/dist/orchestrator.js +0 -1569
- package/dist/orchestrator.js.map +0 -1
- package/dist/prompt.js +0 -65
- package/dist/prompt.js.map +0 -1
- package/dist/reconciler/cache.js +0 -65
- package/dist/reconciler/cache.js.map +0 -1
- package/dist/reconciler/index.js +0 -448
- package/dist/reconciler/index.js.map +0 -1
- package/dist/reconciler/ledger.js +0 -131
- package/dist/reconciler/ledger.js.map +0 -1
- package/dist/reconciler/pr-adapters.js +0 -174
- package/dist/reconciler/pr-adapters.js.map +0 -1
- package/dist/reconciler/pr-decide.js.map +0 -1
- package/dist/reconciler/pr.js +0 -422
- package/dist/reconciler/pr.js.map +0 -1
- package/dist/reconciler/types.js +0 -12
- package/dist/reconciler/types.js.map +0 -1
- package/dist/reconciler/vm.js +0 -243
- package/dist/reconciler/vm.js.map +0 -1
- package/dist/reconciler/workspace-defaults.js +0 -83
- package/dist/reconciler/workspace-defaults.js.map +0 -1
- package/dist/reconciler/workspace.js +0 -272
- package/dist/reconciler/workspace.js.map +0 -1
- package/dist/runlog.js +0 -403
- package/dist/runlog.js.map +0 -1
- package/dist/scaffold.js +0 -165
- package/dist/scaffold.js.map +0 -1
- package/dist/trackers/local.js +0 -445
- package/dist/trackers/local.js.map +0 -1
- package/dist/trackers/types.js +0 -10
- package/dist/trackers/types.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/util/clock.js +0 -12
- package/dist/util/clock.js.map +0 -1
- package/dist/util/crypto.js +0 -25
- package/dist/util/crypto.js.map +0 -1
- package/dist/util/frontmatter.js +0 -70
- package/dist/util/frontmatter.js.map +0 -1
- package/dist/util/fs-issues.js +0 -22
- package/dist/util/fs-issues.js.map +0 -1
- package/dist/util/process.js +0 -152
- package/dist/util/process.js.map +0 -1
- package/dist/util/workspace-key.js +0 -10
- package/dist/util/workspace-key.js.map +0 -1
- package/dist/workflow-loader.js +0 -147
- package/dist/workflow-loader.js.map +0 -1
- package/dist/workflow.js +0 -822
- package/dist/workflow.js.map +0 -1
- package/dist/workspace-types.js +0 -8
- package/dist/workspace-types.js.map +0 -1
- package/dist/workspace.js +0 -443
- package/dist/workspace.js.map +0 -1
package/scripts/vm-agent.mjs
CHANGED
|
@@ -1,38 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// symphony-vm-agent — in-guest proxy that runs the ACP adapter and bridges its stdio
|
|
3
|
-
// to a
|
|
3
|
+
// to a WebSocket dialed back to the host orchestrator's unified HTTP server.
|
|
4
4
|
//
|
|
5
|
-
// Why
|
|
6
|
-
// stdio
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// plain socket the host owns. the VM exec is reduced to a launcher: its stdio just
|
|
14
|
-
// carries this proxy's own diagnostic stderr and the adapter's stderr (`inherit`'d).
|
|
5
|
+
// Why a WebSocket and not stdio or raw TCP: the original proxy piped the in-VM adapter's
|
|
6
|
+
// stdio through the VM-exec stdio channel, which has a stdin-pump bug that wedges the
|
|
7
|
+
// adapter after the SDK's first notification. That was replaced by a raw-TCP bridge on a
|
|
8
|
+
// dedicated host port; this version collapses that bridge onto the SAME host listener
|
|
9
|
+
// that already serves MCP + the dashboard — ACP now rides a `/acp` WebSocket `Upgrade`
|
|
10
|
+
// reached through the SAME `tcp.hosts` tunnel MCP uses (the guest dials
|
|
11
|
+
// `ws://symphony-mcp:7001/acp`). One mapping, one synthetic host, one host port; the
|
|
12
|
+
// VM-exec stdio is reduced to a launcher whose stderr carries only diagnostics.
|
|
15
13
|
//
|
|
16
|
-
// The
|
|
17
|
-
//
|
|
14
|
+
// The proxy authenticates with a per-dispatch bearer sent as the FIRST WebSocket
|
|
15
|
+
// message (the WHATWG `WebSocket` client cannot set custom upgrade headers, so a header
|
|
16
|
+
// bearer is not an option for the guest dialer). The auth frame is
|
|
17
|
+
// `{"type":"auth","token":"<token>","proto":1}`; the host validates the bearer
|
|
18
|
+
// constant-time and then replies `{"type":"ready","proto":1}` BEFORE any ACP byte
|
|
19
|
+
// flows. Only after that ready frame does the proxy spawn the adapter and start pumping
|
|
20
|
+
// ACP JSON-RPC bytes as WebSocket frames: inbound frame payloads are written to the
|
|
21
|
+
// adapter's stdin; the adapter's stdout is sent as binary frames. Each chunk is
|
|
22
|
+
// forwarded straight through as it arrives — NEVER buffered — preserving streaming
|
|
23
|
+
// (issue 135). Backpressure pauses the SOURCE stream (child.stdout or the WS receive
|
|
24
|
+
// side), it never accumulates a frame/turn before forwarding.
|
|
25
|
+
//
|
|
26
|
+
// Liveness: the proxy sends a WS PING every ~15 s and tracks the last pong; if no pong
|
|
27
|
+
// arrives for ~3 intervals the half-open socket is torn down so the dispatch fails fast
|
|
28
|
+
// instead of hanging until the host's prompt timeout. Both ends ping independently, so a
|
|
29
|
+
// long silent tool call (no ACP frames) does NOT trip the watchdog.
|
|
18
30
|
//
|
|
19
31
|
// Configuration (all required unless noted):
|
|
20
|
-
// SYMPHONY_ACP_URL — `
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// first line.
|
|
32
|
+
// SYMPHONY_ACP_URL — `ws://host:port/acp` (or `wss://…`) to dial. Under the
|
|
33
|
+
// Gondolin tunnel this is `ws://symphony-mcp:7001/acp`.
|
|
34
|
+
// SYMPHONY_ACP_TOKEN — opaque per-dispatch bearer; sent as the first WS message
|
|
35
|
+
// `{"type":"auth","token":"<token>","proto":1}`.
|
|
25
36
|
// SYMPHONY_ADAPTER_BIN — adapter executable to spawn (PATH-resolved).
|
|
26
37
|
// SYMPHONY_ADAPTER_ARGS — JSON array of extra argv. Optional; defaults to `[]`.
|
|
38
|
+
// SYMPHONY_HEARTBEAT_INTERVAL_MS — optional; WS ping interval. Defaults to 15000.
|
|
39
|
+
// SYMPHONY_HEARTBEAT_TIMEOUT_MS — optional; dead-peer threshold. Defaults to 45000.
|
|
27
40
|
// SYMPHONY_VM_AGENT_DEBUG — optional; truthy → log lifecycle to stderr.
|
|
28
41
|
//
|
|
29
42
|
// Exit code: mirrors the adapter's exit when it exits, or 1 if the connection fails
|
|
30
43
|
// before the adapter starts.
|
|
31
44
|
|
|
32
|
-
import { connect as netConnect } from 'node:net';
|
|
33
45
|
import { spawn } from 'node:child_process';
|
|
34
46
|
import { constants as osConstants } from 'node:os';
|
|
35
47
|
import { URL } from 'node:url';
|
|
48
|
+
import { Buffer } from 'node:buffer';
|
|
49
|
+
|
|
50
|
+
// Wire protocol version exchanged in the auth/ready handshake; bumped lockstep with the
|
|
51
|
+
// host (src/acp-ws.ts). The launcher and host now move together (PR-A injects this file
|
|
52
|
+
// from the host's own disk at dispatch), so a single integer suffices.
|
|
53
|
+
const PROTO = 1;
|
|
54
|
+
|
|
55
|
+
// Outbound backpressure (child.stdout → ws.send): when the WebSocket's send queue grows
|
|
56
|
+
// past the high-water mark we PAUSE child.stdout (stops the source — never accumulates a
|
|
57
|
+
// frame on our side) and resume once the queue drains below the low-water mark. WHATWG
|
|
58
|
+
// WebSocket exposes no 'drain' event, only `bufferedAmount`, so the drain is detected by a
|
|
59
|
+
// short poll. The marks are generous so a normal streaming turn never pauses.
|
|
60
|
+
const WS_HIGH_WATER_BYTES = 1 << 20; // 1 MiB queued in the WS send buffer → pause source
|
|
61
|
+
const WS_LOW_WATER_BYTES = 256 * 1024; // resume once it drains below this
|
|
62
|
+
const WS_DRAIN_POLL_MS = 10;
|
|
36
63
|
|
|
37
64
|
const requiredEnv = (name) => {
|
|
38
65
|
const v = process.env[name];
|
|
@@ -43,9 +70,18 @@ const requiredEnv = (name) => {
|
|
|
43
70
|
return v;
|
|
44
71
|
};
|
|
45
72
|
|
|
73
|
+
const intEnv = (name, dflt) => {
|
|
74
|
+
const raw = process.env[name];
|
|
75
|
+
if (!raw) return dflt;
|
|
76
|
+
const n = Number(raw);
|
|
77
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : dflt;
|
|
78
|
+
};
|
|
79
|
+
|
|
46
80
|
const acpUrl = requiredEnv('SYMPHONY_ACP_URL');
|
|
47
81
|
const acpToken = requiredEnv('SYMPHONY_ACP_TOKEN');
|
|
48
82
|
const adapterBin = requiredEnv('SYMPHONY_ADAPTER_BIN');
|
|
83
|
+
const heartbeatIntervalMs = intEnv('SYMPHONY_HEARTBEAT_INTERVAL_MS', 15_000);
|
|
84
|
+
const heartbeatTimeoutMs = intEnv('SYMPHONY_HEARTBEAT_TIMEOUT_MS', 45_000);
|
|
49
85
|
|
|
50
86
|
let adapterArgs = [];
|
|
51
87
|
const rawArgs = process.env.SYMPHONY_ADAPTER_ARGS;
|
|
@@ -74,83 +110,278 @@ try {
|
|
|
74
110
|
process.stderr.write(`symphony-vm-agent: SYMPHONY_ACP_URL invalid: ${err.message}\n`);
|
|
75
111
|
process.exit(2);
|
|
76
112
|
}
|
|
77
|
-
if (parsed.protocol !== '
|
|
113
|
+
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
|
|
78
114
|
process.stderr.write(
|
|
79
|
-
`symphony-vm-agent: SYMPHONY_ACP_URL must use
|
|
115
|
+
`symphony-vm-agent: SYMPHONY_ACP_URL must use ws:// or wss:// scheme (got ${parsed.protocol})\n`,
|
|
80
116
|
);
|
|
81
117
|
process.exit(2);
|
|
82
118
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
process.stderr.write(`symphony-vm-agent: SYMPHONY_ACP_URL has no port\n`);
|
|
119
|
+
|
|
120
|
+
if (typeof WebSocket !== 'function') {
|
|
121
|
+
process.stderr.write('symphony-vm-agent: global WebSocket is unavailable (need Node >= 21)\n');
|
|
87
122
|
process.exit(2);
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
log(`dialing ${
|
|
125
|
+
log(`dialing ${acpUrl}`);
|
|
91
126
|
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
let child = null;
|
|
128
|
+
// The adapter's exit code/signal, captured on `child` close so we can flush the WebSocket
|
|
129
|
+
// before exiting. Null until the adapter exits.
|
|
130
|
+
let pendingExit = null;
|
|
131
|
+
let finalized = false;
|
|
132
|
+
let opened = false;
|
|
133
|
+
// The host must send `{type:'ready',proto:1}` after validating the bearer; only then do we
|
|
134
|
+
// spawn the adapter and start the ACP byte pump. Guards against starting the adapter before
|
|
135
|
+
// the host has accepted us (and lets the host gate the spawn until it is ready to relay).
|
|
136
|
+
let ready = false;
|
|
137
|
+
let bridgeStarted = false;
|
|
94
138
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
// --- Heartbeat (WS ping / pong watchdog) ----------------------------------------------
|
|
140
|
+
// Both ends ping independently. We send an application PING frame every interval and record
|
|
141
|
+
// the last pong arrival; if the gap exceeds the dead-peer threshold we tear the socket down
|
|
142
|
+
// so the dispatch fails fast (host classifies the drop as a transport_error). The watchdog
|
|
143
|
+
// is INDEPENDENT of ACP frames, so a long silent tool call does not trip it. Every timer is
|
|
144
|
+
// cleared on close (no dangling interval after teardown).
|
|
145
|
+
let pingTimer = null;
|
|
146
|
+
let lastPongAt = 0;
|
|
147
|
+
function startHeartbeat() {
|
|
148
|
+
lastPongAt = Date.now();
|
|
149
|
+
pingTimer = setInterval(() => {
|
|
150
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
151
|
+
if (Date.now() - lastPongAt > heartbeatTimeoutMs) {
|
|
152
|
+
log(`no pong within ${heartbeatTimeoutMs}ms; tearing down (dead host)`);
|
|
153
|
+
stopHeartbeat();
|
|
154
|
+
try {
|
|
155
|
+
ws.close(1001, 'heartbeat timeout');
|
|
156
|
+
} catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
// Belt-and-braces: a half-open socket may never deliver `close`; kill the adapter
|
|
160
|
+
// and finalize so the dispatch unwinds rather than hanging.
|
|
161
|
+
if (child) {
|
|
162
|
+
try {
|
|
163
|
+
child.kill('SIGTERM');
|
|
164
|
+
} catch {
|
|
165
|
+
/* ignore */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const t = setTimeout(finalize, 2_000);
|
|
169
|
+
if (typeof t.unref === 'function') t.unref();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
ws.send(JSON.stringify({ type: 'ping', t: Date.now() }));
|
|
174
|
+
} catch {
|
|
175
|
+
/* socket closing — the close handler clears the timer */
|
|
176
|
+
}
|
|
177
|
+
}, heartbeatIntervalMs);
|
|
178
|
+
if (typeof pingTimer.unref === 'function') pingTimer.unref();
|
|
179
|
+
}
|
|
180
|
+
function stopHeartbeat() {
|
|
181
|
+
if (pingTimer) {
|
|
182
|
+
clearInterval(pingTimer);
|
|
183
|
+
pingTimer = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ws = new WebSocket(acpUrl);
|
|
188
|
+
ws.binaryType = 'arraybuffer';
|
|
189
|
+
|
|
190
|
+
function finalize() {
|
|
191
|
+
if (finalized) return;
|
|
192
|
+
finalized = true;
|
|
193
|
+
stopHeartbeat();
|
|
194
|
+
stopOutboundDrainPoll();
|
|
195
|
+
if (pendingExit && pendingExit.signal) {
|
|
196
|
+
const sig = osConstants.signals[pendingExit.signal] ?? 0;
|
|
197
|
+
process.exit(128 + sig);
|
|
198
|
+
}
|
|
199
|
+
process.exit(pendingExit ? (pendingExit.code ?? 0) : 0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ws.addEventListener('open', () => {
|
|
203
|
+
opened = true;
|
|
204
|
+
log('connected; sending auth message');
|
|
205
|
+
// The bearer is the FIRST message; the host validates it before resolving the session,
|
|
206
|
+
// then replies `{type:'ready',proto:1}`. We spawn the adapter only after that ready frame.
|
|
207
|
+
ws.send(JSON.stringify({ type: 'auth', token: acpToken, proto: PROTO }));
|
|
208
|
+
startHeartbeat();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
ws.addEventListener('error', (ev) => {
|
|
212
|
+
const msg = ev?.message || (ev?.error && ev.error.message) || 'websocket error';
|
|
213
|
+
if (!opened) {
|
|
214
|
+
// Couldn't reach the host at all → exit hard so symphony's connect-timeout error path
|
|
215
|
+
// fires (mirrors the raw bridge's pre-connect error handling).
|
|
216
|
+
process.stderr.write(`symphony-vm-agent: websocket error before open: ${msg}\n`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
log(`websocket error after open: ${msg}`);
|
|
220
|
+
if (child) {
|
|
221
|
+
try {
|
|
222
|
+
child.kill('SIGTERM');
|
|
223
|
+
} catch {
|
|
224
|
+
/* ignore */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
ws.addEventListener('close', (ev) => {
|
|
230
|
+
log(`websocket closed code=${ev?.code}`);
|
|
231
|
+
stopHeartbeat();
|
|
232
|
+
stopOutboundDrainPoll();
|
|
233
|
+
// Adapter already exited: the close completes our flush → exit with its code.
|
|
234
|
+
if (pendingExit) return finalize();
|
|
235
|
+
// Host closed first (cancel / attempt teardown): end the adapter's stdin so it unwinds
|
|
236
|
+
// (parity with the raw socket half-close), and kill it as a belt-and-braces.
|
|
237
|
+
if (child) {
|
|
238
|
+
try {
|
|
239
|
+
child.stdin.end();
|
|
240
|
+
} catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
child.kill('SIGTERM');
|
|
245
|
+
} catch {
|
|
246
|
+
/* ignore */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// The control + handshake frame handler. Installed before `startBridge()` so the ready
|
|
252
|
+
// frame (and pongs) are observed even before the adapter exists. `startBridge()` adds a
|
|
253
|
+
// SECOND `message` listener for ACP bytes once ready; this one only consumes JSON control
|
|
254
|
+
// frames (auth-ready handshake + pong) and ignores everything else, so it never competes
|
|
255
|
+
// with the ACP byte pump for opaque adapter bytes.
|
|
256
|
+
ws.addEventListener('message', (ev) => {
|
|
257
|
+
const ctrl = parseControlFrame(ev.data);
|
|
258
|
+
if (!ctrl) return; // opaque ACP bytes — handled by startBridge's pump (once ready)
|
|
259
|
+
if (ctrl.type === 'ready') {
|
|
260
|
+
if (ready) return;
|
|
261
|
+
ready = true;
|
|
262
|
+
log('host ready; spawning adapter');
|
|
263
|
+
startBridge();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (ctrl.type === 'pong' || ctrl.type === 'ping') {
|
|
267
|
+
// Any control traffic proves the peer is alive; refresh the watchdog. We reply to a
|
|
268
|
+
// host ping with a pong so the host's own watchdog stays satisfied symmetrically.
|
|
269
|
+
lastPongAt = Date.now();
|
|
270
|
+
if (ctrl.type === 'ping') {
|
|
271
|
+
try {
|
|
272
|
+
ws.send(JSON.stringify({ type: 'pong', t: Date.now() }));
|
|
273
|
+
} catch {
|
|
274
|
+
/* socket closing */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
111
278
|
});
|
|
112
279
|
|
|
280
|
+
// Parse a host→guest control frame. Returns `{type}` for our small JSON control vocabulary
|
|
281
|
+
// (`ready`/`ping`/`pong`), or null when the payload is opaque ACP bytes (binary frame, or
|
|
282
|
+
// text that is not one of our control messages). Kept deliberately strict so adapter JSON
|
|
283
|
+
// that happens to be text can never be mistaken for a control frame: ACP bytes are sent by
|
|
284
|
+
// the host as BINARY frames (ArrayBuffer here), so a string payload that parses to a known
|
|
285
|
+
// control `type` is the only thing treated as control.
|
|
286
|
+
function parseControlFrame(data) {
|
|
287
|
+
if (typeof data !== 'string') return null; // binary → ACP bytes
|
|
288
|
+
let msg;
|
|
289
|
+
try {
|
|
290
|
+
msg = JSON.parse(data);
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (msg && (msg.type === 'ready' || msg.type === 'ping' || msg.type === 'pong')) {
|
|
295
|
+
return { type: msg.type };
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- Outbound backpressure poll (child.stdout → ws.send) -------------------------------
|
|
301
|
+
let outboundDrainTimer = null;
|
|
302
|
+
function stopOutboundDrainPoll() {
|
|
303
|
+
if (outboundDrainTimer) {
|
|
304
|
+
clearInterval(outboundDrainTimer);
|
|
305
|
+
outboundDrainTimer = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
113
309
|
function startBridge() {
|
|
310
|
+
if (bridgeStarted) return;
|
|
311
|
+
bridgeStarted = true;
|
|
114
312
|
log(`spawn ${adapterBin} ${JSON.stringify(adapterArgs)}`);
|
|
115
|
-
|
|
313
|
+
child = spawn(adapterBin, adapterArgs, {
|
|
116
314
|
// Adapter stdio: kernel pipes we fully own. Adapter stderr is inherited so any
|
|
117
315
|
// crashes / warnings show up on this proxy's stderr → VM-exec stderr → host
|
|
118
|
-
// orchestrator stderr capture. ACP frames flow ONLY over the
|
|
316
|
+
// orchestrator stderr capture. ACP frames flow ONLY over the WebSocket.
|
|
119
317
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
120
318
|
});
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
319
|
+
|
|
320
|
+
// Inbound (host → adapter): each WS binary frame's payload is written to the adapter's
|
|
321
|
+
// stdin AS IT ARRIVES — never buffered. We deliberately do NOT track `write()`'s backpressure
|
|
322
|
+
// return here: the WHATWG WebSocket receive side has no pause/pull API, so there is nothing to
|
|
323
|
+
// throttle locally. The real backpressure is end-to-end instead — a slow adapter fills its
|
|
324
|
+
// stdin pipe, then the guest's TCP receive window, which pauses the HOST's socket reads
|
|
325
|
+
// (src/acp-ws.ts) where the source actually lives. We never accumulate a frame here; Node's
|
|
326
|
+
// own bounded stdin buffer absorbs the in-flight bytes during the brief drain window.
|
|
327
|
+
ws.addEventListener('message', (ev) => {
|
|
328
|
+
const data = ev.data;
|
|
329
|
+
if (typeof data === 'string') {
|
|
330
|
+
// A string frame post-ready is one of our control messages (handled by the control
|
|
331
|
+
// listener above), never ACP bytes. Skip it here so control traffic is not written
|
|
332
|
+
// into the adapter's stdin.
|
|
333
|
+
if (parseControlFrame(data)) return;
|
|
334
|
+
}
|
|
335
|
+
const buf = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data);
|
|
127
336
|
try {
|
|
128
|
-
|
|
337
|
+
// A `false` return (kernel pipe momentarily full) is intentionally not acted on — see
|
|
338
|
+
// above; Node buffers the write and the host-side socket pause is what throttles the source.
|
|
339
|
+
child.stdin.write(buf);
|
|
129
340
|
} catch {
|
|
130
341
|
/* adapter stdin closed while we were writing — see exit handler */
|
|
131
342
|
}
|
|
132
343
|
});
|
|
344
|
+
|
|
345
|
+
// Outbound (adapter → host): adapter stdout is sent as binary frames. Each chunk is
|
|
346
|
+
// forwarded as it arrives — never buffered (issue 135). Backpressure GATES on the WS send
|
|
347
|
+
// queue: when `ws.bufferedAmount` exceeds the high-water mark we PAUSE child.stdout (the
|
|
348
|
+
// SOURCE) and resume once the queue drains below the low-water mark. Pausing the source —
|
|
349
|
+
// not accumulating — is what keeps this from re-introducing the issue-135 full-buffering
|
|
350
|
+
// stall while still bounding memory if the host socket stalls.
|
|
351
|
+
const maybeResumeStdout = () => {
|
|
352
|
+
if (outboundDrainTimer && ws.bufferedAmount <= WS_LOW_WATER_BYTES) {
|
|
353
|
+
stopOutboundDrainPoll();
|
|
354
|
+
if (!child.stdout.destroyed) child.stdout.resume();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
133
357
|
child.stdout.on('data', (chunk) => {
|
|
358
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
134
359
|
try {
|
|
135
|
-
|
|
360
|
+
// Send a tight ArrayBuffer copy of exactly the chunk's bytes as one binary frame.
|
|
361
|
+
// `chunk` is a Buffer that may be a view into Node's shared allocation pool (non-zero
|
|
362
|
+
// byteOffset); slicing its backing ArrayBuffer yields a fresh buffer of exactly the
|
|
363
|
+
// chunk's data, so no surrounding pool bytes can ever leak into the frame regardless
|
|
364
|
+
// of how the WebSocket impl treats ArrayBufferView offsets.
|
|
365
|
+
ws.send(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
136
366
|
} catch {
|
|
137
|
-
/* host socket
|
|
367
|
+
/* host socket closing while we were writing — see exit handlers */
|
|
138
368
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
/* ignore */
|
|
369
|
+
// Gate AFTER forwarding this chunk: the chunk is already on the wire, so we never hold
|
|
370
|
+
// it back; we only pause the NEXT read when the send queue is over the high-water mark.
|
|
371
|
+
if (ws.bufferedAmount > WS_HIGH_WATER_BYTES && !outboundDrainTimer) {
|
|
372
|
+
child.stdout.pause();
|
|
373
|
+
// WHATWG WebSocket has no 'drain'; poll `bufferedAmount` until it falls to low-water,
|
|
374
|
+
// then resume. The timer is cleared on resume AND on close/finalize (no dangling poll).
|
|
375
|
+
outboundDrainTimer = setInterval(maybeResumeStdout, WS_DRAIN_POLL_MS);
|
|
376
|
+
if (typeof outboundDrainTimer.unref === 'function') outboundDrainTimer.unref();
|
|
148
377
|
}
|
|
149
378
|
});
|
|
379
|
+
// Half-close: when the adapter closes stdout, close the WebSocket write side so the host
|
|
380
|
+
// sees end-of-stream.
|
|
150
381
|
child.stdout.on('end', () => {
|
|
151
|
-
log('adapter closed stdout;
|
|
382
|
+
log('adapter closed stdout; closing websocket');
|
|
152
383
|
try {
|
|
153
|
-
|
|
384
|
+
ws.close(1000, 'adapter stdout end');
|
|
154
385
|
} catch {
|
|
155
386
|
/* ignore */
|
|
156
387
|
}
|
|
@@ -158,44 +389,35 @@ function startBridge() {
|
|
|
158
389
|
child.stdin.on('error', (err) => {
|
|
159
390
|
if (err.code !== 'EPIPE') log(`adapter stdin error: ${err.message}`);
|
|
160
391
|
});
|
|
161
|
-
socket.on('error', (err) => {
|
|
162
|
-
log(`socket error after auth: ${err.message}`);
|
|
163
|
-
try {
|
|
164
|
-
child.kill('SIGTERM');
|
|
165
|
-
} catch {
|
|
166
|
-
/* ignore */
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
392
|
child.on('error', (err) => {
|
|
170
393
|
process.stderr.write(`symphony-vm-agent: failed to spawn adapter: ${err.message}\n`);
|
|
171
394
|
try {
|
|
172
|
-
|
|
395
|
+
ws.close(1011, 'spawn failed');
|
|
173
396
|
} catch {
|
|
174
397
|
/* ignore */
|
|
175
398
|
}
|
|
176
399
|
process.exit(127);
|
|
177
400
|
});
|
|
178
|
-
// Use `close` not `exit`: `exit` fires when the adapter process terminates but its
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// shutdown and the callback fires after the write buffer has drained.
|
|
401
|
+
// Use `close` not `exit`: `exit` fires when the adapter process terminates but its stdio
|
|
402
|
+
// pipes may still have buffered bytes our `data` handlers haven't drained yet. `close`
|
|
403
|
+
// fires only after all stdio streams have closed AND drained, so by then every byte the
|
|
404
|
+
// adapter wrote has been forwarded to ws.send() above. We then close the WebSocket and
|
|
405
|
+
// let its `close` event (or the grace timer) finalize the exit so the forwarded frames
|
|
406
|
+
// flush to the host before we terminate.
|
|
185
407
|
child.on('close', (code, signal) => {
|
|
186
408
|
log(`adapter close code=${code} signal=${signal}`);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
process.exit(128 + sig);
|
|
191
|
-
}
|
|
192
|
-
process.exit(code ?? 0);
|
|
193
|
-
};
|
|
409
|
+
pendingExit = { code, signal };
|
|
410
|
+
stopHeartbeat();
|
|
411
|
+
stopOutboundDrainPoll();
|
|
194
412
|
try {
|
|
195
|
-
|
|
413
|
+
ws.close(1000, 'adapter exit');
|
|
196
414
|
} catch {
|
|
197
415
|
finalize();
|
|
416
|
+
return;
|
|
198
417
|
}
|
|
418
|
+
// Safety net: if the close handshake stalls, exit anyway rather than hang the dispatch.
|
|
419
|
+
const t = setTimeout(finalize, 2_000);
|
|
420
|
+
if (typeof t.unref === 'function') t.unref();
|
|
199
421
|
});
|
|
200
422
|
// Forward host-side termination signals to the adapter.
|
|
201
423
|
for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|