@wjarka/cezarion 0.15.1 → 0.16.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/README.md +167 -46
- package/dist/agent-config/catalog.d.ts +6 -2
- package/dist/agent-config/catalog.js +78 -3
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/agent-config/model-settings/shared.js +6 -3
- package/dist/agent-config/model-settings/shared.js.map +1 -1
- package/dist/agent-config/models.js +4 -0
- package/dist/agent-config/models.js.map +1 -1
- package/dist/agent-config/validate.js +6 -0
- package/dist/agent-config/validate.js.map +1 -1
- package/dist/artifacts/cli.js +10 -2
- package/dist/artifacts/cli.js.map +1 -1
- package/dist/automations/coordinator.d.ts +7 -0
- package/dist/automations/coordinator.js +22 -2
- package/dist/automations/coordinator.js.map +1 -1
- package/dist/automations/github-poller.d.ts +4 -4
- package/dist/automations/github-poller.js.map +1 -1
- package/dist/automations/schedule-runner.d.ts +130 -0
- package/dist/automations/schedule-runner.js +326 -0
- package/dist/automations/schedule-runner.js.map +1 -0
- package/dist/automations/scheduler.d.ts +38 -7
- package/dist/automations/scheduler.js +90 -22
- package/dist/automations/scheduler.js.map +1 -1
- package/dist/automations/store.d.ts +80 -3
- package/dist/automations/store.js +208 -16
- package/dist/automations/store.js.map +1 -1
- package/dist/automations/task-template.d.ts +42 -6
- package/dist/automations/task-template.js +152 -24
- package/dist/automations/task-template.js.map +1 -1
- package/dist/automations/types.d.ts +142 -6
- package/dist/automations/types.js +64 -6
- package/dist/automations/types.js.map +1 -1
- package/dist/autosave-holders.js +8 -12
- package/dist/autosave-holders.js.map +1 -1
- package/dist/ci-wait/client.d.ts +8 -1
- package/dist/ci-wait/client.js +43 -1
- package/dist/ci-wait/client.js.map +1 -1
- package/dist/ci-wait/controller.d.ts +83 -4
- package/dist/ci-wait/controller.js +76 -11
- package/dist/ci-wait/controller.js.map +1 -1
- package/dist/ci-wait/errors.d.ts +7 -1
- package/dist/ci-wait/errors.js +36 -0
- package/dist/ci-wait/errors.js.map +1 -1
- package/dist/ci-wait/github.d.ts +2 -0
- package/dist/ci-wait/github.js +3 -1
- package/dist/ci-wait/github.js.map +1 -1
- package/dist/ci-wait/mcp.d.ts +2 -98
- package/dist/ci-wait/mcp.js +11 -25
- package/dist/ci-wait/mcp.js.map +1 -1
- package/dist/ci-wait/supervisor.d.ts +2 -1
- package/dist/ci-wait/supervisor.js +2 -1
- package/dist/ci-wait/supervisor.js.map +1 -1
- package/dist/ci-wait/tools.d.ts +307 -0
- package/dist/ci-wait/tools.js +63 -0
- package/dist/ci-wait/tools.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/contract/agent-config.d.ts +5 -0
- package/dist/contract/agent-profiles.d.ts +15 -0
- package/dist/contract/automation-schedule.d.ts +60 -0
- package/dist/contract/automations.d.ts +208 -36
- package/dist/contract/delegation.d.ts +96 -0
- package/dist/contract/discovery.d.ts +7 -0
- package/dist/contract/github.d.ts +1 -0
- package/dist/contract/health.d.ts +6 -0
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.js +1445 -745
- package/dist/contract/preview.d.ts +508 -0
- package/dist/contract/runs.d.ts +716 -15
- package/dist/contract/skills.d.ts +39 -0
- package/dist/contract/workflows.d.ts +6 -0
- package/dist/contract/workspace.d.ts +65 -0
- package/dist/contract/zoned-time.d.ts +35 -0
- package/dist/core/agent-env.d.ts +11 -0
- package/dist/core/agent-env.js +27 -5
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-profiles.js +7 -0
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-runner.d.ts +49 -1
- package/dist/core/agent-runner.js +2 -1
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/backend-detect.d.ts +3 -2
- package/dist/core/backend-detect.js +27 -1
- package/dist/core/backend-detect.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +1 -0
- package/dist/core/claude-cli-runner.js +69 -19
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.d.ts +1 -0
- package/dist/core/codex-app-server-runner.js +200 -25
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +7 -3
- package/dist/core/codex-app-server-transport.js +11 -9
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/cursor-acp-runner.d.ts +1 -0
- package/dist/core/cursor-acp-runner.js +24 -9
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/host-model-catalog.js +2 -0
- package/dist/core/host-model-catalog.js.map +1 -1
- package/dist/core/managed-session.js +2 -0
- package/dist/core/managed-session.js.map +1 -1
- package/dist/core/model-identity.js +2 -0
- package/dist/core/model-identity.js.map +1 -1
- package/dist/core/model-presets.js +3 -0
- package/dist/core/model-presets.js.map +1 -1
- package/dist/core/omp-model-catalog.d.ts +43 -0
- package/dist/core/omp-model-catalog.js +205 -0
- package/dist/core/omp-model-catalog.js.map +1 -0
- package/dist/core/omp-runner.d.ts +73 -0
- package/dist/core/omp-runner.js +990 -0
- package/dist/core/omp-runner.js.map +1 -0
- package/dist/core/omp-ui-mapper.d.ts +106 -0
- package/dist/core/omp-ui-mapper.js +882 -0
- package/dist/core/omp-ui-mapper.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +18 -4
- package/dist/core/opencode-server-runner.js +337 -96
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/opencode-session-error.d.ts +11 -0
- package/dist/core/opencode-session-error.js +26 -0
- package/dist/core/opencode-session-error.js.map +1 -0
- package/dist/core/opencode-ui-mapper.js +4 -36
- package/dist/core/opencode-ui-mapper.js.map +1 -1
- package/dist/core/pi-runner.d.ts +1 -0
- package/dist/core/pi-runner.js +75 -12
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/provider-auth.d.ts +9 -2
- package/dist/core/provider-auth.js +43 -6
- package/dist/core/provider-auth.js.map +1 -1
- package/dist/core/runner-factory.js +3 -0
- package/dist/core/runner-factory.js.map +1 -1
- package/dist/core/runner-model-catalog.js +1 -0
- package/dist/core/runner-model-catalog.js.map +1 -1
- package/dist/core/runner-stderr.d.ts +2 -0
- package/dist/core/runner-stderr.js +20 -0
- package/dist/core/runner-stderr.js.map +1 -0
- package/dist/core/session-process.d.ts +22 -0
- package/dist/core/session-process.js +202 -0
- package/dist/core/session-process.js.map +1 -0
- package/dist/core/ui-events.d.ts +1 -1
- package/dist/core/usage-limit.js +3 -60
- package/dist/core/usage-limit.js.map +1 -1
- package/dist/data-gitignore.d.ts +6 -0
- package/dist/data-gitignore.js +57 -0
- package/dist/data-gitignore.js.map +1 -0
- package/dist/delegation/capacity.d.ts +19 -0
- package/dist/delegation/capacity.js +34 -0
- package/dist/delegation/capacity.js.map +1 -0
- package/dist/delegation/cli.js +13 -4
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/conversations.d.ts +4 -2
- package/dist/delegation/conversations.js +8 -12
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/credentials.d.ts +2 -0
- package/dist/delegation/credentials.js +10 -0
- package/dist/delegation/credentials.js.map +1 -1
- package/dist/delegation/destroy-observation.d.ts +42 -0
- package/dist/delegation/destroy-observation.js +115 -0
- package/dist/delegation/destroy-observation.js.map +1 -0
- package/dist/delegation/execution-identity.d.ts +6 -0
- package/dist/delegation/execution-identity.js +1 -1
- package/dist/delegation/execution-identity.js.map +1 -1
- package/dist/delegation/policy.d.ts +3 -2
- package/dist/delegation/policy.js +7 -5
- package/dist/delegation/policy.js.map +1 -1
- package/dist/delegation/process-liveness.d.ts +57 -20
- package/dist/delegation/process-liveness.js +114 -92
- package/dist/delegation/process-liveness.js.map +1 -1
- package/dist/delegation/provision.js +6 -6
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/results.js +4 -2
- package/dist/delegation/results.js.map +1 -1
- package/dist/delegation/retry-backoff.d.ts +23 -0
- package/dist/delegation/retry-backoff.js +18 -0
- package/dist/delegation/retry-backoff.js.map +1 -0
- package/dist/delegation/routes.d.ts +64 -58
- package/dist/delegation/scratch-cleanup.d.ts +29 -0
- package/dist/delegation/scratch-cleanup.js +143 -0
- package/dist/delegation/scratch-cleanup.js.map +1 -0
- package/dist/delegation/service.d.ts +20 -1
- package/dist/delegation/service.js +233 -70
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +12 -6
- package/dist/delegation/workspace.d.ts +23 -4
- package/dist/delegation/workspace.js +147 -31
- package/dist/delegation/workspace.js.map +1 -1
- package/dist/git-worktree-lock-helper.js +8 -3
- package/dist/git-worktree-lock-helper.js.map +1 -1
- package/dist/git-worktree-lock.d.ts +6 -1
- package/dist/git-worktree-lock.js +10 -4
- package/dist/git-worktree-lock.js.map +1 -1
- package/dist/git-worktree-release.d.ts +20 -0
- package/dist/git-worktree-release.js +27 -0
- package/dist/git-worktree-release.js.map +1 -0
- package/dist/git-worktree.d.ts +8 -2
- package/dist/git-worktree.js +19 -2
- package/dist/git-worktree.js.map +1 -1
- package/dist/handoff.d.ts +2 -2
- package/dist/handoff.js +1 -1
- package/dist/index.js +109 -76
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +5 -1
- package/dist/paths.js.map +1 -1
- package/dist/preview/cdp.d.ts +13 -0
- package/dist/preview/cdp.js +69 -0
- package/dist/preview/cdp.js.map +1 -0
- package/dist/preview/chromium.d.ts +75 -0
- package/dist/preview/chromium.js +394 -0
- package/dist/preview/chromium.js.map +1 -0
- package/dist/preview/dev-server.d.ts +68 -0
- package/dist/preview/dev-server.js +341 -0
- package/dist/preview/dev-server.js.map +1 -0
- package/dist/preview/host.d.ts +154 -0
- package/dist/preview/host.js +681 -0
- package/dist/preview/host.js.map +1 -0
- package/dist/preview/registration.d.ts +63 -0
- package/dist/preview/registration.js +109 -0
- package/dist/preview/registration.js.map +1 -0
- package/dist/preview/session.d.ts +73 -0
- package/dist/preview/session.js +347 -0
- package/dist/preview/session.js.map +1 -0
- package/dist/preview/url.d.ts +6 -0
- package/dist/preview/url.js +17 -0
- package/dist/preview/url.js.map +1 -0
- package/dist/runs/agent-tmpdir-removal.d.ts +15 -0
- package/dist/runs/agent-tmpdir-removal.js +60 -0
- package/dist/runs/agent-tmpdir-removal.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +12 -1
- package/dist/runs/agent-tmpdir.js +123 -34
- package/dist/runs/agent-tmpdir.js.map +1 -1
- package/dist/runs/delegation-state.d.ts +5 -1
- package/dist/runs/delegation-state.js +9 -0
- package/dist/runs/delegation-state.js.map +1 -1
- package/dist/runs/event-history.d.ts +4 -4
- package/dist/runs/event-history.js +97 -117
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/history-compressor.d.ts +29 -0
- package/dist/runs/history-compressor.js +116 -0
- package/dist/runs/history-compressor.js.map +1 -0
- package/dist/runs/history-file.d.ts +36 -0
- package/dist/runs/history-file.js +463 -0
- package/dist/runs/history-file.js.map +1 -0
- package/dist/runs/human-ask-summary.js +18 -13
- package/dist/runs/human-ask-summary.js.map +1 -1
- package/dist/runs/legacy-index.d.ts +62 -0
- package/dist/runs/legacy-index.js +168 -0
- package/dist/runs/legacy-index.js.map +1 -0
- package/dist/runs/raw-record.d.ts +52 -0
- package/dist/runs/raw-record.js +145 -0
- package/dist/runs/raw-record.js.map +1 -0
- package/dist/runs/retention.d.ts +13 -6
- package/dist/runs/retention.js +42 -13
- package/dist/runs/retention.js.map +1 -1
- package/dist/runs/run-claims.d.ts +23 -0
- package/dist/runs/run-claims.js +24 -0
- package/dist/runs/run-claims.js.map +1 -0
- package/dist/runs/run-database.d.ts +385 -0
- package/dist/runs/run-database.js +930 -0
- package/dist/runs/run-database.js.map +1 -0
- package/dist/runs/run-index.d.ts +35 -9
- package/dist/runs/run-index.js +200 -25
- package/dist/runs/run-index.js.map +1 -1
- package/dist/runs/run-row.d.ts +44 -0
- package/dist/runs/run-row.js +84 -0
- package/dist/runs/run-row.js.map +1 -0
- package/dist/runs/run-search.d.ts +29 -0
- package/dist/runs/run-search.js +63 -0
- package/dist/runs/run-search.js.map +1 -0
- package/dist/runs/store-open-error.d.ts +24 -0
- package/dist/runs/store-open-error.js +61 -0
- package/dist/runs/store-open-error.js.map +1 -0
- package/dist/runs/store.d.ts +667 -32
- package/dist/runs/store.js +2153 -324
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/transcript-facts.d.ts +77 -0
- package/dist/runs/transcript-facts.js +302 -0
- package/dist/runs/transcript-facts.js.map +1 -0
- package/dist/runs/webhook.js +5 -4
- package/dist/runs/webhook.js.map +1 -1
- package/dist/runs/worker-execution.d.ts +22 -0
- package/dist/runs/worker-execution.js +30 -0
- package/dist/runs/worker-execution.js.map +1 -0
- package/dist/server/capabilities.d.ts +1 -0
- package/dist/server/capabilities.js +2 -0
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/cockpit-ownership.d.ts +8 -0
- package/dist/server/cockpit-ownership.js +19 -0
- package/dist/server/cockpit-ownership.js.map +1 -1
- package/dist/server/forge/github.d.ts +1 -4
- package/dist/server/forge/github.js +47 -23
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/git.js +1 -1
- package/dist/server/git.js.map +1 -1
- package/dist/server/open-in-app.js +1 -0
- package/dist/server/open-in-app.js.map +1 -1
- package/dist/server/preview-socket.d.ts +36 -0
- package/dist/server/preview-socket.js +126 -0
- package/dist/server/preview-socket.js.map +1 -0
- package/dist/server/project-context.d.ts +31 -4
- package/dist/server/project-context.js +73 -9
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/provider-action-gate.js +1 -0
- package/dist/server/provider-action-gate.js.map +1 -1
- package/dist/server/repo-branches.d.ts +4 -3
- package/dist/server/repo-branches.js +5 -4
- package/dist/server/repo-branches.js.map +1 -1
- package/dist/server/server.d.ts +4500 -2727
- package/dist/server/server.js +651 -202
- package/dist/server/server.js.map +1 -1
- package/dist/server/upgrade-router.d.ts +23 -0
- package/dist/server/upgrade-router.js +53 -0
- package/dist/server/upgrade-router.js.map +1 -0
- package/dist/server/validators.d.ts +6 -1
- package/dist/server/validators.js +4 -4
- package/dist/server/validators.js.map +1 -1
- package/dist/server/ws.d.ts +16 -16
- package/dist/server/ws.js +56 -54
- package/dist/server/ws.js.map +1 -1
- package/dist/skills-remote.d.ts +14 -0
- package/dist/skills-remote.js +91 -15
- package/dist/skills-remote.js.map +1 -1
- package/dist/task-cli/cli.js +40 -16
- package/dist/task-cli/cli.js.map +1 -1
- package/dist/task-cli/discovery.d.ts +4 -3
- package/dist/task-cli/discovery.js +4 -3
- package/dist/task-cli/discovery.js.map +1 -1
- package/dist/task-cli/http.d.ts +2 -0
- package/dist/task-cli/http.js +10 -4
- package/dist/task-cli/http.js.map +1 -1
- package/dist/task-cli/projections.d.ts +5 -2
- package/dist/task-cli/projections.js +13 -0
- package/dist/task-cli/projections.js.map +1 -1
- package/dist/task-cli/watch.d.ts +15 -2
- package/dist/task-cli/watch.js +31 -9
- package/dist/task-cli/watch.js.map +1 -1
- package/dist/ui-state.d.ts +4 -3
- package/dist/ui-state.js +8 -3
- package/dist/ui-state.js.map +1 -1
- package/dist/workflows/grouped-command.d.ts +31 -0
- package/dist/workflows/grouped-command.js +143 -0
- package/dist/workflows/grouped-command.js.map +1 -0
- package/dist/workflows/run.d.ts +223 -42
- package/dist/workflows/run.js +1316 -398
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/types.d.ts +12 -0
- package/dist/workflows/types.js +20 -0
- package/dist/workflows/types.js.map +1 -1
- package/dist/workspace/agent-accounts.d.ts +5 -1
- package/dist/workspace/agent-accounts.js +1 -0
- package/dist/workspace/agent-accounts.js.map +1 -1
- package/dist/workspace/agent-profiles.js +1 -1
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.d.ts +3 -1
- package/dist/workspace/config.js +1 -0
- package/dist/workspace/config.js.map +1 -1
- package/dist/worktree-setup.d.ts +85 -0
- package/dist/worktree-setup.js +164 -0
- package/dist/worktree-setup.js.map +1 -0
- package/package.json +7 -5
- package/scripts/benchmark-run-store-fixture.ts +428 -0
- package/scripts/benchmark-run-store.ts +743 -0
- package/scripts/benchmark-stuck-destroys.ts +143 -0
- package/scripts/mock-autonomous.mjs +11 -0
- package/scripts/mock-ci-tool.mjs +20 -5
- package/scripts/mock-claude.mjs +110 -1
- package/scripts/mock-codex-app-server.mjs +117 -2
- package/scripts/mock-codex-coalesce-steer.mjs +59 -0
- package/scripts/mock-codex-reverse-steer.mjs +130 -0
- package/scripts/mock-cursor-acp.mjs +75 -1
- package/scripts/mock-omp-rpc.mjs +605 -0
- package/scripts/mock-opencode-serve.mjs +312 -22
- package/scripts/mock-pi-rpc.mjs +109 -3
- package/scripts/mock-runner-crash.mjs +42 -0
- package/scripts/mock-silent-tail.mjs +78 -0
- package/scripts/mock-turn-messages.mjs +5 -0
- package/scripts/omp-ci-wait.mjs +29 -0
- package/scripts/omp-restrict-delegation.yml +4 -0
- package/scripts/pi-ci-wait.mjs +12 -9
- package/scripts/seed-archive-profile.ts +3 -3
- package/web/dist/assets/agent-accounts-CFOBMVHc.js +1 -0
- package/web/dist/assets/alert-dialog-BrpNUwMt.js +1 -0
- package/web/dist/assets/app-window-CSHwDX9N.js +1 -0
- package/web/dist/assets/button-C_rd7CPp.js +43 -0
- package/web/dist/assets/compare-variants-D6WXhvXA.js +1 -0
- package/web/dist/assets/diff-DrEARXTV.js +3 -0
- package/web/dist/assets/diff-controls-B1-j7nFg.js +1 -0
- package/web/dist/assets/{diff-view-DPn6bQ5W.js → diff-view-DvGLjdTk.js} +3 -3
- package/web/dist/assets/dropdown-menu-BegOZFrj.js +1 -0
- package/web/dist/assets/format-DpNe3S0A.js +1 -0
- package/web/dist/assets/git-toolbar-Cw4ljmhB.js +1 -0
- package/web/dist/assets/github-CfV5sqL0.js +1 -0
- package/web/dist/assets/{image-preview-DJ8cgb1Z.js → image-preview-FOEfqpZP.js} +1 -1
- package/web/dist/assets/index-DgWWlOLy.js +13 -0
- package/web/dist/assets/index-fBFnuvxu.css +1 -0
- package/web/dist/assets/{nav-row-styles-3j64kH5s.js → nav-row-styles-vp2JbhuJ.js} +1 -1
- package/web/dist/assets/play-CmVAj_fu.js +1 -0
- package/web/dist/assets/popover-BohP6WTx.js +1 -0
- package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +3 -0
- package/web/dist/assets/preview-state-vuMviq6q.js +1 -0
- package/web/dist/assets/prompt-templates-Cubdut_C.js +15 -0
- package/web/dist/assets/repo-git-Cf4t5PD0.js +1 -0
- package/web/dist/assets/{run-diff-5-RRdcVz.js → run-diff-BpkUW37Z.js} +3 -3
- package/web/dist/assets/run-header-68WHf9wV.js +1 -0
- package/web/dist/assets/skills-DiVIgKeU.js +1 -0
- package/web/dist/assets/square-terminal-B6RYYieR.js +1 -0
- package/web/dist/assets/status-dot-CqTJHSP9.js +1 -0
- package/web/dist/assets/{tab-link-K0hjzEXR.js → tab-link-B07Hu4QX.js} +1 -1
- package/web/dist/assets/task-changes--lQF6EYf.js +1 -0
- package/web/dist/assets/task-commits-Cx0qy4DN.js +1 -0
- package/web/dist/assets/task-files-D50s-gKW.js +2 -0
- package/web/dist/assets/task-github-item-DaqmSwYT.js +1 -0
- package/web/dist/assets/task-thread-BPQ64lui.js +7 -0
- package/web/dist/assets/{task-thread-Bw_nQuTx.css → task-thread-zNE2Q9E-.css} +1 -1
- package/web/dist/assets/textarea-D20O4Pw0.js +1 -0
- package/web/dist/assets/thread-state-DWcFMrvb.js +4 -0
- package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +1 -0
- package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +1 -0
- package/web/dist/assets/{utils-fzNfbPzA.js → utils-JsL3ZsJf.js} +1 -1
- package/web/dist/assets/workflows-jwG7uxqi.js +11 -0
- package/web/dist/assets/{zoomable-image-CDmZ_Rtl.js → zoomable-image-BJJ64KGc.js} +1 -1
- package/web/dist/cezarion-lockup-dark.svg +1 -13
- package/web/dist/cezarion-lockup-light.svg +1 -13
- package/web/dist/cezarion-mark-dark.svg +1 -12
- package/web/dist/cezarion-mark-light.svg +1 -12
- package/web/dist/index.html +16 -21
- package/web/dist/assets/agent-accounts-CU2ZDv24.js +0 -1
- package/web/dist/assets/alert-dialog-C-xYE_dg.js +0 -43
- package/web/dist/assets/chevron-right-BENZ-1ZN.js +0 -1
- package/web/dist/assets/collapsible-DD9J91Iv.js +0 -1
- package/web/dist/assets/compare-variants-5FmqpFSN.js +0 -1
- package/web/dist/assets/diff-controls-Bj0YrioE.js +0 -1
- package/web/dist/assets/diff-rE8rm75J2.js +0 -3
- package/web/dist/assets/diff-stat-BKDCSVfJ.js +0 -1
- package/web/dist/assets/dropdown-menu-Ch5BYQh_.js +0 -1
- package/web/dist/assets/format-dBiNDNVe.js +0 -1
- package/web/dist/assets/git-toolbar-ClVvf5iF.js +0 -1
- package/web/dist/assets/github-BkNOyour.css +0 -1
- package/web/dist/assets/github-DeZMeMwJ.js +0 -1
- package/web/dist/assets/github-item-detail-W5F6GVjF.js +0 -1
- package/web/dist/assets/github-sidebar-model-DcMu-IK2.js +0 -1
- package/web/dist/assets/highlighter-CdHqIwFr.js +0 -3
- package/web/dist/assets/index-CatemBEL.js +0 -8
- package/web/dist/assets/index-MvfD8bZJ.css +0 -1
- package/web/dist/assets/markdown-zdKeLIYA.js +0 -2
- package/web/dist/assets/mobile-run-bar-BCfmFi8V.js +0 -1
- package/web/dist/assets/prompt-templates-CBPp39IU.js +0 -15
- package/web/dist/assets/reference-chip-D6owncJT.js +0 -1
- package/web/dist/assets/repo-git-CtdkZFey.js +0 -1
- package/web/dist/assets/run-header-BcjoDi0W.js +0 -1
- package/web/dist/assets/sheet-ChnYVrp1.js +0 -1
- package/web/dist/assets/skills-C8YKp2vr.js +0 -1
- package/web/dist/assets/task-changes-BW8YdWG8.js +0 -1
- package/web/dist/assets/task-commits-q2kB2y6L.js +0 -1
- package/web/dist/assets/task-files-pPL6_3Cr.js +0 -2
- package/web/dist/assets/task-github-item-BXF5R52U.js +0 -1
- package/web/dist/assets/task-thread-DtkFL3EC.js +0 -10
- package/web/dist/assets/textarea-DxlkE-Yd.js +0 -1
- package/web/dist/assets/token-metrics-DIAjrPlM.js +0 -1
- package/web/dist/assets/use-desktop-CrDny2uG.js +0 -1
- package/web/dist/assets/use-submit-shortcut-KWJsC906.js +0 -1
- package/web/dist/assets/workflows-TmcPEj2K.js +0 -11
- /package/web/dist/assets/{lib-BQXq3kEf2.js → lib-BQXq3kEf.js} +0 -0
package/dist/workflows/run.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { workerEvidenceRunIds } from '../runs/worker-execution.js';
|
|
1
2
|
import { ciErrorMessage } from '../ci-wait/errors.js';
|
|
2
|
-
import { ciWaitRequestSchema, ciWaitResultSchema } from '../contract/index.js';
|
|
3
|
+
import { ciWaitRequestSchema, ciWaitResultSchema, previewStopRequestSchema, previewServeRequestSchema } from '../contract/index.js';
|
|
4
|
+
import { previewToolEnabled } from '../ci-wait/tools.js';
|
|
5
|
+
import { previewStopResult, previewResult, validateRegistration } from '../preview/registration.js';
|
|
3
6
|
import { acquireCiResources } from '../ci-wait/resources.js';
|
|
4
7
|
import { artifactInstructions, provisionArtifactDirectory } from '../artifacts/lifecycle.js';
|
|
5
8
|
import { workerContextHash, workerWorkflowHash, workerStepIdentity, captureWorkerAccount, boundWorkerAccountEnv, WorkerIdentityError } from '../delegation/execution-identity.js';
|
|
6
9
|
import { buildChildEnv } from '../core/agent-env.js';
|
|
7
10
|
import { randomUUID } from 'node:crypto';
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
9
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
12
|
import { dirname, join } from 'node:path';
|
|
11
13
|
import { parseAskMarkerResult, stripAskMarker, } from '../core/ask.js';
|
|
12
14
|
import { hasRegisteredRunProcess, onUsage, registerRunProcess, unregisterRunProcess } from '../core/process-usage.js';
|
|
13
|
-
import {
|
|
15
|
+
import { processGroupAlive, processGroupOf, sessionGroupOf, signalProcessGroup } from '../core/session-process.js';
|
|
16
|
+
import { WorkerScratchCleanup } from '../delegation/scratch-cleanup.js';
|
|
17
|
+
import { noJitter, ORPHAN_BACKOFF, retryDelayMs } from '../delegation/retry-backoff.js';
|
|
18
|
+
import { inspectExecutionGeneration, isCurrentProcess, processesWithCwdUnder, processStartToken, recordedGroupSignalable, recordedProcessLive, sharedCwdScan } from '../delegation/process-liveness.js';
|
|
14
19
|
import { parseUsageLimit } from '../core/usage-limit.js';
|
|
15
20
|
import { startManagedSession } from '../core/managed-session.js';
|
|
16
21
|
import { createRunner } from '../core/runner-factory.js';
|
|
@@ -23,14 +28,16 @@ import { todosPath } from '../todos.js';
|
|
|
23
28
|
// Contract VALUES, like `workspaceUiStateSchema` in workspace/migrations.ts: the attachment
|
|
24
29
|
// vocabulary the routes validate with is the same one the engine stores and re-reads by, so the
|
|
25
30
|
// wire and the disk can never disagree about what counts as an image (#950).
|
|
26
|
-
import { requestWaitRequestSchema, workerWaitRequestSchema, attachmentExtension, delegationStateSchema,
|
|
31
|
+
import { requestWaitRequestSchema, workerWaitRequestSchema, attachmentExtension, delegationStateSchema, askRequestSchema, isImageAttachmentName, isImageMediaType, } from '../contract/index.js';
|
|
27
32
|
import { inputDeliveryOf } from '../core/agent-runner.js';
|
|
28
33
|
import { discoverSkills } from '../skills.js';
|
|
29
|
-
import { materializeSkillDir } from '../skills-remote.js';
|
|
34
|
+
import { materializeSkillDir, waitForCachedTeamSkills, waitForTeamSkills } from '../skills-remote.js';
|
|
30
35
|
import { seedAgentConfigLocalLayer } from '../agent-config/seed.js';
|
|
31
36
|
import { readAgentModelSettings, readAgentModelProvider } from '../agent-config/models.js';
|
|
32
37
|
import { loadConfig, resolveWorktreeRetention } from '../config.js';
|
|
33
38
|
import { autosaveCommit, createWorktree, resolveBaseRef, worktreeDiff, worktreeShortstat } from '../git-worktree.js';
|
|
39
|
+
import { runGroupedCommand } from './grouped-command.js';
|
|
40
|
+
import { resolveWorktreeSetup, runWorktreeSetup, worktreeSetupAgentNote, worktreeSetupEndNote, worktreeSetupEnv, worktreeSetupRecordError, worktreeSetupStartNote, } from '../worktree-setup.js';
|
|
34
41
|
import { getHeadCommit, getRepoInfo } from '../server/git.js';
|
|
35
42
|
import { ensureOwnedWorkspace, verifyOwnedWorkspace } from '../delegation/workspace.js';
|
|
36
43
|
import { verifyWorkerContext } from '../delegation/context.js';
|
|
@@ -43,8 +50,9 @@ import { parentReadiness } from '../delegation/readiness.js';
|
|
|
43
50
|
import { answersQuestion, openQuestions, questionMessage } from '../delegation/questions.js';
|
|
44
51
|
import { workerOutcome } from '../runs/delegation-state.js';
|
|
45
52
|
import { loadWorkflows } from './load.js';
|
|
53
|
+
import { RUN_IN_USE_ELSEWHERE } from '../runs/store.js';
|
|
46
54
|
import { isReclaimable, reclaimWorktrees, rematerializeReclaimedWorktree } from '../runs/retention.js';
|
|
47
|
-
import { AgentTempDirError, agentTmpEnv,
|
|
55
|
+
import { AgentTempDirError, agentTmpEnv, agentTmpDirLocationEvidence, removeAgentTmpDir, } from '../runs/agent-tmpdir.js';
|
|
48
56
|
import { extractTaskRefs, refineTaskRefs, titleRefNumber } from '../runs/task-refs.js';
|
|
49
57
|
import { parseTaskMarkers, stripTaskMarkers } from '../runs/task-markers.js';
|
|
50
58
|
import { autoNamingActive, generateRunName, liveTitleUpdatesEnabled, postValidateTitle } from '../runs/auto-name.js';
|
|
@@ -61,24 +69,81 @@ async function configuredModelProvider(backend, repoRoot) {
|
|
|
61
69
|
/** An interactive session that hears nothing from the user closes itself. */
|
|
62
70
|
export const IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
63
71
|
/**
|
|
64
|
-
* Task-completion marker from the agent contract (HANDOFF_INSTRUCTIONS):
|
|
65
|
-
*
|
|
72
|
+
* Task-completion marker from the agent contract (HANDOFF_INSTRUCTIONS):
|
|
73
|
+
* an active `CEZ:DONE` declaration means "goal achieved, nothing to ask" —
|
|
66
74
|
* the session is closed right away instead of parking at `waiting` (#347).
|
|
67
75
|
* Detection runs on the accumulated turn text so delta-streaming backends
|
|
68
76
|
* (codex, opencode) can't split the marker across text events.
|
|
69
77
|
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
78
|
+
import { PROSE_HUMAN_GATE, workerOutcomeKey } from '../runs/transcript-facts.js';
|
|
79
|
+
/** Classify only active prose: quoted/indented examples and fenced code cannot
|
|
80
|
+
* declare a park or request a human. A standalone monitoring line belongs to
|
|
81
|
+
* this turn even when a later assistant block acknowledges a new instruction. */
|
|
82
|
+
function turnParkSignals(text) {
|
|
83
|
+
let fence;
|
|
84
|
+
let monitoring = false;
|
|
85
|
+
let humanGate = false;
|
|
86
|
+
let done = false;
|
|
87
|
+
let askResult = { kind: 'none' };
|
|
88
|
+
let previous = '';
|
|
89
|
+
for (const raw of text.split('\n')) {
|
|
90
|
+
const delimiter = raw.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
91
|
+
if (delimiter) {
|
|
92
|
+
previous = ''; // a fenced example consumes its heading, not the next declaration
|
|
93
|
+
const token = delimiter[1];
|
|
94
|
+
if (!fence)
|
|
95
|
+
fence = { char: token[0], length: token.length };
|
|
96
|
+
else if (token[0] === fence.char && token.length >= fence.length)
|
|
97
|
+
fence = undefined;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (fence)
|
|
101
|
+
continue;
|
|
102
|
+
if (/^(?: {4}|\t|\s*>)/.test(raw)) {
|
|
103
|
+
if (raw.trim())
|
|
104
|
+
previous = '';
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Lists and emphasis format active prose; they do not quote it. Exclude
|
|
108
|
+
// fenced/indented/blockquote examples before removing their decoration.
|
|
109
|
+
const line = raw.trim().replace(/^(?:[-+*]|\d+[.)])\s+(?:\[[ xX]\]\s+)?/, '')
|
|
110
|
+
.replace(/^#{1,6}\s+/, '').replace(/\*{1,2}|_{1,2}/g, '');
|
|
111
|
+
if (!line)
|
|
112
|
+
continue;
|
|
113
|
+
const example = /\b(?:example|sample|literal|quoted)(?: marker)?\s*:\s*$/i.test(previous);
|
|
114
|
+
if (!example && raw.trim() === 'CEZ:MONITORING') {
|
|
115
|
+
monitoring = true;
|
|
116
|
+
humanGate = false; // a later declaration supersedes earlier, resolved prose
|
|
117
|
+
done = false;
|
|
118
|
+
}
|
|
119
|
+
if (!example && raw.trim() === 'CEZ:DONE') {
|
|
120
|
+
done = true;
|
|
121
|
+
humanGate = false;
|
|
122
|
+
askResult = { kind: 'none' }; // retain the existing later-DONE precedence
|
|
123
|
+
}
|
|
124
|
+
// Preserve the existing payload validation/repair and latest malformed
|
|
125
|
+
// marker diagnostics. Only active declarations participate: an ACK after
|
|
126
|
+
// a real ASK must not erase its card, nor turn examples into questions.
|
|
127
|
+
const candidate = example ? { kind: 'none' } : parseAskMarkerResult(raw);
|
|
128
|
+
if (candidate.kind !== 'none') {
|
|
129
|
+
askResult = candidate;
|
|
130
|
+
done = false;
|
|
131
|
+
}
|
|
132
|
+
// Deliberately require a direct question/request, not words like "review",
|
|
133
|
+
// "checks" or "approval" in progress reports (hearsay-tools/cezarion#772 and hearsay-tools/cezarion#609).
|
|
134
|
+
const prose = line.replace(/`[^`]*`|"[^"]*"/g, '');
|
|
135
|
+
if (!example && prose.split(/(?<=[.!?])\s+/).some(sentence => /^(?:(?:should|shall|can|may) I\b|(?:do|would|could|can|will|are) you\b).*\?\s*$/i.test(sentence) ||
|
|
136
|
+
/^(?:which|what|how|where|when|who)\b.*\b(?:you|(?:should|shall|can|may) (?:I|we))\b.*\?\s*$/i.test(sentence) ||
|
|
137
|
+
/^(?:which|what) (?:module|option|approach|strategy|framework|library)\?\s*$/i.test(sentence) ||
|
|
138
|
+
/^(?:is|are) (?:this|these|that|it)\b.*\b(?:ok(?:ay)?|acceptable|approved)\?\s*$/i.test(sentence) ||
|
|
139
|
+
/^(?:please\s+(?:review|approve|confirm|choose|decide|answer|select|provide)\b|(?:I(?:['’]m| am)?\s+)?(?:need|awaiting|waiting for)\s+your\s+(?:review|approval|confirmation|answer|decision|input|permission)\b|(?:the )?PR (?:is )?ready for (?:your )?review\b)/i.test(sentence))) {
|
|
140
|
+
humanGate = true;
|
|
141
|
+
done = false;
|
|
142
|
+
}
|
|
143
|
+
previous = line;
|
|
144
|
+
}
|
|
145
|
+
return { monitoring, humanGate, done, askResult };
|
|
146
|
+
}
|
|
82
147
|
/** Claude's native scheduler is the backend-level equivalent of the textual
|
|
83
148
|
* monitoring marker. Keep this recognition here, at the workflow boundary,
|
|
84
149
|
* so the v1 event protocol stays unchanged and other backends do not acquire
|
|
@@ -117,6 +182,17 @@ function appendCompletedAssistantText(current, event) {
|
|
|
117
182
|
}
|
|
118
183
|
return appendTurnText(current, event.item.text);
|
|
119
184
|
}
|
|
185
|
+
/** Last top-level completed item: a non-empty assistant message is a visible tail; reasoning or a tool is silent even after earlier visible text (#544). An empty turn stays `none` and never nudges. */
|
|
186
|
+
export function trackTurnTail(tail, event) {
|
|
187
|
+
if (event.type !== 'item.completed' || event.item.parentItemId !== undefined)
|
|
188
|
+
return tail;
|
|
189
|
+
const item = event.item;
|
|
190
|
+
if (item.kind === 'message' && item.role === 'assistant' && item.text.trim() !== '')
|
|
191
|
+
return 'visible';
|
|
192
|
+
if (item.kind === 'reasoning' || item.kind === 'tool')
|
|
193
|
+
return 'silent';
|
|
194
|
+
return tail;
|
|
195
|
+
}
|
|
120
196
|
/** Strip a trailing marker from one text event so transcripts stay free of
|
|
121
197
|
* protocol noise. Delta backends may split the marker across events — then
|
|
122
198
|
* it stays visible; detection above is unaffected. */
|
|
@@ -166,15 +242,14 @@ function askMarkerRecovery(result) {
|
|
|
166
242
|
* they are hand-duplicated, and `AGENTS.md` warns that a lifecycle change
|
|
167
243
|
* applied to only one of them ships half a fix — the notes and their tones are
|
|
168
244
|
* exactly that kind of change. `enabled` is the caller's own precondition (the
|
|
169
|
-
* session is open
|
|
170
|
-
*
|
|
171
|
-
function resolveAskTurn(
|
|
245
|
+
* session is open and the turn is not a terminating `CEZ:DONE`); when false
|
|
246
|
+
* there is no marker to look for. Intermediate steps can ask too (#427). */
|
|
247
|
+
function resolveAskTurn(v1Result, v2Result, enabled) {
|
|
172
248
|
if (!enabled)
|
|
173
249
|
return { ask: null, notes: [] };
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
const result = v1Result.kind === 'none' ? parseAskMarkerResult(completedAssistantText) : v1Result;
|
|
250
|
+
// For unreconciled channels, v1 remains authoritative when it carries an
|
|
251
|
+
// ASK; a marker absent from v1 can still come from the complete v2 message.
|
|
252
|
+
const result = v1Result.kind === 'none' ? v2Result : v1Result;
|
|
178
253
|
const notes = [];
|
|
179
254
|
const rejection = askMarkerRejection(result);
|
|
180
255
|
if (rejection)
|
|
@@ -184,11 +259,57 @@ function resolveAskTurn(turnText, completedAssistantText, enabled) {
|
|
|
184
259
|
notes.push({ message: recovery, tone: 'danger' });
|
|
185
260
|
return { ask: result.kind === 'valid' ? result.request : null, notes };
|
|
186
261
|
}
|
|
187
|
-
/** A
|
|
262
|
+
/** A partial channel cannot keep an earlier gate alive when the other carries
|
|
263
|
+
* the same transcript plus later declarations. Compare raw lines in order,
|
|
264
|
+
* preserving fences/examples and ACKs between declarations. Neither channel
|
|
265
|
+
* always wins: v1 reconstructs omitted snapshots, v2 can retain refreshed ones.
|
|
266
|
+
* Incomparable transcripts keep the existing conservative channel policy. */
|
|
267
|
+
function orderedTurnText(v1, v2) {
|
|
268
|
+
if (!v1)
|
|
269
|
+
return v2;
|
|
270
|
+
if (!v2 || v1 === v2)
|
|
271
|
+
return v1;
|
|
272
|
+
const contains = (full, partial) => {
|
|
273
|
+
const lines = partial.split('\n');
|
|
274
|
+
let next = 0;
|
|
275
|
+
for (const line of full.split('\n')) {
|
|
276
|
+
if (line === lines[next])
|
|
277
|
+
next++;
|
|
278
|
+
}
|
|
279
|
+
return next === lines.length;
|
|
280
|
+
};
|
|
281
|
+
if (contains(v2, v1))
|
|
282
|
+
return v2;
|
|
283
|
+
if (contains(v1, v2))
|
|
284
|
+
return v1;
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
/** One turn-end decision for fresh and Continue, also retained until an input
|
|
288
|
+
* ACK settles. DONE and human/completion gates take precedence over a quiet
|
|
289
|
+
* declaration or live workers; a false pending-ASK flag alone proves nothing. */
|
|
290
|
+
function classifyTurnEnd(turnText, completedAssistantText, options) {
|
|
291
|
+
const complete = orderedTurnText(turnText, completedAssistantText);
|
|
292
|
+
const v1 = turnParkSignals(complete ?? turnText);
|
|
293
|
+
const v2 = complete === undefined ? turnParkSignals(completedAssistantText) : v1;
|
|
294
|
+
const done = options.interactive && options.sessionOpen && (v1.done || v2.done) &&
|
|
295
|
+
v1.askResult.kind === 'none' && v2.askResult.kind === 'none' && !v1.humanGate && !v2.humanGate;
|
|
296
|
+
const askTurn = resolveAskTurn(v1.askResult, v2.askResult, options.sessionOpen && !done);
|
|
297
|
+
// Ordinary markerless autonomous turns keep their existing nudge policy.
|
|
298
|
+
// Prose and rejected-ASK fallbacks override a quiet park this turn would
|
|
299
|
+
// otherwise enter. Recovery notes beside a valid card are not rejections.
|
|
300
|
+
const quietPark = v1.monitoring || v2.monitoring || options.scheduledWake || options.liveWorkers || options.dependencyWait;
|
|
301
|
+
const rejectedAsk = !askTurn.ask && askTurn.notes.length > 0;
|
|
302
|
+
const humanGate = !done && quietPark && (v1.humanGate || v2.humanGate || rejectedAsk);
|
|
303
|
+
const monitoring = options.interactive && options.sessionOpen && !done && !askTurn.ask &&
|
|
304
|
+
!options.pendingHumanAsk && !options.completionAttention && !humanGate &&
|
|
305
|
+
(v1.monitoring || v2.monitoring || options.scheduledWake || options.liveWorkers);
|
|
306
|
+
return { ...askTurn, done, monitoring, humanGate };
|
|
307
|
+
}
|
|
308
|
+
/** An active ASK marker — valid card or rejected payload — means the user
|
|
188
309
|
* must answer. Drop follow-ups queued mid-turn so they cannot start a new
|
|
189
310
|
* turn the moment idle fires (OpenCode serializes `sendMessage` that way). */
|
|
190
311
|
function discardQueuedMessagesOnAsk(session, outcome) {
|
|
191
|
-
if (outcome.ask || outcome.notes.length)
|
|
312
|
+
if (outcome.ask || outcome.notes.length || outcome.humanGate)
|
|
192
313
|
session?.discardQueuedMessages();
|
|
193
314
|
}
|
|
194
315
|
function isSyntheticContinuation(run, step) {
|
|
@@ -202,10 +323,7 @@ function isSyntheticContinuation(run, step) {
|
|
|
202
323
|
}
|
|
203
324
|
/** Periodic "cezar autosave" commit in the task worktree (spec 006). */
|
|
204
325
|
export const AUTOSAVE_INTERVAL_MS = 90_000;
|
|
205
|
-
|
|
206
|
-
export const CHECK_KILL_GRACE_MS = 3_000;
|
|
207
|
-
/** After SIGKILL, how long to wait for the group to disappear before resolving unconfirmed. */
|
|
208
|
-
export const CHECK_TERMINATION_CONFIRM_MS = 5_000;
|
|
326
|
+
export { CHECK_KILL_GRACE_MS, CHECK_TERMINATION_CONFIRM_MS } from './grouped-command.js';
|
|
209
327
|
/** The periodic autosave timer is opt-in (#471): off, a task branch carries only the
|
|
210
328
|
* agent's own commits plus the turn-end/pre-PR flushes — no mid-run "cezar autosave"
|
|
211
329
|
* noise interleaving PR history. The flushes (`autosaveCommit` at turn end and before
|
|
@@ -224,13 +342,23 @@ export function repositoryRootLockDisabled(env = process.env) {
|
|
|
224
342
|
return env.CEZ_DISABLE_REPO_LOCK === '1';
|
|
225
343
|
}
|
|
226
344
|
const REPOSITORY_ROOT_LOCK_DISABLED_NOTE = 'repository-root lock disabled by CEZ_DISABLE_REPO_LOCK=1 (shared checkout is unsafe)';
|
|
227
|
-
/** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever. */
|
|
228
345
|
/** #505: a real steer is read at the harness's next model step; a quiet boundary this long
|
|
229
346
|
* with input still unread means the harness will not read it without help. */
|
|
230
347
|
export const UNREAD_INPUT_GRACE_MS = 30_000;
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
348
|
+
/** #486: recheck a turn-end that deferred close/park only because the harness still held a human follow-up. */
|
|
349
|
+
export const HELD_HUMAN_INPUT_RECHECK_MS = 250;
|
|
350
|
+
function unreadInputUnconfirmedNote(count) {
|
|
351
|
+
return `delivered ${count} message${count === 1 ? '' : 's'}; the agent did not confirm reading ${count === 1 ? 'it' : 'them'}`;
|
|
352
|
+
}
|
|
353
|
+
/** Bound a final-message nudge that is ACKed but never produces a turn (#544).
|
|
354
|
+
* OpenCode heartbeats re-arm the managed-session no-progress guard, so this is
|
|
355
|
+
* the DEFAULT-path exit when the agent stays silent. Overridable on RunManager. */
|
|
356
|
+
export const FINAL_MESSAGE_NUDGE_REPLY_MS = 5 * 60_000;
|
|
357
|
+
/** Bound one unattended session; an explicit Continue starts a new budget. */
|
|
358
|
+
export const MAX_AUTO_CONTINUES = 40;
|
|
359
|
+
export const AUTONOMOUS_NUDGE = 'Continue working autonomously until the task is fully complete. Do not ask me for confirmation or clarification — make reasonable assumptions and proceed. When everything is done, end the session with your done signal.';
|
|
360
|
+
export const FINAL_MESSAGE_NUDGE = 'Your last turn ended without a message to the user. Write your user-facing reply now: what you did, where things stand, and anything you need from the user. If the task goal is fully achieved, end that message with CEZ:DONE.';
|
|
361
|
+
const MONITORING_WAKE_NUDGE = 'Re-check the downstream work you were monitoring. Continue toward the task goal. When the watched work is finished and the task goal is complete, end your final message with CEZ:DONE. If the watched work is still pending, end with CEZ:MONITORING. Never yield markerless for a monitoring wrap-up. If you genuinely need user input, follow the normal question path instead.';
|
|
234
362
|
/**
|
|
235
363
|
* Auto-resume after a provider usage limit (spec 2026-08-03-auto-resume-after-usage-limit).
|
|
236
364
|
*
|
|
@@ -439,6 +567,10 @@ const ORPHAN_PROBE_CACHE_MS = 2_000;
|
|
|
439
567
|
class WorkerOrphanAliveError extends Error {
|
|
440
568
|
}
|
|
441
569
|
const admissionError = (error, fallback) => error instanceof WorkerOrphanAliveError ? error.message : fallback;
|
|
570
|
+
/** The delegation family a run belongs to, by its root's id: a worker's parent, else itself. */
|
|
571
|
+
function familyOf(run) {
|
|
572
|
+
return run.delegation?.role === 'worker' ? run.delegation.parentRunId : run.id;
|
|
573
|
+
}
|
|
442
574
|
/**
|
|
443
575
|
* The mini workflow engine: executes a `WorkflowDef` against a repo, one step
|
|
444
576
|
* at a time, persisting every event to the RunStore (which the SSE endpoints
|
|
@@ -468,6 +600,8 @@ export class RunManager {
|
|
|
468
600
|
beginWorkerExecution(runId, admitted = true) {
|
|
469
601
|
if (this.store.getRun(runId)?.delegation?.role !== 'worker')
|
|
470
602
|
return;
|
|
603
|
+
if (this.reclaiming.has(runId))
|
|
604
|
+
throw new Error('Worker resources are being cleaned up');
|
|
471
605
|
// #469: one site for Continue, --resume, parent replies and queued revival after a crash.
|
|
472
606
|
// Fresh: admission is one-shot, so a cached "alive" must not refuse an orphan that has since died.
|
|
473
607
|
if (!this.executions.has(runId))
|
|
@@ -561,18 +695,17 @@ export class RunManager {
|
|
|
561
695
|
return { state: 'unknown' };
|
|
562
696
|
if (record !== 'absent' && isCurrentProcess(record.controller))
|
|
563
697
|
return { state: 'none' };
|
|
564
|
-
//
|
|
698
|
+
// Legacy execution proof also considers scratch holders; cleanup uses a separate strict proof.
|
|
699
|
+
const locations = agentTmpDirLocationEvidence(this.dataDir, runId);
|
|
565
700
|
return { state: 'orphan', generation: proof.generation, ...(record === 'absent' ? {} : { record }),
|
|
566
|
-
paths: [run.delegation.workspace.path, ...
|
|
567
|
-
// No process of this worker can predate its record (1 s slack for tick rounding).
|
|
568
|
-
...(Number.isFinite(Date.parse(run.createdAt)) ? { since: Date.parse(run.createdAt) - 1_000 } : {}) };
|
|
701
|
+
paths: [run.delegation.workspace.path, ...locations.paths], pathsComplete: locations.complete };
|
|
569
702
|
}
|
|
570
703
|
orphanedWorkerGeneration(runId, admitting = false) {
|
|
571
704
|
const orphan = this.orphanState(runId, admitting);
|
|
572
705
|
return orphan.state === 'orphan' ? orphan : undefined;
|
|
573
706
|
}
|
|
574
|
-
/**
|
|
575
|
-
*
|
|
707
|
+
/** Settles a crashed generation after exit proof. Sync and signal-free; recovery, resume,
|
|
708
|
+
* delivery, collect and destroy then take their normal paths.
|
|
576
709
|
* A non-gone probe is cached briefly: on darwin the scan is a synchronous `lsof` on the event loop. */
|
|
577
710
|
settleOrphanedWorkerExecution(runId, opts = {}) {
|
|
578
711
|
const orphan = this.orphanedWorkerGeneration(runId, opts.admitting);
|
|
@@ -581,7 +714,7 @@ export class RunManager {
|
|
|
581
714
|
const cached = this.orphanProbes.get(runId);
|
|
582
715
|
if (!opts.fresh && cached?.generation === orphan.generation && Date.now() - cached.at < ORPHAN_PROBE_CACHE_MS)
|
|
583
716
|
return false;
|
|
584
|
-
const probe =
|
|
717
|
+
const probe = inspectExecutionGeneration({ ...orphan, cwds: opts.cwds });
|
|
585
718
|
if (probe.liveness !== 'gone') {
|
|
586
719
|
this.orphanProbes.set(runId, { generation: orphan.generation, at: Date.now(), probe });
|
|
587
720
|
return false;
|
|
@@ -604,45 +737,70 @@ export class RunManager {
|
|
|
604
737
|
return true;
|
|
605
738
|
}
|
|
606
739
|
orphanProbes = new Map();
|
|
607
|
-
|
|
740
|
+
/** Pending orphan reprobes by run id. One timer serves them all (hearsay-tools/cezarion#879). */
|
|
741
|
+
orphanDue = new Map();
|
|
742
|
+
orphanTimer;
|
|
743
|
+
/** The `at` the timer was armed for; with the clock, it decides what a tick takes. */
|
|
744
|
+
orphanArmedFor = 0;
|
|
608
745
|
orphanBlockers = new Map();
|
|
609
746
|
reportedOrphanBlockers = new Map();
|
|
610
747
|
// Private and overridable so tests need not wait out production cadence.
|
|
611
|
-
|
|
612
|
-
orphanReprobeLimitMs = 15 * 60_000;
|
|
613
|
-
orphanReprobeSlowMs = 60_000;
|
|
748
|
+
orphanBackoff = ORPHAN_BACKOFF;
|
|
614
749
|
orphanTermGraceMs = 10_000;
|
|
615
750
|
/** #469: a survivor that dies after recovery has no other wake source (no exit callback for a
|
|
616
751
|
* process another cezar spawned). Unref'd; finalization's `run` event then lets worker waits and
|
|
617
|
-
* outcomes observe it.
|
|
618
|
-
* survivor is still noticed when it exits.
|
|
619
|
-
*
|
|
620
|
-
*
|
|
752
|
+
* outcomes observe it. Every 15 s for the first 15 minutes, then backing off to hourly
|
|
753
|
+
* (hearsay-tools/cezarion#879), so a long-lived survivor is still noticed when it exits. One
|
|
754
|
+
* timer serves every orphan, and a tick shares one `/proc` scan across them. A tick while this
|
|
755
|
+
* manager holds the run is skipped; only terminal reasons (proof settled, generation changed,
|
|
756
|
+
* run gone, unknown record, dispose) stop it. */
|
|
621
757
|
armOrphanReprobe(runId) {
|
|
622
758
|
const armed = this.orphanedWorkerGeneration(runId);
|
|
623
|
-
if (this.disposed || this.
|
|
624
|
-
return;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
759
|
+
if (this.disposed || this.orphanDue.has(runId) || !armed)
|
|
760
|
+
return;
|
|
761
|
+
this.orphanDue.set(runId, { generation: armed.generation, at: Date.now() + retryDelayMs(0, this.orphanBackoff, noJitter), attempts: 0 });
|
|
762
|
+
this.wakeOrphanReprobes();
|
|
763
|
+
}
|
|
764
|
+
wakeOrphanReprobes() {
|
|
765
|
+
clearTimeout(this.orphanTimer);
|
|
766
|
+
this.orphanTimer = undefined;
|
|
767
|
+
if (this.disposed || !this.orphanDue.size)
|
|
768
|
+
return;
|
|
769
|
+
let next = Infinity;
|
|
770
|
+
for (const { at } of this.orphanDue.values())
|
|
771
|
+
next = Math.min(next, at);
|
|
772
|
+
this.orphanArmedFor = next;
|
|
773
|
+
this.orphanTimer = setTimeout(() => this.orphanReprobeTick(), Math.max(0, next - Date.now()));
|
|
774
|
+
this.orphanTimer.unref?.();
|
|
775
|
+
}
|
|
776
|
+
orphanReprobeTick() {
|
|
777
|
+
this.orphanTimer = undefined;
|
|
778
|
+
if (this.disposed)
|
|
779
|
+
return;
|
|
780
|
+
// Orphans due within 5 % of the fast cadence ride along, so near neighbours share the scan, and
|
|
781
|
+
// every orphan this tick keeps is re-armed from the same instant, so they stay together.
|
|
782
|
+
const now = Date.now();
|
|
783
|
+
const dueBy = Math.max(this.orphanArmedFor, now) + this.orphanBackoff.fastMs / 20;
|
|
784
|
+
const cwds = sharedCwdScan();
|
|
785
|
+
for (const [runId, pending] of [...this.orphanDue]) {
|
|
786
|
+
if (pending.at > dueBy)
|
|
787
|
+
continue;
|
|
788
|
+
this.orphanDue.delete(runId);
|
|
789
|
+
const orphan = this.orphanState(runId);
|
|
790
|
+
let attempts = pending.attempts;
|
|
791
|
+
if (orphan.state !== 'busy') {
|
|
792
|
+
// Ticks are seconds apart, so the brief probe cache would only skip them: probe fresh.
|
|
793
|
+
if (orphan.state !== 'orphan' || orphan.generation !== pending.generation ||
|
|
794
|
+
this.settleOrphanedWorkerExecution(runId, { fresh: true, cwds }))
|
|
795
|
+
continue;
|
|
796
|
+
attempts++;
|
|
797
|
+
}
|
|
798
|
+
this.orphanDue.set(runId, { ...pending, attempts, at: now + retryDelayMs(attempts, this.orphanBackoff, noJitter) });
|
|
799
|
+
}
|
|
800
|
+
this.wakeOrphanReprobes();
|
|
640
801
|
}
|
|
641
802
|
clearOrphanReprobe(runId) {
|
|
642
|
-
|
|
643
|
-
if (timer)
|
|
644
|
-
clearTimeout(timer);
|
|
645
|
-
this.orphanReprobes.delete(runId);
|
|
803
|
+
this.orphanDue.delete(runId);
|
|
646
804
|
}
|
|
647
805
|
/** Why the last destroy could not prove termination (#469), and whether that differs from the
|
|
648
806
|
* reason last reported, so a retry loop appends one lifecycle event per distinct blocker set. */
|
|
@@ -683,29 +841,16 @@ export class RunManager {
|
|
|
683
841
|
// >= 500 ms: every probe may be a synchronous darwin `lsof`.
|
|
684
842
|
const pause = () => new Promise(resolve => setTimeout(resolve, Math.max(0, Math.min(500, deadline - Date.now()))));
|
|
685
843
|
// A live controller is another cezar's: nothing to signal or wait for. Otherwise signal only
|
|
686
|
-
// recorded, token-verified survivors (none for a legacy
|
|
687
|
-
//
|
|
844
|
+
// recorded, token-verified survivors and recorded session groups (none for a legacy
|
|
845
|
+
// record-less generation), then wait for readable cwd holders until the deadline.
|
|
688
846
|
if (!orphan.record || !recordedProcessLive(orphan.record.controller)) {
|
|
689
847
|
if (orphan.record) {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (processStartToken(entry.pid) === entry.startToken)
|
|
697
|
-
try {
|
|
698
|
-
process.kill(entry.pid, name);
|
|
699
|
-
}
|
|
700
|
-
catch { /* already gone */ }
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
signal('SIGTERM');
|
|
704
|
-
const killAt = Math.min(deadline, Date.now() + this.orphanTermGraceMs);
|
|
705
|
-
while (Date.now() < killAt && targets.some(recordedProcessLive))
|
|
706
|
-
await pause();
|
|
707
|
-
if (targets.some(recordedProcessLive))
|
|
708
|
-
signal('SIGKILL');
|
|
848
|
+
const { processes } = orphan.record;
|
|
849
|
+
await this.terminateRecorded({
|
|
850
|
+
pids: processes.filter(entry => entry.pgid === undefined && entry.startToken !== undefined && recordedProcessLive(entry)),
|
|
851
|
+
groups: processes.filter(entry => entry.pgid !== undefined),
|
|
852
|
+
paths: orphan.paths,
|
|
853
|
+
}, deadline, pause, same);
|
|
709
854
|
}
|
|
710
855
|
while (!this.settleOrphanedWorkerExecution(runId, { fresh: true }) && Date.now() < deadline && same())
|
|
711
856
|
await pause();
|
|
@@ -718,13 +863,70 @@ export class RunManager {
|
|
|
718
863
|
else if (probe?.pids.length)
|
|
719
864
|
this.orphanBlockers.set(runId, { kind: 'processes', pids: probe.pids });
|
|
720
865
|
}
|
|
866
|
+
/** Destroy only: SIGTERM recorded survivors, then SIGKILL what outlives `orphanTermGraceMs`.
|
|
867
|
+
* `pids` are signalled by token-verified pid; `groups` are session leaders whose whole process
|
|
868
|
+
* group is signalled (hearsay-tools/cezarion#890), so what an agent left behind exits with it. A
|
|
869
|
+
* dead leader's group counts only while a process holding `paths` is in it. Every signal
|
|
870
|
+
* re-verifies first: a reused PID or group number is never touched. */
|
|
871
|
+
async terminateRecorded(targets, deadline, pause, current = () => true) {
|
|
872
|
+
const holders = processesWithCwdUnder(targets.paths);
|
|
873
|
+
const holderGroups = () => (holders === 'unknown' ? [] : holders).map(processGroupOf).filter((pgid) => pgid !== undefined);
|
|
874
|
+
const groups = targets.groups.filter(entry => recordedGroupSignalable(entry, holderGroups()) && processGroupAlive(entry.pgid));
|
|
875
|
+
const living = () => targets.pids.some(recordedProcessLive) || groups.some(entry => processGroupAlive(entry.pgid));
|
|
876
|
+
const signal = (name) => {
|
|
877
|
+
for (const entry of targets.pids) {
|
|
878
|
+
if (!current())
|
|
879
|
+
return;
|
|
880
|
+
if (processStartToken(entry.pid) === entry.startToken)
|
|
881
|
+
try {
|
|
882
|
+
process.kill(entry.pid, name);
|
|
883
|
+
}
|
|
884
|
+
catch { /* already gone */ }
|
|
885
|
+
}
|
|
886
|
+
const evidence = holderGroups();
|
|
887
|
+
for (const entry of groups) {
|
|
888
|
+
if (!current())
|
|
889
|
+
return;
|
|
890
|
+
if (recordedGroupSignalable(entry, evidence))
|
|
891
|
+
signalProcessGroup(entry.pgid, name);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
if (!living())
|
|
895
|
+
return;
|
|
896
|
+
signal('SIGTERM');
|
|
897
|
+
const killAt = Math.min(deadline, Date.now() + this.orphanTermGraceMs);
|
|
898
|
+
while (Date.now() < killAt && living())
|
|
899
|
+
await pause();
|
|
900
|
+
if (living())
|
|
901
|
+
signal('SIGKILL');
|
|
902
|
+
}
|
|
903
|
+
/** Destroy of a finished generation (hearsay-tools/cezarion#890): a dev server or watcher its
|
|
904
|
+
* agent left in its process group holds the worktree, and nothing else ties it to the worker.
|
|
905
|
+
* A record whose controller is another live cezar is not ours to act on. */
|
|
906
|
+
async terminateFinishedWorkerGroups(runId, timeoutMs) {
|
|
907
|
+
const proof = this.store.readWorkerExecution(runId);
|
|
908
|
+
if (proof?.phase !== 'complete')
|
|
909
|
+
return;
|
|
910
|
+
const record = this.store.readWorkerProcesses(runId, proof.generation);
|
|
911
|
+
const run = this.store.getRun(runId);
|
|
912
|
+
if (typeof record === 'string' || run?.delegation?.role !== 'worker' ||
|
|
913
|
+
(recordedProcessLive(record.controller) && !isCurrentProcess(record.controller)))
|
|
914
|
+
return;
|
|
915
|
+
const groups = record.processes.filter(entry => entry.pgid !== undefined);
|
|
916
|
+
if (!groups.length)
|
|
917
|
+
return;
|
|
918
|
+
const deadline = Date.now() + Math.min(30_000, Math.max(0, Number.isFinite(timeoutMs) ? timeoutMs : 0));
|
|
919
|
+
const pause = () => new Promise(resolve => setTimeout(resolve, Math.max(0, Math.min(100, deadline - Date.now()))));
|
|
920
|
+
const paths = [run.delegation.workspace.path, ...agentTmpDirLocationEvidence(this.dataDir, runId).paths];
|
|
921
|
+
await this.terminateRecorded({ pids: [], groups, paths }, deadline, pause);
|
|
922
|
+
}
|
|
721
923
|
/** Best effort: a failed write leaves the working-directory scan as this process's evidence. */
|
|
722
924
|
recordWorkerProcess(runId, pid) {
|
|
723
925
|
const generation = this.executions.get(runId)?.generation;
|
|
724
926
|
if (!generation)
|
|
725
927
|
return;
|
|
726
928
|
try {
|
|
727
|
-
if (!this.store.appendWorkerProcess(runId, generation, pid))
|
|
929
|
+
if (!this.store.appendWorkerProcess(runId, generation, pid, sessionGroupOf(pid)))
|
|
728
930
|
console.warn(`[cez] worker ${runId} process record unavailable; relying on the working-directory scan`);
|
|
729
931
|
}
|
|
730
932
|
catch {
|
|
@@ -746,6 +948,12 @@ export class RunManager {
|
|
|
746
948
|
return { workerId: runId, state: !this.executions.has(runId) && this.store.readWorkerExecution(runId)?.phase === 'complete' ? 'terminated' : 'stopping' };
|
|
747
949
|
}
|
|
748
950
|
async awaitRunTermination(runId, timeoutMs, opts = {}) {
|
|
951
|
+
const terminated = await this.proveRunTermination(runId, timeoutMs, opts);
|
|
952
|
+
if (terminated && opts.reapOrphans && !this.disposed)
|
|
953
|
+
await this.terminateFinishedWorkerGroups(runId, timeoutMs);
|
|
954
|
+
return terminated;
|
|
955
|
+
}
|
|
956
|
+
async proveRunTermination(runId, timeoutMs, opts) {
|
|
749
957
|
const execution = this.executions.get(runId);
|
|
750
958
|
if (!execution) {
|
|
751
959
|
const generation = this.finalizedWorkers.get(runId);
|
|
@@ -767,25 +975,6 @@ export class RunManager {
|
|
|
767
975
|
});
|
|
768
976
|
return !this.executions.has(runId) && this.store.readWorkerExecution(runId)?.phase === 'complete';
|
|
769
977
|
}
|
|
770
|
-
/** Private cleanup capability, never serialized. Obtain after stop/termination
|
|
771
|
-
* under the service's destroy lock; each invocation rechecks the same generation. */
|
|
772
|
-
getWorkerNoMaterializationProof(runId) {
|
|
773
|
-
const initial = this.store.readWorkerExecution(runId);
|
|
774
|
-
const recorded = this.store.getRun(runId)?.delegation;
|
|
775
|
-
if (!initial?.neverMaterialized || initial.phase !== 'complete' || recorded?.role !== 'worker')
|
|
776
|
-
return undefined;
|
|
777
|
-
const workspace = { ...recorded.workspace };
|
|
778
|
-
const verify = candidate => {
|
|
779
|
-
const run = this.store.getRun(runId);
|
|
780
|
-
const current = this.store.readWorkerExecution(runId);
|
|
781
|
-
const delegation = run?.delegation;
|
|
782
|
-
return !this.disposed && !this.isActive(runId) && run?.status === 'cancelled' &&
|
|
783
|
-
delegation?.role === 'worker' && !!delegation.destroy &&
|
|
784
|
-
current?.generation === initial.generation && current.phase === 'complete' && current.neverMaterialized === true &&
|
|
785
|
-
Object.keys(workspace).every(key => candidate[key] === workspace[key] && delegation.workspace[key] === workspace[key]);
|
|
786
|
-
};
|
|
787
|
-
return verify(workspace) ? verify : undefined;
|
|
788
|
-
}
|
|
789
978
|
/** Irreversible history removal is an execution tombstone, including for legacy children. */
|
|
790
979
|
historyDeletionPending(runId) {
|
|
791
980
|
const run = this.store.getRun(runId);
|
|
@@ -816,6 +1005,14 @@ export class RunManager {
|
|
|
816
1005
|
ciRegistrations = new Map();
|
|
817
1006
|
ciRetries = new Map();
|
|
818
1007
|
ciResources;
|
|
1008
|
+
/** #781: the workspace-wide preview host. Absent (`cez run`, tests) means every registration is `headless`. */
|
|
1009
|
+
preview;
|
|
1010
|
+
/** The same host, for a removal outside this manager that must release a run's preview first. */
|
|
1011
|
+
get previewHost() {
|
|
1012
|
+
return this.preview;
|
|
1013
|
+
}
|
|
1014
|
+
/** #781: cezar's own listening port, which no dev server may register. */
|
|
1015
|
+
cezarPort;
|
|
819
1016
|
ciSupervisor;
|
|
820
1017
|
workerWaiting = new Set();
|
|
821
1018
|
workerWakeAdmitted = new Set();
|
|
@@ -825,13 +1022,40 @@ export class RunManager {
|
|
|
825
1022
|
reconcilingWorkers = false;
|
|
826
1023
|
reconcilingFamily;
|
|
827
1024
|
pendingReconciliations = new Set();
|
|
828
|
-
|
|
1025
|
+
/** What the recovery in progress covers: every run at boot (`family` undefined), or the one
|
|
1026
|
+
* family an adoption hands it (#779, plan step 3). Undefined: no recovery in progress. */
|
|
1027
|
+
recoveryScope;
|
|
1028
|
+
/** Settles when the recovery in progress (if any) has finished. */
|
|
1029
|
+
recoveryInFlight;
|
|
1030
|
+
/** Adoptions of dead processes' runs, one at a time (`adoptOrphanedRun`). */
|
|
1031
|
+
adoptionTail = Promise.resolve();
|
|
829
1032
|
/** Quiet window before unread input is resubmitted or left unconfirmed (#505). */
|
|
830
1033
|
unreadInputGraceMs;
|
|
1034
|
+
/** Quiet window after a final-message nudge before a still-silent session parks (#544). */
|
|
1035
|
+
finalMessageNudgeReplyMs;
|
|
831
1036
|
disposed = false;
|
|
1037
|
+
/** Whether any recovery is in progress: one at a time (`recover`), and `pump` waits for it. */
|
|
1038
|
+
get recovering() {
|
|
1039
|
+
return this.recoveryScope !== undefined;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Whether a recovery in progress holds this run, so its wakes, input flushes and delegation
|
|
1043
|
+
* reconciles wait for that recovery's own passes: every run during boot recovery, and only the
|
|
1044
|
+
* adopted family's during a recovery scoped to one. The rest of a running project stays live: a
|
|
1045
|
+
* wake held back for an unrelated family would be lost, since the scoped passes never revisit it.
|
|
1046
|
+
*/
|
|
1047
|
+
recoveringRun(run) {
|
|
1048
|
+
const scope = this.recoveryScope;
|
|
1049
|
+
if (!scope)
|
|
1050
|
+
return false;
|
|
1051
|
+
if (scope.family === undefined)
|
|
1052
|
+
return true;
|
|
1053
|
+
const record = typeof run === 'string' ? this.store.getRun(run) : run;
|
|
1054
|
+
return record !== undefined && familyOf(record) === scope.family;
|
|
1055
|
+
}
|
|
832
1056
|
onDelegationRun = (run, source) => {
|
|
833
1057
|
// Delegation metadata checkpoints do not change task status.
|
|
834
|
-
if (!this.disposed && source
|
|
1058
|
+
if (!this.disposed && source === undefined)
|
|
835
1059
|
this.reapTerminalScratch(run.id);
|
|
836
1060
|
if (!this.disposed && !['queued', 'running', 'waiting'].includes(run.status))
|
|
837
1061
|
this.withdrawCiWait(run.id);
|
|
@@ -839,7 +1063,10 @@ export class RunManager {
|
|
|
839
1063
|
this.active.get(run.id)?.revokeDelegation?.();
|
|
840
1064
|
// Cleanup checkpoints emit terminal records too. With delegation disabled,
|
|
841
1065
|
// these observations must not replay the project's conversation histories.
|
|
842
|
-
if (this.disposed || this.
|
|
1066
|
+
if (this.disposed || this.recoveringRun(run) || (source === 'delegation-checkpoint' && process.env.CEZ_DELEGATION !== '1'))
|
|
1067
|
+
return;
|
|
1068
|
+
// A destroy retry changes nothing the family reconcile reads (#880).
|
|
1069
|
+
if (source === 'delegation-destroy-progress')
|
|
843
1070
|
return;
|
|
844
1071
|
if (run.delegation && run.delegation.role !== 'invalid' && !['queued', 'running', 'waiting'].includes(run.status)) {
|
|
845
1072
|
this.reconcileWorkerWaits(run.delegation.role === 'root' ? run.id : run.delegation.parentRunId);
|
|
@@ -850,6 +1077,10 @@ export class RunManager {
|
|
|
850
1077
|
* explicit user Continue, these are bulk scheduler work and must re-enter
|
|
851
1078
|
* through `pump()` so both workspace and per-project caps are honored. */
|
|
852
1079
|
pendingContinuations = new Map();
|
|
1080
|
+
/** Accepted Continues supersede in-flight settlement even before launch or after
|
|
1081
|
+
* idle close. Retain these across dropActive/dispose so absence cannot look like
|
|
1082
|
+
* the earlier execution again; disposal alone never revokes terminal intent. */
|
|
1083
|
+
continuationGenerations = new Map();
|
|
853
1084
|
/** Per-run image counter behind `pasted-<n>` / `screenshot-<n>` (#472). Lives on
|
|
854
1085
|
* the manager rather than the `ActiveRun` so a *queued* run — which has no
|
|
855
1086
|
* `ActiveRun` at all — can persist attachments. Seeded lazily from disk. */
|
|
@@ -904,7 +1135,11 @@ export class RunManager {
|
|
|
904
1135
|
this.store = store;
|
|
905
1136
|
this.repoRoot = repoRoot;
|
|
906
1137
|
this.dataDir = join(repoRoot, '.ai/cezar');
|
|
1138
|
+
this.workerScratchCleanup = new WorkerScratchCleanup(store, this.dataDir, id => this.isActive(id) || this.executions.has(id) || this.reclaiming.has(id));
|
|
1139
|
+
this.preview = options.preview;
|
|
1140
|
+
this.cezarPort = options.cezarPort;
|
|
907
1141
|
this.unreadInputGraceMs = options.unreadInputGraceMs ?? UNREAD_INPUT_GRACE_MS;
|
|
1142
|
+
this.finalMessageNudgeReplyMs = options.finalMessageNudgeReplyMs ?? FINAL_MESSAGE_NUDGE_REPLY_MS;
|
|
908
1143
|
this.semaphore = options.semaphore ?? new WorkspaceSemaphore();
|
|
909
1144
|
this.ciResources = acquireCiResources(this.semaphore);
|
|
910
1145
|
this.ciSupervisor = this.ciResources.supervisor;
|
|
@@ -933,6 +1168,7 @@ export class RunManager {
|
|
|
933
1168
|
*/
|
|
934
1169
|
dispose() {
|
|
935
1170
|
this.disposed = true;
|
|
1171
|
+
this.workerScratchCleanup.pause();
|
|
936
1172
|
for (const task of this.ciWatches.values())
|
|
937
1173
|
task.abort.abort();
|
|
938
1174
|
this.ciWatches.clear();
|
|
@@ -945,8 +1181,9 @@ export class RunManager {
|
|
|
945
1181
|
this.ciResources.release();
|
|
946
1182
|
this.delegationProvisioner = undefined;
|
|
947
1183
|
this.finalizedWorkers.clear();
|
|
948
|
-
|
|
949
|
-
|
|
1184
|
+
this.orphanDue.clear();
|
|
1185
|
+
clearTimeout(this.orphanTimer);
|
|
1186
|
+
this.orphanTimer = undefined;
|
|
950
1187
|
this.orphanProbes.clear();
|
|
951
1188
|
this.orphanBlockers.clear();
|
|
952
1189
|
this.reportedOrphanBlockers.clear();
|
|
@@ -1209,10 +1446,77 @@ export class RunManager {
|
|
|
1209
1446
|
const controller = await this.ciResources.controller();
|
|
1210
1447
|
if (!controller || this.disposed || state.cancelled || this.active.get(runId) !== state)
|
|
1211
1448
|
return;
|
|
1212
|
-
const provisioned = controller.provision((request, signal) => this.registerCiWait(runId, request, generation, signal));
|
|
1449
|
+
const provisioned = controller.provision((request, signal) => this.registerCiWait(runId, request, generation, signal), (request, signal) => this.registerPreviewServer(runId, request, signal), (request, signal) => this.stopPreviewServer(runId, request, signal));
|
|
1213
1450
|
state.revokeCiTools = provisioned.revoke;
|
|
1214
1451
|
return provisioned;
|
|
1215
1452
|
}
|
|
1453
|
+
async stopPreviewServer(runId, request, signal) {
|
|
1454
|
+
const parsed = previewStopRequestSchema.parse(request);
|
|
1455
|
+
if (!previewToolEnabled())
|
|
1456
|
+
return previewStopResult('preview_disabled');
|
|
1457
|
+
if (!this.preview)
|
|
1458
|
+
return previewStopResult('headless');
|
|
1459
|
+
if (this.disposed || signal?.aborted)
|
|
1460
|
+
return previewStopResult('unavailable');
|
|
1461
|
+
const run = this.store.getRun(runId);
|
|
1462
|
+
if (!run?.worktreePath || !existsSync(run.worktreePath))
|
|
1463
|
+
return previewStopResult('worktree_missing');
|
|
1464
|
+
const owner = this.preview.portOwner(parsed.port);
|
|
1465
|
+
if (owner && owner.runId !== runId)
|
|
1466
|
+
return previewStopResult('port_held');
|
|
1467
|
+
if (!run.previewServers?.some(server => server.port === parsed.port))
|
|
1468
|
+
return previewStopResult('not_registered');
|
|
1469
|
+
return this.preview.stopPreview(runId, parsed, signal);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* #781: record a dev server the agent registered. One probe at registration becomes
|
|
1473
|
+
* `answeredAtRegistration`; the run is re-validated after it, since the probe awaits.
|
|
1474
|
+
*/
|
|
1475
|
+
async registerPreviewServer(runId, request, signal) {
|
|
1476
|
+
const parsed = previewServeRequestSchema.parse(request);
|
|
1477
|
+
const validate = () => {
|
|
1478
|
+
const run = this.store.getRun(runId);
|
|
1479
|
+
const owner = this.preview?.portOwner(parsed.port);
|
|
1480
|
+
const result = validateRegistration({
|
|
1481
|
+
request: parsed,
|
|
1482
|
+
worktreePath: run?.worktreePath && existsSync(run.worktreePath) ? run.worktreePath : undefined,
|
|
1483
|
+
cezarPort: this.cezarPort?.(),
|
|
1484
|
+
existing: run?.previewServers ?? [],
|
|
1485
|
+
owner,
|
|
1486
|
+
runId,
|
|
1487
|
+
enabled: previewToolEnabled(),
|
|
1488
|
+
headless: !this.preview,
|
|
1489
|
+
});
|
|
1490
|
+
const withoutWorktree = !!run && !run.worktreePath;
|
|
1491
|
+
return { ...result, answer: () => previewResult(result.code, { ...parsed, ownerTitle: owner?.title, withoutWorktree }) };
|
|
1492
|
+
};
|
|
1493
|
+
const first = validate();
|
|
1494
|
+
if (!first.server || !this.preview)
|
|
1495
|
+
return first.answer();
|
|
1496
|
+
// A server process supplies `cezarPort`, but recovered runs can call before the cockpit binds:
|
|
1497
|
+
// until it knows its port, `cezar_port` cannot be checked, so the tool asks for a retry instead.
|
|
1498
|
+
if (this.cezarPort && this.cezarPort() === undefined)
|
|
1499
|
+
return previewResult('unavailable', parsed);
|
|
1500
|
+
const answered = await this.preview.probe(parsed.port).catch(() => false);
|
|
1501
|
+
// A session revoked during the probe (cancel, stop) must leave no card behind.
|
|
1502
|
+
signal?.throwIfAborted();
|
|
1503
|
+
const checked = validate();
|
|
1504
|
+
if (!checked.server)
|
|
1505
|
+
return checked.answer();
|
|
1506
|
+
const server = { ...checked.server, answeredAtRegistration: answered };
|
|
1507
|
+
const existing = this.store.getRun(runId)?.previewServers ?? [];
|
|
1508
|
+
const previous = existing.find(entry => entry.port === server.port);
|
|
1509
|
+
const changed = !!previous && (previous.command !== server.command || previous.cwd !== server.cwd || previous.path !== server.path);
|
|
1510
|
+
const previewServers = checked.code === 'replaced'
|
|
1511
|
+
? existing.map(entry => entry.port === server.port ? server : entry)
|
|
1512
|
+
: [...existing, server];
|
|
1513
|
+
this.store.updateRun(runId, { previewServers });
|
|
1514
|
+
this.store.appendEvent(runId, { type: 'preview.server-registered', server });
|
|
1515
|
+
// A copy cezar runs from the old command must not keep answering for the new registration.
|
|
1516
|
+
if (changed)
|
|
1517
|
+
await this.preview.replaced(runId, server.port).catch(() => { });
|
|
1518
|
+
return checked.answer();
|
|
1519
|
+
}
|
|
1216
1520
|
/** Trusted capability callbacks supply run identity; the model supplies only a PR and deadline. */
|
|
1217
1521
|
async registerCiWait(runId, request, generation, capabilitySignal) {
|
|
1218
1522
|
const parsed = ciWaitRequestSchema.parse(request);
|
|
@@ -1390,7 +1694,7 @@ export class RunManager {
|
|
|
1390
1694
|
queueCiWake(runId) {
|
|
1391
1695
|
const run = this.store.getRun(runId);
|
|
1392
1696
|
const wait = run?.ciWait;
|
|
1393
|
-
if (this.disposed || this.
|
|
1697
|
+
if (this.disposed || this.recoveringRun(run) || !run || !wait?.result || !wait.wakeId ||
|
|
1394
1698
|
!['running', 'waiting', 'queued'].includes(run.status) || run.stopping ||
|
|
1395
1699
|
this.hasPendingHumanAsk(runId) || this.workerExecutionStopped(runId) || this.executionBlockedByRootFinish(run))
|
|
1396
1700
|
return;
|
|
@@ -1693,6 +1997,7 @@ export class RunManager {
|
|
|
1693
1997
|
* root (spec 006 degradation rule), which is always the tighter bound.
|
|
1694
1998
|
*/
|
|
1695
1999
|
async pump() {
|
|
2000
|
+
// Any recovery, scoped or not: each ends with a pump of its own, so nothing waits for long.
|
|
1696
2001
|
if (this.disposed || this.recovering)
|
|
1697
2002
|
return;
|
|
1698
2003
|
this.reconcileMonitoringWakeTimers();
|
|
@@ -2004,13 +2309,25 @@ export class RunManager {
|
|
|
2004
2309
|
* - `running` → mark interrupted, then immediately resume the last agent
|
|
2005
2310
|
* session via the Continue path, pointing the agent at its handoff file.
|
|
2006
2311
|
* Call once, before the server starts taking requests.
|
|
2312
|
+
*
|
|
2313
|
+
* With `familyRootId`, the same recovery for the one delegation family a dead process left
|
|
2314
|
+
* behind, which `adoptOrphanedRun` has just loaded. Either way only runs this store owns are
|
|
2315
|
+
* touched: another live process's runs are not in the live set, and the passes that read every
|
|
2316
|
+
* worker or root skip their families (#779, plan step 3).
|
|
2007
2317
|
*/
|
|
2008
|
-
async recover() {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2318
|
+
async recover(familyRootId) {
|
|
2319
|
+
// A store that could not open (#779) knows none of its runs: recovering it would read "none".
|
|
2320
|
+
if (this.disposed || this.recovering || this.store.unavailable)
|
|
2321
|
+
return;
|
|
2322
|
+
this.recoveryScope = { ...(familyRootId === undefined ? {} : { family: familyRootId }) };
|
|
2323
|
+
let settled;
|
|
2324
|
+
this.recoveryInFlight = new Promise((resolve) => { settled = resolve; });
|
|
2325
|
+
const inScope = (run) => familyRootId === undefined || familyOf(run) === familyRootId;
|
|
2012
2326
|
try {
|
|
2013
|
-
|
|
2327
|
+
// Recovery reads the live set (#779): `listRuns()` is the runs `open` loaded by the `live`
|
|
2328
|
+
// column, and finished runs have nothing to recover. The worker passes below go through
|
|
2329
|
+
// indexed queries instead.
|
|
2330
|
+
for (const run of this.store.listRuns().filter(inScope)) {
|
|
2014
2331
|
this.store.clearExpiredInboxClaims(run.id, new Date().toISOString());
|
|
2015
2332
|
this.armInboxClaimExpiry(run.id);
|
|
2016
2333
|
// #505: the session that accepted this input died before the model read it.
|
|
@@ -2023,26 +2340,29 @@ export class RunManager {
|
|
|
2023
2340
|
}
|
|
2024
2341
|
// #469: a generation whose processes died with the old controller is finalized first, so it
|
|
2025
2342
|
// re-launches through the ordinary Continue path; live task scratch is retained.
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2343
|
+
// Any worker, finished or not, may hold a generation a dead controller left `starting`. Its
|
|
2344
|
+
// private proof is read first, so only those workers' records are decoded: settling and
|
|
2345
|
+
// re-probing are no-ops for every other proof phase.
|
|
2346
|
+
// Another process's workers are its own to settle.
|
|
2347
|
+
const workerIds = familyRootId === undefined ? this.store.listWorkerIds() : this.store.listWorkersOf(familyRootId).map((run) => run.id);
|
|
2348
|
+
for (const id of workerIds) {
|
|
2349
|
+
if (this.store.readWorkerExecution(id)?.phase === 'starting' && !this.store.writeRefusal(id) &&
|
|
2350
|
+
!this.settleOrphanedWorkerExecution(id))
|
|
2351
|
+
this.armOrphanReprobe(id);
|
|
2352
|
+
}
|
|
2353
|
+
this.reconcileWorkerWaits(familyRootId);
|
|
2030
2354
|
const live = this.store
|
|
2031
2355
|
.listRuns()
|
|
2032
|
-
.filter((r) => ['queued', 'waiting', 'running'].includes(r.status))
|
|
2356
|
+
.filter((r) => inScope(r) && ['queued', 'waiting', 'running'].includes(r.status))
|
|
2033
2357
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
// record may still own a process even while this manager is recovering.
|
|
2043
|
-
// Idle-close ends a process, not its task. Preserve all live task scratch,
|
|
2044
|
-
// including workers whose previous process generation has completed.
|
|
2045
|
-
sweepAgentTmpDirs(this.dataDir, [...live, ...retained].map(run => run.id));
|
|
2358
|
+
// Scratch cleanup discovers private evidence independently of the salvageable run index.
|
|
2359
|
+
// Terminal worker cleanup/retries and the generic orphan sweep share this boundary. An
|
|
2360
|
+
// adopted family only schedules its own members: the sweep belongs to restart.
|
|
2361
|
+
if (familyRootId === undefined)
|
|
2362
|
+
this.workerScratchCleanup.recover();
|
|
2363
|
+
else
|
|
2364
|
+
for (const run of this.familyMembers(familyRootId))
|
|
2365
|
+
this.workerScratchCleanup.schedule(run.id);
|
|
2046
2366
|
for (const run of live) {
|
|
2047
2367
|
if (this.isActive(run.id))
|
|
2048
2368
|
continue;
|
|
@@ -2066,6 +2386,13 @@ export class RunManager {
|
|
|
2066
2386
|
continue;
|
|
2067
2387
|
}
|
|
2068
2388
|
if (run.delegation && run.delegation.role !== 'invalid' && this.hasPendingHumanAsk(run.id)) {
|
|
2389
|
+
// A crash between the prose checkpoint and wait withdrawal must not
|
|
2390
|
+
// recover the superseded wait as an autonomous wake.
|
|
2391
|
+
const gateSeq = this.pendingHumanAskSeq(run.id);
|
|
2392
|
+
if (this.store.readEvents(run.id).some(event => event.seq === gateSeq && event.type === 'note' && event.code === PROSE_HUMAN_GATE)) {
|
|
2393
|
+
this.withdrawCiWait(run.id);
|
|
2394
|
+
this.withdrawWorkerWait(run.id);
|
|
2395
|
+
}
|
|
2069
2396
|
this.store.updateRun(run.id, { status: 'waiting', activity: undefined });
|
|
2070
2397
|
// Only the durable, explicitly human payload may resume an unanswered
|
|
2071
2398
|
// question. A refused recovery leaves attention, not terminal failure.
|
|
@@ -2147,6 +2474,18 @@ export class RunManager {
|
|
|
2147
2474
|
});
|
|
2148
2475
|
const interruptedIndex = run.workflowDef?.steps.findIndex(step => step.id === interruptedStepId) ?? -1;
|
|
2149
2476
|
const interruptedDef = interruptedIndex >= 0 ? run.workflowDef?.steps[interruptedIndex] : undefined;
|
|
2477
|
+
const requeueInput = () => ({ task: run.task, runner: run.runner, model: run.model, effort: run.effort,
|
|
2478
|
+
agentProfile: run.agentProfile, systemPrompt: run.systemPrompt, autonomous: run.autonomous,
|
|
2479
|
+
generateFollowups: run.generateFollowups, worktree: run.worktree });
|
|
2480
|
+
// Worktree setup (#917) interrupted before any agent session: there is no session to
|
|
2481
|
+
// continue, so start the workflow over. `execute` sees setup still `running` and reruns it.
|
|
2482
|
+
if (run.workflowDef && run.worktreeSetup?.status === 'running' && !run.steps.some(step => step.sessionId)) {
|
|
2483
|
+
this.store.updateRun(run.id, { status: 'queued', error: undefined, finishedAt: undefined, currentStepId: undefined });
|
|
2484
|
+
this.store.flush();
|
|
2485
|
+
this.pendingJobs.set(run.id, { workflow: run.workflowDef, startAt: 0, input: requeueInput() });
|
|
2486
|
+
this.queue.push(run.id);
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2150
2489
|
if (run.workflowDef && interruptedDef && stepKind(interruptedDef) === 'check') {
|
|
2151
2490
|
this.store.updateStep(run.id, interruptedDef.id, { status: 'pending', error: undefined, finishedAt: undefined });
|
|
2152
2491
|
this.store.updateRun(run.id, { status: 'queued', error: undefined, finishedAt: undefined, currentStepId: undefined });
|
|
@@ -2154,9 +2493,7 @@ export class RunManager {
|
|
|
2154
2493
|
this.pendingJobs.set(run.id, {
|
|
2155
2494
|
workflow: run.workflowDef,
|
|
2156
2495
|
startAt: interruptedIndex,
|
|
2157
|
-
input:
|
|
2158
|
-
agentProfile: run.agentProfile, systemPrompt: run.systemPrompt, autonomous: run.autonomous,
|
|
2159
|
-
generateFollowups: run.generateFollowups, worktree: run.worktree },
|
|
2496
|
+
input: requeueInput(),
|
|
2160
2497
|
});
|
|
2161
2498
|
this.queue.push(run.id);
|
|
2162
2499
|
continue;
|
|
@@ -2177,17 +2514,27 @@ export class RunManager {
|
|
|
2177
2514
|
// one — see `reconcileAutoResumes`.
|
|
2178
2515
|
}
|
|
2179
2516
|
finally {
|
|
2180
|
-
this.
|
|
2517
|
+
this.recoveryScope = undefined;
|
|
2518
|
+
this.recoveryInFlight = undefined;
|
|
2519
|
+
settled();
|
|
2181
2520
|
}
|
|
2182
2521
|
// A question a crash caught before or after its commit to the parent still gets routed (#505).
|
|
2183
|
-
|
|
2522
|
+
// Only a family with a live member has anyone left to route to or fall back from.
|
|
2523
|
+
const families = familyRootId === undefined ? this.liveFamilyMembers() : this.familyMembers(familyRootId);
|
|
2524
|
+
for (const run of families)
|
|
2184
2525
|
if (run.delegation?.role === 'worker')
|
|
2185
2526
|
this.routeWorkerQuestion(run.id);
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2527
|
+
if (familyRootId === undefined)
|
|
2528
|
+
this.reconcileAllWorkerFamilies();
|
|
2529
|
+
else
|
|
2530
|
+
this.reconcileWorkerWaits(familyRootId);
|
|
2531
|
+
// A parent reply accepted just before the crash still answers its worker (#505), and only a
|
|
2532
|
+
// live worker takes one.
|
|
2533
|
+
for (const run of this.store.listRuns().filter(inScope))
|
|
2189
2534
|
if (run.delegation?.role === 'worker')
|
|
2190
2535
|
this.answerRoutedQuestion(run.id);
|
|
2536
|
+
// The rest re-arms wakes from durable state and is idempotent, so it runs for every live run
|
|
2537
|
+
// even after one adopted family: anything the recovery window held back goes out now.
|
|
2191
2538
|
for (const run of this.store.listRuns()) {
|
|
2192
2539
|
if (run.ciWait && !['running', 'waiting', 'queued'].includes(run.status))
|
|
2193
2540
|
this.withdrawCiWait(run.id);
|
|
@@ -2197,6 +2544,33 @@ export class RunManager {
|
|
|
2197
2544
|
this.reconcileAutoResumes();
|
|
2198
2545
|
void this.pump();
|
|
2199
2546
|
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Take over a run whose owning process is gone (#779, plan step 3): a control (Stop, Continue,
|
|
2549
|
+
* archive…) reached a run `runOwnership` calls `orphaned`. Its family is claimed, its live runs
|
|
2550
|
+
* are settled rather than resumed (`settleOrphanedRun`: interrupted, or cancelled before they
|
|
2551
|
+
* began), and the family is recovered as a restart would, which repairs it but finds nothing live
|
|
2552
|
+
* to resume. The control that follows then acts on a run this process now owns — rather than on
|
|
2553
|
+
* a record nobody owns, a dead end until a restart — and only Continue starts agent work, with
|
|
2554
|
+
* the user's own input. With `stop` the control is Stop, which the adoption carries out itself:
|
|
2555
|
+
* `runId` ends cancelled, as an accepted Stop reads, rather than interrupted. False when a live
|
|
2556
|
+
* process holds it after all (or it is gone): the control is refused as another process's.
|
|
2557
|
+
* Adoptions run one at a time, after any recovery already in progress.
|
|
2558
|
+
*/
|
|
2559
|
+
adoptOrphanedRun(runId, opts = {}) {
|
|
2560
|
+
const adoption = this.adoptionTail.then(async () => {
|
|
2561
|
+
while (this.recovering && this.recoveryInFlight)
|
|
2562
|
+
await this.recoveryInFlight;
|
|
2563
|
+
if (this.disposed)
|
|
2564
|
+
return false;
|
|
2565
|
+
const family = this.store.adoptFamily(runId, opts);
|
|
2566
|
+
if (family === undefined)
|
|
2567
|
+
return false;
|
|
2568
|
+
await this.recover(family);
|
|
2569
|
+
return true;
|
|
2570
|
+
});
|
|
2571
|
+
this.adoptionTail = adoption.catch(() => undefined);
|
|
2572
|
+
return adoption;
|
|
2573
|
+
}
|
|
2200
2574
|
/** The persisted definition when it looks sane, else the catalog by name. */
|
|
2201
2575
|
async reviveWorkflow(run) {
|
|
2202
2576
|
// "Looks sane" is the STORE's job now: it parses `workflowDef` against the definition schema
|
|
@@ -2216,6 +2590,17 @@ export class RunManager {
|
|
|
2216
2590
|
preserveRunAfterDisposal(runId, state) {
|
|
2217
2591
|
return this.isDisposedDelegatedRun(runId) && !state.cancelled && !state.finishRequested;
|
|
2218
2592
|
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Register an executing run: the ONE way an `ActiveRun` enters `active`. Both construction
|
|
2595
|
+
* sites (`execute` and `runContinuation`, which serves Continue and restart recovery) call it,
|
|
2596
|
+
* so the store pins every run this manager executes (#779) — its record stays the held object
|
|
2597
|
+
* every turn-end handler reads and writes until `dropActive` lets it go. A second site that set
|
|
2598
|
+
* `active` directly would execute a run the store could evict mid-turn (the #811 shape).
|
|
2599
|
+
*/
|
|
2600
|
+
activate(runId, state) {
|
|
2601
|
+
this.active.set(runId, state);
|
|
2602
|
+
this.store.pin(runId, 'active');
|
|
2603
|
+
}
|
|
2219
2604
|
/** Remove a run from the live registries — keeps `waiting ⊆ active`. */
|
|
2220
2605
|
dropActive(runId) {
|
|
2221
2606
|
const state = this.active.get(runId);
|
|
@@ -2225,6 +2610,7 @@ export class RunManager {
|
|
|
2225
2610
|
state.releaseRepoRoot = undefined;
|
|
2226
2611
|
this.clearIdleTimer(state);
|
|
2227
2612
|
this.clearAutosaveTimer(state);
|
|
2613
|
+
this.clearHeldHumanInputTimer(state);
|
|
2228
2614
|
}
|
|
2229
2615
|
this.waiting.delete(runId);
|
|
2230
2616
|
this.monitoring.delete(runId);
|
|
@@ -2239,6 +2625,7 @@ export class RunManager {
|
|
|
2239
2625
|
if (state)
|
|
2240
2626
|
this.clearMonitoringWakeTimer(state, runId);
|
|
2241
2627
|
this.active.delete(runId);
|
|
2628
|
+
this.store.unpin(runId, 'active');
|
|
2242
2629
|
if (!this.disposed && this.store.getRun(runId)?.ciWait)
|
|
2243
2630
|
this.queueCiWake(runId);
|
|
2244
2631
|
if (!this.executions.has(runId) && this.store.getRun(runId)?.stopping)
|
|
@@ -2261,9 +2648,16 @@ export class RunManager {
|
|
|
2261
2648
|
// left a window — measured as exactly one extra task — where the queue saw a free slot and
|
|
2262
2649
|
// an account that looked healthy, and started work that was already doomed.
|
|
2263
2650
|
this.scheduleAutoResumeIfLimited(runId);
|
|
2264
|
-
// Closed-session success may have parked a completion wait without a live wire.
|
|
2265
|
-
|
|
2651
|
+
// Closed-session success may have parked a completion wait without a live wire. The global
|
|
2652
|
+
// pass covers the families still in memory (#779); this run's own family may have just left
|
|
2653
|
+
// the live set with it, so it is reconciled by name first.
|
|
2654
|
+
const settled = this.store.getRun(runId);
|
|
2655
|
+
if (settled?.delegation) {
|
|
2656
|
+
const familyRoot = settled.delegation.role === 'root' ? settled.id : settled.delegation.role === 'worker' ? settled.delegation.parentRunId : undefined;
|
|
2657
|
+
if (familyRoot !== undefined)
|
|
2658
|
+
this.reconcileWorkerWaits(familyRoot);
|
|
2266
2659
|
this.reconcileWorkerWaits();
|
|
2660
|
+
}
|
|
2267
2661
|
this.releaseSlot();
|
|
2268
2662
|
// A run leaving the active registry may have reached a terminal transition
|
|
2269
2663
|
// (done/review/failed/cancelled), increasing the finished-worktree count.
|
|
@@ -2273,6 +2667,18 @@ export class RunManager {
|
|
|
2273
2667
|
void this.enforceRetention();
|
|
2274
2668
|
this.reapTerminalScratch(runId);
|
|
2275
2669
|
}
|
|
2670
|
+
workerScratchCleanup;
|
|
2671
|
+
/** Project detach cancels autonomous cleanup; reattach reconstructs the durable intents. */
|
|
2672
|
+
pauseWorkerCleanup() { this.workerScratchCleanup.pause(); }
|
|
2673
|
+
recoverWorkerCleanup() { if (!this.disposed)
|
|
2674
|
+
this.workerScratchCleanup.recover(); }
|
|
2675
|
+
/** Hold off every admission path until destructive work (including async Git) completes. */
|
|
2676
|
+
claimWorkerCleanup(runId) {
|
|
2677
|
+
if (this.disposed || this.reclaiming.has(runId) || this.isActive(runId) || this.executions.has(runId))
|
|
2678
|
+
return undefined;
|
|
2679
|
+
this.reclaiming.add(runId);
|
|
2680
|
+
return () => { this.reclaiming.delete(runId); this.workerScratchCleanup.schedule(runId); };
|
|
2681
|
+
}
|
|
2276
2682
|
/** Scratch belongs to the task, not its current agent process (#515).
|
|
2277
2683
|
* Terminal status alone is not process-exit proof: an active session or an
|
|
2278
2684
|
* unfinished private worker generation must retain its files until exit. */
|
|
@@ -2282,8 +2688,11 @@ export class RunManager {
|
|
|
2282
2688
|
return;
|
|
2283
2689
|
if (this.active.has(runId) || this.starting.has(runId) || this.queue.includes(runId))
|
|
2284
2690
|
return;
|
|
2285
|
-
|
|
2691
|
+
const privateWorkers = workerEvidenceRunIds(this.dataDir);
|
|
2692
|
+
if (run.delegation?.role === 'worker' || run.delegation?.role === 'invalid' || !privateWorkers || privateWorkers.includes(runId)) {
|
|
2693
|
+
this.workerScratchCleanup.schedule(runId);
|
|
2286
2694
|
return;
|
|
2695
|
+
}
|
|
2287
2696
|
removeAgentTmpDir(this.dataDir, runId);
|
|
2288
2697
|
}
|
|
2289
2698
|
// ---- usage-limit auto-resume (spec 2026-08-03-auto-resume-after-usage-limit) --------------
|
|
@@ -2625,7 +3034,7 @@ export class RunManager {
|
|
|
2625
3034
|
async enforceRetention() {
|
|
2626
3035
|
try {
|
|
2627
3036
|
const keep = await resolveWorktreeRetention(this.repoRoot);
|
|
2628
|
-
await reclaimWorktrees(this.repoRoot, this.store, keep, { claim: (run) => this.claimWorktreeReclaim(run.id) });
|
|
3037
|
+
await reclaimWorktrees(this.repoRoot, this.store, keep, { claim: (run) => this.claimWorktreeReclaim(run.id), previewHost: this.preview });
|
|
2629
3038
|
}
|
|
2630
3039
|
catch {
|
|
2631
3040
|
// retention is best-effort; swallow so terminal transitions never break.
|
|
@@ -2691,6 +3100,9 @@ export class RunManager {
|
|
|
2691
3100
|
return true;
|
|
2692
3101
|
}
|
|
2693
3102
|
cancel(runId) {
|
|
3103
|
+
// Another process's run (#779, plan step 3): only its owner can stop it.
|
|
3104
|
+
if (this.store.writeRefusal(runId))
|
|
3105
|
+
return false;
|
|
2694
3106
|
if (this.store.getRun(runId)?.delegation?.role === 'worker') {
|
|
2695
3107
|
const proof = this.store.readWorkerExecution(runId);
|
|
2696
3108
|
this.store.commitWorkerCancellation(runId);
|
|
@@ -2753,13 +3165,38 @@ export class RunManager {
|
|
|
2753
3165
|
* forced removal is deleting — nor a removal start under a session that was just admitted.
|
|
2754
3166
|
*/
|
|
2755
3167
|
claimWorktreeReclaim(runId) {
|
|
2756
|
-
if (this.reclaiming.has(runId) || this.isActive(runId))
|
|
3168
|
+
if (this.reclaiming.has(runId) || this.isActive(runId) || this.store.writeRefusal(runId))
|
|
2757
3169
|
return null;
|
|
2758
3170
|
const run = this.store.getRun(runId);
|
|
2759
|
-
if (!run || !isReclaimable(run, this.store.
|
|
3171
|
+
if (!run || !isReclaimable(run, (id) => this.store.getRun(id)))
|
|
2760
3172
|
return null;
|
|
2761
|
-
this.
|
|
2762
|
-
|
|
3173
|
+
return this.holdForMaintenance([runId]);
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Hold `runIds` against Continue, reclaim, publish and branch cleanup here AND in every other
|
|
3177
|
+
* cezar process for the length of an async operation (#779, plan step 3; PR #851 review). An
|
|
3178
|
+
* ownership check alone is a moment: pinning claims each run's family, so another process
|
|
3179
|
+
* cannot claim and Continue it while this one rematerializes, publishes or deletes. Null when
|
|
3180
|
+
* any hold fails (another process took the claim first); nothing stays held then.
|
|
3181
|
+
*/
|
|
3182
|
+
holdForMaintenance(runIds) {
|
|
3183
|
+
const pinned = [];
|
|
3184
|
+
for (const id of runIds) {
|
|
3185
|
+
if (!this.store.pin(id, 'maintenance')) {
|
|
3186
|
+
for (const done of pinned)
|
|
3187
|
+
this.store.unpin(done, 'maintenance');
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
pinned.push(id);
|
|
3191
|
+
}
|
|
3192
|
+
for (const id of runIds)
|
|
3193
|
+
this.reclaiming.add(id);
|
|
3194
|
+
return () => {
|
|
3195
|
+
for (const id of runIds) {
|
|
3196
|
+
this.reclaiming.delete(id);
|
|
3197
|
+
this.store.unpin(id, 'maintenance');
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
2763
3200
|
}
|
|
2764
3201
|
/**
|
|
2765
3202
|
* Claim every run that owns a branch about to be deleted (issue 08 §B3), or null when any is in
|
|
@@ -2774,19 +3211,15 @@ export class RunManager {
|
|
|
2774
3211
|
* branch cleanup and Continue off the checkout until the push and `gh` are done with it.
|
|
2775
3212
|
*/
|
|
2776
3213
|
claimForPublish(runId) {
|
|
2777
|
-
if (this.reclaiming.has(runId) || this.isActive(runId))
|
|
3214
|
+
if (this.reclaiming.has(runId) || this.isActive(runId) || this.store.writeRefusal(runId))
|
|
2778
3215
|
return null;
|
|
2779
|
-
this.
|
|
2780
|
-
return () => { this.reclaiming.delete(runId); };
|
|
3216
|
+
return this.holdForMaintenance([runId]);
|
|
2781
3217
|
}
|
|
2782
3218
|
claimForBranchCleanup(runIds) {
|
|
2783
3219
|
const unfinished = (id) => !['done', 'failed', 'cancelled'].includes(this.store.getRun(id)?.status ?? 'done');
|
|
2784
|
-
if (runIds.some((id) => this.reclaiming.has(id) || this.isActive(id) || unfinished(id)))
|
|
3220
|
+
if (runIds.some((id) => this.reclaiming.has(id) || this.isActive(id) || unfinished(id) || this.store.writeRefusal(id)))
|
|
2785
3221
|
return null;
|
|
2786
|
-
|
|
2787
|
-
this.reclaiming.add(id);
|
|
2788
|
-
return () => { for (const id of runIds)
|
|
2789
|
-
this.reclaiming.delete(id); };
|
|
3222
|
+
return this.holdForMaintenance(runIds);
|
|
2790
3223
|
}
|
|
2791
3224
|
/**
|
|
2792
3225
|
* Fold a queued run's persisted prompt — `run.task` plus everything stacked
|
|
@@ -3098,7 +3531,7 @@ export class RunManager {
|
|
|
3098
3531
|
if (parent?.delegation?.role !== 'root')
|
|
3099
3532
|
return [];
|
|
3100
3533
|
const ids = new Set([...parent.delegation.receipts.map(receipt => receipt.workerId),
|
|
3101
|
-
...this.store.
|
|
3534
|
+
...this.store.listWorkersOf(runId).map(run => run.id)]);
|
|
3102
3535
|
return parentReadiness(parent, [...ids].map(workerId => ({ workerId, run: this.store.getRun(workerId),
|
|
3103
3536
|
terminated: this.store.readWorkerExecution(workerId)?.phase === 'complete', result: this.store.getRun(workerId)
|
|
3104
3537
|
? this.store.readWorkerResult(runId, workerId) : this.store.readDeletedWorkerResult(runId, workerId),
|
|
@@ -3432,7 +3865,7 @@ export class RunManager {
|
|
|
3432
3865
|
}
|
|
3433
3866
|
this.queueWorkerWake(runId);
|
|
3434
3867
|
}
|
|
3435
|
-
else if (!this.
|
|
3868
|
+
else if (!this.recoveringRun(runId))
|
|
3436
3869
|
this.flushAgentInputs(runId);
|
|
3437
3870
|
}
|
|
3438
3871
|
/** A worker's pending question goes to its owning parent as a conversation request
|
|
@@ -3442,11 +3875,8 @@ export class RunManager {
|
|
|
3442
3875
|
const worker = this.store.getRun(workerId);
|
|
3443
3876
|
if (worker?.delegation?.role !== 'worker')
|
|
3444
3877
|
return;
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
if (!ask)
|
|
3448
|
-
return;
|
|
3449
|
-
if (events.some(event => (event.type === 'worker-question-routed' || event.type === 'worker-question-fallback') && event.askSeq === ask.seq))
|
|
3878
|
+
const ask = this.store.transcriptFacts(workerId).pendingAsk;
|
|
3879
|
+
if (!ask || ask.routedMessageId !== undefined || ask.fallback)
|
|
3450
3880
|
return;
|
|
3451
3881
|
const fallback = (reason) => { this.store.appendEvent(workerId, { type: 'worker-question-fallback', askSeq: ask.seq, reason }); };
|
|
3452
3882
|
const request = askRequestSchema.safeParse({ questions: ask.questions });
|
|
@@ -3494,23 +3924,20 @@ export class RunManager {
|
|
|
3494
3924
|
answersFallenBackAsk(runId, opts, deferred) {
|
|
3495
3925
|
if (deferred || !(opts.text?.trim() || opts.images?.length))
|
|
3496
3926
|
return false;
|
|
3497
|
-
|
|
3498
|
-
const ask = pendingHumanAsk(events);
|
|
3499
|
-
return !!ask && events.some(event => event.type === 'worker-question-fallback' && event.askSeq === ask.seq);
|
|
3927
|
+
return !!this.store.transcriptFacts(runId).pendingAsk?.fallback;
|
|
3500
3928
|
}
|
|
3501
3929
|
/** The worker's pending ask when it went to the parent and has not fallen back to a human. */
|
|
3502
3930
|
routedAsk(runId) {
|
|
3503
3931
|
const run = this.store.getRun(runId);
|
|
3504
3932
|
if (run?.delegation?.role !== 'worker')
|
|
3505
3933
|
return undefined;
|
|
3506
|
-
|
|
3507
|
-
const ask =
|
|
3508
|
-
if (!ask ||
|
|
3934
|
+
// Indexed at append time (#880): the reconcile and every message to a worker ask this.
|
|
3935
|
+
const ask = this.store.transcriptFacts(runId).pendingAsk;
|
|
3936
|
+
if (!ask || ask.fallback || ask.routedMessageId === undefined)
|
|
3509
3937
|
return undefined;
|
|
3510
|
-
const routed = events.find(event => event.type === 'worker-question-routed' && event.askSeq === ask.seq);
|
|
3511
3938
|
const root = this.store.getRun(run.delegation.parentRunId);
|
|
3512
|
-
const message =
|
|
3513
|
-
? root.delegation.conversation?.messages.find(entry => entry.id ===
|
|
3939
|
+
const message = root?.delegation?.role === 'root'
|
|
3940
|
+
? root.delegation.conversation?.messages.find(entry => entry.id === ask.routedMessageId) : undefined;
|
|
3514
3941
|
return message ? { askSeq: ask.seq, message } : undefined;
|
|
3515
3942
|
}
|
|
3516
3943
|
/** The parent's reply answers the routed question like a human answer would: through the
|
|
@@ -3620,9 +4047,7 @@ export class RunManager {
|
|
|
3620
4047
|
}
|
|
3621
4048
|
/** Routed questions this parent has not answered yet, by asking worker. */
|
|
3622
4049
|
unansweredQuestions(parentId) {
|
|
3623
|
-
return this.store.
|
|
3624
|
-
if (run.delegation?.role !== 'worker' || run.delegation.parentRunId !== parentId)
|
|
3625
|
-
return [];
|
|
4050
|
+
return this.store.listWorkersOf(parentId).flatMap(run => {
|
|
3626
4051
|
const routed = this.routedAsk(run.id);
|
|
3627
4052
|
const root = this.store.getRun(parentId);
|
|
3628
4053
|
const answered = root?.delegation?.role === 'root' && root.delegation.conversation?.outcomes.some(outcome => outcome.requestId === routed?.message.id);
|
|
@@ -3636,8 +4061,8 @@ export class RunManager {
|
|
|
3636
4061
|
if (parent?.delegation?.role !== 'root')
|
|
3637
4062
|
return;
|
|
3638
4063
|
const state = parent.delegation.conversation ?? { messages: [], outcomes: [] };
|
|
3639
|
-
const routed = this.store.
|
|
3640
|
-
const ask =
|
|
4064
|
+
const routed = this.store.listWorkersOf(parentId).flatMap(run => {
|
|
4065
|
+
const ask = this.routedAsk(run.id);
|
|
3641
4066
|
// An accepted reply still on its way to a live worker answers it; no human card beside it.
|
|
3642
4067
|
const replied = ask && state.outcomes.some(outcome => outcome.requestId === ask.message.id && outcome.status === 'replied');
|
|
3643
4068
|
return ask && !(replied && ['queued', 'running', 'waiting'].includes(run.status) && !run.stopping) ? [{ workerId: run.id, ...ask }] : [];
|
|
@@ -3725,7 +4150,7 @@ export class RunManager {
|
|
|
3725
4150
|
for (const root of family) {
|
|
3726
4151
|
if (root.delegation?.role !== 'root' || !root.delegation.conversation)
|
|
3727
4152
|
continue;
|
|
3728
|
-
const next = reconcileConversationState(root, this.store.
|
|
4153
|
+
const next = reconcileConversationState(root, (id) => this.store.getRun(id), now, run => run.delegation?.role === 'worker' ? this.store.readWorkerExecution(run.id)?.phase === 'complete' : !this.isActive(run.id));
|
|
3729
4154
|
if (next && next !== root.delegation.conversation)
|
|
3730
4155
|
this.store.commitConversation(root.id, next);
|
|
3731
4156
|
projectConversationEvents(this.store, this.store.getRun(root.id));
|
|
@@ -3742,7 +4167,10 @@ export class RunManager {
|
|
|
3742
4167
|
const arm = (delay) => {
|
|
3743
4168
|
const handle = setTimeout(() => {
|
|
3744
4169
|
this.workerWaitTimers.delete(key);
|
|
4170
|
+
// This root's own family by name: it may have no live member left by its deadline
|
|
4171
|
+
// (#779), which the pass over live families would skip.
|
|
3745
4172
|
try {
|
|
4173
|
+
this.reconcileWorkerWaits(root.id);
|
|
3746
4174
|
this.reconcileWorkerWaits();
|
|
3747
4175
|
}
|
|
3748
4176
|
catch {
|
|
@@ -3759,7 +4187,41 @@ export class RunManager {
|
|
|
3759
4187
|
}
|
|
3760
4188
|
}
|
|
3761
4189
|
}
|
|
3762
|
-
/**
|
|
4190
|
+
/** One delegation family: its root, then the root's direct workers (the store's
|
|
4191
|
+
* `parent_run_id` query). Workers cannot delegate, so this never recurses. */
|
|
4192
|
+
familyMembers(rootId) {
|
|
4193
|
+
const root = this.store.getRun(rootId);
|
|
4194
|
+
return [...(root ? [root] : []), ...this.store.listWorkersOf(rootId)];
|
|
4195
|
+
}
|
|
4196
|
+
/** Every member of every family with a live member, each once: the families the store holds
|
|
4197
|
+
* in memory (#779), which is where every delegation sweep that is not about one family looks. */
|
|
4198
|
+
liveFamilyMembers() {
|
|
4199
|
+
const roots = new Set();
|
|
4200
|
+
for (const run of this.store.listRuns()) {
|
|
4201
|
+
if (run.delegation?.role === 'root')
|
|
4202
|
+
roots.add(run.id);
|
|
4203
|
+
else if (run.delegation?.role === 'worker')
|
|
4204
|
+
roots.add(run.delegation.parentRunId);
|
|
4205
|
+
}
|
|
4206
|
+
return [...roots].flatMap(rootId => this.familyMembers(rootId));
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Recovery's full pass (#661): every family whose root carries a conversation — settled ones
|
|
4210
|
+
* too, so a projection or outcome a crash interrupted is repaired — then every live family.
|
|
4211
|
+
* Only roots with a conversation are read; a settled family without one has nothing to settle:
|
|
4212
|
+
* every other repair the sweep makes on a finished member (a stale `wait`, a cancelled root's
|
|
4213
|
+
* Finish intent) is a clause of `isLiveRecord`, so open already holds that family.
|
|
4214
|
+
* Everything else asks `reconcileWorkerWaits()`, which covers the live families (#779).
|
|
4215
|
+
*/
|
|
4216
|
+
reconcileAllWorkerFamilies() {
|
|
4217
|
+
// Another process's families are its own to repair (#779, plan step 3).
|
|
4218
|
+
for (const rootId of this.store.listConversationRootIds())
|
|
4219
|
+
if (!this.store.writeRefusal(rootId))
|
|
4220
|
+
this.reconcileWorkerWaits(rootId);
|
|
4221
|
+
this.reconcileWorkerWaits();
|
|
4222
|
+
}
|
|
4223
|
+
/** Authority and wait live on disk; these collections are admission/timer caches only. One
|
|
4224
|
+
* family by its root's id, or (no id) every family with a live member. */
|
|
3763
4225
|
reconcileWorkerWaits(familyRootId) {
|
|
3764
4226
|
if (this.disposed)
|
|
3765
4227
|
return;
|
|
@@ -3783,20 +4245,21 @@ export class RunManager {
|
|
|
3783
4245
|
this.reconcilingWorkers = true;
|
|
3784
4246
|
this.reconcilingFamily = familyRootId;
|
|
3785
4247
|
try {
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
this.
|
|
3789
|
-
|
|
4248
|
+
// One family by name, or every family with a live member (#779): a family nobody in it is
|
|
4249
|
+
// live has nothing left to settle, and its members are no longer in memory.
|
|
4250
|
+
const family = familyRootId === undefined ? this.liveFamilyMembers() : this.familyMembers(familyRootId);
|
|
4251
|
+
this.reconcileConversations(family);
|
|
4252
|
+
for (const parent of family) {
|
|
3790
4253
|
if (!parent.delegation || parent.delegation.role === 'invalid' || this.historyDeletionPending(parent.id))
|
|
3791
4254
|
continue;
|
|
3792
4255
|
if (parent.status === 'cancelled' && parent.delegation.role === 'root' && parent.delegation.finishRequestedAt) {
|
|
3793
4256
|
this.store.commitRootFinishCancellation(parent.id);
|
|
3794
4257
|
}
|
|
3795
4258
|
if (!['queued', 'running', 'waiting'].includes(parent.status)) {
|
|
3796
|
-
if (!this.
|
|
4259
|
+
if (!this.recoveringRun(parent)) {
|
|
3797
4260
|
this.withdrawWorkerWait(parent.id);
|
|
3798
4261
|
this.fallbackRoutedQuestions(parent.id, `parent-${parent.status}`);
|
|
3799
|
-
for (const child of
|
|
4262
|
+
for (const child of parent.delegation.role === 'root' && parent.status !== 'review' ? this.store.listWorkersOf(parent.id) : []) {
|
|
3800
4263
|
if (['queued', 'running', 'waiting'].includes(child.status))
|
|
3801
4264
|
this.cancel(child.id);
|
|
3802
4265
|
}
|
|
@@ -3812,7 +4275,7 @@ export class RunManager {
|
|
|
3812
4275
|
if (delivered) {
|
|
3813
4276
|
// Receipt persistence precedes the live status update. After a crash
|
|
3814
4277
|
// this is interrupted execution, not generic waiting-run success.
|
|
3815
|
-
if (this.
|
|
4278
|
+
if (this.recoveringRun(parent) && !delivered.inboxClaim?.acknowledgedAt && parent.status === 'waiting' && !this.hasPendingHumanAsk(parent.id)) {
|
|
3816
4279
|
this.store.updateRun(parent.id, { status: 'running', activity: undefined });
|
|
3817
4280
|
}
|
|
3818
4281
|
const state = this.active.get(parent.id);
|
|
@@ -3829,7 +4292,7 @@ export class RunManager {
|
|
|
3829
4292
|
continue;
|
|
3830
4293
|
}
|
|
3831
4294
|
const now = new Date().toISOString();
|
|
3832
|
-
const outcomes = this.store.
|
|
4295
|
+
const outcomes = this.store.listWorkersOf(parent.id).flatMap(child => {
|
|
3833
4296
|
const outcome = workerOutcome(child, now);
|
|
3834
4297
|
return outcome && this.store.readWorkerExecution(child.id)?.phase === 'complete' ? [outcome] : [];
|
|
3835
4298
|
});
|
|
@@ -3892,9 +4355,7 @@ export class RunManager {
|
|
|
3892
4355
|
} }]);
|
|
3893
4356
|
}
|
|
3894
4357
|
for (const outcome of next.outcomes) {
|
|
3895
|
-
if (!this.store.
|
|
3896
|
-
event.waitId === wait.id && event.outcome?.workerId === outcome.workerId &&
|
|
3897
|
-
(event.outcome?.revision ?? 0) === (outcome.revision ?? 0))) {
|
|
4358
|
+
if (!this.store.transcriptFacts(parent.id).workerOutcomeKeys.includes(workerOutcomeKey(wait.id, outcome.workerId, outcome.revision ?? 0))) {
|
|
3898
4359
|
this.store.appendEvent(parent.id, { type: 'worker-outcome', waitId: wait.id, outcome });
|
|
3899
4360
|
}
|
|
3900
4361
|
}
|
|
@@ -3974,7 +4435,7 @@ export class RunManager {
|
|
|
3974
4435
|
this.store.commitAgentInputs(parentId, (run.agentInputs ?? []).map(entry => entry.id === input.id ? input : entry));
|
|
3975
4436
|
}
|
|
3976
4437
|
// Rebuild ordinary queued work before appending new wake admissions on restart.
|
|
3977
|
-
if (this.
|
|
4438
|
+
if (this.recoveringRun(run))
|
|
3978
4439
|
return;
|
|
3979
4440
|
const state = this.active.get(parentId);
|
|
3980
4441
|
// A parent's reply to a routed question (#505) is the one message a pending ask admits.
|
|
@@ -4058,7 +4519,9 @@ export class RunManager {
|
|
|
4058
4519
|
/** Transcript bubbles include refused sends. Only a successful delivery
|
|
4059
4520
|
* checkpoint can answer the specific ask observed before that send. */
|
|
4060
4521
|
pendingHumanAskSeq(runId) {
|
|
4061
|
-
|
|
4522
|
+
// Fallback human gates use the same exact successful-delivery receipt as asks,
|
|
4523
|
+
// without inventing a structured question card or changing its summary. Indexed (#880).
|
|
4524
|
+
return this.store.transcriptFacts(runId).pendingGateSeq;
|
|
4062
4525
|
}
|
|
4063
4526
|
hasPendingHumanAsk(runId) {
|
|
4064
4527
|
return this.pendingHumanAskSeq(runId) !== undefined;
|
|
@@ -4190,6 +4653,7 @@ export class RunManager {
|
|
|
4190
4653
|
* goes back to the queue so a later session delivers it; never shown as delivered. */
|
|
4191
4654
|
requeueUnreadAtClose(runId, state, session) {
|
|
4192
4655
|
this.clearUnreadInputTimer(state);
|
|
4656
|
+
this.clearHeldHumanInputTimer(state);
|
|
4193
4657
|
// Everything this session accepted but never saw read: steered input, messages bundled
|
|
4194
4658
|
// behind a human answer, and an opening instruction whose turn never succeeded.
|
|
4195
4659
|
const opening = state.openingAgentInputId && this.store.getRun(runId)?.agentInputs?.some(input => input.id === state.openingAgentInputId && input.deliveredAt && !input.consumedAt) ? [state.openingAgentInputId] : [];
|
|
@@ -4209,7 +4673,11 @@ export class RunManager {
|
|
|
4209
4673
|
* run parks as waiting or monitoring. Shared by acknowledgements that land after a
|
|
4210
4674
|
* turn ended and by the unread-input grace timer (#505). */
|
|
4211
4675
|
settleIdleBoundary(runId, state, session) {
|
|
4212
|
-
|
|
4676
|
+
// An in-flight acknowledgement still owns this boundary; its .then re-settles.
|
|
4677
|
+
if (this.harnessHoldsHumanInput(state) || state.agentInputFlight)
|
|
4678
|
+
return;
|
|
4679
|
+
const proseGate = state.parkAfterAck?.session === session && state.parkAfterAck.humanGate;
|
|
4680
|
+
if (state.atTurnBoundary !== session || (state.pendingHumanAsk && !proseGate) || this.workerWait(runId) || this.store.getRun(runId)?.ciWait || (!proseGate && this.hasQueuedAgentInputs(runId)))
|
|
4213
4681
|
return;
|
|
4214
4682
|
if (state.doneAtBoundary === session) {
|
|
4215
4683
|
if (this.deferParentCompletion(runId))
|
|
@@ -4219,9 +4687,20 @@ export class RunManager {
|
|
|
4219
4687
|
session.end();
|
|
4220
4688
|
}
|
|
4221
4689
|
else if (state.parkAfterAck?.session === session && !this.waiting.has(runId) && !this.monitoring.has(runId)) {
|
|
4690
|
+
// The nudged turn may finish before its transport ACK (OpenCode HTTP/SSE).
|
|
4691
|
+
// Its turn-end deferred to agentInputFlight; now apply the same policy
|
|
4692
|
+
// before parking, with DONE and queued/accepted input still taking priority.
|
|
4693
|
+
if (!state.parkAfterAck.humanGate && state.currentStepId && this.tryAutonomousNudge(runId, state, state.currentStepId, null)) {
|
|
4694
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'turn complete — status=running (autonomous nudge)');
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
if (state.autonomousNudgePending === session) {
|
|
4698
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'turn complete — status=running (awaiting input readiness)');
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4222
4701
|
// A wake turn can finish before its HTTP/RPC acknowledgement. Retiring
|
|
4223
4702
|
// that wait must release its completed turn, not invent another turn.
|
|
4224
|
-
if ((state.parkAfterAck.monitoring || this.hasLiveWorkers(runId)) && !this.parentCompletionAttention(runId)) {
|
|
4703
|
+
if ((state.parkAfterAck.monitoring || (!state.parkAfterAck.humanGate && this.hasLiveWorkers(runId))) && !this.parentCompletionAttention(runId)) {
|
|
4225
4704
|
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
4226
4705
|
if (state.currentStepId)
|
|
4227
4706
|
this.store.updateStep(runId, state.currentStepId, { status: 'running' });
|
|
@@ -4229,6 +4708,16 @@ export class RunManager {
|
|
|
4229
4708
|
this.clearIdleTimer(state);
|
|
4230
4709
|
this.armMonitoringWakeTimer(runId, state);
|
|
4231
4710
|
}
|
|
4711
|
+
else if (state.currentStepId && this.maybeFinalMessageNudge(runId, state, state.currentStepId, {
|
|
4712
|
+
ask: false,
|
|
4713
|
+
humanGate: !!state.parkAfterAck.humanGate,
|
|
4714
|
+
monitoring: !!state.parkAfterAck.monitoring,
|
|
4715
|
+
silentTail: !!state.parkAfterAck.silentTail,
|
|
4716
|
+
alreadyNudged: !!state.parkAfterAck.alreadyFinalMessageNudged,
|
|
4717
|
+
})) {
|
|
4718
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'turn complete — status=running (final message nudge)');
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4232
4721
|
else {
|
|
4233
4722
|
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
4234
4723
|
if (state.currentStepId)
|
|
@@ -4237,6 +4726,8 @@ export class RunManager {
|
|
|
4237
4726
|
this.armIdleTimer(runId, state);
|
|
4238
4727
|
}
|
|
4239
4728
|
this.waiting.add(runId);
|
|
4729
|
+
if (state.autonomous)
|
|
4730
|
+
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${this.monitoring.has(runId) ? 'monitoring' : 'waiting'}`);
|
|
4240
4731
|
this.releaseSlot();
|
|
4241
4732
|
}
|
|
4242
4733
|
}
|
|
@@ -4246,12 +4737,12 @@ export class RunManager {
|
|
|
4246
4737
|
armUnreadInputTimer(runId, state) {
|
|
4247
4738
|
this.clearUnreadInputTimer(state);
|
|
4248
4739
|
const session = state.session;
|
|
4249
|
-
if (!session?.open || !this.harnessOwesInput(state) || state.agentInputFlight)
|
|
4740
|
+
if (!session?.open || state.pendingHumanAsk || !this.harnessOwesInput(state) || state.agentInputFlight)
|
|
4250
4741
|
return;
|
|
4251
4742
|
const timer = setTimeout(() => {
|
|
4252
4743
|
state.unreadInputTimer = undefined;
|
|
4253
4744
|
if (this.disposed || this.active.get(runId) !== state || state.session !== session || !session.open || state.cancelled ||
|
|
4254
|
-
state.agentInputFlight || !this.harnessOwesInput(state))
|
|
4745
|
+
state.pendingHumanAsk || state.agentInputFlight || !this.harnessOwesInput(state))
|
|
4255
4746
|
return;
|
|
4256
4747
|
const ids = [...state.unreadInputIds];
|
|
4257
4748
|
const retried = ids.filter(id => state.unreadRetried?.has(id));
|
|
@@ -4263,7 +4754,7 @@ export class RunManager {
|
|
|
4263
4754
|
this.store.commitAgentInputsUnconfirmed(runId, retried);
|
|
4264
4755
|
}
|
|
4265
4756
|
catch { /* receipt stays awaiting; a restart replays it */ }
|
|
4266
|
-
this.store.appendEvent(runId, { type: 'note', message:
|
|
4757
|
+
this.store.appendEvent(runId, { type: 'note', message: unreadInputUnconfirmedNote(retried.length) });
|
|
4267
4758
|
}
|
|
4268
4759
|
if (fresh.length) {
|
|
4269
4760
|
state.unreadRetried ??= new Set();
|
|
@@ -4287,14 +4778,85 @@ export class RunManager {
|
|
|
4287
4778
|
clearTimeout(state.unreadInputTimer);
|
|
4288
4779
|
state.unreadInputTimer = undefined;
|
|
4289
4780
|
}
|
|
4781
|
+
/** #486: a turn-end deferred close/park only because the harness still held a human follow-up. */
|
|
4782
|
+
armHeldHumanInputTimer(runId, state) {
|
|
4783
|
+
this.clearHeldHumanInputTimer(state, false);
|
|
4784
|
+
const session = state.session;
|
|
4785
|
+
if (!session?.open || !this.harnessHoldsHumanInput(state)) {
|
|
4786
|
+
state.heldHumanInputSince = undefined;
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
state.heldHumanInputSince ??= Date.now();
|
|
4790
|
+
const timer = setTimeout(() => {
|
|
4791
|
+
state.heldHumanInputTimer = undefined;
|
|
4792
|
+
if (this.disposed || this.active.get(runId) !== state || state.session !== session || !session.open ||
|
|
4793
|
+
state.cancelled || state.idleClosed || state.atTurnBoundary !== session)
|
|
4794
|
+
return;
|
|
4795
|
+
if (this.harnessHoldsHumanInput(state)) {
|
|
4796
|
+
// An in-flight ACK still owns this boundary; its .then re-settles.
|
|
4797
|
+
if (state.agentInputFlight) {
|
|
4798
|
+
this.armHeldHumanInputTimer(runId, state);
|
|
4799
|
+
return;
|
|
4800
|
+
}
|
|
4801
|
+
if (Date.now() - (state.heldHumanInputSince ?? 0) >= this.unreadInputGraceMs) {
|
|
4802
|
+
const held = session.heldHumanInputCount?.() ?? 0;
|
|
4803
|
+
this.store.appendEvent(runId, {
|
|
4804
|
+
type: 'note',
|
|
4805
|
+
message: held > 0
|
|
4806
|
+
? unreadInputUnconfirmedNote(held)
|
|
4807
|
+
: 'delivered held messages; the agent did not confirm reading them',
|
|
4808
|
+
});
|
|
4809
|
+
state.heldHumanInputExpired = true;
|
|
4810
|
+
this.settleIdleBoundary(runId, state, session);
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
this.armHeldHumanInputTimer(runId, state);
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
this.settleIdleBoundary(runId, state, session);
|
|
4817
|
+
}, HELD_HUMAN_INPUT_RECHECK_MS);
|
|
4818
|
+
timer.unref?.();
|
|
4819
|
+
state.heldHumanInputTimer = timer;
|
|
4820
|
+
}
|
|
4821
|
+
clearHeldHumanInputTimer(state, resetClock = true) {
|
|
4822
|
+
if (state.heldHumanInputTimer)
|
|
4823
|
+
clearTimeout(state.heldHumanInputTimer);
|
|
4824
|
+
state.heldHumanInputTimer = undefined;
|
|
4825
|
+
if (resetClock) {
|
|
4826
|
+
state.heldHumanInputSince = undefined;
|
|
4827
|
+
state.heldHumanInputExpired = undefined;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4290
4830
|
/** #505: the harness accepted input this session has not reported reading yet. */
|
|
4291
4831
|
harnessOwesInput(state) {
|
|
4292
4832
|
return !!state?.unreadInputIds?.size;
|
|
4293
4833
|
}
|
|
4834
|
+
/** #486: a human follow-up is still queued in the live session and will still be delivered. */
|
|
4835
|
+
harnessHoldsHumanInput(state) {
|
|
4836
|
+
return !state.heldHumanInputExpired && !!state.session?.open && state.session.holdsHumanInput();
|
|
4837
|
+
}
|
|
4294
4838
|
/** A callback from an old/replaced/disposed session carries no authority. */
|
|
4295
4839
|
handleAgentInputReady(runId, state, session) {
|
|
4296
|
-
if (session
|
|
4297
|
-
|
|
4840
|
+
if (!session || this.disposed || this.active.get(runId) !== state || state.session !== session ||
|
|
4841
|
+
!session.open || state.idleClosed || state.cancelled || state.finishRequested || state.agentInputError || state.agentSessionError)
|
|
4842
|
+
return;
|
|
4843
|
+
// Durable input always gets the first opportunity. Only the same refused,
|
|
4844
|
+
// completed boundary may retry; ordinary/native question parks are not wakes.
|
|
4845
|
+
if (this.flushAgentInputs(runId))
|
|
4846
|
+
return;
|
|
4847
|
+
// #486: held human input drained at this still-current boundary with no new turn.
|
|
4848
|
+
// Runners fire onAgentInputReady from the transport ACK finally/callback while
|
|
4849
|
+
// agentInputFlight is still set; settling here parks before the ACK .then can nudge.
|
|
4850
|
+
if (state.atTurnBoundary === session && !this.harnessHoldsHumanInput(state) &&
|
|
4851
|
+
!state.agentInputFlight && !this.harnessOwesInput(state)) {
|
|
4852
|
+
this.settleIdleBoundary(runId, state, session);
|
|
4853
|
+
}
|
|
4854
|
+
if (state.autonomousNudgePending !== session || state.atTurnBoundary !== session ||
|
|
4855
|
+
state.doneAtBoundary === session || !state.currentStepId)
|
|
4856
|
+
return;
|
|
4857
|
+
if (this.tryAutonomousNudge(runId, state, state.currentStepId, null)) {
|
|
4858
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'turn complete — status=running (autonomous nudge)');
|
|
4859
|
+
}
|
|
4298
4860
|
}
|
|
4299
4861
|
/** Reservation is synchronous; only an acknowledged transport may checkpoint delivery. */
|
|
4300
4862
|
submitAgentInput(runId, state, content, inputIds = []) {
|
|
@@ -4524,6 +5086,8 @@ export class RunManager {
|
|
|
4524
5086
|
/** Restore active lifecycle/accounting when either Cezar or the backend
|
|
4525
5087
|
* resumes work in a parked session. */
|
|
4526
5088
|
resumeParkedRun(runId, state) {
|
|
5089
|
+
this.clearHeldHumanInputTimer(state);
|
|
5090
|
+
state.autonomousNudgePending = undefined;
|
|
4527
5091
|
state.atTurnBoundary = undefined;
|
|
4528
5092
|
state.doneAtBoundary = undefined;
|
|
4529
5093
|
state.parkAfterAck = undefined;
|
|
@@ -4595,6 +5159,9 @@ export class RunManager {
|
|
|
4595
5159
|
: this.submitAgentInput(runId, state, deliverable);
|
|
4596
5160
|
if (delivered) {
|
|
4597
5161
|
if (userAuthored) {
|
|
5162
|
+
state.finalMessageNudged = undefined;
|
|
5163
|
+
state.finalMessageNudgeParked = undefined;
|
|
5164
|
+
state.finalMessageNudgeSawContent = undefined;
|
|
4598
5165
|
if (ciHumanMessage)
|
|
4599
5166
|
this.store.commitQueuedMessageDelivery(runId, ciHumanMessage.id);
|
|
4600
5167
|
if (answeringAskSeq !== undefined) {
|
|
@@ -4682,6 +5249,13 @@ export class RunManager {
|
|
|
4682
5249
|
const current = steps.findIndex((step) => step.id === run.currentStepId);
|
|
4683
5250
|
return current < 0 || current < steps.length - 1;
|
|
4684
5251
|
}
|
|
5252
|
+
/** A clean process exit cannot complete a workflow still blocked on a question.
|
|
5253
|
+
* Keep its existing durable waiting step so Continue can resume the tail. */
|
|
5254
|
+
unansweredWorkflowAsk(runId, state) {
|
|
5255
|
+
const run = this.store.getRun(runId);
|
|
5256
|
+
return !state.finishRequested && !!run && this.hasPendingHumanAsk(runId) &&
|
|
5257
|
+
this.waitingBeforeFinalWorkflowStep(run);
|
|
5258
|
+
}
|
|
4685
5259
|
/** Locate the original workflow step whose closed session a synthetic Continue is resuming. */
|
|
4686
5260
|
waitingWorkflowStepIndex(run) {
|
|
4687
5261
|
const steps = run.workflowDef?.steps ?? run.steps.filter(step => !isSyntheticContinuation(run, step));
|
|
@@ -4727,6 +5301,26 @@ export class RunManager {
|
|
|
4727
5301
|
/** Restart recovery may discover several interrupted tasks at once. Those
|
|
4728
5302
|
* continuations are queued; an explicit user Continue remains immediate. */
|
|
4729
5303
|
deferForCapacity = false, conversation) {
|
|
5304
|
+
// Another process's run (#779, plan step 3) is its owner's to continue.
|
|
5305
|
+
const refusal = this.store.writeRefusal(runId);
|
|
5306
|
+
if (refusal)
|
|
5307
|
+
return { ok: false, error: refusal };
|
|
5308
|
+
// A finished run is loaded and held for the whole admission (#779): every read below sees
|
|
5309
|
+
// the record its own writes change, exactly as for a live run. Admission makes it live
|
|
5310
|
+
// (queued or running) or refuses it; either way the `continue` pin ends here.
|
|
5311
|
+
// The check above is a moment: another process may claim the run before the hold lands
|
|
5312
|
+
// (PR #851 review). Holding it is what claims it, so a failed hold refuses the Continue.
|
|
5313
|
+
if (!this.store.pin(runId, 'continue')) {
|
|
5314
|
+
return { ok: false, error: this.store.getRun(runId) ? this.store.writeRefusal(runId) ?? RUN_IN_USE_ELSEWHERE : 'not found' };
|
|
5315
|
+
}
|
|
5316
|
+
try {
|
|
5317
|
+
return this.admitContinuation(runId, opts, deferForCapacity, conversation);
|
|
5318
|
+
}
|
|
5319
|
+
finally {
|
|
5320
|
+
this.store.unpin(runId, 'continue');
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
admitContinuation(runId, opts, deferForCapacity, conversation) {
|
|
4730
5324
|
if (agentModelsLocked(this.repoRoot) && (opts.model?.trim() || opts.effort?.trim())) {
|
|
4731
5325
|
return { ok: false, error: AGENT_MODELS_LOCKED_ERROR };
|
|
4732
5326
|
}
|
|
@@ -4967,6 +5561,7 @@ export class RunManager {
|
|
|
4967
5561
|
this.store.updateRun(runId, acceptedPatch);
|
|
4968
5562
|
this.store.flush();
|
|
4969
5563
|
}
|
|
5564
|
+
this.continuationGenerations.set(runId, Symbol());
|
|
4970
5565
|
// Keep fresh viewable images even if persistence failed; recovery uses saved URLs.
|
|
4971
5566
|
const images = contentBlocksOf(opts.images ?? []).filter((block) => block.type === 'image');
|
|
4972
5567
|
if (deferForCapacity) {
|
|
@@ -5028,6 +5623,8 @@ export class RunManager {
|
|
|
5028
5623
|
this.workerIdentity(runId);
|
|
5029
5624
|
const record = this.store.getRun(runId);
|
|
5030
5625
|
let cwd;
|
|
5626
|
+
// Retention (#483) rebuilt a reclaimed directory: a bare checkout that needs setup again (#917).
|
|
5627
|
+
let rematerialized = false;
|
|
5031
5628
|
if (record?.delegation?.role === 'worker') {
|
|
5032
5629
|
try {
|
|
5033
5630
|
if (record.delegation.destroy)
|
|
@@ -5047,7 +5644,7 @@ export class RunManager {
|
|
|
5047
5644
|
}
|
|
5048
5645
|
}
|
|
5049
5646
|
else {
|
|
5050
|
-
await rematerializeReclaimedWorktree(this.repoRoot, this.store, runId);
|
|
5647
|
+
rematerialized = await rematerializeReclaimedWorktree(this.repoRoot, this.store, runId);
|
|
5051
5648
|
cwd = record?.worktreePath && existsSync(record.worktreePath) ? record.worktreePath : this.repoRoot;
|
|
5052
5649
|
}
|
|
5053
5650
|
if (this.store.getRun(runId)?.stopping) {
|
|
@@ -5059,8 +5656,14 @@ export class RunManager {
|
|
|
5059
5656
|
// The env is a live ceiling: a run created while the inbox was on must not keep writing
|
|
5060
5657
|
// follow-ups after it is switched off.
|
|
5061
5658
|
const generateFollowups = followupsEnabled() && record?.generateFollowups !== false;
|
|
5062
|
-
|
|
5063
|
-
|
|
5659
|
+
// Continue/recovery constructs a second ActiveRun: hydrate the durable mode here too.
|
|
5660
|
+
const state = {
|
|
5661
|
+
delegationSettings: undefined, revokeDelegation: undefined,
|
|
5662
|
+
cancelled: false, interrupt: () => undefined, cwd,
|
|
5663
|
+
pendingHumanAsk: this.hasPendingHumanAsk(runId),
|
|
5664
|
+
autonomous: record?.autonomous === true, autoContinues: 0,
|
|
5665
|
+
};
|
|
5666
|
+
this.activate(runId, state);
|
|
5064
5667
|
this.starting.delete(runId);
|
|
5065
5668
|
if (state.cwd === this.repoRoot) {
|
|
5066
5669
|
if (repositoryRootLockDisabled()) {
|
|
@@ -5089,6 +5692,9 @@ export class RunManager {
|
|
|
5089
5692
|
this.armAutosave(runId, state);
|
|
5090
5693
|
if (record)
|
|
5091
5694
|
seedHandoffFile(this.dataDir, record); // idempotent — normally already there
|
|
5695
|
+
// Worktree setup (#917) for a directory retention rebuilt, or one a restart interrupted.
|
|
5696
|
+
const setupNote = await this.prepareWorktree(runId, state, (await loadConfig(this.repoRoot)).worktreeSetup, (event) => this.store.appendEvent(runId, event), rematerialized ? 'rematerialized' : 'resume');
|
|
5697
|
+
// A Stop during setup settles below, at the prelaunch cancellation check before any spawn.
|
|
5092
5698
|
// Registry snapshot for `/skill` expansion. `execute` loads this for the workflow's own
|
|
5093
5699
|
// sessions; a continuation builds its OWN ActiveRun, and without this the resumed session
|
|
5094
5700
|
// expanded against an empty registry and leaked `/om-...` verbatim to the backend, which
|
|
@@ -5141,6 +5747,7 @@ export class RunManager {
|
|
|
5141
5747
|
state.pendingHumanAsk = openingAnswerAskSeq === undefined && this.hasPendingHumanAsk(runId);
|
|
5142
5748
|
let turnText = '';
|
|
5143
5749
|
let completedAssistantText = '';
|
|
5750
|
+
let turnTail = 'none';
|
|
5144
5751
|
let sawClaudeScheduleWakeup = false;
|
|
5145
5752
|
let sessionError;
|
|
5146
5753
|
const sink = this.makeUiSink(runId, stepId);
|
|
@@ -5158,6 +5765,8 @@ export class RunManager {
|
|
|
5158
5765
|
}
|
|
5159
5766
|
if (event.type === 'text') {
|
|
5160
5767
|
this.clearUnreadInputTimer(state); // real content: the harness is working
|
|
5768
|
+
this.clearHeldHumanInputTimer(state);
|
|
5769
|
+
this.noteFinalMessageNudgeContent(runId, state, event);
|
|
5161
5770
|
turnText = appendTurnText(turnText, event.text);
|
|
5162
5771
|
const text = stripAskMarker(stripTaskMarkers(stripMonitoringMarker(stripDoneMarker(event.text))), false);
|
|
5163
5772
|
if (text)
|
|
@@ -5168,8 +5777,11 @@ export class RunManager {
|
|
|
5168
5777
|
this.retireUnreadInputs(runId, state, event.inputIds, true);
|
|
5169
5778
|
return;
|
|
5170
5779
|
}
|
|
5171
|
-
if (event.type === 'tool-call' || event.type === 'tool-result')
|
|
5780
|
+
if (event.type === 'tool-call' || event.type === 'tool-result') {
|
|
5172
5781
|
this.clearUnreadInputTimer(state);
|
|
5782
|
+
this.clearHeldHumanInputTimer(state);
|
|
5783
|
+
this.noteFinalMessageNudgeContent(runId, state, event);
|
|
5784
|
+
}
|
|
5173
5785
|
this.store.appendEvent(runId, { ...event, stepId });
|
|
5174
5786
|
if (event.type === 'cost')
|
|
5175
5787
|
this.recordReportedCost(runId, stepId, state, backend, event.usd);
|
|
@@ -5244,36 +5856,45 @@ export class RunManager {
|
|
|
5244
5856
|
sink.flushAll();
|
|
5245
5857
|
this.trackTurnEnd(runId, turnText); // titleSummary + diffStat (#389)
|
|
5246
5858
|
const sessionOpen = !state.cancelled && state.session?.open;
|
|
5247
|
-
const
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
const monitoring =
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
(MONITORING_MARKER_RE.test(turnText.trimEnd()) || sawClaudeScheduleWakeup || this.hasLiveWorkers(runId));
|
|
5859
|
+
const turn = classifyTurnEnd(turnText, completedAssistantText, {
|
|
5860
|
+
sessionOpen: !!sessionOpen, interactive: true, pendingHumanAsk: !!state.pendingHumanAsk,
|
|
5861
|
+
completionAttention: this.parentCompletionAttention(runId), scheduledWake: sawClaudeScheduleWakeup,
|
|
5862
|
+
liveWorkers: this.hasLiveWorkers(runId),
|
|
5863
|
+
dependencyWait: !!(this.workerWait(runId) || this.store.getRun(runId)?.ciWait),
|
|
5864
|
+
});
|
|
5865
|
+
const { done, monitoring, humanGate, ask, notes: askNotes } = turn;
|
|
5866
|
+
discardQueuedMessagesOnAsk(state.session, turn);
|
|
5867
|
+
const silentTail = turnTail === 'silent';
|
|
5868
|
+
const alreadyFinalMessageNudged = state.finalMessageNudged === state.session;
|
|
5869
|
+
state.finalMessageNudged = undefined;
|
|
5259
5870
|
turnText = '';
|
|
5260
5871
|
completedAssistantText = '';
|
|
5872
|
+
turnTail = 'none';
|
|
5261
5873
|
sawClaudeScheduleWakeup = false;
|
|
5262
5874
|
for (const note of askNotes)
|
|
5263
5875
|
this.store.appendEvent(runId, { type: 'note', ...note, stepId });
|
|
5264
5876
|
this.store.updateRun(runId, { invalidAsk: !ask && askNotes.length > 0 ? true : undefined });
|
|
5265
|
-
|
|
5877
|
+
// Only a newly parsed portable ASK can be overridden. Do this before
|
|
5878
|
+
// prepareHumanAsk latches it; an existing/native ask still blocks the helper.
|
|
5879
|
+
let autoContinued = !!ask && !!sessionOpen && this.tryAutonomousNudge(runId, state, stepId, ask);
|
|
5880
|
+
if (ask && !autoContinued)
|
|
5266
5881
|
this.prepareHumanAsk(runId, state);
|
|
5267
|
-
|
|
5882
|
+
if (humanGate && !ask && !state.pendingHumanAsk)
|
|
5883
|
+
this.prepareProseHumanGate(runId, state);
|
|
5884
|
+
const ciWaitParked = !humanGate && !!sessionOpen && this.parkCiWait(runId, state);
|
|
5268
5885
|
const completionBlocked = !ciWaitParked && !!done && this.deferParentCompletion(runId);
|
|
5269
|
-
const workerWaitParked = ciWaitParked || completionBlocked || (!!sessionOpen && this.parkWorkerWait(runId, state));
|
|
5886
|
+
const workerWaitParked = ciWaitParked || completionBlocked || (!humanGate && !!sessionOpen && this.parkWorkerWait(runId, state));
|
|
5270
5887
|
state.doneAtBoundary = done ? state.session : undefined;
|
|
5271
|
-
state.parkAfterAck = sessionOpen && state.session ? { session: state.session, monitoring
|
|
5888
|
+
state.parkAfterAck = sessionOpen && state.session ? { session: state.session, monitoring, humanGate, silentTail, alreadyFinalMessageNudged } : undefined;
|
|
5272
5889
|
// #505: input the harness accepted but has not read yet runs as its next turn.
|
|
5273
5890
|
// An in-flight submission is settled by its acknowledgement (parkAfterAck), not here.
|
|
5274
|
-
const agentInputDelivered = !ask && !workerWaitParked && (this.flushAgentInputs(runId) || this.harnessOwesInput(state) || !!state.agentInputFlight);
|
|
5891
|
+
const agentInputDelivered = autoContinued || (!ask && !workerWaitParked && (this.flushAgentInputs(runId) || (!humanGate && this.harnessOwesInput(state)) || !!state.agentInputFlight || this.harnessHoldsHumanInput(state)));
|
|
5275
5892
|
if (!ask && !workerWaitParked)
|
|
5276
5893
|
this.armUnreadInputTimer(runId, state);
|
|
5894
|
+
if (!ask && !workerWaitParked && this.harnessHoldsHumanInput(state))
|
|
5895
|
+
this.armHeldHumanInputTimer(runId, state);
|
|
5896
|
+
else
|
|
5897
|
+
this.clearHeldHumanInputTimer(state);
|
|
5277
5898
|
if (state.agentInputError)
|
|
5278
5899
|
return;
|
|
5279
5900
|
if (done && !workerWaitParked && !state.pendingHumanAsk && !agentInputDelivered && !state.agentInputFlight && !this.hasQueuedAgentInputs(runId)) {
|
|
@@ -5284,26 +5905,13 @@ export class RunManager {
|
|
|
5284
5905
|
return;
|
|
5285
5906
|
}
|
|
5286
5907
|
if (sessionOpen && !agentInputDelivered && !workerWaitParked && !this.workerWakeAdmitted.has(runId)) {
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
!state.cancelled &&
|
|
5295
|
-
(() => {
|
|
5296
|
-
const sent = this.submitAgentInput(runId, state, [{ type: 'text', text: AUTONOMOUS_NUDGE }]);
|
|
5297
|
-
if (!sent)
|
|
5298
|
-
return false;
|
|
5299
|
-
state.autoContinues = (state.autoContinues ?? 0) + 1;
|
|
5300
|
-
this.store.appendEvent(runId, {
|
|
5301
|
-
type: 'note',
|
|
5302
|
-
message: `autonomous — continuing without pausing (${state.autoContinues}/${MAX_AUTO_CONTINUES})`,
|
|
5303
|
-
});
|
|
5304
|
-
return true;
|
|
5305
|
-
})();
|
|
5306
|
-
if (!autoContinued) {
|
|
5908
|
+
if (!ask && !humanGate)
|
|
5909
|
+
autoContinued = this.tryAutonomousNudge(runId, state, stepId, null);
|
|
5910
|
+
if (!autoContinued)
|
|
5911
|
+
autoContinued = this.maybeFinalMessageNudge(runId, state, stepId, {
|
|
5912
|
+
ask: !!ask, humanGate, monitoring, silentTail, alreadyNudged: alreadyFinalMessageNudged,
|
|
5913
|
+
});
|
|
5914
|
+
if (!autoContinued && state.autonomousNudgePending !== state.session) {
|
|
5307
5915
|
// `CEZ:ASK` → park `waiting` (attention) AND surface the structured
|
|
5308
5916
|
// question as an ask card (#473). `CEZ:MONITORING` or Claude's native
|
|
5309
5917
|
// `ScheduleWakeup` → non-attention `running`/`activity:'monitoring'`
|
|
@@ -5341,7 +5949,7 @@ export class RunManager {
|
|
|
5341
5949
|
if (this.store.getRun(runId)?.autoResumeAttempts !== undefined) {
|
|
5342
5950
|
this.store.updateRun(runId, { autoResumeAttempts: undefined });
|
|
5343
5951
|
}
|
|
5344
|
-
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${agentInputDelivered ? 'running' : monitoring ? 'monitoring' : sessionOpen ? 'waiting' : 'running'}`);
|
|
5952
|
+
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${autoContinued ? (state.finalMessageNudged === state.session ? 'running (final message nudge)' : 'running (autonomous nudge)') : state.autonomousNudgePending && state.autonomousNudgePending === state.session ? 'running (awaiting input readiness)' : agentInputDelivered ? 'running' : monitoring ? 'monitoring' : sessionOpen ? 'waiting' : 'running'}`);
|
|
5345
5953
|
}
|
|
5346
5954
|
};
|
|
5347
5955
|
// Backend + model come off the record: the run's current backend by default, or the
|
|
@@ -5466,13 +6074,70 @@ export class RunManager {
|
|
|
5466
6074
|
effort: continueEffort, agentProfile: continueProfile.profileId, accountBinding: continueProfile.accountBinding,
|
|
5467
6075
|
systemPrompt: record?.systemPrompt, allowedTools: grants.allowedTools, bashAllowlist: grants.bashAllowlist };
|
|
5468
6076
|
const runner = createRunner(continueBackend);
|
|
6077
|
+
// Re-expand the continued step's skill from the current registry (#790) BEFORE
|
|
6078
|
+
// provisionSession. `await materializeSkillDir` used to sit after the pre-launch
|
|
6079
|
+
// revalidation, so Stop during that await still launched with revoked tools.
|
|
6080
|
+
// `record.systemPrompt` is only the extra prompt; the opening session put
|
|
6081
|
+
// `skillSystemPrompt(skill)` in front. Skip that prefix only when this
|
|
6082
|
+
// session resumes into its own thread on an in-thread runner — those
|
|
6083
|
+
// already carry the playbook in history. A missing skill degrades to
|
|
6084
|
+
// today's extra-only prompt with one lifecycle warning; in-thread skips
|
|
6085
|
+
// do not warn. Workers still inherit only the extra prompt (#778).
|
|
6086
|
+
const skipSkillOnResume = sessionId !== undefined && runner.systemPromptOnResume === 'in-thread';
|
|
6087
|
+
let continuationSkillPrompt;
|
|
6088
|
+
let fallbackSkillPrompt;
|
|
6089
|
+
const continuedSkillName = toolsStep?.skill;
|
|
6090
|
+
if (continuedSkillName) {
|
|
6091
|
+
let skill = (state.skills ?? []).find((candidate) => candidate.name === continuedSkillName);
|
|
6092
|
+
if (!skill) {
|
|
6093
|
+
// Boot recovery can outrun the async team-skills cache (`getTeamSkillsCached`
|
|
6094
|
+
// returns [] until `initialTeamSkillsLoad` resolves). Wait briefly, then
|
|
6095
|
+
// re-discover; never hang Continue on the network. The on-disk clone's
|
|
6096
|
+
// list arrives before the passive fetch (#859), so stop as soon as it
|
|
6097
|
+
// names this skill instead of waiting out a slow first fetch of the day.
|
|
6098
|
+
try {
|
|
6099
|
+
await new Promise((resolve) => {
|
|
6100
|
+
const timeout = setTimeout(resolve, 1_500);
|
|
6101
|
+
timeout.unref?.();
|
|
6102
|
+
const settle = () => { clearTimeout(timeout); resolve(); };
|
|
6103
|
+
waitForTeamSkills(this.repoRoot).then(settle, settle);
|
|
6104
|
+
waitForCachedTeamSkills(this.repoRoot).then((cached) => {
|
|
6105
|
+
if (cached.some((candidate) => candidate.name === continuedSkillName))
|
|
6106
|
+
settle();
|
|
6107
|
+
}, () => undefined);
|
|
6108
|
+
});
|
|
6109
|
+
state.skills = await discoverSkills(this.repoRoot).catch(() => state.skills ?? []);
|
|
6110
|
+
skill = (state.skills ?? []).find((candidate) => candidate.name === continuedSkillName);
|
|
6111
|
+
}
|
|
6112
|
+
catch {
|
|
6113
|
+
// Best-effort — a slow or failing team-skills load must not fail Continue.
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
if (skill) {
|
|
6117
|
+
const playbook = skillSystemPrompt(skill);
|
|
6118
|
+
if (skipSkillOnResume)
|
|
6119
|
+
fallbackSkillPrompt = playbook;
|
|
6120
|
+
else
|
|
6121
|
+
continuationSkillPrompt = playbook;
|
|
6122
|
+
if (skill.source === 'team' && skill.team?.dir) {
|
|
6123
|
+
// Rematerialize into a #483 rematerialized worktree; do not note it on Continue.
|
|
6124
|
+
await materializeSkillDir(state.cwd, skill).catch(() => false);
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
else if (!skipSkillOnResume) {
|
|
6128
|
+
this.store.appendEvent(runId, {
|
|
6129
|
+
type: 'lifecycle',
|
|
6130
|
+
message: `skill /${continuedSkillName} is not in the skill registry — its instructions were not re-sent to the continued session`,
|
|
6131
|
+
});
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
5469
6134
|
state.currentStepId = stepId;
|
|
5470
6135
|
this.beginUsageInvocation(runId, state, stepId);
|
|
5471
6136
|
// A continuation's opening message becomes the session's `userPrompt` and never passes
|
|
5472
6137
|
// through `deliverMessage`, so it needs the SAME delivery-only `/skill` rewrite the
|
|
5473
6138
|
// live path applies (#811). Delivery-only: the `user-message` event above already
|
|
5474
6139
|
// persisted the user's original text, and the transcript must keep showing that.
|
|
5475
|
-
const openingPrompt = expandRegistrySlashSkillText(prompt, state.skills ?? []);
|
|
6140
|
+
const openingPrompt = expandRegistrySlashSkillText(prompt, state.skills ?? []) + (setupNote ? `\n\n${setupNote}` : '');
|
|
5476
6141
|
state.agentSessionError = undefined;
|
|
5477
6142
|
let session;
|
|
5478
6143
|
if (this.workerExecutionStopped(runId)) {
|
|
@@ -5506,11 +6171,17 @@ export class RunManager {
|
|
|
5506
6171
|
state.unreadInputIds = new Set();
|
|
5507
6172
|
state.consumedBeforeAck = new Set();
|
|
5508
6173
|
try {
|
|
6174
|
+
const continuationTail = composeSystemPrompt(record?.systemPrompt, delegation?.instructions, artifactInstructions(continueProfile.env.CEZ_ARTIFACTS_DIR), generateFollowups ? HANDOFF_INSTRUCTIONS : HANDOFF_ONLY_INSTRUCTIONS);
|
|
5509
6175
|
session = startManagedSession(runner, {
|
|
5510
|
-
//
|
|
5511
|
-
//
|
|
5512
|
-
//
|
|
5513
|
-
|
|
6176
|
+
// Skill body (when resent), then the run's extra system prompt, then
|
|
6177
|
+
// the handoff contract — the same order as the opening session (#790).
|
|
6178
|
+
// In-thread resume omits the skill from systemPrompt; OpenCode's 404
|
|
6179
|
+
// fallback uses resumeFallbackSystemPrompt so the fresh session still
|
|
6180
|
+
// gets the playbook.
|
|
6181
|
+
systemPrompt: composeSystemPrompt(continuationSkillPrompt, continuationTail),
|
|
6182
|
+
...(skipSkillOnResume ? {
|
|
6183
|
+
resumeFallbackSystemPrompt: composeSystemPrompt(fallbackSkillPrompt, continuationTail),
|
|
6184
|
+
} : {}),
|
|
5514
6185
|
userPrompt: attachments.length
|
|
5515
6186
|
? `${openingPrompt}\n\n${pastedAttachmentsText(attachments)}`
|
|
5516
6187
|
: openingPrompt,
|
|
@@ -5530,12 +6201,14 @@ export class RunManager {
|
|
|
5530
6201
|
}, onEvent, {
|
|
5531
6202
|
onUiEvent: (event) => {
|
|
5532
6203
|
completedAssistantText = appendCompletedAssistantText(completedAssistantText, event);
|
|
6204
|
+
turnTail = trackTurnTail(turnTail, event);
|
|
5533
6205
|
if (event.type === 'turn.started')
|
|
5534
6206
|
this.recordOpeningAccepted(runId, state);
|
|
5535
6207
|
this.handleRunnerUiEvent(runId, state, sink, event);
|
|
5536
6208
|
},
|
|
5537
6209
|
onAgentInputReady: () => this.handleAgentInputReady(runId, state, session),
|
|
5538
6210
|
onAgentInputConsumed: (ids) => this.handleAgentInputConsumed(runId, state, session, ids),
|
|
6211
|
+
onPidChange: (pid) => { registerRunProcess(runId, pid); this.recordWorkerProcess(runId, pid); },
|
|
5539
6212
|
});
|
|
5540
6213
|
}
|
|
5541
6214
|
catch (error) {
|
|
@@ -5580,7 +6253,7 @@ export class RunManager {
|
|
|
5580
6253
|
this.store.appendEvent(runId, { type: 'lifecycle', message: 'run cancelled' });
|
|
5581
6254
|
appendHandoffHeartbeat(this.dataDir, runId, `step "${stepId}" complete — status=cancelled`);
|
|
5582
6255
|
}
|
|
5583
|
-
else if (!state.idleClosed) {
|
|
6256
|
+
else if (!state.idleClosed && !this.unansweredWorkflowAsk(runId, state)) {
|
|
5584
6257
|
this.store.updateStep(runId, stepId, { status: 'done', finishedAt: finishedAt() });
|
|
5585
6258
|
this.store.appendEvent(runId, { type: 'step-end', stepId, status: 'done' });
|
|
5586
6259
|
const completed = this.store.getRun(runId);
|
|
@@ -5589,7 +6262,10 @@ export class RunManager {
|
|
|
5589
6262
|
const waitingStep = completed.workflowDef.steps[waitingIndex];
|
|
5590
6263
|
this.store.updateStep(runId, waitingStep.id, { status: 'done', finishedAt: finishedAt() });
|
|
5591
6264
|
this.store.appendEvent(runId, { type: 'step-end', stepId: waitingStep.id, status: 'done' });
|
|
5592
|
-
|
|
6265
|
+
// Finish accepts this resumed step before the async settlement gate,
|
|
6266
|
+
// but must not launch the remaining workflow. A later cancellation
|
|
6267
|
+
// may cancel still-live steps, never this completed one (R30).
|
|
6268
|
+
if (!state.finishRequested && waitingIndex < completed.workflowDef.steps.length - 1) {
|
|
5593
6269
|
resumeWorkflow = {
|
|
5594
6270
|
workflow: completed.workflowDef,
|
|
5595
6271
|
startAt: waitingIndex + 1,
|
|
@@ -5622,7 +6298,7 @@ export class RunManager {
|
|
|
5622
6298
|
}
|
|
5623
6299
|
}
|
|
5624
6300
|
else {
|
|
5625
|
-
await this.
|
|
6301
|
+
await this.settleIdleClosedRun(runId, state);
|
|
5626
6302
|
}
|
|
5627
6303
|
}
|
|
5628
6304
|
catch (err) {
|
|
@@ -5696,7 +6372,7 @@ export class RunManager {
|
|
|
5696
6372
|
autonomous: input.autonomous === true,
|
|
5697
6373
|
autoContinues: 0,
|
|
5698
6374
|
};
|
|
5699
|
-
this.
|
|
6375
|
+
this.activate(runId, state);
|
|
5700
6376
|
this.starting.delete(runId);
|
|
5701
6377
|
const emit = (event) => this.store.appendEvent(runId, event);
|
|
5702
6378
|
// Resolve the agent backend for this run: the task choice (GUI) wins over
|
|
@@ -5872,6 +6548,9 @@ export class RunManager {
|
|
|
5872
6548
|
const seeded = this.store.getRun(runId);
|
|
5873
6549
|
if (seeded)
|
|
5874
6550
|
seedHandoffFile(this.dataDir, seeded);
|
|
6551
|
+
// Worktree setup (#917): before the first step, so the agent starts in a prepared tree. Its
|
|
6552
|
+
// note rides the first agent step's opening message only, like the task's attachments.
|
|
6553
|
+
let startSetupNote = await this.prepareWorktree(runId, state, config.worktreeSetup, emit, 'start');
|
|
5875
6554
|
const skills = await discoverSkills(this.repoRoot);
|
|
5876
6555
|
// Every ActiveRun construction site must carry the registry — `runContinuation` builds
|
|
5877
6556
|
// its own, and the one that skipped this leaked raw `/skill` text to the backend (#811).
|
|
@@ -5943,9 +6622,10 @@ export class RunManager {
|
|
|
5943
6622
|
// The last agent step of the workflow is interactive: after its turn
|
|
5944
6623
|
// the session stays open for follow-ups until finish/idle/cancel.
|
|
5945
6624
|
const interactive = i === lastAgentIdx && i === workflow.steps.length - 1;
|
|
5946
|
-
const failure = await this.runAgentStep(runId, state, step, input, skills, checkFailure, interactive, emit, startImages, taskBackend, extraSystemPrompt, chainStepNote(workflow.steps, i), startAttachments);
|
|
6625
|
+
const failure = await this.runAgentStep(runId, state, step, input, skills, checkFailure, interactive, emit, startImages, taskBackend, extraSystemPrompt, chainStepNote(workflow.steps, i), startAttachments, startSetupNote);
|
|
5947
6626
|
startImages = undefined;
|
|
5948
6627
|
startAttachments = [];
|
|
6628
|
+
startSetupNote = undefined;
|
|
5949
6629
|
checkFailure = null;
|
|
5950
6630
|
// Stale setup cannot finalize a replacement owner. Disposal also clears
|
|
5951
6631
|
// active, but must still honor a previously accepted Stop/Finish intent.
|
|
@@ -5970,9 +6650,12 @@ export class RunManager {
|
|
|
5970
6650
|
runError = `step "${step.id}" failed: ${failure}`;
|
|
5971
6651
|
break;
|
|
5972
6652
|
}
|
|
5973
|
-
if (state.idleClosed)
|
|
6653
|
+
if (state.idleClosed || this.unansweredWorkflowAsk(runId, state))
|
|
5974
6654
|
break;
|
|
5975
6655
|
this.finishStep(runId, step.id, 'done', undefined, emit);
|
|
6656
|
+
// Finish is an explicit stop, not an answer or permission to run checks.
|
|
6657
|
+
if (state.finishRequested)
|
|
6658
|
+
break;
|
|
5976
6659
|
i++;
|
|
5977
6660
|
continue;
|
|
5978
6661
|
}
|
|
@@ -6037,8 +6720,8 @@ export class RunManager {
|
|
|
6037
6720
|
this.store.updateRun(runId, { status: 'failed', error: runError, finishedAt, currentStepId: undefined });
|
|
6038
6721
|
emit({ type: 'lifecycle', message: `run failed — ${runError}` });
|
|
6039
6722
|
}
|
|
6040
|
-
else if (state.idleClosed) {
|
|
6041
|
-
await this.
|
|
6723
|
+
else if (state.idleClosed || this.unansweredWorkflowAsk(runId, state)) {
|
|
6724
|
+
await this.settleIdleClosedRun(runId, state);
|
|
6042
6725
|
this.dropActive(runId);
|
|
6043
6726
|
return;
|
|
6044
6727
|
}
|
|
@@ -6056,7 +6739,9 @@ export class RunManager {
|
|
|
6056
6739
|
/** Pasted attachments already materialized to disk (#357) — their absolute
|
|
6057
6740
|
* paths are appended to `userPrompt` so the agent can operate on the
|
|
6058
6741
|
* real files, not just view the inline image blocks. */
|
|
6059
|
-
attachments = []
|
|
6742
|
+
attachments = [],
|
|
6743
|
+
/** Worktree setup's paragraph for the agent (#917) — appended to the opening message. */
|
|
6744
|
+
openingNote) {
|
|
6060
6745
|
let systemPrompt;
|
|
6061
6746
|
if (step.skill) {
|
|
6062
6747
|
const skill = skills.find((s) => s.name === step.skill);
|
|
@@ -6117,6 +6802,8 @@ export class RunManager {
|
|
|
6117
6802
|
// location of.
|
|
6118
6803
|
if (attachments.length)
|
|
6119
6804
|
userPrompt += `\n\n${pastedAttachmentsText(attachments)}`;
|
|
6805
|
+
if (openingNote)
|
|
6806
|
+
userPrompt += `\n\n${openingNote}`;
|
|
6120
6807
|
const sessionId = randomUUID();
|
|
6121
6808
|
const backend = step.runner ?? taskBackend;
|
|
6122
6809
|
this.store.updateStep(runId, step.id, { sessionId, backend });
|
|
@@ -6124,6 +6811,7 @@ export class RunManager {
|
|
|
6124
6811
|
const startTokens = stepRecord?.tokensUsed ?? 0;
|
|
6125
6812
|
let turnText = '';
|
|
6126
6813
|
let completedAssistantText = '';
|
|
6814
|
+
let turnTail = 'none';
|
|
6127
6815
|
let sawClaudeScheduleWakeup = false;
|
|
6128
6816
|
let sessionError;
|
|
6129
6817
|
const sink = this.makeUiSink(runId, step.id);
|
|
@@ -6141,6 +6829,8 @@ export class RunManager {
|
|
|
6141
6829
|
}
|
|
6142
6830
|
if (event.type === 'text') {
|
|
6143
6831
|
this.clearUnreadInputTimer(state); // real content: the harness is working
|
|
6832
|
+
this.clearHeldHumanInputTimer(state);
|
|
6833
|
+
this.noteFinalMessageNudgeContent(runId, state, event);
|
|
6144
6834
|
turnText = appendTurnText(turnText, event.text);
|
|
6145
6835
|
const text = stripAskMarker(stripTaskMarkers(stripMonitoringMarker(stripDoneMarker(event.text))), false);
|
|
6146
6836
|
if (text)
|
|
@@ -6151,8 +6841,11 @@ export class RunManager {
|
|
|
6151
6841
|
this.retireUnreadInputs(runId, state, event.inputIds, true);
|
|
6152
6842
|
return;
|
|
6153
6843
|
}
|
|
6154
|
-
if (event.type === 'tool-call' || event.type === 'tool-result')
|
|
6844
|
+
if (event.type === 'tool-call' || event.type === 'tool-result') {
|
|
6155
6845
|
this.clearUnreadInputTimer(state);
|
|
6846
|
+
this.clearHeldHumanInputTimer(state);
|
|
6847
|
+
this.noteFinalMessageNudgeContent(runId, state, event);
|
|
6848
|
+
}
|
|
6156
6849
|
emit({ ...event, stepId: step.id });
|
|
6157
6850
|
if (event.type === 'cost')
|
|
6158
6851
|
this.recordReportedCost(runId, step.id, state, backend, event.usd);
|
|
@@ -6186,37 +6879,45 @@ export class RunManager {
|
|
|
6186
6879
|
sink.flushAll();
|
|
6187
6880
|
this.trackTurnEnd(runId, turnText); // titleSummary + diffStat (#389)
|
|
6188
6881
|
const sessionOpen = !state.cancelled && state.session?.open;
|
|
6189
|
-
const
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
const monitoring =
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
!state.pendingHumanAsk &&
|
|
6201
|
-
(MONITORING_MARKER_RE.test(turnText.trimEnd()) || sawClaudeScheduleWakeup || this.hasLiveWorkers(runId));
|
|
6882
|
+
const turn = classifyTurnEnd(turnText, completedAssistantText, {
|
|
6883
|
+
sessionOpen: !!sessionOpen, interactive, pendingHumanAsk: !!state.pendingHumanAsk,
|
|
6884
|
+
completionAttention: this.parentCompletionAttention(runId), scheduledWake: sawClaudeScheduleWakeup,
|
|
6885
|
+
liveWorkers: this.hasLiveWorkers(runId),
|
|
6886
|
+
dependencyWait: !!(this.workerWait(runId) || this.store.getRun(runId)?.ciWait),
|
|
6887
|
+
});
|
|
6888
|
+
const { done, monitoring, humanGate, ask, notes: askNotes } = turn;
|
|
6889
|
+
discardQueuedMessagesOnAsk(state.session, turn);
|
|
6890
|
+
const silentTail = turnTail === 'silent';
|
|
6891
|
+
const alreadyFinalMessageNudged = state.finalMessageNudged === state.session;
|
|
6892
|
+
state.finalMessageNudged = undefined;
|
|
6202
6893
|
turnText = '';
|
|
6203
6894
|
completedAssistantText = '';
|
|
6895
|
+
turnTail = 'none';
|
|
6204
6896
|
sawClaudeScheduleWakeup = false;
|
|
6205
6897
|
for (const note of askNotes)
|
|
6206
6898
|
emit({ type: 'note', stepId: step.id, ...note });
|
|
6207
6899
|
this.store.updateRun(runId, { invalidAsk: !ask && askNotes.length > 0 ? true : undefined });
|
|
6208
|
-
|
|
6900
|
+
// Only a newly parsed portable ASK can be overridden. Do this before
|
|
6901
|
+
// prepareHumanAsk latches it; an existing/native ask still blocks the helper.
|
|
6902
|
+
let autoContinued = !!ask && !!sessionOpen && this.tryAutonomousNudge(runId, state, step.id, ask);
|
|
6903
|
+
if (ask && !autoContinued)
|
|
6209
6904
|
this.prepareHumanAsk(runId, state);
|
|
6210
|
-
|
|
6905
|
+
if (humanGate && !ask && !state.pendingHumanAsk)
|
|
6906
|
+
this.prepareProseHumanGate(runId, state);
|
|
6907
|
+
const ciWaitParked = !humanGate && !!sessionOpen && this.parkCiWait(runId, state);
|
|
6211
6908
|
const completionBlocked = !ciWaitParked && !!done && this.deferParentCompletion(runId);
|
|
6212
|
-
const workerWaitParked = ciWaitParked || completionBlocked || (!!sessionOpen && this.parkWorkerWait(runId, state));
|
|
6909
|
+
const workerWaitParked = ciWaitParked || completionBlocked || (!humanGate && !!sessionOpen && this.parkWorkerWait(runId, state));
|
|
6213
6910
|
state.doneAtBoundary = done ? state.session : undefined;
|
|
6214
|
-
state.parkAfterAck = interactive && sessionOpen && state.session ? { session: state.session, monitoring
|
|
6911
|
+
state.parkAfterAck = (interactive || humanGate) && sessionOpen && state.session ? { session: state.session, monitoring, humanGate, silentTail, alreadyFinalMessageNudged } : undefined;
|
|
6215
6912
|
// #505: input the harness accepted but has not read yet runs as its next turn.
|
|
6216
6913
|
// An in-flight submission is settled by its acknowledgement (parkAfterAck), not here.
|
|
6217
|
-
const agentInputDelivered = !ask && !workerWaitParked && (this.flushAgentInputs(runId) || this.harnessOwesInput(state) || !!state.agentInputFlight);
|
|
6914
|
+
const agentInputDelivered = autoContinued || (!ask && !workerWaitParked && (this.flushAgentInputs(runId) || (!humanGate && this.harnessOwesInput(state)) || !!state.agentInputFlight || this.harnessHoldsHumanInput(state)));
|
|
6218
6915
|
if (!ask && !workerWaitParked)
|
|
6219
6916
|
this.armUnreadInputTimer(runId, state);
|
|
6917
|
+
if (!ask && !workerWaitParked && this.harnessHoldsHumanInput(state))
|
|
6918
|
+
this.armHeldHumanInputTimer(runId, state);
|
|
6919
|
+
else
|
|
6920
|
+
this.clearHeldHumanInputTimer(state);
|
|
6220
6921
|
if (state.agentInputError)
|
|
6221
6922
|
return;
|
|
6222
6923
|
if (done && !workerWaitParked && !state.pendingHumanAsk && !agentInputDelivered && !state.agentInputFlight && !this.hasQueuedAgentInputs(runId)) {
|
|
@@ -6227,8 +6928,14 @@ export class RunManager {
|
|
|
6227
6928
|
state.session?.end();
|
|
6228
6929
|
return;
|
|
6229
6930
|
}
|
|
6230
|
-
const waiting = (interactive || !!ask) && sessionOpen && !agentInputDelivered && !workerWaitParked && !this.workerWakeAdmitted.has(runId);
|
|
6231
|
-
if (waiting)
|
|
6931
|
+
const waiting = (interactive || !!ask || humanGate) && sessionOpen && !agentInputDelivered && !workerWaitParked && !this.workerWakeAdmitted.has(runId);
|
|
6932
|
+
if (waiting && interactive && !ask && !humanGate)
|
|
6933
|
+
autoContinued = this.tryAutonomousNudge(runId, state, step.id, null);
|
|
6934
|
+
if (waiting && interactive && !autoContinued)
|
|
6935
|
+
autoContinued = this.maybeFinalMessageNudge(runId, state, step.id, {
|
|
6936
|
+
ask: !!ask, humanGate, monitoring, silentTail, alreadyNudged: alreadyFinalMessageNudged,
|
|
6937
|
+
});
|
|
6938
|
+
if (waiting && !autoContinued && state.autonomousNudgePending !== state.session) {
|
|
6232
6939
|
// Turn over, session open. Either the ball is in the user's court
|
|
6233
6940
|
// (`waiting`) — optionally with a structured `CEZ:ASK` question the
|
|
6234
6941
|
// cockpit renders as an ask card (#473) — or the agent declared it is
|
|
@@ -6264,7 +6971,7 @@ export class RunManager {
|
|
|
6264
6971
|
}
|
|
6265
6972
|
// Cez's own heartbeat — the handoff stays current even when the
|
|
6266
6973
|
// agent forgets to write (spec 007).
|
|
6267
|
-
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${monitoring ? 'monitoring' : waiting ? 'waiting' : 'running'}`);
|
|
6974
|
+
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${autoContinued ? (state.finalMessageNudged === state.session ? 'running (final message nudge)' : 'running (autonomous nudge)') : state.autonomousNudgePending && state.autonomousNudgePending === state.session ? 'running (awaiting input readiness)' : agentInputDelivered ? 'running' : monitoring ? 'monitoring' : waiting ? 'waiting' : 'running'}`);
|
|
6268
6975
|
}
|
|
6269
6976
|
};
|
|
6270
6977
|
const stepBackend = step.runner ?? taskBackend;
|
|
@@ -6326,15 +7033,17 @@ export class RunManager {
|
|
|
6326
7033
|
const grants = stepIdentity?.grants ?? {
|
|
6327
7034
|
allowedTools: allowedToolsForStep(step, stepBackend), bashAllowlist: step.bashAllowlist,
|
|
6328
7035
|
};
|
|
7036
|
+
// Workers inherit only the extra prompt, never the parent's selected skill (#778).
|
|
6329
7037
|
state.delegationSettings = { cwd: state.cwd, runner: stepBackend, model: backendModel,
|
|
6330
7038
|
effort: effectiveEffort,
|
|
6331
|
-
agentProfile: stepProfile.profileId, accountBinding: stepProfile.accountBinding, systemPrompt:
|
|
7039
|
+
agentProfile: stepProfile.profileId, accountBinding: stepProfile.accountBinding, systemPrompt: extraSystemPrompt,
|
|
6332
7040
|
allowedTools: grants.allowedTools, bashAllowlist: grants.bashAllowlist };
|
|
6333
7041
|
const runner = createRunner(stepBackend);
|
|
6334
7042
|
state.agentSessionError = undefined;
|
|
6335
7043
|
let session;
|
|
6336
7044
|
const shouldAutoEnd = () => this.active.get(runId) !== state || state.session !== session ||
|
|
6337
|
-
(
|
|
7045
|
+
(state.atTurnBoundary === session && !state.pendingHumanAsk && !this.hasPendingHumanAsk(runId) &&
|
|
7046
|
+
!this.workerWait(runId) && !this.store.getRun(runId)?.ciWait && !this.parentCompletionPending(runId) &&
|
|
6338
7047
|
state.workerWakeTurn !== session && state.ciWakeTurn !== session && !state.agentInputFlight &&
|
|
6339
7048
|
!this.hasQueuedAgentInputs(runId));
|
|
6340
7049
|
state.currentStepId = step.id;
|
|
@@ -6385,14 +7094,18 @@ export class RunManager {
|
|
|
6385
7094
|
timeoutMs: step.timeoutMs ?? 0,
|
|
6386
7095
|
}, onEvent, {
|
|
6387
7096
|
autoEndAfterFirstTurn: !interactive,
|
|
6388
|
-
//
|
|
7097
|
+
// Questions and unread inbox claims must survive this close window.
|
|
7098
|
+
// Keep the native timer: the answer's completed turn rearms it, and
|
|
7099
|
+
// admission/worker/CI guards retain authority over when it may close.
|
|
6389
7100
|
shouldAutoEnd,
|
|
6390
7101
|
onUiEvent: (event) => {
|
|
6391
7102
|
completedAssistantText = appendCompletedAssistantText(completedAssistantText, event);
|
|
7103
|
+
turnTail = trackTurnTail(turnTail, event);
|
|
6392
7104
|
this.handleRunnerUiEvent(runId, state, sink, event);
|
|
6393
7105
|
},
|
|
6394
7106
|
onAgentInputReady: () => this.handleAgentInputReady(runId, state, session),
|
|
6395
7107
|
onAgentInputConsumed: (ids) => this.handleAgentInputConsumed(runId, state, session, ids),
|
|
7108
|
+
onPidChange: (pid) => { registerRunProcess(runId, pid); this.recordWorkerProcess(runId, pid); },
|
|
6396
7109
|
});
|
|
6397
7110
|
}
|
|
6398
7111
|
catch (err) {
|
|
@@ -6517,6 +7230,7 @@ export class RunManager {
|
|
|
6517
7230
|
/** Ask precedence also releases an admitted but not yet delivered worker wake.
|
|
6518
7231
|
* Its durable input/wait remain queued until an actual human answer. */
|
|
6519
7232
|
prepareHumanAsk(runId, state) {
|
|
7233
|
+
state.autonomousNudgePending = undefined;
|
|
6520
7234
|
this.withdrawCiWait(runId);
|
|
6521
7235
|
state.pendingHumanAsk = true;
|
|
6522
7236
|
this.workerWaiting.delete(runId);
|
|
@@ -6527,6 +7241,17 @@ export class RunManager {
|
|
|
6527
7241
|
this.queue.splice(at, 1);
|
|
6528
7242
|
}
|
|
6529
7243
|
}
|
|
7244
|
+
/** A prose or rejected-ASK fallback supersedes a registered autonomous wait. Keep the
|
|
7245
|
+
* checkpoint through idle closure/restart and hold worker input until a
|
|
7246
|
+
* successful human delivery, just as for a structured ask. */
|
|
7247
|
+
prepareProseHumanGate(runId, state) {
|
|
7248
|
+
this.store.appendEvent(runId, { type: 'note', stepId: state.currentStepId,
|
|
7249
|
+
code: PROSE_HUMAN_GATE, message: 'waiting for a human response to the assistant’s request' });
|
|
7250
|
+
this.prepareHumanAsk(runId, state);
|
|
7251
|
+
this.withdrawWorkerWait(runId);
|
|
7252
|
+
this.clearUnreadInputTimer(state);
|
|
7253
|
+
this.clearHeldHumanInputTimer(state);
|
|
7254
|
+
}
|
|
6530
7255
|
/** Native backend asks arrive before turn-end. Persist and park immediately
|
|
6531
7256
|
* so the cockpit shows attention and the run releases its workspace slot. */
|
|
6532
7257
|
handleRunnerUiEvent(runId, state, sink, event) {
|
|
@@ -6536,7 +7261,13 @@ export class RunManager {
|
|
|
6536
7261
|
sink.handle(event);
|
|
6537
7262
|
if (state.cancelled)
|
|
6538
7263
|
return;
|
|
7264
|
+
this.noteFinalMessageNudgeContent(runId, state, event);
|
|
6539
7265
|
if (isRunnerActivity(event)) {
|
|
7266
|
+
if (state.autonomousNudgePending) {
|
|
7267
|
+
state.autonomousNudgePending = undefined;
|
|
7268
|
+
this.clearIdleTimer(state);
|
|
7269
|
+
}
|
|
7270
|
+
this.clearHeldHumanInputTimer(state);
|
|
6540
7271
|
state.atTurnBoundary = undefined;
|
|
6541
7272
|
state.doneAtBoundary = undefined;
|
|
6542
7273
|
state.parkAfterAck = undefined;
|
|
@@ -6860,6 +7591,8 @@ export class RunManager {
|
|
|
6860
7591
|
* off — settle straight to `done`, leaving the diff in the worktree untouched.
|
|
6861
7592
|
*/
|
|
6862
7593
|
async settleSuccess(runId, durableRootFinish = false) {
|
|
7594
|
+
const state = this.active.get(runId);
|
|
7595
|
+
const continuationGeneration = this.continuationGenerations.get(runId);
|
|
6863
7596
|
if (this.deferParentCompletion(runId))
|
|
6864
7597
|
return;
|
|
6865
7598
|
const run = this.store.getRun(runId);
|
|
@@ -6872,7 +7605,29 @@ export class RunManager {
|
|
|
6872
7605
|
const config = await loadConfig(this.repoRoot);
|
|
6873
7606
|
review = hasDiff && reviewGateEnabled(config) && run.autonomous !== true;
|
|
6874
7607
|
}
|
|
6875
|
-
// Diff/config I/O can race
|
|
7608
|
+
// Diff/config I/O can race cancellation or a replacement execution. Re-read
|
|
7609
|
+
// the record before touching steps, and never settle another session's work.
|
|
7610
|
+
const current = this.store.getRun(runId);
|
|
7611
|
+
const active = this.active.get(runId);
|
|
7612
|
+
// Disposal clears active, but cannot revoke an already accepted Finish/Stop.
|
|
7613
|
+
// A different active state is still a replacement, even after disposal.
|
|
7614
|
+
const disposedWithTerminalIntent = this.disposed && active === undefined && (state?.finishRequested || state?.cancelled);
|
|
7615
|
+
if (!current || !['queued', 'running', 'waiting'].includes(current.status) ||
|
|
7616
|
+
this.continuationGenerations.get(runId) !== continuationGeneration ||
|
|
7617
|
+
(active !== state && !disposedWithTerminalIntent))
|
|
7618
|
+
return;
|
|
7619
|
+
if (state?.cancelled) {
|
|
7620
|
+
const finishedAt = new Date().toISOString();
|
|
7621
|
+
for (const step of current.steps) {
|
|
7622
|
+
if (step.status === 'running' || step.status === 'waiting') {
|
|
7623
|
+
this.store.updateStep(runId, step.id, { status: 'cancelled', finishedAt: step.finishedAt ?? finishedAt });
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
this.store.updateRun(runId, { status: 'cancelled', finishedAt, currentStepId: undefined });
|
|
7627
|
+
this.store.appendEvent(runId, { type: 'lifecycle', message: 'run cancelled' });
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
// A worker's accepted continuation or a new child can defer parent completion.
|
|
6876
7631
|
if (this.deferParentCompletion(runId))
|
|
6877
7632
|
return;
|
|
6878
7633
|
if (durableRootFinish) {
|
|
@@ -6882,9 +7637,17 @@ export class RunManager {
|
|
|
6882
7637
|
return;
|
|
6883
7638
|
}
|
|
6884
7639
|
else {
|
|
7640
|
+
const finishedAt = new Date().toISOString();
|
|
7641
|
+
// An idle-closed Continue remains waiting until the whole task succeeds.
|
|
7642
|
+
// Complete those intermediate steps before publishing terminal run status.
|
|
7643
|
+
for (const step of this.store.getRun(runId)?.steps ?? []) {
|
|
7644
|
+
if (step.status === 'running' || step.status === 'waiting') {
|
|
7645
|
+
this.store.updateStep(runId, step.id, { status: 'done', finishedAt: step.finishedAt ?? finishedAt });
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
6885
7648
|
this.store.updateRun(runId, {
|
|
6886
7649
|
status: review ? 'review' : 'done',
|
|
6887
|
-
finishedAt
|
|
7650
|
+
finishedAt,
|
|
6888
7651
|
currentStepId: undefined,
|
|
6889
7652
|
// A run that got all the way to a settled turn is not in a limit loop, so the resume
|
|
6890
7653
|
// counter starts over — otherwise a task that legitimately met the limit once a week would
|
|
@@ -6899,6 +7662,25 @@ export class RunManager {
|
|
|
6899
7662
|
: 'run finished',
|
|
6900
7663
|
});
|
|
6901
7664
|
}
|
|
7665
|
+
/** Called only after session/process settlement. A refused nudge retained its
|
|
7666
|
+
* slot and running status until idle close; ordinary roots must now become
|
|
7667
|
+
* resumable. Previously parked roots and delegated lifecycle policy stay intact. */
|
|
7668
|
+
async settleIdleClosedRun(runId, state) {
|
|
7669
|
+
const run = this.store.getRun(runId);
|
|
7670
|
+
if (run?.delegation?.role === 'worker') {
|
|
7671
|
+
await this.settleIdleClosedWorker(runId);
|
|
7672
|
+
return;
|
|
7673
|
+
}
|
|
7674
|
+
if (!run || !state.autonomousNudgePending || run.ciWait || this.workerWait(runId) || this.hasPendingHumanAsk(runId))
|
|
7675
|
+
return;
|
|
7676
|
+
for (const step of run.steps) {
|
|
7677
|
+
if (step.status === 'running')
|
|
7678
|
+
this.store.updateStep(runId, step.id, { status: 'waiting' });
|
|
7679
|
+
}
|
|
7680
|
+
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
7681
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'session idle-closed — status=waiting; Continue to resume');
|
|
7682
|
+
this.store.flush();
|
|
7683
|
+
}
|
|
6902
7684
|
/** An ordinary owned worker cannot accept an inactive Continue. Once its
|
|
6903
7685
|
* idle-closed process has ended, publish the same terminal outcome recovery
|
|
6904
7686
|
* would produce so its parent can collect it. Explicit asks and worker waits
|
|
@@ -7005,6 +7787,161 @@ export class RunManager {
|
|
|
7005
7787
|
return null;
|
|
7006
7788
|
}
|
|
7007
7789
|
}
|
|
7790
|
+
/** Shared stop/input/liveness guards for an automatic continuation. */
|
|
7791
|
+
canAutoContinue(runId, state) {
|
|
7792
|
+
const run = this.store.getRun(runId);
|
|
7793
|
+
return !!run && !this.disposed && !run.stopping && !state.cancelled && !state.finishRequested &&
|
|
7794
|
+
!state.agentInputError && !state.pendingHumanAsk && !this.hasPendingHumanAsk(runId) && !!state.session?.open &&
|
|
7795
|
+
!this.workerExecutionStopped(runId) && !this.executionBlockedByRootFinish(run) && !this.parentCompletionPending(runId) &&
|
|
7796
|
+
!this.workerWait(runId) && !run.ciWait && !this.workerWakeAdmitted.has(runId) &&
|
|
7797
|
+
!state.agentInputFlight && !this.harnessOwesInput(state) && !this.hasQueuedAgentInputs(runId);
|
|
7798
|
+
}
|
|
7799
|
+
/**
|
|
7800
|
+
* Both turn-end paths keep a nudged session running, with no user-wait timer or
|
|
7801
|
+
* released slot. The next turn can finish with DONE, nudge again, or park at the
|
|
7802
|
+
* cap (waiting/monitoring with their existing wake/idle rules). Queued/accepted
|
|
7803
|
+
* input, worker/CI waits and parent completion keep priority. Cancel, Finish,
|
|
7804
|
+
* disposal and memory pause stop nudging; provider errors/exits use normal
|
|
7805
|
+
* teardown. Native mid-turn asks still park for an answer (outside this policy).
|
|
7806
|
+
* A refused ordinary nudge waits for readiness at this exact boundary; the idle
|
|
7807
|
+
* timeout bounds a missing readiness signal. New activity/input invalidates it.
|
|
7808
|
+
* A refused portable override falls back to normal question parking.
|
|
7809
|
+
*/
|
|
7810
|
+
tryAutonomousNudge(runId, state, stepId, ask) {
|
|
7811
|
+
const session = state.session;
|
|
7812
|
+
if (!state.autonomous || !session || !this.canAutoContinue(runId, state))
|
|
7813
|
+
return false;
|
|
7814
|
+
state.autonomousNudgePending = undefined;
|
|
7815
|
+
this.clearIdleTimer(state);
|
|
7816
|
+
if ((state.autoContinues ?? 0) >= MAX_AUTO_CONTINUES) {
|
|
7817
|
+
this.store.appendEvent(runId, { type: 'note', stepId, message: `autonomous — safety cap reached (${MAX_AUTO_CONTINUES}); automatic continuation paused` });
|
|
7818
|
+
return false;
|
|
7819
|
+
}
|
|
7820
|
+
const content = [{ type: 'text', text: AUTONOMOUS_NUDGE }];
|
|
7821
|
+
// sendAgentMessage deliberately refuses portable asks on some native wires.
|
|
7822
|
+
// The existing answer transport clears that marker, but is safe ONLY for this
|
|
7823
|
+
// fresh turn-end ASK: the guards above exclude every pending native/persisted
|
|
7824
|
+
// question. Do not route via deliverMessage, which attributes a human answer.
|
|
7825
|
+
// Ordinary nudges keep #505's acknowledged agent-input failure semantics.
|
|
7826
|
+
const sent = ask ? session.sendMessage(content) : this.submitAgentInput(runId, state, content);
|
|
7827
|
+
if (!sent) {
|
|
7828
|
+
// A portable override uses sendMessage: its POST may still be awaiting ACK
|
|
7829
|
+
// after the next SSE turn ends. It is not an agentInputFlight. Keep this
|
|
7830
|
+
// exact boundary running until onAgentInputReady retries, without charging
|
|
7831
|
+
// the cap or claiming delivery. Missing readiness closes via the idle bound.
|
|
7832
|
+
if (!ask && session.open && !state.agentInputError && !state.agentSessionError) {
|
|
7833
|
+
state.autonomousNudgePending = session;
|
|
7834
|
+
this.armIdleTimer(runId, state);
|
|
7835
|
+
}
|
|
7836
|
+
return false;
|
|
7837
|
+
}
|
|
7838
|
+
if (ask)
|
|
7839
|
+
this.resumeParkedRun(runId, state);
|
|
7840
|
+
state.autoContinues = (state.autoContinues ?? 0) + 1;
|
|
7841
|
+
this.store.appendEvent(runId, {
|
|
7842
|
+
type: 'note', stepId,
|
|
7843
|
+
message: `autonomous — continuing without pausing (${state.autoContinues}/${MAX_AUTO_CONTINUES})`,
|
|
7844
|
+
});
|
|
7845
|
+
// The marker is stripped from visible text. Retain the question as a note,
|
|
7846
|
+
// never an ask card or a fabricated human-input-delivered acknowledgement.
|
|
7847
|
+
if (ask)
|
|
7848
|
+
this.store.appendEvent(runId, {
|
|
7849
|
+
type: 'note', stepId,
|
|
7850
|
+
message: `autonomous — question overridden by the auto-continue nudge: ${ask.questions.map(question => question.question).join(' | ')}`,
|
|
7851
|
+
});
|
|
7852
|
+
return true;
|
|
7853
|
+
}
|
|
7854
|
+
/** Shared silent-tail conditions for the one-shot visible-message nudge. Callers must
|
|
7855
|
+
* invoke this only when nothing else already continued. */
|
|
7856
|
+
maybeFinalMessageNudge(runId, state, stepId, turn) {
|
|
7857
|
+
if (turn.ask || turn.humanGate || turn.monitoring || !turn.silentTail || turn.alreadyNudged)
|
|
7858
|
+
return false;
|
|
7859
|
+
if (state.autonomousNudgePending === state.session)
|
|
7860
|
+
return false;
|
|
7861
|
+
return this.tryFinalMessageNudge(runId, state, stepId);
|
|
7862
|
+
}
|
|
7863
|
+
/** One-shot visible-message nudge when a turn ends on reasoning or a tool (#544, #119).
|
|
7864
|
+
* Liveness: this path has no inputIds, so #505's unread-input timer does not apply, and
|
|
7865
|
+
* resumeParkedRun / clearIdleTimer drop the user-wait idle bound. Do not re-arm idle until
|
|
7866
|
+
* content arrives — some harnesses omit `turn.started` and still run (N8); idle would then
|
|
7867
|
+
* kill a live turn. OpenCode heartbeats re-arm the managed-session no-progress guard without
|
|
7868
|
+
* opening a turn, so an ACK that stays silent is bounded by FINAL_MESSAGE_NUDGE_REPLY_MS:
|
|
7869
|
+
* parent-level v1 content (text, tool-call, tool-result) and UiEvent items count at any time,
|
|
7870
|
+
* including before the timer is armed; `turn.started` counts after arm or while
|
|
7871
|
+
* `finalMessageNudgeParked` matches the session (OpenCode/Cursor emit a synthetic one inside
|
|
7872
|
+
* sendAgentMessage). Nested `item.*` with `parentItemId` is ignored.
|
|
7873
|
+
* Expiry parks `waiting` and latches `finalMessageNudgeParked`; a late reply from that session
|
|
7874
|
+
* resumes. User-authored delivery resets the one-shot latch; expiry does not. */
|
|
7875
|
+
tryFinalMessageNudge(runId, state, stepId) {
|
|
7876
|
+
if (state.finalMessageNudged === state.session || !this.canAutoContinue(runId, state))
|
|
7877
|
+
return false;
|
|
7878
|
+
state.finalMessageNudgeSawContent = false;
|
|
7879
|
+
const sent = this.submitAgentInput(runId, state, [{ type: 'text', text: FINAL_MESSAGE_NUDGE }]);
|
|
7880
|
+
if (!sent)
|
|
7881
|
+
return false;
|
|
7882
|
+
state.finalMessageNudged = state.session;
|
|
7883
|
+
this.clearIdleTimer(state);
|
|
7884
|
+
this.armFinalMessageNudgeReplyTimer(runId, state);
|
|
7885
|
+
this.store.appendEvent(runId, { type: 'note', stepId, message: 'no final message — asking the agent for a visible reply' });
|
|
7886
|
+
return true;
|
|
7887
|
+
}
|
|
7888
|
+
/** Heartbeats and nested subagent items are not a reply. OpenCode/Cursor synthetic `turn.started` inside `sendAgentMessage` cannot false-resume after expiry because a later send goes through `submitAgentInput` → `resumeParkedRun`. */
|
|
7889
|
+
isFinalMessageNudgeReply(state, event) {
|
|
7890
|
+
if (event.type === 'turn.started')
|
|
7891
|
+
return state.finalMessageNudgeReplyTimer !== undefined || state.finalMessageNudgeParked === state.session;
|
|
7892
|
+
if (event.type === 'text' || event.type === 'tool-call' || event.type === 'tool-result')
|
|
7893
|
+
return true;
|
|
7894
|
+
if (event.type === 'item.delta' || !event.type.startsWith('item.'))
|
|
7895
|
+
return false;
|
|
7896
|
+
return event.item?.parentItemId === undefined;
|
|
7897
|
+
}
|
|
7898
|
+
noteFinalMessageNudgeContent(runId, state, event) {
|
|
7899
|
+
if (!this.isFinalMessageNudgeReply(state, event))
|
|
7900
|
+
return;
|
|
7901
|
+
state.finalMessageNudgeSawContent = true;
|
|
7902
|
+
this.clearFinalMessageNudgeReplyTimer(state);
|
|
7903
|
+
if (state.finalMessageNudgeParked !== state.session || !this.waiting.has(runId))
|
|
7904
|
+
return;
|
|
7905
|
+
if (state.pendingHumanAsk || this.hasPendingHumanAsk(runId))
|
|
7906
|
+
return;
|
|
7907
|
+
this.resumeParkedRun(runId, state);
|
|
7908
|
+
state.finalMessageNudgeParked = undefined;
|
|
7909
|
+
}
|
|
7910
|
+
armFinalMessageNudgeReplyTimer(runId, state) {
|
|
7911
|
+
this.clearFinalMessageNudgeReplyTimer(state);
|
|
7912
|
+
if (state.finalMessageNudgeSawContent)
|
|
7913
|
+
return;
|
|
7914
|
+
const session = state.session;
|
|
7915
|
+
if (!session?.open)
|
|
7916
|
+
return;
|
|
7917
|
+
const timer = setTimeout(() => {
|
|
7918
|
+
state.finalMessageNudgeReplyTimer = undefined;
|
|
7919
|
+
if (this.disposed || this.active.get(runId) !== state || state.session !== session || !session.open ||
|
|
7920
|
+
state.cancelled || state.finishRequested || this.waiting.has(runId) || this.monitoring.has(runId))
|
|
7921
|
+
return;
|
|
7922
|
+
state.finalMessageNudgeParked = session;
|
|
7923
|
+
this.store.appendEvent(runId, {
|
|
7924
|
+
type: 'note', stepId: state.currentStepId,
|
|
7925
|
+
message: 'no reply to the final-message nudge — waiting for you',
|
|
7926
|
+
});
|
|
7927
|
+
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
7928
|
+
if (state.currentStepId)
|
|
7929
|
+
this.store.updateStep(runId, state.currentStepId, { status: 'waiting' });
|
|
7930
|
+
this.store.flush();
|
|
7931
|
+
this.waiting.add(runId);
|
|
7932
|
+
this.armIdleTimer(runId, state);
|
|
7933
|
+
this.releaseSlot();
|
|
7934
|
+
appendHandoffHeartbeat(this.dataDir, runId, 'turn complete — status=waiting');
|
|
7935
|
+
}, this.finalMessageNudgeReplyMs);
|
|
7936
|
+
timer.unref?.();
|
|
7937
|
+
state.finalMessageNudgeReplyTimer = timer;
|
|
7938
|
+
}
|
|
7939
|
+
clearFinalMessageNudgeReplyTimer(state) {
|
|
7940
|
+
if (state.finalMessageNudgeReplyTimer) {
|
|
7941
|
+
clearTimeout(state.finalMessageNudgeReplyTimer);
|
|
7942
|
+
state.finalMessageNudgeReplyTimer = undefined;
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7008
7945
|
armIdleTimer(runId, state) {
|
|
7009
7946
|
this.clearIdleTimer(state);
|
|
7010
7947
|
state.idleClosed = false;
|
|
@@ -7025,6 +7962,7 @@ export class RunManager {
|
|
|
7025
7962
|
clearTimeout(state.idleTimer);
|
|
7026
7963
|
state.idleTimer = undefined;
|
|
7027
7964
|
}
|
|
7965
|
+
this.clearFinalMessageNudgeReplyTimer(state);
|
|
7028
7966
|
}
|
|
7029
7967
|
reconcileMonitoringWakeTimers() {
|
|
7030
7968
|
for (const runId of this.monitoring) {
|
|
@@ -7136,114 +8074,94 @@ export class RunManager {
|
|
|
7136
8074
|
state.autosaveTimer = undefined;
|
|
7137
8075
|
}
|
|
7138
8076
|
}
|
|
7139
|
-
|
|
8077
|
+
/**
|
|
8078
|
+
* Worktree setup (#917, spec `.ai/specs/2026-10-07-worktree-setup.md`): run the project's
|
|
8079
|
+
* `worktreeSetup` commands in this run's isolated worktree when it needs them, and return the
|
|
8080
|
+
* paragraph for the agent's opening message. `start` is a new task or worker, `rematerialized`
|
|
8081
|
+
* a Continue that rebuilt a reclaimed directory, and `resume` any other Continue, which only
|
|
8082
|
+
* finishes a setup a restart interrupted. In-place and non-Git runs never get setup: that tree
|
|
8083
|
+
* is the user's own checkout.
|
|
8084
|
+
*/
|
|
8085
|
+
async prepareWorktree(runId, state, raw, emit, trigger) {
|
|
8086
|
+
const record = this.store.getRun(runId);
|
|
8087
|
+
if (!record || state.cwd === this.repoRoot)
|
|
8088
|
+
return undefined;
|
|
8089
|
+
const plan = resolveWorktreeSetup(raw);
|
|
8090
|
+
const interrupted = record.worktreeSetup?.status === 'running';
|
|
8091
|
+
if (plan.kind === 'none') {
|
|
8092
|
+
// A restart interrupted setup the project has since removed: nothing is left to finish.
|
|
8093
|
+
if (interrupted)
|
|
8094
|
+
this.store.updateRun(runId, { worktreeSetup: undefined });
|
|
8095
|
+
return undefined;
|
|
8096
|
+
}
|
|
8097
|
+
const fresh = trigger === 'start' && record.worktreeSetup === undefined &&
|
|
8098
|
+
!record.steps.some((step) => step.kind === 'agent' && step.iterations > 0);
|
|
8099
|
+
if (!interrupted && !fresh && trigger !== 'rematerialized')
|
|
8100
|
+
return undefined;
|
|
8101
|
+
const startedAt = new Date().toISOString();
|
|
8102
|
+
let outcome;
|
|
8103
|
+
if (plan.kind === 'invalid') {
|
|
8104
|
+
outcome = { status: 'invalid', issue: plan.issue };
|
|
8105
|
+
}
|
|
8106
|
+
else {
|
|
8107
|
+
// Durable before the first command: recovery reads `running` to requeue a run a restart
|
|
8108
|
+
// interrupted here, instead of failing it with no agent session to resume.
|
|
8109
|
+
this.store.updateRun(runId, { worktreeSetup: { status: 'running', startedAt } });
|
|
8110
|
+
this.store.flush();
|
|
8111
|
+
emit({ type: 'note', message: worktreeSetupStartNote(plan.commands.length) });
|
|
8112
|
+
let tmpEnv = {};
|
|
8113
|
+
try {
|
|
8114
|
+
tmpEnv = agentTmpEnv(this.dataDir, runId);
|
|
8115
|
+
}
|
|
8116
|
+
catch (err) {
|
|
8117
|
+
if (!(err instanceof AgentTempDirError))
|
|
8118
|
+
throw err;
|
|
8119
|
+
}
|
|
8120
|
+
outcome = await runWorktreeSetup({
|
|
8121
|
+
plan,
|
|
8122
|
+
cwd: state.cwd,
|
|
8123
|
+
env: worktreeSetupEnv({ projectRoot: this.repoRoot, runId, tmpEnv }),
|
|
8124
|
+
setInterrupt: (stop) => { state.interrupt = stop; },
|
|
8125
|
+
isCancelled: () => state.cancelled,
|
|
8126
|
+
onCommandResult: (result) => emit({
|
|
8127
|
+
type: 'check-output',
|
|
8128
|
+
command: result.command,
|
|
8129
|
+
text: result.output,
|
|
8130
|
+
exitCode: result.exitCode ?? -1,
|
|
8131
|
+
}),
|
|
8132
|
+
});
|
|
8133
|
+
}
|
|
8134
|
+
const finishedAt = new Date().toISOString();
|
|
8135
|
+
const error = worktreeSetupRecordError(outcome);
|
|
8136
|
+
this.store.updateRun(runId, {
|
|
8137
|
+
worktreeSetup: {
|
|
8138
|
+
status: outcome.status === 'done' ? 'done' : 'failed',
|
|
8139
|
+
startedAt,
|
|
8140
|
+
finishedAt,
|
|
8141
|
+
durationMs: 'durationMs' in outcome ? outcome.durationMs : 0,
|
|
8142
|
+
...(error !== undefined ? { error } : {}),
|
|
8143
|
+
},
|
|
8144
|
+
});
|
|
8145
|
+
const endNote = worktreeSetupEndNote(outcome);
|
|
8146
|
+
emit({ type: 'note', message: endNote });
|
|
8147
|
+
appendHandoffHeartbeat(this.dataDir, runId, endNote);
|
|
8148
|
+
return worktreeSetupAgentNote(outcome);
|
|
8149
|
+
}
|
|
8150
|
+
async runCheckStep(state, step, emit) {
|
|
7140
8151
|
const command = step.command;
|
|
7141
8152
|
emit({ type: 'note', stepId: step.id, message: `$ ${command}` });
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
let stopping = false;
|
|
7152
|
-
let killTimer;
|
|
7153
|
-
let stopAt = 0;
|
|
7154
|
-
const signalGroup = (signal) => {
|
|
7155
|
-
if (finished || !pid)
|
|
7156
|
-
return;
|
|
7157
|
-
try {
|
|
7158
|
-
if (grouped)
|
|
7159
|
-
process.kill(-pid, signal);
|
|
7160
|
-
else
|
|
7161
|
-
child.kill(signal);
|
|
7162
|
-
}
|
|
7163
|
-
catch { /* already gone */ }
|
|
7164
|
-
};
|
|
7165
|
-
const groupAlive = () => {
|
|
7166
|
-
if (!grouped || !pid)
|
|
7167
|
-
return false;
|
|
7168
|
-
try {
|
|
7169
|
-
process.kill(-pid, 0);
|
|
7170
|
-
return true;
|
|
7171
|
-
}
|
|
7172
|
-
catch (err) {
|
|
7173
|
-
return err.code === 'EPERM';
|
|
7174
|
-
}
|
|
7175
|
-
};
|
|
7176
|
-
const finish = (code) => {
|
|
7177
|
-
if (finished)
|
|
7178
|
-
return;
|
|
7179
|
-
finished = true;
|
|
7180
|
-
if (killTimer)
|
|
7181
|
-
clearTimeout(killTimer);
|
|
7182
|
-
state.interrupt = () => undefined;
|
|
7183
|
-
child.stdout?.destroy();
|
|
7184
|
-
child.stderr?.destroy();
|
|
7185
|
-
const trimmed = output.trim() || '(no output)';
|
|
7186
|
-
emit({ type: 'check-output', stepId: step.id, command, text: trimmed, exitCode: code ?? -1 });
|
|
7187
|
-
resolve({ ok: code === 0, output: trimmed });
|
|
7188
|
-
};
|
|
7189
|
-
// After Stop: the KILL timer escalates; resolve only once the group is
|
|
7190
|
-
// confirmed gone (or the confirmation bound passes, reported honestly).
|
|
7191
|
-
let reaping = false;
|
|
7192
|
-
const confirmTermination = async (code) => {
|
|
7193
|
-
if (reaping)
|
|
7194
|
-
return;
|
|
7195
|
-
reaping = true;
|
|
7196
|
-
const deadline = stopAt + CHECK_KILL_GRACE_MS + CHECK_TERMINATION_CONFIRM_MS;
|
|
7197
|
-
while (groupAlive() && Date.now() < deadline)
|
|
7198
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
7199
|
-
if (groupAlive()) {
|
|
7200
|
-
output += '\n… (process group did not confirm termination)';
|
|
7201
|
-
}
|
|
7202
|
-
finish(code);
|
|
7203
|
-
};
|
|
7204
|
-
state.interrupt = () => {
|
|
7205
|
-
if (stopping || finished)
|
|
7206
|
-
return;
|
|
7207
|
-
stopping = true;
|
|
7208
|
-
stopAt = Date.now();
|
|
7209
|
-
signalGroup('SIGTERM');
|
|
7210
|
-
killTimer = setTimeout(() => signalGroup('SIGKILL'), CHECK_KILL_GRACE_MS);
|
|
7211
|
-
killTimer.unref?.();
|
|
7212
|
-
// Leader already gone but a descendant holds the pipes: no `exit` will follow.
|
|
7213
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
7214
|
-
void confirmTermination(child.exitCode);
|
|
7215
|
-
};
|
|
7216
|
-
const collect = (chunk) => {
|
|
7217
|
-
if (output.length < CHECK_OUTPUT_CAP) {
|
|
7218
|
-
output += chunk.toString('utf8');
|
|
7219
|
-
if (output.length >= CHECK_OUTPUT_CAP)
|
|
7220
|
-
output += '\n… (output truncated)';
|
|
7221
|
-
}
|
|
7222
|
-
};
|
|
7223
|
-
child.stdout.on('data', collect);
|
|
7224
|
-
child.stderr.on('data', collect);
|
|
7225
|
-
child.on('error', (err) => {
|
|
7226
|
-
finished = true;
|
|
7227
|
-
if (killTimer)
|
|
7228
|
-
clearTimeout(killTimer);
|
|
7229
|
-
state.interrupt = () => undefined;
|
|
7230
|
-
const message = `failed to spawn: ${err.message}`;
|
|
7231
|
-
emit({ type: 'check-output', stepId: step.id, command, text: message, exitCode: -1 });
|
|
7232
|
-
resolve({ ok: false, output: message });
|
|
7233
|
-
});
|
|
7234
|
-
// A descendant can hold the inherited pipes open, so `close` may never
|
|
7235
|
-
// fire after Stop. `exit` of the leader is the signal to reap the group.
|
|
7236
|
-
child.on('exit', (code) => {
|
|
7237
|
-
if (stopping)
|
|
7238
|
-
void confirmTermination(code);
|
|
7239
|
-
});
|
|
7240
|
-
child.on('close', (code) => {
|
|
7241
|
-
if (stopping)
|
|
7242
|
-
void confirmTermination(code);
|
|
7243
|
-
else
|
|
7244
|
-
finish(code);
|
|
7245
|
-
});
|
|
8153
|
+
// Check steps run in the same cwd as the agent steps — the worktree — in their own process
|
|
8154
|
+
// group, shared with worktree setup (#917).
|
|
8155
|
+
const result = await runGroupedCommand({
|
|
8156
|
+
command,
|
|
8157
|
+
cwd: state.cwd,
|
|
8158
|
+
env: process.env,
|
|
8159
|
+
setInterrupt: (stop) => { state.interrupt = stop; },
|
|
8160
|
+
keep: 'head',
|
|
8161
|
+
cap: CHECK_OUTPUT_CAP,
|
|
7246
8162
|
});
|
|
8163
|
+
emit({ type: 'check-output', stepId: step.id, command, text: result.output, exitCode: result.exitCode ?? -1 });
|
|
8164
|
+
return { ok: result.exitCode === 0, output: result.output };
|
|
7247
8165
|
}
|
|
7248
8166
|
finishStep(runId, stepId, status, error, emit) {
|
|
7249
8167
|
this.store.updateStep(runId, stepId, {
|