@wjarka/cezarion 0.16.0 → 0.17.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/LICENSE +1 -0
- package/README.md +594 -207
- package/dist/agent-config/account-identity.d.ts +3 -19
- package/dist/agent-config/account-identity.js +8 -5
- package/dist/agent-config/account-identity.js.map +1 -1
- package/dist/agent-config/catalog.d.ts +3 -14
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/agent-config/files.d.ts +2 -6
- package/dist/agent-config/files.js +2 -6
- package/dist/agent-config/files.js.map +1 -1
- package/dist/agent-config/model-settings/types.d.ts +1 -1
- package/dist/agent-config/seed.d.ts +11 -3
- package/dist/agent-config/seed.js +52 -48
- package/dist/agent-config/seed.js.map +1 -1
- package/dist/agent-config/service.js +3 -2
- package/dist/agent-config/service.js.map +1 -1
- package/dist/agent-config/validate.d.ts +1 -4
- package/dist/agent-config/validate.js.map +1 -1
- package/dist/application-update/helper.d.ts +2 -2
- package/dist/application-update/helper.js +7 -12
- package/dist/application-update/helper.js.map +1 -1
- package/dist/application-update/launcher.d.ts +4 -0
- package/dist/application-update/launcher.js +18 -12
- package/dist/application-update/launcher.js.map +1 -1
- package/dist/application-update/service.d.ts +3 -3
- package/dist/application-update/service.js +1 -1
- package/dist/application-update/service.js.map +1 -1
- package/dist/archive-on-merge-setting.d.ts +15 -0
- package/dist/archive-on-merge-setting.js +14 -0
- package/dist/archive-on-merge-setting.js.map +1 -0
- package/dist/artifacts/cli.d.ts +8 -0
- package/dist/artifacts/cli.js +21 -2
- package/dist/artifacts/cli.js.map +1 -1
- package/dist/artifacts/resolve.js +2 -1
- package/dist/artifacts/resolve.js.map +1 -1
- package/dist/artifacts/store.js.map +1 -1
- package/dist/automations/automation-cli.d.ts +59 -0
- package/dist/automations/automation-cli.js +509 -0
- package/dist/automations/automation-cli.js.map +1 -0
- package/dist/automations/builtin-skill.d.ts +7 -0
- package/dist/automations/builtin-skill.js +14 -0
- package/dist/automations/builtin-skill.js.map +1 -0
- package/dist/automations/github-baseline.d.ts +3 -0
- package/dist/automations/github-baseline.js +17 -0
- package/dist/automations/github-baseline.js.map +1 -0
- package/dist/automations/github-poller.d.ts +23 -4
- package/dist/automations/github-poller.js +157 -88
- package/dist/automations/github-poller.js.map +1 -1
- package/dist/automations/prompts.d.ts +12 -0
- package/dist/automations/prompts.js +160 -0
- package/dist/automations/prompts.js.map +1 -0
- package/dist/automations/schedule-runner.d.ts +43 -24
- package/dist/automations/schedule-runner.js +207 -20
- package/dist/automations/schedule-runner.js.map +1 -1
- package/dist/automations/scheduler.d.ts +10 -2
- package/dist/automations/scheduler.js +200 -54
- package/dist/automations/scheduler.js.map +1 -1
- package/dist/automations/service.d.ts +777 -0
- package/dist/automations/service.js +444 -0
- package/dist/automations/service.js.map +1 -0
- package/dist/automations/store.d.ts +17 -7
- package/dist/automations/store.js +88 -17
- package/dist/automations/store.js.map +1 -1
- package/dist/automations/task-template.d.ts +7 -2
- package/dist/automations/task-template.js +93 -44
- package/dist/automations/task-template.js.map +1 -1
- package/dist/automations/tool-adapter.d.ts +6 -0
- package/dist/automations/tool-adapter.js +108 -0
- package/dist/automations/tool-adapter.js.map +1 -0
- package/dist/automations/types.d.ts +1 -405
- package/dist/automations/types.js +1 -218
- package/dist/automations/types.js.map +1 -1
- package/dist/autosave-cleanup.d.ts +26 -0
- package/dist/autosave-cleanup.js +87 -0
- package/dist/autosave-cleanup.js.map +1 -0
- package/dist/autosave-git.d.ts +3 -0
- package/dist/autosave-git.js +67 -35
- package/dist/autosave-git.js.map +1 -1
- package/dist/autosave-group.d.ts +8 -0
- package/dist/autosave-group.js +61 -0
- package/dist/autosave-group.js.map +1 -0
- package/dist/autosave-holders.d.ts +31 -4
- package/dist/autosave-holders.js +92 -7
- package/dist/autosave-holders.js.map +1 -1
- package/dist/autosave-windows.d.ts +14 -0
- package/dist/autosave-windows.js +36 -2
- package/dist/autosave-windows.js.map +1 -1
- package/dist/ci-wait/client.d.ts +20 -3
- package/dist/ci-wait/client.js +40 -44
- package/dist/ci-wait/client.js.map +1 -1
- package/dist/ci-wait/controller.d.ts +622 -36
- package/dist/ci-wait/controller.js +152 -20
- package/dist/ci-wait/controller.js.map +1 -1
- package/dist/ci-wait/errors.d.ts +19 -0
- package/dist/ci-wait/errors.js +180 -2
- package/dist/ci-wait/errors.js.map +1 -1
- package/dist/ci-wait/github.d.ts +25 -1
- package/dist/ci-wait/github.js +76 -9
- package/dist/ci-wait/github.js.map +1 -1
- package/dist/ci-wait/mcp.d.ts +0 -1
- package/dist/ci-wait/mcp.js +0 -2
- package/dist/ci-wait/mcp.js.map +1 -1
- package/dist/ci-wait/supervisor.d.ts +7 -0
- package/dist/ci-wait/supervisor.js +113 -17
- package/dist/ci-wait/supervisor.js.map +1 -1
- package/dist/ci-wait/tools.d.ts +536 -16
- package/dist/ci-wait/tools.js +165 -20
- package/dist/ci-wait/tools.js.map +1 -1
- package/dist/config.d.ts +3 -17
- package/dist/config.js +4 -60
- package/dist/config.js.map +1 -1
- package/dist/contract/active-provider.d.ts +12 -0
- package/dist/contract/agent-profiles.d.ts +2 -10
- package/dist/contract/ask-schema.d.ts +22 -4
- package/dist/contract/ask.d.ts +6 -4
- package/dist/contract/attachments.d.ts +35 -0
- package/dist/contract/attention.d.ts +46 -37
- package/dist/contract/automation-requests.d.ts +308 -0
- package/dist/contract/automation-schedule.d.ts +40 -11
- package/dist/contract/automation-storage.d.ts +512 -0
- package/dist/contract/automations.d.ts +421 -40
- package/dist/contract/ci-wait.d.ts +41 -4
- package/dist/contract/config-requests.d.ts +36 -0
- package/dist/contract/conversations.d.ts +24 -10
- package/dist/contract/defined.d.ts +8 -0
- package/dist/contract/delegation.d.ts +312 -8
- package/dist/contract/events.d.ts +24 -3
- package/dist/contract/github-requests.d.ts +26 -0
- package/dist/contract/github.d.ts +60 -27
- package/dist/contract/handoff.d.ts +43 -0
- package/dist/contract/health.d.ts +2 -2
- package/dist/contract/image-path.d.ts +6 -0
- package/dist/contract/index.d.ts +31 -0
- package/dist/contract/index.js +3756 -1525
- package/dist/contract/live-coordination.d.ts +158 -0
- package/dist/contract/live-limits.d.ts +8 -0
- package/dist/contract/live.d.ts +123 -0
- package/dist/contract/pending-ask.d.ts +58 -0
- package/dist/contract/permissions.d.ts +54 -0
- package/dist/contract/preview.d.ts +33 -0
- package/dist/contract/project-limits.d.ts +33 -0
- package/dist/contract/projects.d.ts +486 -20
- package/dist/contract/provider-requests.d.ts +77 -0
- package/dist/contract/query-requests.d.ts +52 -0
- package/dist/contract/repo-requests.d.ts +12 -0
- package/dist/contract/run-labels.d.ts +8 -0
- package/dist/contract/run-limits.d.ts +8 -0
- package/dist/contract/run-predicates.d.ts +32 -0
- package/dist/contract/run-requests.d.ts +33 -0
- package/dist/contract/run-search.d.ts +193 -0
- package/dist/contract/runs.d.ts +2232 -1894
- package/dist/contract/scheduled-tasks.d.ts +187 -0
- package/dist/contract/skills.d.ts +22 -772
- package/dist/contract/team-skill-ref.d.ts +10 -0
- package/dist/{core → contract}/tool-display.d.ts +4 -10
- package/dist/contract/ui-state-requests.d.ts +60 -0
- package/dist/contract/version.d.ts +3 -0
- package/dist/contract/voice.d.ts +213 -0
- package/dist/contract/waits.d.ts +1204 -0
- package/dist/contract/workflow-file.d.ts +37 -0
- package/dist/contract/workflow-requests.d.ts +7 -0
- package/dist/contract/workflows.d.ts +1 -2
- package/dist/contract/workspace.d.ts +34 -82
- package/dist/contract/zoned-time.d.ts +2 -11
- package/dist/core/agent-env.d.ts +4 -16
- package/dist/core/agent-env.js +14 -16
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-profiles.d.ts +28 -40
- package/dist/core/agent-profiles.js +56 -51
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-runner.d.ts +88 -44
- package/dist/core/agent-runner.js +17 -18
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/ask.d.ts +3 -0
- package/dist/core/ask.js +12 -0
- package/dist/core/ask.js.map +1 -1
- package/dist/core/backend-detect.js +16 -2
- package/dist/core/backend-detect.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +23 -4
- package/dist/core/claude-cli-runner.js +280 -76
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/claude-model-catalog.d.ts +2 -10
- package/dist/core/claude-model-catalog.js +2 -10
- package/dist/core/claude-model-catalog.js.map +1 -1
- package/dist/core/claude-ui-mapper.d.ts +2 -2
- package/dist/core/claude-ui-mapper.js +94 -2
- package/dist/core/claude-ui-mapper.js.map +1 -1
- package/dist/core/codex-app-server-runner.d.ts +3 -4
- package/dist/core/codex-app-server-runner.js +146 -51
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +4 -3
- package/dist/core/codex-app-server-transport.js +11 -7
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/codex-approvals.d.ts +12 -0
- package/dist/core/codex-approvals.js +161 -0
- package/dist/core/codex-approvals.js.map +1 -0
- package/dist/core/codex-permissions.d.ts +1 -1
- package/dist/core/codex-ui-mapper.js +45 -1
- package/dist/core/codex-ui-mapper.js.map +1 -1
- package/dist/core/cursor-acp-runner.d.ts +1 -1
- package/dist/core/cursor-acp-runner.js +39 -15
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/cursor-cli-flags.d.ts +19 -0
- package/dist/core/cursor-cli-flags.js +47 -0
- package/dist/core/cursor-cli-flags.js.map +1 -0
- package/dist/core/cursor-model-catalog.d.ts +22 -0
- package/dist/core/cursor-model-catalog.js +53 -24
- package/dist/core/cursor-model-catalog.js.map +1 -1
- package/dist/core/cursor-print-capabilities.d.ts +16 -0
- package/dist/core/cursor-print-capabilities.js +141 -0
- package/dist/core/cursor-print-capabilities.js.map +1 -0
- package/dist/core/cursor-print-process.d.ts +31 -0
- package/dist/core/cursor-print-process.js +117 -0
- package/dist/core/cursor-print-process.js.map +1 -0
- package/dist/core/cursor-print-runner.d.ts +31 -0
- package/dist/core/cursor-print-runner.js +555 -0
- package/dist/core/cursor-print-runner.js.map +1 -0
- package/dist/core/cursor-print-tool-catalog.d.ts +10 -0
- package/dist/core/cursor-print-tool-catalog.js +91 -0
- package/dist/core/cursor-print-tool-catalog.js.map +1 -0
- package/dist/core/cursor-print-ui-mapper.d.ts +47 -0
- package/dist/core/cursor-print-ui-mapper.js +553 -0
- package/dist/core/cursor-print-ui-mapper.js.map +1 -0
- package/dist/core/cursor-runner.d.ts +24 -0
- package/dist/core/cursor-runner.js +149 -0
- package/dist/core/cursor-runner.js.map +1 -0
- package/dist/core/cursor-ui-mapper.js +5 -2
- package/dist/core/cursor-ui-mapper.js.map +1 -1
- package/dist/core/defined.d.ts +1 -0
- package/dist/core/defined.js +2 -0
- package/dist/core/defined.js.map +1 -0
- package/dist/core/hook-items.d.ts +14 -0
- package/dist/core/hook-items.js +13 -0
- package/dist/core/hook-items.js.map +1 -0
- package/dist/core/host-model-catalog.d.ts +2 -9
- package/dist/core/host-model-catalog.js +2 -9
- package/dist/core/host-model-catalog.js.map +1 -1
- package/dist/core/managed-session.js +15 -3
- package/dist/core/managed-session.js.map +1 -1
- package/dist/core/model-identity.d.ts +4 -21
- package/dist/core/model-identity.js.map +1 -1
- package/dist/core/model-presets.d.ts +2 -13
- package/dist/core/model-presets.js +2 -13
- package/dist/core/model-presets.js.map +1 -1
- package/dist/core/omp-model-catalog.js.map +1 -1
- package/dist/core/omp-runner.d.ts +6 -7
- package/dist/core/omp-runner.js +92 -33
- package/dist/core/omp-runner.js.map +1 -1
- package/dist/core/omp-ui-mapper.js +1 -1
- package/dist/core/omp-ui-mapper.js.map +1 -1
- package/dist/core/opencode-model-catalog.js.map +1 -1
- package/dist/core/opencode-permissions.d.ts +52 -0
- package/dist/core/opencode-permissions.js +101 -0
- package/dist/core/opencode-permissions.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +10 -9
- package/dist/core/opencode-server-runner.js +286 -28
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/opencode-session-error.d.ts +15 -7
- package/dist/core/opencode-session-error.js +25 -9
- package/dist/core/opencode-session-error.js.map +1 -1
- package/dist/core/opencode-ui-mapper.js +2 -2
- package/dist/core/opencode-ui-mapper.js.map +1 -1
- package/dist/core/pi-model-catalog.js.map +1 -1
- package/dist/core/pi-runner.d.ts +12 -4
- package/dist/core/pi-runner.js +171 -63
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/pi-ui-mapper.js +1 -1
- package/dist/core/pi-ui-mapper.js.map +1 -1
- package/dist/core/process-usage.d.ts +5 -0
- package/dist/core/process-usage.js +24 -4
- package/dist/core/process-usage.js.map +1 -1
- package/dist/core/provider-auth.d.ts +16 -107
- package/dist/core/provider-auth.js +16 -132
- package/dist/core/provider-auth.js.map +1 -1
- package/dist/core/runner-factory.d.ts +4 -1
- package/dist/core/runner-factory.js +3 -3
- package/dist/core/runner-factory.js.map +1 -1
- package/dist/core/runner-runtime.d.ts +22 -0
- package/dist/core/runner-runtime.js +66 -5
- package/dist/core/runner-runtime.js.map +1 -1
- package/dist/core/runner-tool-grants.d.ts +21 -0
- package/dist/core/runner-tool-grants.js +64 -0
- package/dist/core/runner-tool-grants.js.map +1 -0
- package/dist/core/secret-redaction.d.ts +3 -20
- package/dist/core/secret-redaction.js +3 -20
- package/dist/core/secret-redaction.js.map +1 -1
- package/dist/core/session-process.d.ts +22 -2
- package/dist/core/session-process.js +170 -12
- package/dist/core/session-process.js.map +1 -1
- package/dist/core/shell-env.d.ts +2 -15
- package/dist/core/shell-env.js +2 -15
- package/dist/core/shell-env.js.map +1 -1
- package/dist/core/ui-events.d.ts +60 -47
- package/dist/core/ui-events.js +0 -23
- package/dist/core/ui-events.js.map +1 -1
- package/dist/core/usage-limit.js.map +1 -1
- package/dist/data-gitignore.js +3 -1
- package/dist/data-gitignore.js.map +1 -1
- package/dist/delegation/capacity.d.ts +4 -8
- package/dist/delegation/capacity.js +4 -8
- package/dist/delegation/capacity.js.map +1 -1
- package/dist/delegation/cli.js +123 -21
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/context.d.ts +19 -3
- package/dist/delegation/context.js +217 -22
- package/dist/delegation/context.js.map +1 -1
- package/dist/delegation/conversations.d.ts +4 -3
- package/dist/delegation/conversations.js +7 -5
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/destroy-observation.d.ts +1 -1
- package/dist/delegation/destroy-observation.js.map +1 -1
- package/dist/delegation/effort.js.map +1 -1
- package/dist/delegation/execution-identity.d.ts +1 -1
- package/dist/delegation/execution-identity.js +41 -7
- package/dist/delegation/execution-identity.js.map +1 -1
- package/dist/delegation/idle-test-app.d.ts +4 -0
- package/dist/delegation/idle-test-app.js +174 -0
- package/dist/delegation/idle-test-app.js.map +1 -0
- package/dist/delegation/input.js.map +1 -1
- package/dist/delegation/park-notice.d.ts +42 -0
- package/dist/delegation/park-notice.js +71 -0
- package/dist/delegation/park-notice.js.map +1 -0
- package/dist/delegation/policy.d.ts +2 -0
- package/dist/delegation/policy.js +9 -1
- package/dist/delegation/policy.js.map +1 -1
- package/dist/delegation/process-liveness.d.ts +10 -15
- package/dist/delegation/process-liveness.js +11 -35
- package/dist/delegation/process-liveness.js.map +1 -1
- package/dist/delegation/provision.js +21 -19
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/questions.d.ts +3 -1
- package/dist/delegation/questions.js +10 -5
- package/dist/delegation/questions.js.map +1 -1
- package/dist/delegation/readiness.d.ts +2 -2
- package/dist/delegation/results.d.ts +4 -1
- package/dist/delegation/results.js +49 -18
- package/dist/delegation/results.js.map +1 -1
- package/dist/delegation/retry-backoff.d.ts +8 -0
- package/dist/delegation/retry-backoff.js +8 -0
- package/dist/delegation/retry-backoff.js.map +1 -1
- package/dist/delegation/routes.d.ts +114 -30
- package/dist/delegation/routes.js +4 -1
- package/dist/delegation/routes.js.map +1 -1
- package/dist/delegation/scratch-cleanup.d.ts +4 -5
- package/dist/delegation/scratch-cleanup.js +10 -8
- package/dist/delegation/scratch-cleanup.js.map +1 -1
- package/dist/delegation/service.d.ts +12 -4
- package/dist/delegation/service.js +76 -28
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +154 -70
- package/dist/delegation/wait.d.ts +11 -0
- package/dist/delegation/wait.js +24 -0
- package/dist/delegation/wait.js.map +1 -1
- package/dist/delegation/windows-test-app.d.ts +5 -0
- package/dist/delegation/windows-test-app.js +63 -0
- package/dist/delegation/windows-test-app.js.map +1 -0
- package/dist/delegation/windows-workspace-identity.d.ts +3 -0
- package/dist/delegation/windows-workspace-identity.js +22 -0
- package/dist/delegation/windows-workspace-identity.js.map +1 -0
- package/dist/delegation/workspace.d.ts +5 -10
- package/dist/delegation/workspace.js +136 -75
- package/dist/delegation/workspace.js.map +1 -1
- package/dist/detached-process.d.ts +103 -0
- package/dist/detached-process.js +301 -0
- package/dist/detached-process.js.map +1 -0
- package/dist/discovery/catalog.d.ts +2 -1
- package/dist/discovery/catalog.js.map +1 -1
- package/dist/discovery/cli.js +3 -1
- package/dist/discovery/cli.js.map +1 -1
- package/dist/git-diff-base.d.ts +6 -61
- package/dist/git-diff-base.js +6 -70
- package/dist/git-diff-base.js.map +1 -1
- package/dist/git-index-copy.d.ts +4 -0
- package/dist/git-index-copy.js +10 -0
- package/dist/git-index-copy.js.map +1 -0
- package/dist/git-worktree-lock.d.ts +2 -4
- package/dist/git-worktree-lock.js +2 -4
- package/dist/git-worktree-lock.js.map +1 -1
- package/dist/git-worktree-release.d.ts +2 -2
- package/dist/git-worktree-release.js.map +1 -1
- package/dist/git-worktree.d.ts +47 -76
- package/dist/git-worktree.js +240 -94
- package/dist/git-worktree.js.map +1 -1
- package/dist/handoff.d.ts +51 -41
- package/dist/handoff.js +130 -78
- package/dist/handoff.js.map +1 -1
- package/dist/index.js +60 -39
- package/dist/index.js.map +1 -1
- package/dist/merge-observations.d.ts +13 -0
- package/dist/merge-observations.js +55 -0
- package/dist/merge-observations.js.map +1 -0
- package/dist/open-url.js +4 -14
- package/dist/open-url.js.map +1 -1
- package/dist/paths.d.ts +10 -13
- package/dist/paths.js +36 -17
- package/dist/paths.js.map +1 -1
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +9 -6
- package/dist/planner.js.map +1 -1
- package/dist/preview/agent-address.d.ts +34 -0
- package/dist/preview/agent-address.js +51 -0
- package/dist/preview/agent-address.js.map +1 -0
- package/dist/preview/agent-marker.d.ts +17 -0
- package/dist/preview/agent-marker.js +71 -0
- package/dist/preview/agent-marker.js.map +1 -0
- package/dist/preview/cdp-proxy.d.ts +54 -0
- package/dist/preview/cdp-proxy.js +151 -0
- package/dist/preview/cdp-proxy.js.map +1 -0
- package/dist/preview/cdp.d.ts +39 -3
- package/dist/preview/cdp.js +155 -57
- package/dist/preview/cdp.js.map +1 -1
- package/dist/preview/chromium.d.ts +18 -6
- package/dist/preview/chromium.js +112 -25
- package/dist/preview/chromium.js.map +1 -1
- package/dist/preview/dev-server.d.ts +1 -1
- package/dist/preview/dev-server.js +20 -44
- package/dist/preview/dev-server.js.map +1 -1
- package/dist/preview/host.d.ts +70 -3
- package/dist/preview/host.js +314 -28
- package/dist/preview/host.js.map +1 -1
- package/dist/preview/registration.d.ts +10 -5
- package/dist/preview/registration.js +2 -0
- package/dist/preview/registration.js.map +1 -1
- package/dist/preview/session.d.ts +2 -0
- package/dist/preview/session.js +10 -0
- package/dist/preview/session.js.map +1 -1
- package/dist/process-wait/exit-file.d.ts +29 -0
- package/dist/process-wait/exit-file.js +67 -0
- package/dist/process-wait/exit-file.js.map +1 -0
- package/dist/process-wait/supervisor.d.ts +30 -0
- package/dist/process-wait/supervisor.js +71 -0
- package/dist/process-wait/supervisor.js.map +1 -0
- package/dist/release/snapshot.d.ts +6 -6
- package/dist/runs/agent-tmpdir.d.ts +51 -36
- package/dist/runs/agent-tmpdir.js +389 -132
- package/dist/runs/agent-tmpdir.js.map +1 -1
- package/dist/runs/auto-name.d.ts +6 -6
- package/dist/runs/auto-name.js +1 -1
- package/dist/runs/auto-name.js.map +1 -1
- package/dist/runs/client-request.d.ts +3 -3
- package/dist/runs/client-request.js +3 -3
- package/dist/runs/client-request.js.map +1 -1
- package/dist/runs/delegation-state.d.ts +11 -2
- package/dist/runs/delegation-state.js.map +1 -1
- package/dist/runs/event-history-agent-context.d.ts +14 -0
- package/dist/runs/event-history-agent-context.js +105 -0
- package/dist/runs/event-history-agent-context.js.map +1 -0
- package/dist/runs/event-history-context.d.ts +15 -0
- package/dist/runs/event-history-context.js +109 -0
- package/dist/runs/event-history-context.js.map +1 -0
- package/dist/runs/event-history.d.ts +4 -1
- package/dist/runs/event-history.js +29 -130
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/history-stream.d.ts +2 -0
- package/dist/runs/history-stream.js +62 -0
- package/dist/runs/history-stream.js.map +1 -0
- package/dist/runs/legacy-index.d.ts +6 -19
- package/dist/runs/legacy-index.js +6 -19
- package/dist/runs/legacy-index.js.map +1 -1
- package/dist/runs/live-item-snapshots.d.ts +13 -0
- package/dist/runs/live-item-snapshots.js +79 -0
- package/dist/runs/live-item-snapshots.js.map +1 -0
- package/dist/runs/loaded-run-healing.d.ts +3 -0
- package/dist/runs/loaded-run-healing.js +17 -0
- package/dist/runs/loaded-run-healing.js.map +1 -0
- package/dist/runs/outcome-commit.d.ts +98 -0
- package/dist/runs/outcome-commit.js +132 -0
- package/dist/runs/outcome-commit.js.map +1 -0
- package/dist/runs/pull-requests.d.ts +14 -0
- package/dist/runs/pull-requests.js +96 -0
- package/dist/runs/pull-requests.js.map +1 -0
- package/dist/runs/raw-record.d.ts +3 -15
- package/dist/runs/raw-record.js +3 -15
- package/dist/runs/raw-record.js.map +1 -1
- package/dist/runs/retention.d.ts +2 -2
- package/dist/runs/retention.js +7 -0
- package/dist/runs/retention.js.map +1 -1
- package/dist/runs/review-gate.d.ts +1 -1
- package/dist/runs/review-gate.js.map +1 -1
- package/dist/runs/run-claims.d.ts +2 -9
- package/dist/runs/run-claims.js.map +1 -1
- package/dist/runs/run-database.d.ts +29 -34
- package/dist/runs/run-database.js +124 -46
- package/dist/runs/run-database.js.map +1 -1
- package/dist/runs/run-index.d.ts +6 -25
- package/dist/runs/run-index.js +25 -38
- package/dist/runs/run-index.js.map +1 -1
- package/dist/runs/run-row.d.ts +5 -34
- package/dist/runs/run-row.js +13 -38
- package/dist/runs/run-row.js.map +1 -1
- package/dist/runs/store.d.ts +1597 -744
- package/dist/runs/store.js +1231 -739
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/task-markers.d.ts +1 -0
- package/dist/runs/task-markers.js +5 -1
- package/dist/runs/task-markers.js.map +1 -1
- package/dist/runs/task-refs.d.ts +3 -3
- package/dist/runs/transcript-facts-fold.d.ts +64 -0
- package/dist/runs/transcript-facts-fold.js +109 -0
- package/dist/runs/transcript-facts-fold.js.map +1 -0
- package/dist/runs/transcript-facts-load.d.ts +11 -0
- package/dist/runs/transcript-facts-load.js +66 -0
- package/dist/runs/transcript-facts-load.js.map +1 -0
- package/dist/runs/transcript-facts-queue.d.ts +30 -0
- package/dist/runs/transcript-facts-queue.js +227 -0
- package/dist/runs/transcript-facts-queue.js.map +1 -0
- package/dist/runs/transcript-facts-worker.d.ts +15 -0
- package/dist/runs/transcript-facts-worker.js +26 -0
- package/dist/runs/transcript-facts-worker.js.map +1 -0
- package/dist/runs/transcript-facts.d.ts +13 -51
- package/dist/runs/transcript-facts.js +86 -202
- package/dist/runs/transcript-facts.js.map +1 -1
- package/dist/runs/ui-event-sink.d.ts +5 -30
- package/dist/runs/ui-event-sink.js +8 -30
- package/dist/runs/ui-event-sink.js.map +1 -1
- package/dist/runs/webhook.d.ts +104 -38
- package/dist/runs/webhook.js +180 -77
- package/dist/runs/webhook.js.map +1 -1
- package/dist/server/app-type.d.ts +2 -17
- package/dist/server/archive-on-merge.d.ts +35 -0
- package/dist/server/archive-on-merge.js +202 -0
- package/dist/server/archive-on-merge.js.map +1 -0
- package/dist/server/capabilities.d.ts +3 -15
- package/dist/server/capabilities.js +6 -19
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/checkout-progress.d.ts +10 -0
- package/dist/server/checkout-progress.js +27 -0
- package/dist/server/checkout-progress.js.map +1 -0
- package/dist/server/checkout.d.ts +10 -19
- package/dist/server/checkout.js +15 -45
- package/dist/server/checkout.js.map +1 -1
- package/dist/server/cockpit-ownership.d.ts +2 -0
- package/dist/server/cockpit-ownership.js +9 -0
- package/dist/server/cockpit-ownership.js.map +1 -1
- package/dist/server/cold-repo-handles.d.ts +2 -7
- package/dist/server/cold-repo-handles.js +2 -7
- package/dist/server/cold-repo-handles.js.map +1 -1
- package/dist/server/completion-actions.d.ts +30 -0
- package/dist/server/completion-actions.js +204 -0
- package/dist/server/completion-actions.js.map +1 -0
- package/dist/server/create-project.d.ts +71 -0
- package/dist/server/create-project.js +448 -0
- package/dist/server/create-project.js.map +1 -0
- package/dist/server/forge/github-filters.d.ts +7 -1
- package/dist/server/forge/github-filters.js +66 -51
- package/dist/server/forge/github-filters.js.map +1 -1
- package/dist/server/forge/github-merge-requirements.d.ts +18 -0
- package/dist/server/forge/github-merge-requirements.js +137 -0
- package/dist/server/forge/github-merge-requirements.js.map +1 -0
- package/dist/server/forge/github.d.ts +98 -111
- package/dist/server/forge/github.js +625 -306
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/forge/types.d.ts +25 -22
- package/dist/server/fs-browse.d.ts +3 -28
- package/dist/server/fs-browse.js.map +1 -1
- package/dist/server/git-changes.d.ts +5 -15
- package/dist/server/git-changes.js +3 -50
- package/dist/server/git-changes.js.map +1 -1
- package/dist/server/git.d.ts +3 -1
- package/dist/server/git.js +12 -0
- package/dist/server/git.js.map +1 -1
- package/dist/server/github.d.ts +1 -1
- package/dist/server/github.js +1 -1
- package/dist/server/github.js.map +1 -1
- package/dist/server/launch-key.d.ts +2 -4
- package/dist/server/launch-key.js +2 -4
- package/dist/server/launch-key.js.map +1 -1
- package/dist/server/live-routes.d.ts +68 -0
- package/dist/server/live-routes.js +147 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/open-in-app.d.ts +4 -13
- package/dist/server/open-in-app.js +5 -41
- package/dist/server/open-in-app.js.map +1 -1
- package/dist/server/open-in-targets.d.ts +42 -0
- package/dist/server/open-in-targets.js +110 -0
- package/dist/server/open-in-targets.js.map +1 -0
- package/dist/server/open-in-terminal.d.ts +3 -0
- package/dist/server/open-in-terminal.js +6 -26
- package/dist/server/open-in-terminal.js.map +1 -1
- package/dist/server/preview-cdp-socket.d.ts +21 -0
- package/dist/server/preview-cdp-socket.js +112 -0
- package/dist/server/preview-cdp-socket.js.map +1 -0
- package/dist/server/preview-socket.d.ts +11 -11
- package/dist/server/preview-socket.js +22 -22
- package/dist/server/preview-socket.js.map +1 -1
- package/dist/server/preview-sockets.d.ts +14 -0
- package/dist/server/preview-sockets.js +18 -0
- package/dist/server/preview-sockets.js.map +1 -0
- package/dist/server/project-context.d.ts +54 -18
- package/dist/server/project-context.js +178 -5
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/provider-action-gate.d.ts +0 -2
- package/dist/server/provider-action-gate.js +0 -21
- package/dist/server/provider-action-gate.js.map +1 -1
- package/dist/server/provider-auth-runtime.d.ts +3 -18
- package/dist/server/provider-auth-runtime.js +3 -18
- package/dist/server/provider-auth-runtime.js.map +1 -1
- package/dist/server/repo-branches.d.ts +3 -9
- package/dist/server/repo-branches.js +6 -22
- package/dist/server/repo-branches.js.map +1 -1
- package/dist/server/run-event-feed.d.ts +20 -0
- package/dist/server/run-event-feed.js +190 -0
- package/dist/server/run-event-feed.js.map +1 -0
- package/dist/server/server-deps.d.ts +37 -0
- package/dist/server/server-deps.js +114 -0
- package/dist/server/server-deps.js.map +1 -0
- package/dist/server/server.d.ts +14082 -11998
- package/dist/server/server.js +785 -1647
- package/dist/server/server.js.map +1 -1
- package/dist/server/start-run-options.d.ts +82 -0
- package/dist/server/start-run-options.js +111 -0
- package/dist/server/start-run-options.js.map +1 -0
- package/dist/server/validators.d.ts +3 -29
- package/dist/server/validators.js +1 -2
- package/dist/server/validators.js.map +1 -1
- package/dist/server/ws.d.ts +3 -23
- package/dist/server/ws.js +3 -23
- package/dist/server/ws.js.map +1 -1
- package/dist/server-install/engine.d.ts +6 -9
- package/dist/server-install/engine.js +2 -9
- package/dist/server-install/engine.js.map +1 -1
- package/dist/server-install/platforms/ubuntu-vps.d.ts +1 -1
- package/dist/server-install/state.d.ts +2 -4
- package/dist/server-install/state.js +2 -4
- package/dist/server-install/state.js.map +1 -1
- package/dist/server-install/steps.d.ts +2 -5
- package/dist/server-install/steps.js +3 -6
- package/dist/server-install/steps.js.map +1 -1
- package/dist/server-install/types.d.ts +3 -6
- package/dist/server-install/types.js +2 -5
- package/dist/server-install/types.js.map +1 -1
- package/dist/server-install/ui.d.ts +2 -8
- package/dist/server-install/ui.js +3 -3
- package/dist/server-install/ui.js.map +1 -1
- package/dist/session-context.d.ts +64 -0
- package/dist/session-context.js +264 -0
- package/dist/session-context.js.map +1 -0
- package/dist/skills-provision.d.ts +62 -0
- package/dist/skills-provision.js +325 -0
- package/dist/skills-provision.js.map +1 -0
- package/dist/skills-remote.d.ts +29 -27
- package/dist/skills-remote.js +236 -100
- package/dist/skills-remote.js.map +1 -1
- package/dist/skills.d.ts +25 -36
- package/dist/skills.js +43 -54
- package/dist/skills.js.map +1 -1
- package/dist/subcommands.d.ts +11 -0
- package/dist/subcommands.js +24 -0
- package/dist/subcommands.js.map +1 -0
- package/dist/task-cli/cli.js +78 -25
- package/dist/task-cli/cli.js.map +1 -1
- package/dist/task-cli/discovery.d.ts +4 -2
- package/dist/task-cli/discovery.js +14 -13
- package/dist/task-cli/discovery.js.map +1 -1
- package/dist/task-cli/hint.d.ts +20 -0
- package/dist/task-cli/hint.js +14 -0
- package/dist/task-cli/hint.js.map +1 -0
- package/dist/task-cli/http.d.ts +1 -0
- package/dist/task-cli/http.js +3 -1
- package/dist/task-cli/http.js.map +1 -1
- package/dist/task-cli/projections.d.ts +14 -11
- package/dist/task-cli/projections.js +15 -11
- package/dist/task-cli/projections.js.map +1 -1
- package/dist/task-cli/watch.d.ts +18 -10
- package/dist/task-cli/watch.js +128 -20
- package/dist/task-cli/watch.js.map +1 -1
- package/dist/ui-state.d.ts +17 -3
- package/dist/ui-state.js +56 -9
- package/dist/ui-state.js.map +1 -1
- package/dist/update-check.d.ts +0 -10
- package/dist/update-check.js +1 -16
- package/dist/update-check.js.map +1 -1
- package/dist/voice/decide.d.ts +32 -0
- package/dist/voice/decide.js +297 -0
- package/dist/voice/decide.js.map +1 -0
- package/dist/voice/decision-log.d.ts +52 -0
- package/dist/voice/decision-log.js +107 -0
- package/dist/voice/decision-log.js.map +1 -0
- package/dist/voice/engine.d.ts +119 -0
- package/dist/voice/engine.js +60 -0
- package/dist/voice/engine.js.map +1 -0
- package/dist/voice/fake-engine.d.ts +10 -0
- package/dist/voice/fake-engine.js +192 -0
- package/dist/voice/fake-engine.js.map +1 -0
- package/dist/voice/jev-engine.d.ts +17 -0
- package/dist/voice/jev-engine.js +110 -0
- package/dist/voice/jev-engine.js.map +1 -0
- package/dist/voice/pending-ask.d.ts +17 -0
- package/dist/voice/pending-ask.js +50 -0
- package/dist/voice/pending-ask.js.map +1 -0
- package/dist/voice/routes.d.ts +115 -0
- package/dist/voice/routes.js +91 -0
- package/dist/voice/routes.js.map +1 -0
- package/dist/voice/sentence.d.ts +21 -0
- package/dist/voice/sentence.js +61 -0
- package/dist/voice/sentence.js.map +1 -0
- package/dist/voice/thresholds.d.ts +28 -0
- package/dist/voice/thresholds.js +29 -0
- package/dist/voice/thresholds.js.map +1 -0
- package/dist/webhooks/cli.d.ts +18 -0
- package/dist/webhooks/cli.js +145 -0
- package/dist/webhooks/cli.js.map +1 -0
- package/dist/webhooks/prompt.d.ts +5 -0
- package/dist/webhooks/prompt.js +26 -0
- package/dist/webhooks/prompt.js.map +1 -0
- package/dist/workflows/active-run.d.ts +167 -0
- package/dist/workflows/active-run.js +21 -0
- package/dist/workflows/active-run.js.map +1 -0
- package/dist/workflows/continuation-admission.d.ts +913 -0
- package/dist/workflows/continuation-admission.js +265 -0
- package/dist/workflows/continuation-admission.js.map +1 -0
- package/dist/workflows/execution-account.d.ts +13 -0
- package/dist/workflows/execution-account.js +18 -0
- package/dist/workflows/execution-account.js.map +1 -0
- package/dist/workflows/input-acknowledgement.d.ts +152 -0
- package/dist/workflows/input-acknowledgement.js +38 -0
- package/dist/workflows/input-acknowledgement.js.map +1 -0
- package/dist/workflows/line-order.d.ts +5 -0
- package/dist/workflows/line-order.js +25 -0
- package/dist/workflows/line-order.js.map +1 -0
- package/dist/workflows/outcome-batches.d.ts +20 -0
- package/dist/workflows/outcome-batches.js +25 -0
- package/dist/workflows/outcome-batches.js.map +1 -0
- package/dist/workflows/question-routing.d.ts +8 -0
- package/dist/workflows/question-routing.js +18 -0
- package/dist/workflows/question-routing.js.map +1 -0
- package/dist/workflows/run-scheduling.d.ts +35 -0
- package/dist/workflows/run-scheduling.js +61 -0
- package/dist/workflows/run-scheduling.js.map +1 -0
- package/dist/workflows/run-states.d.ts +190 -0
- package/dist/workflows/run-states.js +295 -0
- package/dist/workflows/run-states.js.map +1 -0
- package/dist/workflows/run.d.ts +1296 -278
- package/dist/workflows/run.js +5730 -3265
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/turn-end.d.ts +116 -0
- package/dist/workflows/turn-end.js +68 -0
- package/dist/workflows/turn-end.js.map +1 -0
- package/dist/workflows/types.d.ts +35 -130
- package/dist/workflows/types.js +47 -100
- package/dist/workflows/types.js.map +1 -1
- package/dist/workflows/waits.d.ts +74 -0
- package/dist/workflows/waits.js +197 -0
- package/dist/workflows/waits.js.map +1 -0
- package/dist/workspace/agent-accounts.d.ts +5 -38
- package/dist/workspace/agent-accounts.js +14 -40
- package/dist/workspace/agent-accounts.js.map +1 -1
- package/dist/workspace/agent-profiles.d.ts +40 -16
- package/dist/workspace/agent-profiles.js +77 -17
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.d.ts +48 -49
- package/dist/workspace/config.js +68 -72
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/migrations.d.ts +3 -13
- package/dist/workspace/migrations.js.map +1 -1
- package/dist/workspace/projects-cli.d.ts +1 -1
- package/dist/workspace/projects-cli.js.map +1 -1
- package/dist/workspace/projects.d.ts +15 -18
- package/dist/workspace/projects.js +20 -16
- package/dist/workspace/projects.js.map +1 -1
- package/dist/workspace/semaphore.d.ts +11 -45
- package/dist/workspace/semaphore.js +6 -18
- package/dist/workspace/semaphore.js.map +1 -1
- package/dist/workspace/ui-state.d.ts +6 -4
- package/dist/workspace/ui-state.js +6 -4
- package/dist/workspace/ui-state.js.map +1 -1
- package/dist/workspace/webhooks.d.ts +40 -0
- package/dist/workspace/webhooks.js +146 -0
- package/dist/workspace/webhooks.js.map +1 -0
- package/dist/worktree-manifest.d.ts +165 -0
- package/dist/worktree-manifest.js +530 -0
- package/dist/worktree-manifest.js.map +1 -0
- package/dist/worktree-setup.js.map +1 -1
- package/package.json +6 -5
- package/scripts/benchmark-run-store-fixture.ts +7 -3
- package/scripts/check-pack.mjs +3 -1
- package/scripts/inline-contract.mjs +80 -27
- package/scripts/install-as-command.mjs +3 -1
- package/scripts/measure-transcript-facts-health.mjs +87 -0
- package/scripts/measure-transcript-facts-lag.mjs +68 -0
- package/scripts/mock-ci-tool.mjs +49 -6
- package/scripts/mock-claude-model-refusal.mjs +42 -0
- package/scripts/mock-claude.mjs +833 -591
- package/scripts/mock-codex-app-server.mjs +785 -418
- package/scripts/mock-cursor-acp.mjs +296 -125
- package/scripts/mock-cursor-print.mjs +256 -0
- package/scripts/mock-deferred-turn-start.mjs +16 -0
- package/scripts/mock-delivery-control.d.mts +23 -0
- package/scripts/mock-delivery-control.mjs +80 -0
- package/scripts/mock-env-record.mjs +11 -0
- package/scripts/mock-failure-control.d.mts +13 -0
- package/scripts/mock-failure-control.mjs +68 -0
- package/scripts/mock-multi-pr.mjs +7 -0
- package/scripts/mock-nudge.mjs +45 -0
- package/scripts/mock-omp-rpc.mjs +429 -255
- package/scripts/mock-opencode-serve.mjs +972 -557
- package/scripts/mock-pi-rpc.mjs +558 -331
- package/scripts/mock-runner-crash.mjs +18 -2
- package/scripts/mock-session-context.d.mts +5 -0
- package/scripts/mock-session-context.mjs +56 -0
- package/scripts/mock-silent-tail.mjs +10 -3
- package/scripts/pi-default-tools.mjs +22 -0
- package/scripts/pi-project-skills.mjs +59 -0
- package/scripts/probe-cursor-print.mjs +627 -0
- package/scripts/transcript-facts-fixture.mjs +53 -0
- package/scripts/voice-replay.ts +50 -0
- package/scripts/windows-workspace-identity.cs +208 -0
- package/scripts/windows-workspace-identity.ps1 +10 -0
- package/web/dist/assets/alert-dialog-DfctKUMw.js +1 -0
- package/web/dist/assets/button-CeupvoKG.js +41 -0
- package/web/dist/assets/centered-state-hDsyXrnH.js +1 -0
- package/web/dist/assets/cez-idle-D3aj_9lp.js +1 -0
- package/web/dist/assets/diff-BnIXZK3i2.js +3 -0
- package/web/dist/assets/diff-controls-GewpW9aF.js +1 -0
- package/web/dist/assets/diff-view-Byo7gpvW.js +11 -0
- package/web/dist/assets/dropdown-menu-COj_J5wd.js +6 -0
- package/web/dist/assets/eye-B8okNWXU.js +1 -0
- package/web/dist/assets/git-toolbar-DY0z0n4S.js +1 -0
- package/web/dist/assets/github-DwU6ALRK.js +1 -0
- package/web/dist/assets/{image-preview-FOEfqpZP.js → image-preview-C_t9q45f.js} +1 -1
- package/web/dist/assets/index-BOfE8pyD.js +34 -0
- package/web/dist/assets/index-D9HpsmdW.css +1 -0
- package/web/dist/assets/live-protocol-Uce2NwEX.js +64 -0
- package/web/dist/assets/live-worker-BizfZSoh.ts +33 -0
- package/web/dist/assets/markdown-BBDDg-4t.js +1 -0
- package/web/dist/assets/nav-row-styles-AoIhhIwf.js +1 -0
- package/web/dist/assets/popover-D56fhiXj.js +1 -0
- package/web/dist/assets/preview-pane-connected-BmVqfSU5.js +3 -0
- package/web/dist/assets/reference-chip-gQNk-4jc.js +1 -0
- package/web/dist/assets/repo-git-B5ECDUuv.js +1 -0
- package/web/dist/assets/{repo-git-CoKp-g9K.css → repo-git-DVXNOJDT.css} +1 -1
- package/web/dist/assets/run-header-C0Y1NvEM.js +2 -0
- package/web/dist/assets/run-header-CC7VUTg9.css +1 -0
- package/web/dist/assets/skills-DUeTkZ9z.js +1 -0
- package/web/dist/assets/status-dot-1PR0i69P.js +1 -0
- package/web/dist/assets/tab-link-DmWk4l5O.js +1 -0
- package/web/dist/assets/task-changes-DKZXuW89.js +1 -0
- package/web/dist/assets/task-commits-BZ6KxWdF.js +1 -0
- package/web/dist/assets/task-files-DlF7Apcn.js +2 -0
- package/web/dist/assets/task-github-item-CVPo3geZ.js +1 -0
- package/web/dist/assets/task-thread-DtQ6uGfQ.css +1 -0
- package/web/dist/assets/task-thread-mm4-ECyo.js +9 -0
- package/web/dist/assets/thread-state-CIw2BlhT.js +4 -0
- package/web/dist/assets/use-hand-to-agent-state-BCDjzHnG.js +1 -0
- package/web/dist/assets/use-hand-to-agent-state-C7esPJ-N.css +1 -0
- package/web/dist/assets/workflows-D8vYD-ql.js +7 -0
- package/web/dist/assets/zoomable-image-CxQSsfcn.js +1 -0
- package/web/dist/index.html +13 -17
- package/web/dist/live-worker.js +4 -0
- package/dist/core/tool-display.js +0 -179
- package/dist/core/tool-display.js.map +0 -1
- package/dist/runs/human-ask-summary.d.ts +0 -9
- package/dist/runs/human-ask-summary.js +0 -39
- package/dist/runs/human-ask-summary.js.map +0 -1
- package/dist/runs/run-search.d.ts +0 -29
- package/dist/runs/run-search.js +0 -63
- package/dist/runs/run-search.js.map +0 -1
- package/dist/skills-banner.d.ts +0 -22
- package/dist/skills-banner.js +0 -41
- package/dist/skills-banner.js.map +0 -1
- package/dist/skills-update.d.ts +0 -88
- package/dist/skills-update.js +0 -392
- package/dist/skills-update.js.map +0 -1
- package/dist/todos.d.ts +0 -48
- package/dist/todos.js +0 -201
- package/dist/todos.js.map +0 -1
- package/web/dist/assets/agent-accounts-CFOBMVHc.js +0 -1
- package/web/dist/assets/alert-dialog-BrpNUwMt.js +0 -1
- package/web/dist/assets/button-C_rd7CPp.js +0 -43
- package/web/dist/assets/compare-variants-D6WXhvXA.js +0 -1
- package/web/dist/assets/diff-DrEARXTV.js +0 -3
- package/web/dist/assets/diff-controls-B1-j7nFg.js +0 -1
- package/web/dist/assets/diff-view-DvGLjdTk.js +0 -6
- package/web/dist/assets/dropdown-menu-BegOZFrj.js +0 -1
- package/web/dist/assets/format-DpNe3S0A.js +0 -1
- package/web/dist/assets/git-toolbar-Cw4ljmhB.js +0 -1
- package/web/dist/assets/github-CfV5sqL0.js +0 -1
- package/web/dist/assets/index-DgWWlOLy.js +0 -13
- package/web/dist/assets/index-fBFnuvxu.css +0 -1
- package/web/dist/assets/nav-row-styles-vp2JbhuJ.js +0 -1
- package/web/dist/assets/popover-BohP6WTx.js +0 -1
- package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +0 -3
- package/web/dist/assets/preview-state-vuMviq6q.js +0 -1
- package/web/dist/assets/prompt-templates-Cubdut_C.js +0 -15
- package/web/dist/assets/repo-git-Cf4t5PD0.js +0 -1
- package/web/dist/assets/run-diff-BpkUW37Z.js +0 -3
- package/web/dist/assets/run-header-68WHf9wV.js +0 -1
- package/web/dist/assets/run-header-8GObNx6F.css +0 -1
- package/web/dist/assets/search-x-I9Q8pgUa.js +0 -1
- package/web/dist/assets/skills-DFBiAuJG.js +0 -1
- package/web/dist/assets/skills-DiVIgKeU.js +0 -1
- package/web/dist/assets/square-terminal-B6RYYieR.js +0 -1
- package/web/dist/assets/status-dot-CqTJHSP9.js +0 -1
- package/web/dist/assets/tab-link-B07Hu4QX.js +0 -1
- package/web/dist/assets/task-changes--lQF6EYf.js +0 -1
- package/web/dist/assets/task-commits-Cx0qy4DN.js +0 -1
- package/web/dist/assets/task-files-D50s-gKW.js +0 -2
- package/web/dist/assets/task-github-item-DaqmSwYT.js +0 -1
- package/web/dist/assets/task-thread-BPQ64lui.js +0 -7
- package/web/dist/assets/task-thread-zNE2Q9E-.css +0 -1
- package/web/dist/assets/textarea-D20O4Pw0.js +0 -1
- package/web/dist/assets/thread-state-DWcFMrvb.js +0 -4
- package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +0 -1
- package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +0 -1
- package/web/dist/assets/utils-JsL3ZsJf.js +0 -64
- package/web/dist/assets/workflows-jwG7uxqi.js +0 -11
- package/web/dist/assets/zoomable-image-BJJ64KGc.js +0 -1
package/dist/runs/store.js
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
|
+
import { defined } from '../core/defined.js';
|
|
2
|
+
import { autosaveCleanupBlocker, clearAutosaveCleanup } from '../autosave-cleanup.js';
|
|
1
3
|
import { EventEmitter } from 'node:events';
|
|
2
4
|
import { workerEvidenceRunIds, workerExecutionSchema } from './worker-execution.js';
|
|
3
5
|
import { agentTmpDirLocations, agentTmpDirMayExist, agentTmpDirOwnershipProven, removeAgentTmpDir } from './agent-tmpdir.js';
|
|
4
6
|
import { removeArtifacts } from '../artifacts/lifecycle.js';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
8
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
9
|
import { appendFileSync, closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readSync, realpathSync, readdirSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
10
|
import { join, resolve } from 'node:path';
|
|
9
11
|
import { z } from 'zod';
|
|
10
|
-
import { ciWaitSchema
|
|
11
|
-
import {
|
|
12
|
+
import { ciWaitSchema } from '../contract/index.js';
|
|
13
|
+
import { workerWaitSchema } from '../contract/index.js';
|
|
14
|
+
import { agentInputSchema, inboxClaimSchema, delegationStateSchema, workerCreationReceiptSchema, workerCollectedResultSchema, workerResultFileSchema, workerDestroyRetrySchema, } from '../contract/index.js';
|
|
15
|
+
import { automationIdOf, stepStateSchema as contractStepStateSchema, queuedMessageSchema, toRunSummary, runRecordSchema as contractRunRecordSchema, runPullRequestSchema, mergedPullRequestSchema, } from '../contract/index.js';
|
|
16
|
+
import { previewServerSchema } from '../contract/index.js';
|
|
17
|
+
import { isUsageLimitHold, waitSchema, LAST_WAITS_LIMIT, TIMER_MAX_SECONDS, TIMER_MIN_SECONDS, WAIT_LIMIT, } from '../contract/index.js';
|
|
18
|
+
import { engineOwnsRun, isSweepable } from '../contract/index.js';
|
|
19
|
+
import { matchesRunSearch, sqlPrefilterTokens } from '../contract/index.js';
|
|
12
20
|
import { storedDelegationStateSchema } from './delegation-state.js';
|
|
13
21
|
import { HistoryCompressor } from './history-compressor.js';
|
|
22
|
+
import { LiveItemSnapshots } from './live-item-snapshots.js';
|
|
14
23
|
import { emptyFacts, stampOf, TranscriptFactsIndex } from './transcript-facts.js';
|
|
15
24
|
import { hasPlainHistory, historyPaths, readHistoryText, readHistoryTextAsync, removeHistory, restoreHistory } from './history-file.js';
|
|
16
25
|
import { workerExecutionIdentitySchema } from '../delegation/execution-identity.js';
|
|
17
|
-
import { reconcileWorkerWait } from '../delegation/wait.js';
|
|
18
26
|
import { capacityError, workerCapacity } from '../delegation/capacity.js';
|
|
19
27
|
import { inspectGeneration, isCurrentProcess, recordedProcessLive, processStartToken } from '../delegation/process-liveness.js';
|
|
20
28
|
import { collectSecretValues, redactDeep, redactSecrets } from '../core/secret-redaction.js';
|
|
21
29
|
// Pure, dependency-free reference helpers — the same sanity bound the marker parser applies.
|
|
22
|
-
import {
|
|
30
|
+
import { archivePullRequests, pullRequestKey, mergePullRequests } from './pull-requests.js';
|
|
31
|
+
import { extractTaskRefs, MAX_REF } from './task-refs.js';
|
|
23
32
|
// Type-only module (zod + nothing else), so this cannot cycle back into the store.
|
|
24
33
|
import { workflowDefSchema } from '../workflows/types.js';
|
|
34
|
+
import { applyOutcomes } from './outcome-commit.js';
|
|
25
35
|
import { RUNNER_IDS } from '../core/agent-runner.js';
|
|
26
36
|
import { ARCHIVED_ROOT, RUNS_DB_FILE, RUNS_IMPORT_COMPLETE_KEY, RunConflictError, RunDatabase, RunDatabaseBusyError, } from './run-database.js';
|
|
37
|
+
import { restoreLostReferencedPullRequest } from './loaded-run-healing.js';
|
|
27
38
|
import { encodeRunRow, isLiveRecord, isLiveStatus } from './run-row.js';
|
|
28
39
|
import { collectRawExtras, encodeRawRecord } from './raw-record.js';
|
|
29
40
|
import { assertNoLegacyCockpit, assertNoLegacyWriter, backUpLegacyIndex, LEGACY_INDEX_FILE, LegacyWriterError, readLegacyIndex } from './legacy-index.js';
|
|
30
41
|
import { toRunStoreOpenError } from './store-open-error.js';
|
|
31
42
|
import { claimOwnerLive, closeClaimSession, openClaimSession } from './run-claims.js';
|
|
32
|
-
const usageCounterSchema = z.number().finite().nonnegative();
|
|
33
43
|
/**
|
|
34
44
|
* A runner id as it may appear in a PERSISTED record, normalized to the three
|
|
35
45
|
* ids the rest of cezar speaks (#547).
|
|
@@ -53,317 +63,346 @@ const usageCounterSchema = z.number().finite().nonnegative();
|
|
|
53
63
|
const storedRunnerSchema = z
|
|
54
64
|
.enum([...RUNNER_IDS, 'claude-cli'])
|
|
55
65
|
.transform((id) => (id === 'claude-cli' ? 'claude' : id));
|
|
56
|
-
const stepStateSchema =
|
|
57
|
-
id: z.string(),
|
|
58
|
-
name: z.string(),
|
|
59
|
-
kind: z.enum(['agent', 'check']),
|
|
60
|
-
status: z.enum(['pending', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled', 'skipped']),
|
|
61
|
-
iterations: z.number(),
|
|
62
|
-
/** Durable count of onFail retry loops consumed by this check. */
|
|
63
|
-
retriesUsed: z.number().int().nonnegative().optional(),
|
|
64
|
-
tokensUsed: z.number(),
|
|
65
|
-
inputTokens: usageCounterSchema.optional(),
|
|
66
|
-
outputTokens: usageCounterSchema.optional(),
|
|
67
|
-
usageInvocationsStarted: usageCounterSchema.optional(),
|
|
68
|
-
usageInvocationsObserved: usageCounterSchema.optional(),
|
|
69
|
-
usageTurnsStarted: usageCounterSchema.optional(),
|
|
70
|
-
usageTurnsRecorded: usageCounterSchema.optional(),
|
|
71
|
-
usageInvocationEpoch: usageCounterSchema.optional(),
|
|
72
|
-
startedAt: z.string().optional(),
|
|
73
|
-
finishedAt: z.string().optional(),
|
|
74
|
-
error: z.string().optional(),
|
|
75
|
-
/** Latest backend-owned session id, used for same-backend Continue. */
|
|
76
|
-
sessionId: z.string().optional(),
|
|
77
|
-
/** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse;
|
|
78
|
-
* `storedRunnerSchema` so a legacy `claude-cli` folds to `claude` instead of failing (#547). */
|
|
79
|
-
backend: storedRunnerSchema.optional(),
|
|
80
|
-
/** Agent profile (account) this step actually spawned under — `default`, or a stored profile
|
|
81
|
-
* id (spec 2026-07-29-agent-profiles). Recorded rather than re-derived because a session id
|
|
82
|
-
* only means something inside the config dir that created it: `sessionId` and `profileId` are
|
|
83
|
-
* a PAIR. Without it, changing the project's account would silently make Continue resume
|
|
84
|
-
* against the wrong account's session store. Absent = the discovered default. */
|
|
85
|
-
profileId: z.string().optional(),
|
|
86
|
-
/** Dollar cost reported by the claude CLI for this step's turns. */
|
|
87
|
-
costUsd: z.number().optional(),
|
|
88
|
-
/** Explicit provenance for engine-generated continuation steps. */
|
|
89
|
-
synthetic: z.literal('continuation').optional(),
|
|
90
|
-
});
|
|
91
|
-
/** One prompt message stacked onto a run while it waits for a free agent slot
|
|
92
|
-
* (#472). Folded into `{{task}}` at dequeue by `hydrateQueuedInput`; never
|
|
93
|
-
* delivered as its own turn — a follow-up turn would reach only the first step
|
|
94
|
-
* of a chain and would race the opening turn. */
|
|
95
|
-
const queuedMessageSchema = z.object({
|
|
96
|
-
id: z.string(),
|
|
97
|
-
text: z.string(),
|
|
98
|
-
/** `/api/v1/runs/:id/images/…` URLs — the base64 never enters `runs.json`. */
|
|
99
|
-
images: z.array(z.string()).optional(),
|
|
100
|
-
createdAt: z.string(),
|
|
101
|
-
});
|
|
66
|
+
const stepStateSchema = contractStepStateSchema.extend({ backend: storedRunnerSchema.optional() });
|
|
102
67
|
/** Exported for `./run-index.ts`, the read-only reader of the same records. Nothing else should
|
|
103
68
|
* parse a stored run — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
|
|
104
|
-
export const runRecordSchema =
|
|
105
|
-
id: z.string(),
|
|
106
|
-
title: z.string(),
|
|
107
|
-
/** Display title (#389): the auto-derived summary of the first agent turn,
|
|
108
|
-
* or the user's inline edit (`PATCH /api/runs/:id` sets it together with
|
|
109
|
-
* `title` so edits always win). The UI shows `titleSummary ?? title`. */
|
|
110
|
-
titleSummary: z.string().optional(),
|
|
111
|
-
/** `git diff --shortstat` of the worktree vs its base, refreshed on every
|
|
112
|
-
* turn-end (#389) — what the quick list / table shows without a git call.
|
|
113
|
-
* `repointed` (#751) is optional and only ever written as `true`: it marks the
|
|
114
|
-
* runs whose numbers were narrowed to uncommitted work because the agent had
|
|
115
|
-
* checked another branch out into the worktree. Optional is load-bearing here —
|
|
116
|
-
* `runs.json` is `safeParse`d as one array, so a required addition would
|
|
117
|
-
* silently drop every pre-existing run. */
|
|
118
|
-
diffStat: z
|
|
119
|
-
.object({
|
|
120
|
-
adds: z.number(),
|
|
121
|
-
dels: z.number(),
|
|
122
|
-
files: z.number(),
|
|
123
|
-
repointed: z.boolean().optional(),
|
|
124
|
-
})
|
|
125
|
-
.optional(),
|
|
126
|
-
workflow: z.string(),
|
|
127
|
-
task: z.string(),
|
|
128
|
-
/** Prompt messages stacked onto this run while it was queued (#472). Optional:
|
|
129
|
-
* `undefined` on every pre-#472 record reads as an empty stack. Like `task`,
|
|
130
|
-
* `text` is the user's own prompt and is replayed into `{{task}}`, so it is
|
|
131
|
-
* deliberately NOT in `redactPatch`'s field list — scrubbing it would corrupt
|
|
132
|
-
* the run the same way scrubbing `task` would. */
|
|
133
|
-
queuedMessages: z.array(queuedMessageSchema).optional(),
|
|
134
|
-
/** Durable Continue opening message; id is its synthetic step id. No base64 in the index. */
|
|
135
|
-
continuationMessage: continuationMessageSchema.optional(),
|
|
136
|
-
/** A malformed explicit ASK must never be collected as worker success. */
|
|
137
|
-
invalidAsk: z.boolean().optional(),
|
|
69
|
+
export const runRecordSchema = contractRunRecordSchema.extend({
|
|
138
70
|
delegation: storedDelegationStateSchema,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
lastCiWait: ciWaitSchema.optional(),
|
|
143
|
-
/** Retained recovery observation when previous CI metadata cannot be trusted. */
|
|
144
|
-
lastCiWaitError: z.string().max(256).optional(),
|
|
145
|
-
/** Dev servers the agent registered with `cezar_preview_serve` (#781). Optional so old files parse. */
|
|
146
|
-
previewServers: z.array(previewServerSchema).optional(),
|
|
147
|
-
/** URLs of images attached to the initial task prompt, for the thread's first bubble
|
|
148
|
-
* (#image-display) — persisted like agent screenshots, served from `/images/`. */
|
|
149
|
-
taskImages: z.array(z.string()).optional(),
|
|
150
|
-
model: z.string().optional(),
|
|
151
|
-
/** Reasoning-effort pin (#45). Canonical `low`/`medium`/`high`/`xhigh`/`max`.
|
|
152
|
-
* Absent = harness default. Additive: pre-#45 records omit it and still parse. */
|
|
153
|
-
effort: z.string().max(32).optional(),
|
|
154
|
-
/** Canonical provider/model identity (#405) — the normalised `provider/model`
|
|
155
|
-
* (e.g. `anthropic/claude-opus-4-8`) the run actually used, resolved from the
|
|
156
|
-
* free-text `model` against the chosen runner. Additive and optional: pre-#405
|
|
157
|
-
* records carry only `model`, and it stays the human/hand-edit surface; this
|
|
158
|
-
* is the parseable identity cost attribution and reproducible replay key off.
|
|
159
|
-
*
|
|
160
|
-
* Read in production by the session header's agent badge (#546), which shows it
|
|
161
|
-
* whenever it says something `model` does not — so this is no longer a
|
|
162
|
-
* write-only field whose next reader has to guess whether it is load-bearing. */
|
|
163
|
-
modelIdentity: z.string().optional(),
|
|
164
|
-
/** Agent backend this run used — drives "open in CLI" resume command. `storedRunnerSchema`
|
|
165
|
-
* so a legacy `claude-cli` record folds to `claude` instead of failing the whole index (#547). */
|
|
71
|
+
lastWaitsError: contractRunRecordSchema.shape.lastWaitsError.catch(undefined),
|
|
72
|
+
automaticWakes: contractRunRecordSchema.shape.automaticWakes.catch(undefined),
|
|
73
|
+
waitDeliveryFailures: contractRunRecordSchema.shape.waitDeliveryFailures.catch(undefined),
|
|
166
74
|
runner: storedRunnerSchema.optional(),
|
|
167
|
-
/** Per-task agent-account override from the composer (spec 2026-07-29-agent-profiles), applying
|
|
168
|
-
* to steps that run on `runner`. Steps on a DIFFERENT backend still resolve from the project's
|
|
169
|
-
* own selection — an override for Claude says nothing about which Codex account a mixed
|
|
170
|
-
* workflow's codex step should use. Absent = follow the project. */
|
|
171
|
-
agentProfile: z.string().optional(),
|
|
172
|
-
/** Echo of the extra system prompt this run actually used (R2): the
|
|
173
|
-
* `POST /api/runs` override, or the `config.json` default it fell back to.
|
|
174
|
-
* Deliberately NOT the full composed prompt — skill bodies and the handoff
|
|
175
|
-
* contract are derivable from the persisted workflow and would bloat the
|
|
176
|
-
* index. Resolved at execute time (a queued run picks up config edits). */
|
|
177
|
-
systemPrompt: z.string().optional(),
|
|
178
|
-
/** Per-task follow-up inbox contract (spec 007, #444). Missing on old runs
|
|
179
|
-
* means enabled — the historical behavior. */
|
|
180
|
-
generateFollowups: z.boolean().optional(),
|
|
181
|
-
/** Effective autonomous mode (#489), set at creation from StartRunInput.autonomous.
|
|
182
|
-
* This records the resolved launch mode, not whether a user clicked a checkbox:
|
|
183
|
-
* the composer may select it through a skill or workspace default (#458).
|
|
184
|
-
* Used by the autonomous nudge and terminal review policy. Continue/recovery
|
|
185
|
-
* preserve it; absent on legacy records means non-autonomous. */
|
|
186
|
-
autonomous: z.boolean().optional(),
|
|
187
|
-
/** Task webhook opt-in (#589). Absent = off; `POST /runs/:id/notify` flips it at any time. */
|
|
188
75
|
notify: z.boolean().optional().catch(undefined),
|
|
189
|
-
/** The last task-webhook delivery's outcome (#589), written by `runs/webhook.ts`. */
|
|
190
76
|
webhook: contractRunRecordSchema.shape.webhook.catch(undefined),
|
|
191
|
-
/** Optional provenance for tasks launched by a project GitHub automation. */
|
|
192
|
-
automation: z
|
|
193
|
-
.object({
|
|
194
|
-
automationId: z.string(),
|
|
195
|
-
automationRevision: z.number().int().positive(),
|
|
196
|
-
receiptId: z.string(),
|
|
197
|
-
event: z.string(),
|
|
198
|
-
githubUrl: z.string().url(),
|
|
199
|
-
})
|
|
200
|
-
.optional(),
|
|
201
|
-
/** Provenance for a task a scheduled automation launched. Its own key so a pre-schedule cezar
|
|
202
|
-
* strips it instead of failing the whole index on a missing `githubUrl`. */
|
|
203
|
-
automationTrigger: z
|
|
204
|
-
.object({
|
|
205
|
-
automationId: z.string(),
|
|
206
|
-
automationRevision: z.number().int().positive(),
|
|
207
|
-
receiptId: z.string(),
|
|
208
|
-
trigger: z.enum(['schedule', 'catch-up', 'manual']),
|
|
209
|
-
occurrenceAt: z.string(),
|
|
210
|
-
})
|
|
211
|
-
.optional(),
|
|
212
|
-
/** A pending worker destroy's automatic retry state (hearsay-tools/cezarion#879); a malformed one is dropped, never the run. */
|
|
213
77
|
destroyRetry: contractRunRecordSchema.shape.destroyRetry.catch(undefined),
|
|
214
|
-
status: z.enum(['queued', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled']),
|
|
215
78
|
stopping: contractRunRecordSchema.shape.stopping.catch(undefined),
|
|
216
|
-
|
|
217
|
-
* `monitoring` while the agent is still working on its own downstream work.
|
|
218
|
-
* Optional/absent on old runs; cleared when the run resumes or ends. */
|
|
219
|
-
activity: z.enum(['monitoring']).optional(),
|
|
220
|
-
hasPendingHumanAsk: contractRunRecordSchema.shape.hasPendingHumanAsk.catch(undefined),
|
|
221
|
-
/** Exact server-computed deadline for the next automatic monitoring check. */
|
|
222
|
-
monitoringWakeAt: z.string().datetime().optional().catch(undefined),
|
|
223
|
-
/** True only for the live epoch that exhausted all automatic monitoring checks. */
|
|
224
|
-
monitoringWakeCapReached: z.boolean().optional(),
|
|
225
|
-
/**
|
|
226
|
-
* Exact deadline at which a run stopped by a provider USAGE LIMIT resumes itself
|
|
227
|
-
* (spec 2026-08-03-auto-resume-after-usage-limit) — the reset instant the provider named plus a
|
|
228
|
-
* short grace. Present only while such a resume is pending: the run is `failed`, the timer is
|
|
229
|
-
* armed, and the cockpit says so. Deliberately survives a restart (`RunStore.open` keeps it) —
|
|
230
|
-
* it is what lets `recover()` re-arm a wait that may be hours long.
|
|
231
|
-
*/
|
|
79
|
+
hasPendingPermission: contractRunRecordSchema.shape.hasPendingPermission.catch(undefined),
|
|
232
80
|
autoResumeAt: z.string().datetime().optional().catch(undefined),
|
|
233
|
-
/** Consecutive automatic resumes since the last human turn — the safety cap's counter.
|
|
234
|
-
* Persisted so a restart cannot reset a loop back to zero. */
|
|
235
81
|
autoResumeAttempts: z.number().int().min(0).optional().catch(undefined),
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
costUsd: z.number().optional(),
|
|
243
|
-
/** First GitHub PR URL spotted in the transcript (the janitor trick). */
|
|
244
|
-
pullRequestUrl: z.string().optional(),
|
|
245
|
-
/** The PR this task is ABOUT (#407, spec 2026-07-16-pr-autodiscovery):
|
|
246
|
-
* auto-discovered from conversation references for tasks that work on an
|
|
247
|
-
* existing PR (review/continue/merge). Display-only tier — `pullRequestUrl`
|
|
248
|
-
* (the PR this task CREATED) always wins, and action gates ignore this. */
|
|
249
|
-
referencedPullRequestUrl: z.string().optional(),
|
|
250
|
-
/** The PR/issue number this task is ABOUT (spec 2026-07-17-task-auto-naming):
|
|
251
|
-
* regex-extracted from the task prompt, upgradable by the namer's
|
|
252
|
-
* cross-checked output. Display tier — never gates actions. */
|
|
253
|
-
prNumber: z.number().optional(),
|
|
254
|
-
issueNumber: z.number().optional(),
|
|
255
|
-
/** Provenance for an `issueNumber` seeded by referenced-issue discovery.
|
|
256
|
-
* Persisted so ambiguity can revoke only the janitor's own value, including
|
|
257
|
-
* after a restart. Any prompt, namer, or marker write clears this flag. */
|
|
258
|
-
referencedIssueNumberSeeded: z.boolean().optional(),
|
|
259
|
-
/** Who owns the display title: `user` (PATCH rename — never auto-overwritten),
|
|
260
|
-
* `marker` (agent-declared via `CEZ:TITLE`, spec 2026-07-18-task-ref-markers —
|
|
261
|
-
* beats the namer, silences live refresh) or `auto` (namer-owned — a later
|
|
262
|
-
* namer result may replace it). Missing on old runs = legacy behavior (auto
|
|
263
|
-
* fills only an unset titleSummary). Precedence: user > marker > auto. */
|
|
264
|
-
titleOrigin: z.enum(['user', 'auto', 'marker']).optional(),
|
|
265
|
-
/** References the agent itself declared via `CEZ:PR=` / `CEZ:ISSUE=` markers
|
|
266
|
-
* (spec 2026-07-18-task-ref-markers). Presence of a kind makes it
|
|
267
|
-
* authoritative: the namer may no longer write that kind, and a declared PR
|
|
268
|
-
* owns the referenced tier's resolution. */
|
|
269
|
-
markerRefs: z.object({ pr: z.number().optional(), issue: z.number().optional() }).optional(),
|
|
270
|
-
/** Distinct PR URLs spotted so far — the referenced tier's working set,
|
|
271
|
-
* persisted so a resumed run keeps disambiguating against the full history
|
|
272
|
-
* instead of re-adopting the next URL as "the only one". Capped. */
|
|
273
|
-
referencedPrCandidates: z.array(z.string()).optional(),
|
|
274
|
-
/** The issue this task is ABOUT (spec 2026-07-21-report-ref-discovery):
|
|
275
|
-
* auto-discovered from `github.com/…/issues/N` links in the conversation,
|
|
276
|
-
* mirroring the referenced-PR tier. Display-only; never gates actions. */
|
|
277
|
-
referencedIssueUrl: z.string().optional(),
|
|
278
|
-
/** Distinct issue URLs spotted so far — the referenced-issue working set,
|
|
279
|
-
* persisted like `referencedPrCandidates`. Capped. */
|
|
280
|
-
referencedIssueCandidates: z.array(z.string()).optional(),
|
|
281
|
-
/** Explicit execution policy. `false` means the run intentionally uses the repo root;
|
|
282
|
-
* absent on older runs and for the default isolated-worktree mode. */
|
|
283
|
-
worktree: z.literal(false).optional(),
|
|
284
|
-
/** Task worktree (spec 006) — absent for in-place runs and after explicit cleanup. */
|
|
285
|
-
worktreePath: z.string().optional(),
|
|
286
|
-
/** The task's own branch (`cez/<id8>`), created off `baseBranch`. */
|
|
287
|
-
branch: z.string().optional(),
|
|
288
|
-
/** Stable baseline for session git views: a worktree's fork ref, or an in-place run's starting commit. */
|
|
289
|
-
baseBranch: z.string().optional(),
|
|
290
|
-
/** Set when count-based retention (#483) reclaimed this run's worktree
|
|
291
|
-
* *directory* (the `cez/<id8>` branch is kept). Presence means "materialized
|
|
292
|
-
* dir gone, recoverable via `git worktree add`"; it excludes the run from the
|
|
293
|
-
* retention budget until the dir is re-materialized (resume clears it). */
|
|
294
|
-
worktreeReclaimedAt: z.string().optional(),
|
|
295
|
-
/** Worktree setup (#917). An unreadable value drops the field, never the run. */
|
|
82
|
+
pullRequests: z.preprocess((value) => Array.isArray(value)
|
|
83
|
+
? value.flatMap((entry) => {
|
|
84
|
+
const parsed = runPullRequestSchema.safeParse(entry);
|
|
85
|
+
return parsed.success ? [parsed.data] : [];
|
|
86
|
+
})
|
|
87
|
+
: undefined, z.array(runPullRequestSchema).optional()),
|
|
296
88
|
worktreeSetup: contractRunRecordSchema.shape.worktreeSetup.catch(undefined),
|
|
297
|
-
/** Parallel variants (spec 010): tasks sharing a groupId are one group. */
|
|
298
|
-
groupId: z.string().optional(),
|
|
299
|
-
/** Variant letter within the group — 'A' | 'B' | 'C' (kept as a string). */
|
|
300
|
-
variant: z.string().optional(),
|
|
301
|
-
/** Idempotent start (#504): the caller's request id and the hash of the start payload it
|
|
302
|
-
* named (`src/runs/client-request.ts`). Absent on every run started without one. */
|
|
303
|
-
clientRequestId: z.string().optional(),
|
|
304
|
-
clientRequestHash: z.string().optional(),
|
|
305
|
-
/** Peak resident memory (bytes) / process count observed across the run's
|
|
306
|
-
* agent process trees (#348) — written when a session's telemetry ends.
|
|
307
|
-
* Optional: old runs.json files and `ps`-less platforms have neither. */
|
|
308
|
-
peakRssBytes: z.number().optional(),
|
|
309
|
-
peakProcCount: z.number().optional(),
|
|
310
89
|
archived: z.boolean().default(false),
|
|
311
|
-
archivedAt: z.string().optional(),
|
|
312
|
-
/** Pinned to the top of this project's task list (#935): plain per-task state, the
|
|
313
|
-
* same class as `archived` and `seenAt`. Optional with NO default, unlike `archived`:
|
|
314
|
-
* absent is what every runs.json written before this carries and it already means
|
|
315
|
-
* "not pinned", so nothing needs filling in on parse and an unpin can simply delete
|
|
316
|
-
* the key rather than persist a `false` older cezars never wrote. */
|
|
317
|
-
pinned: z.boolean().optional(),
|
|
318
|
-
pinnedAt: z.string().optional(),
|
|
319
|
-
/** Read receipt (#unread-done-items): the ISO time the cockpit last opened this
|
|
320
|
-
* run's thread. A finished run reads as "unread" until it has been seen since it
|
|
321
|
-
* finished — see `isUnread()` in the cockpit's `lib/read-state.ts`. Absent on old
|
|
322
|
-
* runs, on every run not yet opened, and on one `setUnread` put back to unread
|
|
323
|
-
* (#775) — the unread rule treats all three alike. */
|
|
324
|
-
seenAt: z.string().optional(),
|
|
325
|
-
currentStepId: z.string().optional(),
|
|
326
|
-
error: z.string().optional(),
|
|
327
|
-
steps: z.array(stepStateSchema),
|
|
328
|
-
/** Full workflow definition, persisted so a `queued` run can be re-enqueued
|
|
329
|
-
* after a restart (#367) — including ad-hoc "(planned)" chains that exist
|
|
330
|
-
* nowhere else.
|
|
331
|
-
*
|
|
332
|
-
* Typed, not `z.record(z.string(), z.unknown())`: this key goes out over the
|
|
333
|
-
* wire on every run route, and `unknown` is wider than anything the server
|
|
334
|
-
* can serialize — which made the route's own type (hono's `JSONValue`, whose
|
|
335
|
-
* index signature admits `object | symbol | undefined`) impossible for the
|
|
336
|
-
* contract to describe. `.catch(undefined)` keeps an older or hand-edited
|
|
337
|
-
* entry from failing the whole index parse: a def that no longer fits simply
|
|
338
|
-
* drops, and `reviveWorkflow` falls back to the catalog by name.
|
|
339
|
-
*
|
|
340
|
-
* That drop is PERMANENT, not per-boot — the index is re-serialized from the
|
|
341
|
-
* parsed records (`saveNow`), so the next save writes runs.json back without
|
|
342
|
-
* it. Harmless for a catalog workflow, which re-resolves by name; fatal for
|
|
343
|
-
* the ad-hoc "(planned)" chain this field exists to preserve, which has no
|
|
344
|
-
* catalog entry to fall back to. Nothing written since #367 fails the schema
|
|
345
|
-
* (`name`, `source` and `steps` have been on every persisted def), so tighten
|
|
346
|
-
* `workflowStepSchema` only with that in mind: a narrowing here silently eats
|
|
347
|
-
* queued runs rather than degrading them. */
|
|
348
90
|
workflowDef: workflowDefSchema.optional().catch(undefined),
|
|
91
|
+
steps: z.array(stepStateSchema),
|
|
92
|
+
automation: contractRunRecordSchema.shape.automation.unwrap().extend({
|
|
93
|
+
automationRevision: z.number().int().positive(),
|
|
94
|
+
githubUrl: z.string().url(),
|
|
95
|
+
}).optional(),
|
|
96
|
+
automationTrigger: contractRunRecordSchema.shape.automationTrigger.unwrap().extend({
|
|
97
|
+
automationRevision: z.number().int().positive(),
|
|
98
|
+
}).optional(),
|
|
99
|
+
/** The handoff file's autosave records (hearsay-tools/cezarion#1809); unreadable drops the field. */
|
|
100
|
+
handoffRecords: contractRunRecordSchema.shape.handoffRecords.catch(undefined),
|
|
349
101
|
});
|
|
350
|
-
/**
|
|
102
|
+
/** The keys an older record used for parks. Schema 4 reads the CI and monitoring ones into
|
|
103
|
+
* `waits`; phase 4b (hearsay-tools/cezarion#1236) reads the pending-question summary into a human
|
|
104
|
+
* wait. None is written again (spec 2026-10-17-unified-waits, § Compatibility). */
|
|
105
|
+
const LEGACY_WAIT_KEYS = ['ciWait', 'lastCiWait', 'lastCiWaitError', 'monitoringWakeAt', 'monitoringWakeCapReached', 'activity', 'hasPendingHumanAsk'];
|
|
106
|
+
const UNREADABLE_WAIT = 'Wait unavailable — a saved wait is unreadable; register a new wait.';
|
|
107
|
+
/** A stored `waits`/`lastWaits` entry of kind `ci`, the kind `pr` replaced (hearsay-tools/cezarion#1425). */
|
|
108
|
+
const isCiKindEntry = (entry) => !!entry && typeof entry === 'object' && entry.kind === 'ci';
|
|
109
|
+
function hasCiKindEntries(row) {
|
|
110
|
+
return ['waits', 'lastWaits'].some((key) => Array.isArray(row[key]) && row[key].some(isCiKindEntry));
|
|
111
|
+
}
|
|
112
|
+
function hasLegacyWaitFields(row) {
|
|
113
|
+
return !!row && typeof row === 'object' && (LEGACY_WAIT_KEYS.some((key) => key in row) || hasCiKindEntries(row) || agentWorkerWaits(row).length > 0 || rootCompletion(row) !== undefined);
|
|
114
|
+
}
|
|
115
|
+
/** A root's stored `delegation.completion` (the old completion gate's marker), which schema 7 reads
|
|
116
|
+
* into the list and drops (hearsay-tools/cezarion#1301). */
|
|
117
|
+
function rootCompletion(row) {
|
|
118
|
+
const delegation = row.delegation;
|
|
119
|
+
if (!delegation || typeof delegation !== 'object' || delegation.role !== 'root')
|
|
120
|
+
return undefined;
|
|
121
|
+
const { completion } = delegation;
|
|
122
|
+
return completion && typeof completion === 'object' ? completion : undefined;
|
|
123
|
+
}
|
|
124
|
+
/** The worker waits a record still holds in `delegation` (store schemas 6 and 7,
|
|
125
|
+
* hearsay-tools/cezarion#1235, #1301), in the order they settled: `lastWait`, then `wait`. The
|
|
126
|
+
* completion gate's wait, the one `delegation.completion.waitId` names, is flagged `completion`: it
|
|
127
|
+
* was Cezar's own, not the agent's. A value that does not parse is left where it is, for the
|
|
128
|
+
* delegation schema to quarantine as it always has. */
|
|
129
|
+
function agentWorkerWaits(row) {
|
|
130
|
+
const delegation = row.delegation;
|
|
131
|
+
if (!delegation || typeof delegation !== 'object')
|
|
132
|
+
return [];
|
|
133
|
+
const { role } = delegation;
|
|
134
|
+
if (role !== 'root' && role !== 'worker')
|
|
135
|
+
return [];
|
|
136
|
+
const waitId = rootCompletion(row)?.waitId;
|
|
137
|
+
const found = [];
|
|
138
|
+
for (const key of ['lastWait', 'wait']) {
|
|
139
|
+
const parsed = workerWaitSchema.safeParse(delegation[key]);
|
|
140
|
+
if (parsed.success)
|
|
141
|
+
found.push({ key, wait: parsed.data, completion: waitId === parsed.data.id });
|
|
142
|
+
}
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
/** A legacy `CiWait` as a wait entry. Its wake input's id was the wait's own (`wakeId`, which
|
|
146
|
+
* `run.ts` set to the wait id), so the entry keeps that id and delivery still finds the input. */
|
|
147
|
+
export function ciWaitEntry(wait) {
|
|
148
|
+
const { wakeId: _wakeId, phase, result, deliveredAt, ...identity } = wait;
|
|
149
|
+
return {
|
|
150
|
+
...identity, kind: 'pr', source: 'agent', via: 'cezar_wait_for_ci',
|
|
151
|
+
phase: phase === 'registered' || phase === 'parked' ? 'open' : phase === 'wake-pending' ? 'resolved' : phase === 'delivered' ? 'delivered' : 'cancelled',
|
|
152
|
+
...(result ? { result, resolvedAt: result.observedAt } : {}),
|
|
153
|
+
...(deliveredAt ? { deliveredAt } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A legacy agent `delegation.wait` as a worker wait entry (store schema 6, hearsay-tools/cezarion#1235),
|
|
158
|
+
* judged from the wait alone: what became of its wake input is `upgradeLegacyWaits`' to apply.
|
|
159
|
+
* `registered`/`parked` → open; `wake-pending` → resolved with its `outcome`/`timeout` reason, or
|
|
160
|
+
* cancelled. Undefined for the `message` stand-in, which was never an agent's wait: a message no
|
|
161
|
+
* longer resolves a wait, and its input still wakes the run on its own.
|
|
162
|
+
*
|
|
163
|
+
* Deterministic, never read from the clock: a row is upgraded on every read until open rewrites
|
|
164
|
+
* it, and two reads must agree. A `WorkerWait` kept no registration time, so `timeoutSeconds` is
|
|
165
|
+
* the 600 default, `registeredAt` is 600 s before the original deadline (kept as it was), and the
|
|
166
|
+
* settled stamps come from the wait itself: `resolvedAt` is the latest observation it holds, else
|
|
167
|
+
* its deadline, and `cancelledAt` is its deadline. `now` is only the fallback for a deadline that
|
|
168
|
+
* does not read, which the wait schema never admits.
|
|
169
|
+
*/
|
|
170
|
+
export function workerWaitEntry(wait, now) {
|
|
171
|
+
if (wait.phase === 'wake-pending' && wait.reason === 'message')
|
|
172
|
+
return undefined;
|
|
173
|
+
const deadline = Date.parse(wait.deadline);
|
|
174
|
+
const registeredAt = Number.isFinite(deadline) ? new Date(deadline - 600_000).toISOString() : now;
|
|
175
|
+
const open = wait.phase !== 'wake-pending';
|
|
176
|
+
// A pending wait from before reasons were recorded settled on its persisted observations, as
|
|
177
|
+
// `reconcileWorkerWait` infers it: satisfied by them is an outcome, anything else its deadline.
|
|
178
|
+
const satisfied = (ids, reported) => wait.mode === 'all' ? ids.every((id) => reported.includes(id)) : reported.length > 0;
|
|
179
|
+
const inferred = wait.phase === 'wake-pending' && wait.reason === undefined
|
|
180
|
+
? (wait.requestIds ? satisfied(wait.requestIds, (wait.requestOutcomes ?? []).map((outcome) => outcome.requestId))
|
|
181
|
+
: satisfied(wait.workerIds, (wait.outcomes ?? []).map((outcome) => outcome.workerId))) ? 'outcome' : 'timeout'
|
|
182
|
+
: undefined;
|
|
183
|
+
const reason = wait.reason === 'outcome' || wait.reason === 'timeout' ? wait.reason : inferred;
|
|
184
|
+
const observed = [...(wait.outcomes ?? []), ...(wait.requestOutcomes ?? [])].map((outcome) => outcome.observedAt).sort().at(-1);
|
|
185
|
+
const resolvedAt = reason === 'outcome' && observed ? observed : wait.deadline;
|
|
186
|
+
return {
|
|
187
|
+
id: wait.id, kind: 'worker', source: 'agent', via: 'cez-worker-wait', registeredAt, deadline: wait.deadline, timeoutSeconds: 600, turnId: 'upgraded',
|
|
188
|
+
phase: open ? 'open' : reason ? 'resolved' : 'cancelled',
|
|
189
|
+
...(wait.mode ? { mode: wait.mode } : {}),
|
|
190
|
+
...(wait.requestIds
|
|
191
|
+
? { requestIds: wait.requestIds, requestOutcomes: wait.requestOutcomes ?? [] }
|
|
192
|
+
: { workerIds: wait.workerIds, revisions: wait.revisions ?? [], outcomes: wait.outcomes }),
|
|
193
|
+
...(!open && reason ? { result: { reason }, resolvedAt } : {}),
|
|
194
|
+
...(!open && !reason ? { cancelledAt: wait.deadline } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Whether a record is a root still parked on its workers: an open or resolved worker wait in the
|
|
198
|
+
* list. Such a root has its pending human question re-read from the transcript before a manager
|
|
199
|
+
* parks it. Every stored `delegation.wait`, the completion gate's included, is read into the list
|
|
200
|
+
* on open (store schema 7, hearsay-tools/cezarion#1301), so the list is the only place to look. */
|
|
201
|
+
export function holdsWorkerWait(run) {
|
|
202
|
+
if (run.delegation?.role !== 'root')
|
|
203
|
+
return false;
|
|
204
|
+
return !!run.waits?.some((wait) => wait.kind === 'worker' && (wait.phase === 'open' || wait.phase === 'resolved'));
|
|
205
|
+
}
|
|
206
|
+
/** A stable id for the timer a legacy monitoring park becomes. The row keeps its legacy JSON
|
|
207
|
+
* until it is next written, so every read of it must name the same wait. */
|
|
208
|
+
function legacyTimerId(runId, wakeAt) {
|
|
209
|
+
return legacyWaitId(`legacy-monitoring-timer:${String(runId)}:${wakeAt}`);
|
|
210
|
+
}
|
|
211
|
+
/** The human wait a stored pending-question summary becomes: one per run, the same on every read. */
|
|
212
|
+
function legacyHumanWaitId(runId) {
|
|
213
|
+
return legacyWaitId(`legacy-pending-ask:${String(runId)}`);
|
|
214
|
+
}
|
|
215
|
+
/** A name-based (version 5 shaped) UUID, so a row read twice upgrades to the same entry. */
|
|
216
|
+
function legacyWaitId(name) {
|
|
217
|
+
const hex = createHash('sha256').update(name).digest('hex');
|
|
218
|
+
const variant = (8 | (Number.parseInt(hex[16], 16) & 3)).toString(16);
|
|
219
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Upgrade-on-read (store schema 4, spec 2026-10-17-unified-waits): read a schema-3 record's CI and
|
|
223
|
+
* monitoring fields into its wait list, then delete them. Mutates `row`, before validation.
|
|
224
|
+
*
|
|
225
|
+
* - `ciWait`: `registered`/`parked` → an open `pr` checks wait, `wake-pending` → resolved (resolved when
|
|
226
|
+
* its result was observed), `delivered`/`withdrawn` → `lastWaits` as delivered/cancelled.
|
|
227
|
+
* - `lastCiWait` → `lastWaits`: `delivered` stays delivered, anything else was withdrawn intent and
|
|
228
|
+
* reads as cancelled. `lastCiWaitError` → `lastWaitsError`.
|
|
229
|
+
* - `activity: 'monitoring'` on a live run: with `monitoringWakeAt`, an open timer due then
|
|
230
|
+
* (`seconds` is what remains, clamped to a timer's bounds); without one, no wait. Either way a
|
|
231
|
+
* `running` run parks as `waiting`, its running step with it, as every waiting park writes it.
|
|
232
|
+
* On a finished run the fields are stale and only dropped.
|
|
233
|
+
* - `hasPendingHumanAsk: true` on a `waiting` run with no open human wait (phase 4b,
|
|
234
|
+
* hearsay-tools/cezarion#1236): an open `human` wait, `ask-line`, with no `askSeq`. Recovery shows
|
|
235
|
+
* it (`surfaceHumanWait`), which attaches it to the question still pending in the transcript
|
|
236
|
+
* instead of raising a second card. Anywhere else the summary is dropped: the human wait it stood
|
|
237
|
+
* beside since phase 2 already says the same.
|
|
238
|
+
*
|
|
239
|
+
* - `delegation.wait`/`lastWait` registered by the agent (store schema 6, hearsay-tools/cezarion#1235)
|
|
240
|
+
* → `workerWaitEntry`, then settled by what became of the wake input (`wakeId`, else the wait's own
|
|
241
|
+
* id): a wait whose input was delivered reads as delivered, a `wake-pending` one whose input is
|
|
242
|
+
* still queued stays resolved and records it as `wakeInputId`, so it is delivered once, and a
|
|
243
|
+
* `lastWait` that was never delivered reads as cancelled. The `message` stand-in is dropped. No
|
|
244
|
+
* input is added or removed: a cancelled receipt's undelivered wake stays queued and is delivered
|
|
245
|
+
* as an ordinary input. The completion gate's wait (store schema 7, hearsay-tools/cezarion#1301) becomes a
|
|
246
|
+
* `source: 'cezar'`, `via: 'completion'` entry the same way, and `delegation.completion` is dropped. A current wait
|
|
247
|
+
* (the stand-in included) whose wake a provider took on a `waiting` run is the old engine's crash
|
|
248
|
+
* between the delivery checkpoint and the resume: the run reads `running`, so recovery resumes
|
|
249
|
+
* it. A wake an inbox read acknowledged, or one beside a pending question, resumes nothing.
|
|
250
|
+
*
|
|
251
|
+
* - A `waits`/`lastWaits` entry of kind `ci` (hearsay-tools/cezarion#1425) → kind `pr` with
|
|
252
|
+
* `until: 'checks'` and `wakeOnConflict: true`, what a `ci` wait did.
|
|
253
|
+
*
|
|
254
|
+
* An unreadable legacy CI wait gets the per-entry salvage `parseRunRecords` gives `waits`: it is
|
|
255
|
+
* dropped, `lastWaitsError` says so, and the status is left alone. A `waiting` run with no open
|
|
256
|
+
* wait reads as Needs you, which a human can resume.
|
|
257
|
+
*/
|
|
258
|
+
/** A live run and its live step move from `from` to `to` together, as every park and resume writes them. */
|
|
259
|
+
function setLiveStatus(row, from, to) {
|
|
260
|
+
row.status = to;
|
|
261
|
+
if (Array.isArray(row.steps))
|
|
262
|
+
for (const step of row.steps) {
|
|
263
|
+
if (step && typeof step === 'object' && step.status === from)
|
|
264
|
+
step.status = to;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const resumeParked = (row) => setLiveStatus(row, 'waiting', 'running');
|
|
268
|
+
export function upgradeLegacyWaits(row) {
|
|
269
|
+
if (!hasLegacyWaitFields(row))
|
|
270
|
+
return;
|
|
271
|
+
// Kind `ci` waited for checks and woke on a conflict (hearsay-tools/cezarion#1425).
|
|
272
|
+
for (const key of ['waits', 'lastWaits']) {
|
|
273
|
+
if (Array.isArray(row[key]))
|
|
274
|
+
row[key] = row[key].map((entry) => isCiKindEntry(entry)
|
|
275
|
+
? { ...entry, kind: 'pr', until: 'checks', wakeOnConflict: true } : entry);
|
|
276
|
+
}
|
|
277
|
+
const opened = [];
|
|
278
|
+
const settled = [];
|
|
279
|
+
let unreadable = false;
|
|
280
|
+
const legacyCi = (value, previous) => {
|
|
281
|
+
if (value === undefined)
|
|
282
|
+
return;
|
|
283
|
+
const parsed = ciWaitSchema.safeParse(value);
|
|
284
|
+
if (!parsed.success) {
|
|
285
|
+
unreadable = true;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const entry = ciWaitEntry(parsed.data);
|
|
289
|
+
if (previous && entry.phase !== 'delivered')
|
|
290
|
+
entry.phase = 'cancelled';
|
|
291
|
+
(entry.phase === 'open' || entry.phase === 'resolved' ? opened : settled).push(entry);
|
|
292
|
+
};
|
|
293
|
+
// The previous wait settled before the current one did, so it goes first.
|
|
294
|
+
legacyCi(row.lastCiWait, true);
|
|
295
|
+
legacyCi(row.ciWait, false);
|
|
296
|
+
const agentWaits = agentWorkerWaits(row);
|
|
297
|
+
if (agentWaits.length) {
|
|
298
|
+
// The wait's own stamps decide every time the entry carries (`workerWaitEntry`), not the clock.
|
|
299
|
+
const now = typeof row.createdAt === 'string' ? row.createdAt : new Date(0).toISOString();
|
|
300
|
+
const inputs = Array.isArray(row.agentInputs) ? row.agentInputs : [];
|
|
301
|
+
const current = agentWaits.find((found) => found.key === 'wait');
|
|
302
|
+
// The old engine retired a delivered wake into `lastWait` with the resume: a current wait whose
|
|
303
|
+
// wake a provider took on a `waiting` run is a crash between the two, and resumed. An inbox
|
|
304
|
+
// read starts no provider turn, and a pending question still holds the run, as they did then.
|
|
305
|
+
const interrupted = (key, input) => key === 'wait' && row.status === 'waiting' &&
|
|
306
|
+
typeof input?.deliveredAt === 'string' && !input.inboxClaim?.acknowledgedAt && row.hasPendingHumanAsk !== true;
|
|
307
|
+
for (const { key, wait, completion } of agentWaits) {
|
|
308
|
+
// A wait that settles has its copy in `lastWait` too: it is the same wait.
|
|
309
|
+
if (key === 'lastWait' && current?.wait.id === wait.id)
|
|
310
|
+
continue;
|
|
311
|
+
const input = inputs.find((candidate) => candidate.id === (wait.wakeId ?? wait.id));
|
|
312
|
+
const entry = workerWaitEntry(wait, now);
|
|
313
|
+
// Cezar's completion gate wait is its own: no agent registered it (hearsay-tools/cezarion#1301).
|
|
314
|
+
// It keeps the deadline it always carried, with the 600 s default registration.
|
|
315
|
+
if (entry && completion)
|
|
316
|
+
Object.assign(entry, { source: 'cezar', via: 'completion', mode: 'all' });
|
|
317
|
+
// The `message` stand-in becomes no entry, but its delivered message may be that crash too.
|
|
318
|
+
if (!entry) {
|
|
319
|
+
if (interrupted(key, input))
|
|
320
|
+
resumeParked(row);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const deliveredAt = typeof input?.deliveredAt === 'string' ? input.deliveredAt : undefined;
|
|
324
|
+
if (entry.phase !== 'open' && deliveredAt) {
|
|
325
|
+
Object.assign(entry, { phase: 'delivered', deliveredAt });
|
|
326
|
+
if (interrupted(key, input))
|
|
327
|
+
resumeParked(row);
|
|
328
|
+
}
|
|
329
|
+
else if (key === 'lastWait' && entry.phase !== 'cancelled')
|
|
330
|
+
Object.assign(entry, { phase: 'cancelled', cancelledAt: wait.deadline });
|
|
331
|
+
else if (entry.phase === 'resolved' && input)
|
|
332
|
+
entry.wakeInputId = input.id;
|
|
333
|
+
(entry.phase === 'open' || entry.phase === 'resolved' ? opened : settled).push(entry);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const live = row.status === 'running' || row.status === 'queued' || row.status === 'waiting';
|
|
337
|
+
if (row.activity === 'monitoring' && live) {
|
|
338
|
+
const wakeAt = row.monitoringWakeAt;
|
|
339
|
+
// Schema 3 read an unreadable deadline as none (`.catch(undefined)`): no wait.
|
|
340
|
+
if (typeof wakeAt === 'string' && z.iso.datetime().safeParse(wakeAt).success) {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const seconds = Math.min(TIMER_MAX_SECONDS, Math.max(TIMER_MIN_SECONDS, Math.round((Date.parse(wakeAt) - now) / 1000)));
|
|
343
|
+
opened.push({
|
|
344
|
+
id: legacyTimerId(row.id, wakeAt), kind: 'timer', source: 'agent', via: 'monitoring-line',
|
|
345
|
+
registeredAt: new Date(now).toISOString(), deadline: wakeAt, phase: 'open', turnId: 'legacy', seconds,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (row.status === 'running')
|
|
349
|
+
setLiveStatus(row, 'running', 'waiting');
|
|
350
|
+
}
|
|
351
|
+
const asksHuman = (entry) => !!entry && typeof entry === 'object' && entry.kind === 'human' && entry.phase === 'open';
|
|
352
|
+
if (row.hasPendingHumanAsk === true && row.status === 'waiting' &&
|
|
353
|
+
!(Array.isArray(row.waits) && row.waits.some(asksHuman)) && !opened.some(asksHuman)) {
|
|
354
|
+
opened.push({
|
|
355
|
+
id: legacyHumanWaitId(row.id), kind: 'human', source: 'agent', via: 'ask-line',
|
|
356
|
+
registeredAt: typeof row.createdAt === 'string' && z.iso.datetime().safeParse(row.createdAt).success ? row.createdAt : new Date(0).toISOString(),
|
|
357
|
+
phase: 'open', turnId: 'legacy',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (opened.length) {
|
|
361
|
+
const current = Array.isArray(row.waits) ? row.waits : [];
|
|
362
|
+
if (row.waits !== undefined && !Array.isArray(row.waits))
|
|
363
|
+
unreadable = true;
|
|
364
|
+
row.waits = [...current, ...opened.filter((wait) => !current.some((entry) => entry?.id === wait.id))];
|
|
365
|
+
}
|
|
366
|
+
if (settled.length) {
|
|
367
|
+
const current = Array.isArray(row.lastWaits) ? row.lastWaits : [];
|
|
368
|
+
if (row.lastWaits !== undefined && !Array.isArray(row.lastWaits))
|
|
369
|
+
unreadable = true;
|
|
370
|
+
row.lastWaits = [...settled, ...current].slice(-LAST_WAITS_LIMIT);
|
|
371
|
+
}
|
|
372
|
+
if (typeof row.lastCiWaitError === 'string' && row.lastWaitsError === undefined)
|
|
373
|
+
row.lastWaitsError = row.lastCiWaitError;
|
|
374
|
+
if (unreadable)
|
|
375
|
+
row.lastWaitsError = UNREADABLE_WAIT;
|
|
376
|
+
for (const key of LEGACY_WAIT_KEYS)
|
|
377
|
+
delete row[key];
|
|
378
|
+
if (agentWaits.length || rootCompletion(row)) {
|
|
379
|
+
const delegation = { ...row.delegation };
|
|
380
|
+
for (const { key } of agentWaits)
|
|
381
|
+
delete delegation[key];
|
|
382
|
+
delete delegation.completion;
|
|
383
|
+
row.delegation = delegation;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Read legacy wait fields into the wait list and salvage per entry, identically for live stores
|
|
387
|
+
* and the read-only workspace index. */
|
|
351
388
|
export function parseRunRecords(raw) {
|
|
352
389
|
if (Array.isArray(raw))
|
|
353
390
|
for (const row of raw) {
|
|
354
391
|
if (!row || typeof row !== 'object')
|
|
355
392
|
continue;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
393
|
+
upgradeLegacyWaits(row);
|
|
394
|
+
// One unreadable wait must not evict the run, nor its readable siblings.
|
|
395
|
+
for (const key of ['waits', 'lastWaits']) {
|
|
396
|
+
if (row[key] === undefined)
|
|
397
|
+
continue;
|
|
398
|
+
const entries = Array.isArray(row[key]) ? row[key] : [];
|
|
399
|
+
const readable = entries.filter((entry) => waitSchema.safeParse(entry).success);
|
|
400
|
+
if (!Array.isArray(row[key]) || readable.length !== entries.length)
|
|
401
|
+
row.lastWaitsError = UNREADABLE_WAIT;
|
|
402
|
+
if (readable.length)
|
|
403
|
+
row[key] = readable;
|
|
404
|
+
else
|
|
405
|
+
delete row[key];
|
|
367
406
|
}
|
|
368
407
|
// One unreadable registration must not evict the run, nor its readable siblings (#781).
|
|
369
408
|
if (row.previewServers !== undefined) {
|
|
@@ -384,11 +423,12 @@ function storedRow({ revision, seq, data, extras }) {
|
|
|
384
423
|
* server entry) the runtime has decided to drop, and keeps dropping on the next write, as it always
|
|
385
424
|
* has; only what the schema does not know is put back. */
|
|
386
425
|
function decodeStoredRecord(raw) {
|
|
426
|
+
const upgraded = hasLegacyWaitFields(raw);
|
|
387
427
|
const parsed = parseRunRecords([raw]);
|
|
388
428
|
if (!parsed.success)
|
|
389
429
|
return undefined;
|
|
390
430
|
const run = parsed.data[0];
|
|
391
|
-
return { run, extras: collectRawExtras(raw, run) };
|
|
431
|
+
return { run, extras: collectRawExtras(raw, run), ...(upgraded ? { upgraded: true } : {}) };
|
|
392
432
|
}
|
|
393
433
|
/** One database row's `data` back into a record and its extras. Undefined when it does not parse:
|
|
394
434
|
* one unreadable row costs that row only. */
|
|
@@ -419,6 +459,9 @@ const OPEN_RETRY_DELAYS_MS = [50, 100, 200, 400, 800, 1600];
|
|
|
419
459
|
function sleepSync(ms) {
|
|
420
460
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
421
461
|
}
|
|
462
|
+
/** Runs (and conversation roots, one family query each) the open-time facts warm-up enumerates
|
|
463
|
+
* per event-loop turn (hearsay-tools/cezarion#949), so each slice stays a few milliseconds. */
|
|
464
|
+
const WARM_IDS_PER_SLICE = 100;
|
|
422
465
|
const MAX_RUNS_KEPT = 300;
|
|
423
466
|
/** How many archived root runs the run lists carry per project (#864): `GET /run-summaries?archived=recent`
|
|
424
467
|
* and `GET /workspace/runs-index`. Unarchived runs are never cut; older archived runs page in
|
|
@@ -442,25 +485,9 @@ function refUrlRepo(url) {
|
|
|
442
485
|
return owner && name ? `${owner}/${name}`.toLowerCase() : undefined;
|
|
443
486
|
}
|
|
444
487
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* `github.com/<owner>/<repo>/pull/N`, so a research task that cites one upstream PR handed the
|
|
449
|
-
* resolver exactly one candidate and it became the task's identity — an `oko` task wearing
|
|
450
|
-
* `supabase/cli#6056`. Nothing compared the URL's repository with the project's own.
|
|
451
|
-
*
|
|
452
|
-
* A foreign URL is adoptable only when the TASK PROMPT corroborates it: the prompt names that
|
|
453
|
-
* `owner/repo`, which a pasted URL does inherently. That is the trust boundary this module already
|
|
454
|
-
* uses elsewhere — the prompt and the agent's own turn text are trusted, scraped tool output is
|
|
455
|
-
* not — and it is what keeps the legitimate cross-repo case working (#819:
|
|
456
|
-
* `om-auto-fix-pr https://github.com/open-mercato/open-mercato/pull/1977` started from cezar).
|
|
457
|
-
*
|
|
458
|
-
* Unknown handle → today's behavior exactly (`AGENTS.md` zero config: degrade, never fail). An
|
|
459
|
-
* unparseable URL is left alone for the same reason — the guard only ever removes an association
|
|
460
|
-
* it can PROVE is foreign.
|
|
461
|
-
*
|
|
462
|
-
* Note what this does not touch: `referenced*Candidates` keep recording every URL as evidence.
|
|
463
|
-
* The fix changes what is *promoted*, never what is *collected* (the #526 rule).
|
|
488
|
+
* A foreign referenced URL needs task-prompt corroboration of its repository. Unknown project
|
|
489
|
+
* handles preserve the degraded rule. This check vets the resolved winner, not the candidate
|
|
490
|
+
* list.
|
|
464
491
|
*/
|
|
465
492
|
function isRepoScopedRef(url, task, handle) {
|
|
466
493
|
if (!handle)
|
|
@@ -470,6 +497,13 @@ function isRepoScopedRef(url, task, handle) {
|
|
|
470
497
|
return true;
|
|
471
498
|
if (repo === `${handle.owner}/${handle.name}`.toLowerCase())
|
|
472
499
|
return true;
|
|
500
|
+
return promptNamesRepo(url, task);
|
|
501
|
+
}
|
|
502
|
+
/** Positive scope evidence for permanent declarations; legacy discovery still fails open. */
|
|
503
|
+
function promptNamesRepo(url, task) {
|
|
504
|
+
const repo = refUrlRepo(url);
|
|
505
|
+
if (!repo)
|
|
506
|
+
return false;
|
|
473
507
|
// Match whole owner/repository segments: naming acme/service2 must not corroborate
|
|
474
508
|
// acme/service. Slashes remain valid boundaries for full URLs and their /pull or /issues path.
|
|
475
509
|
const escapedRepo = repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -491,9 +525,30 @@ function isRepoScopedRef(url, task, handle) {
|
|
|
491
525
|
* an archived run, so a cleared field is the whole cancellation.
|
|
492
526
|
*/
|
|
493
527
|
function clearPendingAutoResume(run) {
|
|
528
|
+
// A withdrawn appointment settles the run failed with its kept error (hearsay-tools/cezarion#1236),
|
|
529
|
+
// the state a usage-limited run was in before it waited.
|
|
530
|
+
if (isUsageLimitHold(run)) {
|
|
531
|
+
run.status = 'failed';
|
|
532
|
+
run.finishedAt ??= new Date().toISOString();
|
|
533
|
+
}
|
|
494
534
|
run.autoResumeAt = undefined;
|
|
495
535
|
run.autoResumeAttempts = undefined;
|
|
496
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* The write rule (hearsay-tools/cezarion#1496): a resume time is the appointment of a `waiting`
|
|
539
|
+
* run, so every commit takes it off a record that is not `waiting`. A copy, set to `undefined`
|
|
540
|
+
* and never deleted: a caller may stage the held record itself. `autoResumeAttempts` stays, since
|
|
541
|
+
* `MAX_AUTO_RESUMES` counts attempts across resumes.
|
|
542
|
+
*/
|
|
543
|
+
function withoutStaleResumes(input) {
|
|
544
|
+
let staged;
|
|
545
|
+
for (const [id, run] of input) {
|
|
546
|
+
if (run?.autoResumeAt === undefined || isUsageLimitHold(run))
|
|
547
|
+
continue;
|
|
548
|
+
(staged ??= new Map(input)).set(id, { ...run, autoResumeAt: undefined });
|
|
549
|
+
}
|
|
550
|
+
return staged ?? input;
|
|
551
|
+
}
|
|
497
552
|
/**
|
|
498
553
|
* Archiving is resigning from a task, so it retires the pin too (#935) — a pin on a task the
|
|
499
554
|
* user has filed away is stale by definition, and the archived view collapses into one bucket
|
|
@@ -539,22 +594,15 @@ function eventTextFragments(event) {
|
|
|
539
594
|
return fragments;
|
|
540
595
|
}
|
|
541
596
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* `CREATED_PR_RE` used to be matched against everything an event carried, tool OUTPUT included,
|
|
545
|
-
* so a transcript that merely QUOTES a `gh pr create` line handed the run a PR it never opened.
|
|
546
|
-
* Not hypothetical: the task that fixed the reference chips printed another run's stored events
|
|
547
|
-
* while investigating them, and cezar read `"title": "Ran gh pr create --repo …"` out of that
|
|
548
|
-
* dump and adopted a PR from a DIFFERENT repository as its own — permanently, because the first
|
|
549
|
-
* created URL wins and the real `gh pr create` that followed was never looked at.
|
|
550
|
-
*
|
|
551
|
-
* So the claim must come from the agent's own words, or from the tool title cezar itself renders
|
|
552
|
-
* from the command it saw run. Tool output and tool input are the transcript of the world, not a
|
|
553
|
-
* statement about this run. The URL is still read from the whole event — `gh` prints it in the
|
|
554
|
-
* output — because it is the CLAIM that needs a trustworthy source, not the link.
|
|
597
|
+
* Creation claims come from agent prose or cezar-rendered tool titles, never arbitrary tool
|
|
598
|
+
* output/input. The claimed URL may still come from the event’s command result.
|
|
555
599
|
*/
|
|
556
600
|
function eventCreationClaimFragments(event) {
|
|
557
601
|
const fragments = [];
|
|
602
|
+
const nested = event.item;
|
|
603
|
+
if (event.parentItemId !== undefined || (nested && typeof nested === 'object' &&
|
|
604
|
+
nested.parentItemId !== undefined))
|
|
605
|
+
return fragments;
|
|
558
606
|
// A `tool-result` event's `result` IS raw command output; on every other event the top-level
|
|
559
607
|
// text is the agent's own.
|
|
560
608
|
if (event.type !== 'tool-result') {
|
|
@@ -595,21 +643,9 @@ function eventAgentTextFragments(event) {
|
|
|
595
643
|
return fragments;
|
|
596
644
|
}
|
|
597
645
|
/**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* contradiction clears the chip. Without a declaration: one distinct URL is
|
|
602
|
-
* the subject; among several, prefer the exact URL pasted in the prompt, then
|
|
603
|
-
* the one whose number the task prompt names (and
|
|
604
|
-
* only when exactly one matches); otherwise ambiguous — no chip beats a wrong
|
|
605
|
-
* chip.
|
|
606
|
-
*
|
|
607
|
-
* Whatever that produces is then repo-scoped (#945): a winner from another repository that the
|
|
608
|
-
* prompt does not corroborate is vetoed — see `isRepoScopedRef`. The veto is applied to the
|
|
609
|
-
* RESULT rather than to the candidate list on purpose, so the guard stays strictly subtractive:
|
|
610
|
-
* filtering first would let a project-local candidate win a two-candidate race today's rule calls
|
|
611
|
-
* ambiguous, which is a wider behavior change than the defect warrants. As written this function
|
|
612
|
-
* can only ever lose a value, never gain one.
|
|
646
|
+
* Resolve declarations and prompt-supported candidates without guessing through ambiguity. Apply
|
|
647
|
+
* repository scope to the winner only; filtering candidates first could promote an otherwise
|
|
648
|
+
* ambiguous reference.
|
|
613
649
|
*/
|
|
614
650
|
function resolveReferencedRef(candidates, task, declared, handle) {
|
|
615
651
|
const resolved = resolveCandidate(candidates, task, declared);
|
|
@@ -648,20 +684,8 @@ function refUrlNumber(url) {
|
|
|
648
684
|
return Number.isInteger(n) && n > 0 && n < MAX_REF ? n : undefined;
|
|
649
685
|
}
|
|
650
686
|
/**
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* `CEZ:PR=N` means one of two things depending on when the agent writes it: on the way in it
|
|
654
|
-
* names the PR the task is ABOUT, and once the task has opened a PR of its own the marker
|
|
655
|
-
* contract asks it to re-declare with the new number ("Re-emit with the new number if the subject
|
|
656
|
-
* changes (e.g. you open a PR later in the task)"). A declaration naming the PR this run CREATED
|
|
657
|
-
* is therefore a statement about the CREATED tier, which `pullRequestUrl` already carries — and
|
|
658
|
-
* feeding it to the referenced tier ERASES the about-PR, because `resolveReferencedRef` clears
|
|
659
|
-
* the chip when no candidate matches the declared number (a task on #4326 that opened
|
|
660
|
-
* #5366 dropped from two chips to one the moment it declared #5366).
|
|
661
|
-
*
|
|
662
|
-
* Both tiers stay true instead: the created PR is the created PR, and the reference resolves as
|
|
663
|
-
* if that declaration had not been made — which is exactly what it was before the task opened
|
|
664
|
-
* anything.
|
|
687
|
+
* Ignore a created-PR declaration when resolving the referenced subject, so a task can retain
|
|
688
|
+
* both its original subject and its new PR.
|
|
665
689
|
*/
|
|
666
690
|
function referencedPrDeclaration(run) {
|
|
667
691
|
const declared = run.markerRefs?.pr;
|
|
@@ -669,6 +693,50 @@ function referencedPrDeclaration(run) {
|
|
|
669
693
|
return undefined;
|
|
670
694
|
return declared === refUrlNumber(run.pullRequestUrl) ? undefined : declared;
|
|
671
695
|
}
|
|
696
|
+
/** Prompt attribution survives declarations of follow-up issues. Bare hashes become issues
|
|
697
|
+
* only after an issue signal; explicit PR prompts never acquire an invented issue identity. */
|
|
698
|
+
function originatingIssue(run) {
|
|
699
|
+
// Only an issue-led subject is stable attribution. Mentions inside comparisons or
|
|
700
|
+
// background instructions remain evidence, but an agent declaration may supersede them.
|
|
701
|
+
const prompt = run.task.trimStart()
|
|
702
|
+
.replace(/^(?:please\s+)?(?:(?:can|could|would|will)\s+you\s+|i\s+(?:want|would\s+like)\s+you\s+to\s+)/i, '')
|
|
703
|
+
.replace(/^(?:please\s+)?(?:fix|resolve|address|implement|investigate|review|work(?:\s+on)?)\s+/i, '');
|
|
704
|
+
const subject = /^(?:(?:the\s+)?(?:(?:github|fork|upstream)\s+)?issue\s*|(?:fork|upstream)\s+)?(?:https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/\d+|(?:[\w.-]+\/[\w.-]+)?#?\d+)\b/i.exec(prompt);
|
|
705
|
+
if (!subject)
|
|
706
|
+
return undefined;
|
|
707
|
+
const refs = extractTaskRefs(subject[0]);
|
|
708
|
+
return refs.issueNumber ?? ((run.issueNumber !== undefined || run.markerRefs?.issue !== undefined)
|
|
709
|
+
? refs.ambiguousNumber : undefined);
|
|
710
|
+
}
|
|
711
|
+
function referencedIssueDeclaration(run) {
|
|
712
|
+
return run.markerRefs?.issue === undefined ? undefined : originatingIssue(run) ?? run.markerRefs.issue;
|
|
713
|
+
}
|
|
714
|
+
function resolveIssueReference(run, handle) {
|
|
715
|
+
const declared = referencedIssueDeclaration(run);
|
|
716
|
+
const candidates = run.referencedIssueCandidates ?? [];
|
|
717
|
+
// Prompt URLs remain evidence even in legacy records without a candidate working set.
|
|
718
|
+
const promptUrls = [...new Set([...run.task.matchAll(new RegExp(ISSUE_URL_RE.source, 'g'))]
|
|
719
|
+
.map(match => match[0]).filter(url => refUrlNumber(url) === declared))];
|
|
720
|
+
if (promptUrls.length > 1)
|
|
721
|
+
return undefined;
|
|
722
|
+
if (promptUrls.length === 1)
|
|
723
|
+
return resolveReferencedRef(promptUrls, run.task, declared, handle);
|
|
724
|
+
// A number cannot choose between two repositories' issues with that same number.
|
|
725
|
+
if (declared !== undefined && new Set(candidates.filter(url => refUrlNumber(url) === declared)).size > 1) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
return resolveReferencedRef(candidates, run.task, declared, handle);
|
|
729
|
+
}
|
|
730
|
+
function restoreOriginatingIssue(run) {
|
|
731
|
+
const origin = originatingIssue(run);
|
|
732
|
+
if (origin === undefined || run.markerRefs?.issue === undefined)
|
|
733
|
+
return;
|
|
734
|
+
run.issueNumber = origin;
|
|
735
|
+
delete run.referencedIssueNumberSeeded;
|
|
736
|
+
if (run.referencedIssueCandidates !== undefined || refUrlNumber(run.referencedIssueUrl) !== origin) {
|
|
737
|
+
run.referencedIssueUrl = resolveIssueReference(run);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
672
740
|
/**
|
|
673
741
|
* The PR URL a creation phrase *introduces*, or undefined. The created URL is
|
|
674
742
|
* the first one at or after the `CREATED_PR_RE` phrase — a PR the same event
|
|
@@ -690,6 +758,33 @@ function createdPrUrl(haystack) {
|
|
|
690
758
|
}
|
|
691
759
|
return before;
|
|
692
760
|
}
|
|
761
|
+
/** Associate one URL per claim; command outputs remain separate from authored claims. */
|
|
762
|
+
function createdPrUrls(claim, haystack, toolOutput) {
|
|
763
|
+
const phrases = [...claim.matchAll(new RegExp(CREATED_PR_RE.source, 'gi'))];
|
|
764
|
+
if (!phrases.length)
|
|
765
|
+
return [];
|
|
766
|
+
const urls = phrases.flatMap((phrase, index) => {
|
|
767
|
+
const preceding = claim.slice(index ? phrases[index - 1].index + phrases[index - 1][0].length : 0, phrase.index);
|
|
768
|
+
const before = [...preceding.matchAll(new RegExp(PR_URL_RE.source, 'g'))].at(-1);
|
|
769
|
+
// URL-first reports put only punctuation/whitespace between the URL and its claim.
|
|
770
|
+
// An earlier URL followed by unrelated prose is still context, not a created resource.
|
|
771
|
+
if (before && /^[\s:—–-]*(?:draft\s+)?$/i.test(preceding.slice(before.index + before[0].length)))
|
|
772
|
+
return [before[0]];
|
|
773
|
+
const after = PR_URL_RE.exec(claim.slice(phrase.index, phrases[index + 1]?.index))?.[0];
|
|
774
|
+
return after ? [after] : [];
|
|
775
|
+
});
|
|
776
|
+
if (!urls.length && toolOutput !== undefined) {
|
|
777
|
+
// gh prints one URL per successful command. Never promote more output links than commands.
|
|
778
|
+
const outputs = [...toolOutput.matchAll(new RegExp(PR_URL_RE.source, 'g'))];
|
|
779
|
+
urls.push(...outputs.slice(0, phrases.length).map(match => match[0]));
|
|
780
|
+
}
|
|
781
|
+
if (!urls.length && phrases.length === 1) {
|
|
782
|
+
const url = createdPrUrl(`${claim} ${haystack}`);
|
|
783
|
+
if (url)
|
|
784
|
+
urls.push(url);
|
|
785
|
+
}
|
|
786
|
+
return [...new Set(urls)];
|
|
787
|
+
}
|
|
693
788
|
/**
|
|
694
789
|
* Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
|
|
695
790
|
*
|
|
@@ -704,6 +799,18 @@ function createdPrUrl(haystack) {
|
|
|
704
799
|
* are marked failed so no ghost stays behind.
|
|
705
800
|
*/
|
|
706
801
|
export function reconcileLoadedRun(run, opts) {
|
|
802
|
+
settleLoadedStop(run);
|
|
803
|
+
restoreLoadedUsageLimitHold(run);
|
|
804
|
+
failLoadedInterruptedRun(run, opts);
|
|
805
|
+
clearLoadedStaleResume(run);
|
|
806
|
+
restoreLostReferencedPullRequest(run, referencedPrDeclaration, resolveReferencedRef);
|
|
807
|
+
restoreOriginatingIssue(run);
|
|
808
|
+
// Repository scoping follows reconciliation: stores arm a handle asynchronously, while
|
|
809
|
+
// the read-only index applies its cached handle to these fresh records (#97). This ordering
|
|
810
|
+
// also scopes any referenced PR restored above without filtering candidate evidence first.
|
|
811
|
+
return run;
|
|
812
|
+
}
|
|
813
|
+
function settleLoadedStop(run) {
|
|
707
814
|
// An accepted Stop is durable intent, never an interrupted run to auto-resume.
|
|
708
815
|
if (run.stopping) {
|
|
709
816
|
run.status = 'cancelled';
|
|
@@ -714,13 +821,23 @@ export function reconcileLoadedRun(run, opts) {
|
|
|
714
821
|
}
|
|
715
822
|
}
|
|
716
823
|
run.stopping = undefined;
|
|
824
|
+
}
|
|
825
|
+
function restoreLoadedUsageLimitHold(run) {
|
|
826
|
+
// A usage-limited run is waiting for its resume (hearsay-tools/cezarion#1236, phase 4a). Rows a
|
|
827
|
+
// schema-7 cezar wrote kept it failed with the appointment; they read as the waiting run they are.
|
|
828
|
+
if (run.status === 'failed' && run.autoResumeAt !== undefined) {
|
|
829
|
+
run.status = 'waiting';
|
|
830
|
+
run.finishedAt = undefined;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function failLoadedInterruptedRun(run, opts) {
|
|
717
834
|
// A run that was live when the previous process exited can never finish —
|
|
718
835
|
// surface that instead of a forever-"running" ghost. `review` survives
|
|
719
836
|
// restarts on purpose: the gate is pure data (worktree + branch + record)
|
|
720
837
|
// with no live process, so the diff panel, Send back (resume) and Draft PR
|
|
721
838
|
// all still work.
|
|
722
|
-
|
|
723
|
-
|
|
839
|
+
// A usage-limit hold runs no process, so no exit interrupted it: its appointment stands.
|
|
840
|
+
if (!opts?.keepLive && engineOwnsRun(run)) {
|
|
724
841
|
run.status = 'failed';
|
|
725
842
|
run.error = 'interrupted — cezar process exited during the run';
|
|
726
843
|
run.finishedAt = run.finishedAt ?? new Date().toISOString();
|
|
@@ -729,44 +846,19 @@ export function reconcileLoadedRun(run, opts) {
|
|
|
729
846
|
step.status = 'failed';
|
|
730
847
|
}
|
|
731
848
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
849
|
+
}
|
|
850
|
+
function clearLoadedStaleResume(run) {
|
|
851
|
+
// Open waits are left alone at every status: RunManager cancels them when a run ends (spec
|
|
852
|
+
// 2026-10-17-unified-waits), and recovery reads the ones a crash left behind.
|
|
736
853
|
// A pending usage-limit resume survives the restart on purpose (the wait can be
|
|
737
854
|
// hours) — `RunManager.recover()` re-arms it from this field. It can only mean
|
|
738
|
-
// anything on a `
|
|
739
|
-
if (run.status !== '
|
|
855
|
+
// anything on a `waiting` run, so anywhere else it is stale bookkeeping.
|
|
856
|
+
if (run.status !== 'waiting')
|
|
740
857
|
run.autoResumeAt = undefined;
|
|
741
|
-
// The wake counter is intentionally process-local, so a restarted process
|
|
742
|
-
// starts a fresh epoch instead of displaying a stale cap.
|
|
743
|
-
run.monitoringWakeCapReached = undefined;
|
|
744
|
-
// Heal a record written before `referencedPrDeclaration` existed: a task that re-declared
|
|
745
|
-
// `CEZ:PR` with the PR it had just CREATED cleared the PR it was ABOUT, because no candidate
|
|
746
|
-
// could match the created number. The evidence is all still on the record — only the
|
|
747
|
-
// conclusion drawn from it was wrong — so re-resolve without that declaration instead of
|
|
748
|
-
// asking for a migration. Deliberately one-directional: it only runs on a record that HAS no
|
|
749
|
-
// referenced PR, so it can never take one away from a record written by an older cezar whose
|
|
750
|
-
// candidate list no longer explains it. `prNumber` is not recoverable this way (the
|
|
751
|
-
// declaration overwrote it) and is left alone — the restored URL is what paints the chip.
|
|
752
|
-
if (run.referencedPullRequestUrl === undefined &&
|
|
753
|
-
run.markerRefs?.pr !== undefined &&
|
|
754
|
-
referencedPrDeclaration(run) === undefined) {
|
|
755
|
-
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, undefined);
|
|
756
|
-
}
|
|
757
|
-
// Repository scoping follows reconciliation: stores arm a handle asynchronously, while
|
|
758
|
-
// the read-only index applies its cached handle to these fresh records (#97). This ordering
|
|
759
|
-
// also scopes any referenced PR restored above without filtering candidate evidence first.
|
|
760
|
-
return run;
|
|
761
858
|
}
|
|
762
859
|
/**
|
|
763
|
-
* Settle
|
|
764
|
-
*
|
|
765
|
-
* never started is cancelled before it began, which Continue restarts as it would after a Stop (the
|
|
766
|
-
* same "untouched" test as `isUntouchedCancelledRun`); any other live run is interrupted, exactly as
|
|
767
|
-
* an open that does not recover settles it, and Continue resumes its last session. With `stop`
|
|
768
|
-
* (the control is Stop, and this is its run) a live run is cancelled instead, as an accepted Stop
|
|
769
|
-
* always reads. Mutates `run`.
|
|
860
|
+
* Settle an adopted dead owner’s live run without starting work. Untouched runs cancel; started
|
|
861
|
+
* runs become interrupted. Stop explicitly cancels its target. Mutates run.
|
|
770
862
|
*/
|
|
771
863
|
export function settleOrphanedRun(run, opts = {}) {
|
|
772
864
|
if (opts.stop && (run.status === 'queued' || run.status === 'running' || run.status === 'waiting'))
|
|
@@ -788,6 +880,19 @@ export function settleOrphanedRun(run, opts = {}) {
|
|
|
788
880
|
*/
|
|
789
881
|
export function rescopeRun(run, handle) {
|
|
790
882
|
let changed = false;
|
|
883
|
+
if (handle && run.pullRequests) {
|
|
884
|
+
const entries = run.pullRequests.map(entry => {
|
|
885
|
+
if (entry.source !== 'declared' || !entry.url || isRepoScopedRef(entry.url, run.task, handle))
|
|
886
|
+
return entry;
|
|
887
|
+
// Revoke an uncorroborated URL, preserving the accepted declaration and its order.
|
|
888
|
+
return { number: entry.number, source: entry.source };
|
|
889
|
+
});
|
|
890
|
+
const next = mergePullRequests(undefined, entries, `https://github.com/${handle.owner}/${handle.name}`);
|
|
891
|
+
if (!isDeepStrictEqual(next, run.pullRequests)) {
|
|
892
|
+
run.pullRequests = next;
|
|
893
|
+
changed = true;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
791
896
|
if (run.referencedPullRequestUrl &&
|
|
792
897
|
!isRepoScopedRef(run.referencedPullRequestUrl, run.task, handle)) {
|
|
793
898
|
run.referencedPullRequestUrl = undefined;
|
|
@@ -809,15 +914,15 @@ export function rescopeRun(run, handle) {
|
|
|
809
914
|
return changed;
|
|
810
915
|
}
|
|
811
916
|
/**
|
|
812
|
-
* Every field `RunStore.open` may rewrite while loading a record: what `reconcileLoadedRun`
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
* function, or a normalized row stays unsaved (the store tests pin each field).
|
|
917
|
+
* Every field `RunStore.open` may rewrite while loading a record: what `reconcileLoadedRun`
|
|
918
|
+
* assigns. Compared before and after, it tells open() which rows normalization changed, so it
|
|
919
|
+
* marks those dirty and no others. Edit it together with that function, or a normalized row stays unsaved (the store tests pin each field).
|
|
816
920
|
*/
|
|
817
921
|
function loadNormalizedFields(run) {
|
|
818
922
|
return JSON.stringify([
|
|
819
|
-
run.stopping, run.status, run.finishedAt, run.error, run.
|
|
820
|
-
run.
|
|
923
|
+
run.stopping, run.status, run.finishedAt, run.error, run.autoResumeAt,
|
|
924
|
+
run.referencedPullRequestUrl, run.pullRequests,
|
|
925
|
+
run.issueNumber, run.referencedIssueUrl, run.referencedIssueNumberSeeded, run.hasPendingPermission, run.steps.map((step) => step.status),
|
|
821
926
|
]);
|
|
822
927
|
}
|
|
823
928
|
/** Test seam (#779): `beforeTransaction` runs once `runs.json` is read and parsed, before the
|
|
@@ -859,28 +964,9 @@ function legacyIndexRows(bytes) {
|
|
|
859
964
|
return { rows: [...rows.values()], skipped, total: raw.length };
|
|
860
965
|
}
|
|
861
966
|
/**
|
|
862
|
-
* Import
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* (`assertNoLegacyCockpit`): an older cezar still writes `runs.json` then, and the import could
|
|
866
|
-
* not commit. Otherwise the file is read once, through one descriptor, and parsed; it is checked
|
|
867
|
-
* again before anything is written, so a file that moved while it was parsed costs no write. Then
|
|
868
|
-
* ONE transaction writes every record and the completion marker, or nothing:
|
|
869
|
-
* - the marker is checked again once the transaction holds the write lock, so of two processes
|
|
870
|
-
* importing at once exactly one writes; the other finds the marker and writes nothing;
|
|
871
|
-
* - right before COMMIT it refuses (`LegacyWriterError`) when an older cezar may still be writing
|
|
872
|
-
* `runs.json` (`assertNoLegacyWriter`) — the check that decides;
|
|
873
|
-
* - only then are the exact bytes kept beside it (`backUpLegacyIndex`: synced, never half-written,
|
|
874
|
-
* an existing backup trusted only when it holds the same bytes), and the marker records what
|
|
875
|
-
* the import read: size, sha256 and the backup's name. A refused attempt leaves no backup;
|
|
876
|
-
* - a crash or refusal leaves no marker, so the next open imports again from the start.
|
|
877
|
-
* `runs.json` itself is left as it was, and nothing writes it again: an older cezar keeps reading
|
|
878
|
-
* the history as it stood at the upgrade, and what it writes afterwards is never imported.
|
|
879
|
-
*
|
|
880
|
-
* A record that does not parse is skipped and the rest are imported; an index that does not parse
|
|
881
|
-
* at all imports as an empty history. Either way the import that commits says so once, naming the
|
|
882
|
-
* backup: the bytes survive there and in `runs.json`, which used to be overwritten by the next save
|
|
883
|
-
* instead.
|
|
967
|
+
* Import legacy rows and the completion marker atomically, once. Recheck live-writer evidence
|
|
968
|
+
* before commit and preserve exact input bytes in a synced backup. Leave runs.json untouched;
|
|
969
|
+
* invalid entries are skipped and reported.
|
|
884
970
|
*/
|
|
885
971
|
function importLegacyIndex(db, dataDir) {
|
|
886
972
|
assertNoLegacyCockpit(dataDir);
|
|
@@ -915,13 +1001,23 @@ function importLegacyIndex(db, dataDir) {
|
|
|
915
1001
|
}
|
|
916
1002
|
/**
|
|
917
1003
|
* Finished rows whose stored summary an open-time normalization would still change: a referenced
|
|
918
|
-
* PR an older cezar's created-PR declaration erased
|
|
1004
|
+
* PR an older cezar's created-PR declaration erased, or issue attribution displaced by a
|
|
1005
|
+
* follow-up declaration (see `reconcileLoadedRun`). Not live, so not
|
|
919
1006
|
* in the `live` column; open decodes these few and keeps the ones it actually repaired.
|
|
920
1007
|
*/
|
|
921
|
-
const LEGACY_REFERENCE_HEAL_SQL = "json_extract(summary, '$.markerRefs.pr') IS NOT NULL AND json_extract(summary, '$.referencedPullRequestUrl') IS NULL"
|
|
1008
|
+
const LEGACY_REFERENCE_HEAL_SQL = "(json_extract(summary, '$.markerRefs.pr') IS NOT NULL AND json_extract(summary, '$.referencedPullRequestUrl') IS NULL)" +
|
|
1009
|
+
" OR (NOT EXISTS (SELECT 1 FROM meta WHERE key = 'issue-attribution:' || runs.id AND value = CAST(runs.revision AS TEXT)) AND json_extract(summary, '$.markerRefs.issue') IS NOT NULL)";
|
|
1010
|
+
/**
|
|
1011
|
+
* Rows whose record names the automation that launched it while the stored summary does not
|
|
1012
|
+
* (hearsay-tools/cezarion#1332): summaries a cezar wrote before `RunSummary.automationId`
|
|
1013
|
+
* existed. Open rewrites each one's summary once, so the sidebar's Automations tab classifies a
|
|
1014
|
+
* finished task nobody has opened since the upgrade. `instr` keeps the JSON parse off every
|
|
1015
|
+
* other row.
|
|
1016
|
+
*/
|
|
1017
|
+
const AUTOMATION_ORIGIN_HEAL_SQL = "instr(data, '\"automation') > 0 AND json_extract(summary, '$.automationId') IS NULL" +
|
|
1018
|
+
" AND (json_extract(data, '$.automation.automationId') IS NOT NULL OR json_extract(data, '$.automationTrigger.automationId') IS NOT NULL)";
|
|
922
1019
|
/** Mark-all-read's rule (see `RunStore.markAllRead`) over the columns and the stored summary. */
|
|
923
1020
|
const MARK_ALL_READ_SQL = "archived = 0 AND status IN ('done', 'failed') AND finished_at IS NOT NULL" +
|
|
924
|
-
" AND NOT (status = 'failed' AND json_extract(summary, '$.autoResumeAt') IS NOT NULL)" +
|
|
925
1021
|
" AND (json_extract(summary, '$.seenAt') IS NULL OR json_extract(summary, '$.seenAt') < finished_at)";
|
|
926
1022
|
/** History retention's candidates among the given ids (a JSON array): finished, without
|
|
927
1023
|
* delegation, and not pinned. */
|
|
@@ -1089,51 +1185,10 @@ const recordedProcessSchema = z.object({ pid: z.number().int().positive(), start
|
|
|
1089
1185
|
const workerProcessRecordSchema = z.object({ generation: z.string().uuid(), controller: recordedProcessSchema,
|
|
1090
1186
|
processes: z.array(recordedProcessSchema).max(WORKER_PROCESS_CAP) }).strict();
|
|
1091
1187
|
const startToken = (pid) => { const token = processStartToken(pid); return token === undefined ? {} : { startToken: token }; };
|
|
1092
|
-
/** The bulk-archive sweep's one predicate (#780): finished, not archived, not scheduled (a
|
|
1093
|
-
* `failed` run waiting out a usage limit), not an owned worker (those leave with their parent),
|
|
1094
|
-
* then filtered by pin state. Mirrored clause for clause by `isSweepable` in the cockpit's
|
|
1095
|
-
* `lib/tasks-table.ts`. */
|
|
1096
|
-
export function isSweepable(run, scope) {
|
|
1097
|
-
if (run.archived || !['done', 'failed', 'cancelled'].includes(run.status))
|
|
1098
|
-
return false;
|
|
1099
|
-
if (run.status === 'failed' && run.autoResumeAt !== undefined)
|
|
1100
|
-
return false;
|
|
1101
|
-
if (run.delegation?.role === 'worker')
|
|
1102
|
-
return false;
|
|
1103
|
-
if (scope === 'unpinned')
|
|
1104
|
-
return !run.pinned;
|
|
1105
|
-
if (scope === 'pinned')
|
|
1106
|
-
return run.pinned === true;
|
|
1107
|
-
return true;
|
|
1108
|
-
}
|
|
1109
1188
|
/**
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
* Only the held set lives in memory (#779, Amendment 2): live runs, the runs a RunManager pins,
|
|
1115
|
-
* their delegation families, and runs whose latest write has not settled yet (see `isHeld`).
|
|
1116
|
-
* `getRun` answers a held run with the held object, so `commitIndex` still installs into the
|
|
1117
|
-
* record a caller holds; any other run is read from `runs.db` as a fresh copy every time (frozen
|
|
1118
|
-
* under vitest, so an in-place write throws instead of being lost). Writes to a finished run go
|
|
1119
|
-
* through the methods here, which load it, change it and persist the row like any other.
|
|
1120
|
-
*
|
|
1121
|
-
* Two write paths, both through `writeIndex`:
|
|
1122
|
-
* - optimistic: a method changes the held record, marks it dirty and schedules the debounced
|
|
1123
|
-
* save, which writes the dirty rows and deletions only;
|
|
1124
|
-
* - durable (`commitIndex`): one transaction with the changed rows AND everything a debounced
|
|
1125
|
-
* save still owes. Only once it commits do the records change in memory, and only then do
|
|
1126
|
-
* subscribers hear about it.
|
|
1127
|
-
*
|
|
1128
|
-
* Several processes may open one project (`serve` and a headless `cez run`), so a store writes
|
|
1129
|
-
* only the delegation families it claims in `run_claims` (#779, plan step 3; `run-claims.ts`).
|
|
1130
|
-
* Everything it holds is claimed: open claims the live rows nobody alive owns and leaves the rest
|
|
1131
|
-
* alone, and a write to a run not in memory claims its family first, or refuses it while another
|
|
1132
|
-
* live process owns it. Claims are released once a family leaves memory, and all of them on
|
|
1133
|
-
* `close()`; a crashed owner's are taken over only once it is proven dead. Every write is fenced
|
|
1134
|
-
* in its transaction: if a row is not as this store last saw it, or its claim is gone, nothing is
|
|
1135
|
-
* written, the evidence goes to `run_conflicts` and this store stops writing that row
|
|
1136
|
-
* (`handleConflicts`).
|
|
1189
|
+
* Run rows in runs.db plus append-only NDJSON events and an in-process event bus. Hold live,
|
|
1190
|
+
* pinned, related and unsettled records only. Optimistic writes debounce; durable commits
|
|
1191
|
+
* publish after persistence. Family claims fence every write.
|
|
1137
1192
|
*/
|
|
1138
1193
|
export class RunStore extends EventEmitter {
|
|
1139
1194
|
dataDir;
|
|
@@ -1189,6 +1244,11 @@ export class RunStore extends EventEmitter {
|
|
|
1189
1244
|
compressor;
|
|
1190
1245
|
/** Transcript facts the delegation paths ask about, so they never re-read a transcript (#880). */
|
|
1191
1246
|
facts;
|
|
1247
|
+
liveItems = new LiveItemSnapshots();
|
|
1248
|
+
/** Capture both sources on the same synchronous sequence boundary before history IO. */
|
|
1249
|
+
liveReadSnapshot(runId) {
|
|
1250
|
+
return { events: this.liveItems.read(runId), throughSeq: this.seqs.get(runId) ?? Infinity };
|
|
1251
|
+
}
|
|
1192
1252
|
constructor(dataDir) {
|
|
1193
1253
|
super();
|
|
1194
1254
|
this.dataDir = dataDir;
|
|
@@ -1219,7 +1279,7 @@ export class RunStore extends EventEmitter {
|
|
|
1219
1279
|
const started = Date.now();
|
|
1220
1280
|
for (let attempt = 0;; attempt++) {
|
|
1221
1281
|
try {
|
|
1222
|
-
return RunStore.openOnce(dataDir, path, opts?.keepLive === true);
|
|
1282
|
+
return RunStore.openOnce(dataDir, path, opts?.keepLive === true, opts?.maintenance === true);
|
|
1223
1283
|
}
|
|
1224
1284
|
catch (error) {
|
|
1225
1285
|
const transient = error instanceof RunDatabaseBusyError || (error instanceof LegacyWriterError && error.reason === 'cockpit');
|
|
@@ -1230,7 +1290,7 @@ export class RunStore extends EventEmitter {
|
|
|
1230
1290
|
}
|
|
1231
1291
|
}
|
|
1232
1292
|
}
|
|
1233
|
-
static openOnce(dataDir, path, keepLive) {
|
|
1293
|
+
static openOnce(dataDir, path, keepLive, maintenance) {
|
|
1234
1294
|
mkdirSync(join(dataDir, 'runs'), { recursive: true });
|
|
1235
1295
|
const store = new RunStore(dataDir);
|
|
1236
1296
|
store.keepLive = keepLive;
|
|
@@ -1240,8 +1300,10 @@ export class RunStore extends EventEmitter {
|
|
|
1240
1300
|
importLegacyIndex(db, dataDir);
|
|
1241
1301
|
store.db = db;
|
|
1242
1302
|
store.owner = openClaimSession();
|
|
1243
|
-
|
|
1244
|
-
|
|
1303
|
+
if (!maintenance) {
|
|
1304
|
+
store.loadHeldRows();
|
|
1305
|
+
store.warmTranscriptFacts();
|
|
1306
|
+
}
|
|
1245
1307
|
}
|
|
1246
1308
|
catch (error) {
|
|
1247
1309
|
if (store.owner) {
|
|
@@ -1316,8 +1378,26 @@ export class RunStore extends EventEmitter {
|
|
|
1316
1378
|
continue;
|
|
1317
1379
|
const decoded = decode(row);
|
|
1318
1380
|
// Held only when the repair changed it: the next save writes it, then it leaves.
|
|
1319
|
-
if (decoded)
|
|
1320
|
-
|
|
1381
|
+
if (decoded) {
|
|
1382
|
+
// Persist verification once even when legacy issue attribution was already correct.
|
|
1383
|
+
const needsIssueCheck = decoded.run.markerRefs?.issue !== undefined &&
|
|
1384
|
+
this.db.getMeta(`issue-attribution:${row.id}`) !== String(row.revision);
|
|
1385
|
+
this.adoptLoadedRun(decoded, { keepLive: this.keepLive, onlyIfChanged: !needsIssueCheck }, row);
|
|
1386
|
+
if (needsIssueCheck)
|
|
1387
|
+
this.dirty.add(row.id);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const stale = this.db.listWhere(AUTOMATION_ORIGIN_HEAL_SQL).filter((row) => !this.held.has(row.id));
|
|
1391
|
+
const restamp = this.claimFamilies(stale.map(rowFamily));
|
|
1392
|
+
for (const row of stale) {
|
|
1393
|
+
if (!restamp.has(rowFamily(row)))
|
|
1394
|
+
continue;
|
|
1395
|
+
const decoded = decode(row);
|
|
1396
|
+
// Nothing in the record changes, only its summary: held dirty, the next save rewrites it.
|
|
1397
|
+
if (decoded) {
|
|
1398
|
+
this.adoptLoadedRun(decoded, { keepLive: this.keepLive }, row);
|
|
1399
|
+
this.dirty.add(row.id);
|
|
1400
|
+
}
|
|
1321
1401
|
}
|
|
1322
1402
|
for (const rootId of this.anchoredFamilies())
|
|
1323
1403
|
this.holdFamily(rootId, decode);
|
|
@@ -1327,17 +1407,16 @@ export class RunStore extends EventEmitter {
|
|
|
1327
1407
|
}
|
|
1328
1408
|
/** Normalize a record just read from `row` (see `reconcileLoadedRun`) and hold it. Marks the row
|
|
1329
1409
|
* dirty only when normalization changed it, so the next save persists exactly the runs open()
|
|
1330
|
-
* rewrote.
|
|
1331
|
-
|
|
1410
|
+
* rewrote. A row read up from schema-3 wait fields counts as changed: its rewrite drops them
|
|
1411
|
+
* and recomputes its summary and live column (`upgradeLegacyWaits`). The caller has claimed
|
|
1412
|
+
* the row's family. */
|
|
1413
|
+
adoptLoadedRun({ run, extras, upgraded }, opts, row) {
|
|
1332
1414
|
const before = loadNormalizedFields(run);
|
|
1333
|
-
if (run.delegation?.role === 'root' && (run.status === 'waiting' || run.delegation.wait !== undefined)) {
|
|
1334
|
-
this.syncHumanAskSummary(run);
|
|
1335
|
-
}
|
|
1336
1415
|
if (opts.settle)
|
|
1337
1416
|
settleOrphanedRun(run, { stop: opts.stop });
|
|
1338
1417
|
else
|
|
1339
1418
|
reconcileLoadedRun(run, opts);
|
|
1340
|
-
const changed = loadNormalizedFields(run) !== before;
|
|
1419
|
+
const changed = upgraded === true || loadNormalizedFields(run) !== before;
|
|
1341
1420
|
if (opts.onlyIfChanged && !changed)
|
|
1342
1421
|
return;
|
|
1343
1422
|
this.held.set(run.id, run);
|
|
@@ -1492,12 +1571,9 @@ export class RunStore extends EventEmitter {
|
|
|
1492
1571
|
return ownership === 'foreign' || ownership === 'orphaned' ? RUN_IN_USE_ELSEWHERE : undefined;
|
|
1493
1572
|
}
|
|
1494
1573
|
/**
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* control is Stop: `id` itself is settled as cancelled rather than interrupted. Returns the
|
|
1499
|
-
* family's root id, or undefined when a live owner holds it after all or it is gone. The caller
|
|
1500
|
-
* runs recovery for that family next (`RunManager.adoptOrphanedRun`), which then repairs it.
|
|
1574
|
+
* Claim an orphaned family and settle its live rows before a control acts. Stop cancels its
|
|
1575
|
+
* target. Return the root id, or undefined for a live owner or missing run; adoption never
|
|
1576
|
+
* resumes work.
|
|
1501
1577
|
*/
|
|
1502
1578
|
adoptFamily(id, opts = {}) {
|
|
1503
1579
|
const family = this.db?.familyOf(id);
|
|
@@ -1670,6 +1746,14 @@ export class RunStore extends EventEmitter {
|
|
|
1670
1746
|
holders.add(holder);
|
|
1671
1747
|
return run;
|
|
1672
1748
|
}
|
|
1749
|
+
/** Publish a run's current record again without a write: something read beside the record
|
|
1750
|
+
* changed, such as the run leaving the manager's live registries, which a detail's finish
|
|
1751
|
+
* verdict reads (hearsay-tools/cezarion#1301). */
|
|
1752
|
+
republish(id) {
|
|
1753
|
+
const run = this.peek(id);
|
|
1754
|
+
if (run)
|
|
1755
|
+
this.emit('run', run, 'live-release');
|
|
1756
|
+
}
|
|
1673
1757
|
/** Release `holder`'s pin. The run leaves memory at the next sweep once nothing else holds it. */
|
|
1674
1758
|
unpin(id, holder) {
|
|
1675
1759
|
const holders = this.pins.get(id);
|
|
@@ -1680,20 +1764,8 @@ export class RunStore extends EventEmitter {
|
|
|
1680
1764
|
this.sweepIfSettled([id]);
|
|
1681
1765
|
}
|
|
1682
1766
|
/**
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
* A setter rather than an `open()` option because `open()` is synchronous and the handle costs a
|
|
1687
|
-
* `gh` spawn: callers arm this in the background so boot never waits on the network. `null` is a
|
|
1688
|
-
* first-class answer meaning "cannot be known" (no `gh`, no remote, a non-git root) and leaves
|
|
1689
|
-
* the store in exactly its pre-#945 behavior.
|
|
1690
|
-
*
|
|
1691
|
-
* Arming also HEALS records already poisoned by the un-scoped rule, on the `reconcileLoadedRun`
|
|
1692
|
-
* precedent: the evidence is all still on the record (`referenced*Candidates`), only the
|
|
1693
|
-
* conclusion drawn from it was wrong, so re-deciding beats asking for a migration. It rewrites
|
|
1694
|
-
* values, never the format, and is one-directional by construction — see `rescopeRun`. Held
|
|
1695
|
-
* runs are re-checked in memory; of the rest, only rows whose stored summary references another
|
|
1696
|
-
* repository are decoded, and the healed ones are written, so their summaries heal too.
|
|
1767
|
+
* Set discovered repository identity without blocking boot. Recompute conclusions from retained
|
|
1768
|
+
* evidence, including cold records; null retains unknown-handle behavior.
|
|
1697
1769
|
*/
|
|
1698
1770
|
setRepoHandle(handle) {
|
|
1699
1771
|
this.repoHandle = handle;
|
|
@@ -1743,7 +1815,7 @@ export class RunStore extends EventEmitter {
|
|
|
1743
1815
|
/**
|
|
1744
1816
|
* The live set, newest first: runs whose record is live (`isLiveRecord`) or that a RunManager
|
|
1745
1817
|
* pins. Finished runs are not here — read one by id (`getRun`), by an indexed query
|
|
1746
|
-
* (`listWorkersOf`, `findRunByClientRequestId`, `
|
|
1818
|
+
* (`listWorkersOf`, `findRunByClientRequestId`, `listRunsWithWorktree`,
|
|
1747
1819
|
* `listBranchOwners`), or as list rows (`listRunSummaries`).
|
|
1748
1820
|
*/
|
|
1749
1821
|
listRuns() {
|
|
@@ -1819,7 +1891,9 @@ export class RunStore extends EventEmitter {
|
|
|
1819
1891
|
* One page of archived ROOT runs, newest first (#864): `GET /run-summaries/archived`, the
|
|
1820
1892
|
* archived runs past the window. Held runs list by their record, as in `listRunSummaries`.
|
|
1821
1893
|
* `before` is the previous page's `nextCursor`; with `q`, only runs `matchesRunQuery` keeps, and
|
|
1822
|
-
* `total` counts those.
|
|
1894
|
+
* `total` counts those. `automationNames` lets `q` match a task by its automation's name, as the
|
|
1895
|
+
* list's origin line shows it (hearsay-tools/cezarion#1616). A cursor this store did not write
|
|
1896
|
+
* answers `{ error }`.
|
|
1823
1897
|
*
|
|
1824
1898
|
* Ranking reads every archived root's key (id, created_at, seq: no record, no summary), so the
|
|
1825
1899
|
* page and `total` agree with the held overlay; only the page's own rows are read in full.
|
|
@@ -1832,6 +1906,16 @@ export class RunStore extends EventEmitter {
|
|
|
1832
1906
|
return { error: 'before is not a cursor this server wrote' };
|
|
1833
1907
|
}
|
|
1834
1908
|
const q = options.q?.trim() ?? '';
|
|
1909
|
+
const ranked = this.rankArchivedRows(q, options.automationNames);
|
|
1910
|
+
this.overlayHeldArchived(ranked, q, options.automationNames);
|
|
1911
|
+
const ordered = [...ranked.entries()].sort(([, a], [, b]) => newestFirst(a, b));
|
|
1912
|
+
return this.archivedPage(ordered, after, options.limit);
|
|
1913
|
+
}
|
|
1914
|
+
/** The archived roots in `runs.db`, each by its key: every one without `q` (no record, no
|
|
1915
|
+
* summary read), and with `q` the searched rows `matchesRunSearch` keeps, which carry the summary
|
|
1916
|
+
* the search read. A deleted, held or unreadable run's row is left out; a held run joins in
|
|
1917
|
+
* `overlayHeldArchived`. */
|
|
1918
|
+
rankArchivedRows(q, automationNames) {
|
|
1835
1919
|
const skipped = (id) => this.deleted.has(id) || this.held.has(id) || this.unreadable.has(id);
|
|
1836
1920
|
const ranked = new Map();
|
|
1837
1921
|
if (q === '') {
|
|
@@ -1840,30 +1924,33 @@ export class RunStore extends EventEmitter {
|
|
|
1840
1924
|
ranked.set(key.id, key);
|
|
1841
1925
|
}
|
|
1842
1926
|
else {
|
|
1843
|
-
for (const row of this.db?.searchRootSummaries(sqlPrefilterTokens(q)) ?? []) {
|
|
1927
|
+
for (const row of this.db?.searchRootSummaries(sqlPrefilterTokens(q, { automationNames })) ?? []) {
|
|
1844
1928
|
if (skipped(row.id))
|
|
1845
1929
|
continue;
|
|
1846
1930
|
const summary = parseStoredSummary(row.summary) ?? this.coldSummary(row.id);
|
|
1847
|
-
if (summary?.archived &&
|
|
1931
|
+
if (summary?.archived && matchesRunSearch(summary, q, { automationNames }))
|
|
1848
1932
|
ranked.set(row.id, { ...row, summary });
|
|
1849
1933
|
}
|
|
1850
1934
|
}
|
|
1935
|
+
return ranked;
|
|
1936
|
+
}
|
|
1937
|
+
/** Lay the held archived roots over `ranked`, each by its record: it replaces its stored row
|
|
1938
|
+
* (already left out of `ranked`), and with `q` it must match. A held worker never lists. */
|
|
1939
|
+
overlayHeldArchived(ranked, q, automationNames) {
|
|
1851
1940
|
for (const [id, run] of this.held) {
|
|
1852
1941
|
if (!run.archived || run.delegation?.role === 'worker')
|
|
1853
1942
|
continue;
|
|
1854
1943
|
const summary = toRunSummary(run);
|
|
1855
|
-
if (q === '' ||
|
|
1944
|
+
if (q === '' || matchesRunSearch(summary, q, { automationNames }))
|
|
1856
1945
|
ranked.set(id, { ...this.listOrder(run), summary });
|
|
1857
1946
|
}
|
|
1858
|
-
|
|
1947
|
+
}
|
|
1948
|
+
/** The page of `ordered` after the cursor `after`: its summaries, the cursor of the next page,
|
|
1949
|
+
* and `total`, the count of all of `ordered`. */
|
|
1950
|
+
archivedPage(ordered, after, limit) {
|
|
1859
1951
|
const rest = after ? ordered.filter(([, key]) => newestFirst(key, after) > 0) : ordered;
|
|
1860
|
-
const page = rest.slice(0,
|
|
1861
|
-
const
|
|
1862
|
-
.map((row) => [row.id, parseStoredSummary(row.summary) ?? this.coldSummary(row.id)]));
|
|
1863
|
-
const runs = page.flatMap(([id, key]) => {
|
|
1864
|
-
const summary = key.summary ?? cold.get(id);
|
|
1865
|
-
return summary ? [summary] : [];
|
|
1866
|
-
});
|
|
1952
|
+
const page = rest.slice(0, limit);
|
|
1953
|
+
const runs = this.hydrateArchivedPage(page);
|
|
1867
1954
|
const last = page.at(-1)?.[1];
|
|
1868
1955
|
return {
|
|
1869
1956
|
runs,
|
|
@@ -1871,8 +1958,19 @@ export class RunStore extends EventEmitter {
|
|
|
1871
1958
|
total: ordered.length,
|
|
1872
1959
|
};
|
|
1873
1960
|
}
|
|
1961
|
+
/** The page's summaries: a ranked key's own, else its stored one, read for the rows that have
|
|
1962
|
+
* none in a single query. A row whose stored summary does not fit the contract is projected from
|
|
1963
|
+
* its record; a row that cannot be read at all is left out. */
|
|
1964
|
+
hydrateArchivedPage(page) {
|
|
1965
|
+
const cold = new Map((this.db?.getSummaries(page.filter(([, key]) => !key.summary).map(([id]) => id)) ?? [])
|
|
1966
|
+
.map((row) => [row.id, parseStoredSummary(row.summary) ?? this.coldSummary(row.id)]));
|
|
1967
|
+
return page.flatMap(([id, key]) => {
|
|
1968
|
+
const summary = key.summary ?? cold.get(id);
|
|
1969
|
+
return summary ? [summary] : [];
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1874
1972
|
/**
|
|
1875
|
-
* The ROOT runs, archived or not, that match `query` (`
|
|
1973
|
+
* The ROOT runs, archived or not, that match `query` (`matchesRunSearch`), newest first: at most
|
|
1876
1974
|
* `limit`, and whether more matched. The owned half of the workspace search (#864); held runs
|
|
1877
1975
|
* match by their record.
|
|
1878
1976
|
*/
|
|
@@ -1884,7 +1982,7 @@ export class RunStore extends EventEmitter {
|
|
|
1884
1982
|
if (this.deleted.has(row.id) || this.held.has(row.id) || this.unreadable.has(row.id))
|
|
1885
1983
|
return true;
|
|
1886
1984
|
const summary = parseStoredSummary(row.summary) ?? this.coldSummary(row.id);
|
|
1887
|
-
if (summary &&
|
|
1985
|
+
if (summary && matchesRunSearch(summary, query))
|
|
1888
1986
|
matched.set(row.id, [summary, row]);
|
|
1889
1987
|
return matched.size <= limit;
|
|
1890
1988
|
});
|
|
@@ -1892,7 +1990,7 @@ export class RunStore extends EventEmitter {
|
|
|
1892
1990
|
if (run.delegation?.role === 'worker')
|
|
1893
1991
|
continue;
|
|
1894
1992
|
const summary = toRunSummary(run);
|
|
1895
|
-
if (
|
|
1993
|
+
if (matchesRunSearch(summary, query))
|
|
1896
1994
|
matched.set(id, [summary, this.listOrder(run)]);
|
|
1897
1995
|
}
|
|
1898
1996
|
const runs = sortNewestFirst(matched.values());
|
|
@@ -1986,6 +2084,61 @@ export class RunStore extends EventEmitter {
|
|
|
1986
2084
|
}
|
|
1987
2085
|
return [...ids];
|
|
1988
2086
|
}
|
|
2087
|
+
/** All retained tasks this automation started, including variants, finished and archived runs.
|
|
2088
|
+
* Logs and receipts may compact independently. Read only matching keys in SQLite and overlay
|
|
2089
|
+
* held records so a debounced provenance update or deletion is reflected immediately. */
|
|
2090
|
+
countAutomationRuns(automationId) {
|
|
2091
|
+
const where = "json_extract(data, '$.automation.automationId') = ? OR " +
|
|
2092
|
+
"json_extract(data, '$.automationTrigger.automationId') = ?";
|
|
2093
|
+
const ids = new Set((this.db?.listKeysWhere(where, [automationId, automationId]) ?? [])
|
|
2094
|
+
.map((key) => key.id));
|
|
2095
|
+
for (const id of this.deleted)
|
|
2096
|
+
ids.delete(id);
|
|
2097
|
+
for (const [id, run] of this.held) {
|
|
2098
|
+
if (run.automation?.automationId === automationId || run.automationTrigger?.automationId === automationId)
|
|
2099
|
+
ids.add(id);
|
|
2100
|
+
else
|
|
2101
|
+
ids.delete(id);
|
|
2102
|
+
}
|
|
2103
|
+
return ids.size;
|
|
2104
|
+
}
|
|
2105
|
+
/** First non-archived task referencing a project issue, including secondary references. */
|
|
2106
|
+
findNonArchivedRunByIssue(repo, number) {
|
|
2107
|
+
const url = `https://github.com/${repo}/issues/${number}`.toLowerCase();
|
|
2108
|
+
const matches = (run) => {
|
|
2109
|
+
if (run.archived)
|
|
2110
|
+
return false;
|
|
2111
|
+
const urls = [run.referencedIssueUrl, ...(run.referencedIssueCandidates ?? []),
|
|
2112
|
+
...(run.automation?.event.startsWith('issue.') ? [run.automation.githubUrl] : []),
|
|
2113
|
+
...[...run.task.matchAll(new RegExp(ISSUE_URL_RE.source, 'gi'))].map((match) => match[0])]
|
|
2114
|
+
.filter((value) => value !== undefined)
|
|
2115
|
+
.map((value) => value.replace(/\/+$/, '').toLowerCase())
|
|
2116
|
+
.filter((value) => value.endsWith(`/issues/${number}`));
|
|
2117
|
+
// The legacy issueNumber is display-only and cannot establish repository identity.
|
|
2118
|
+
// Explicit task markers remain project-scoped unless a qualified URL contradicts them.
|
|
2119
|
+
return urls.includes(url) || (urls.length === 0 && run.markerRefs?.issue === number);
|
|
2120
|
+
};
|
|
2121
|
+
for (const run of this.held.values())
|
|
2122
|
+
if (matches(run))
|
|
2123
|
+
return run.id;
|
|
2124
|
+
// Secondary references are record-only: query them in SQLite rather than hydrating every
|
|
2125
|
+
// finished transcript or relying on the slim summary's primary issue number.
|
|
2126
|
+
const where = `archived = 0 AND (
|
|
2127
|
+
json_extract(data, '$.issueNumber') = ? OR json_extract(data, '$.markerRefs.issue') = ?
|
|
2128
|
+
OR lower(rtrim(json_extract(data, '$.referencedIssueUrl'), '/')) = ?
|
|
2129
|
+
OR EXISTS (SELECT 1 FROM json_each(data, '$.referencedIssueCandidates') WHERE lower(rtrim(value, '/')) = ?)
|
|
2130
|
+
OR (json_extract(data, '$.automation.event') LIKE 'issue.%' AND lower(rtrim(json_extract(data, '$.automation.githubUrl'), '/')) = ?)
|
|
2131
|
+
OR instr(lower(json_extract(data, '$.task')), ?) > 0
|
|
2132
|
+
)`;
|
|
2133
|
+
for (const key of this.db?.listKeysWhere(where, [number, number, url, url, url, url]) ?? []) {
|
|
2134
|
+
if (this.held.has(key.id) || this.deleted.has(key.id) || this.unreadable.has(key.id))
|
|
2135
|
+
continue;
|
|
2136
|
+
const run = this.getRun(key.id);
|
|
2137
|
+
if (run && matches(run))
|
|
2138
|
+
return key.id;
|
|
2139
|
+
}
|
|
2140
|
+
return undefined;
|
|
2141
|
+
}
|
|
1989
2142
|
/**
|
|
1990
2143
|
* The runs automation receipts launched, by receipt id (`automation.receiptId` or
|
|
1991
2144
|
* `automationTrigger.receiptId`). Memory answers first; a receipt it does not know costs one
|
|
@@ -2041,10 +2194,6 @@ export class RunStore extends EventEmitter {
|
|
|
2041
2194
|
ids.add(id);
|
|
2042
2195
|
return [...ids];
|
|
2043
2196
|
}
|
|
2044
|
-
/** The members of one parallel-variant group (spec 010), in no particular order. */
|
|
2045
|
-
listGroupRuns(groupId) {
|
|
2046
|
-
return this.queryRuns(this.db?.listByGroup(groupId) ?? [], (run) => run.groupId === groupId).map(([run]) => run);
|
|
2047
|
-
}
|
|
2048
2197
|
/** Runs with a materialized worktree directory (`worktreePath`, not reclaimed), most recently
|
|
2049
2198
|
* finished first, then newest first: worktree retention and the worktrees panel. */
|
|
2050
2199
|
listRunsWithWorktree() {
|
|
@@ -2138,13 +2287,16 @@ export class RunStore extends EventEmitter {
|
|
|
2138
2287
|
effort: input.effort,
|
|
2139
2288
|
runner: input.runner,
|
|
2140
2289
|
agentProfile: input.agentProfile,
|
|
2141
|
-
generateFollowups: input.generateFollowups,
|
|
2142
2290
|
autonomous: input.autonomous,
|
|
2291
|
+
archiveWhenDone: input.archiveWhenDone,
|
|
2292
|
+
autoMergePr: input.autoMergePr,
|
|
2143
2293
|
...(input.notify ? { notify: true } : {}),
|
|
2144
2294
|
worktree: input.worktree,
|
|
2145
2295
|
groupId: input.groupId,
|
|
2146
2296
|
variant: input.variant,
|
|
2147
2297
|
...(input.clientRequestId === undefined ? {} : { clientRequestId: input.clientRequestId, clientRequestHash: input.clientRequestHash }),
|
|
2298
|
+
...(input.automation ? { automation: input.automation } : {}),
|
|
2299
|
+
...(input.automationTrigger ? { automationTrigger: input.automationTrigger } : {}),
|
|
2148
2300
|
status: 'queued',
|
|
2149
2301
|
createdAt: new Date().toISOString(),
|
|
2150
2302
|
tokensUsed: 0,
|
|
@@ -2181,31 +2333,13 @@ export class RunStore extends EventEmitter {
|
|
|
2181
2333
|
}
|
|
2182
2334
|
this.commitIndex(staged);
|
|
2183
2335
|
}
|
|
2184
|
-
/**
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
this.commitAgentInputs(id, run.agentInputs.map(input => {
|
|
2192
|
-
if (!ids.includes(input.id))
|
|
2193
|
-
return input;
|
|
2194
|
-
const { deliveredAt: _delivered, consumedAt: _consumed, awaitingRead: _awaiting, ...queued } = input;
|
|
2195
|
-
return queued;
|
|
2196
|
-
}));
|
|
2197
|
-
}
|
|
2198
|
-
/** Model consumption observed by the current session (#505). Never un-sets deliveredAt. */
|
|
2199
|
-
commitAgentInputsConsumed(id, ids, at) {
|
|
2200
|
-
const run = this.peek(id);
|
|
2201
|
-
if (!run?.agentInputs || !ids.length)
|
|
2202
|
-
return;
|
|
2203
|
-
this.commitAgentInputs(id, run.agentInputs.map(input => {
|
|
2204
|
-
if (!ids.includes(input.id) || !input.deliveredAt || input.consumedAt)
|
|
2205
|
-
return input;
|
|
2206
|
-
const { awaitingRead: _awaiting, ...read } = input;
|
|
2207
|
-
return { ...read, consumedAt: at };
|
|
2208
|
-
}));
|
|
2336
|
+
/** Crash recovery (#505): input a harness accepted but was never seen reading goes
|
|
2337
|
+
* back to the queue. Returns the requeued IDs. */
|
|
2338
|
+
requeueAwaitingReadInputs(id) {
|
|
2339
|
+
const ids = (this.peek(id)?.agentInputs ?? []).filter(input => input.awaitingRead && input.deliveredAt && !input.consumedAt).map(input => input.id);
|
|
2340
|
+
if (ids.length)
|
|
2341
|
+
this.commitOutcome(id, { kind: 'requeued', inputIds: ids });
|
|
2342
|
+
return ids;
|
|
2209
2343
|
}
|
|
2210
2344
|
/** Delivered, but the harness never confirmed reading it (#505): stop awaiting a read. */
|
|
2211
2345
|
commitAgentInputsUnconfirmed(id, ids) {
|
|
@@ -2219,18 +2353,18 @@ export class RunStore extends EventEmitter {
|
|
|
2219
2353
|
return unconfirmed;
|
|
2220
2354
|
}));
|
|
2221
2355
|
}
|
|
2222
|
-
/**
|
|
2223
|
-
*
|
|
2224
|
-
|
|
2225
|
-
const ids = (this.peek(id)?.agentInputs ?? []).filter(input => input.awaitingRead && input.deliveredAt && !input.consumedAt).map(input => input.id);
|
|
2226
|
-
this.requeueUnconsumedAgentInputs(id, ids);
|
|
2227
|
-
return ids;
|
|
2228
|
-
}
|
|
2229
|
-
commitAgentInputs(id, inputs, openingContinuationInputId) {
|
|
2356
|
+
/** Observable atomic input checkpoint for the queue itself: enqueue, retire, reorder. Delivery
|
|
2357
|
+
* and reads are outcomes (`commitOutcome`). */
|
|
2358
|
+
commitAgentInputs(id, inputs) {
|
|
2230
2359
|
const run = this.peek(id);
|
|
2231
2360
|
if (!run)
|
|
2232
2361
|
throw new Error('missing agent input target');
|
|
2233
2362
|
const agentInputs = inputs.map(input => agentInputSchema.parse(input));
|
|
2363
|
+
// Receipts are outcomes (hearsay-tools/cezarion#1805): only `commitOutcome` writes them.
|
|
2364
|
+
const stored = new Map((run.agentInputs ?? []).map(input => [input.id, input]));
|
|
2365
|
+
if (agentInputs.some(input => input.deliveredAt !== stored.get(input.id)?.deliveredAt || input.consumedAt !== stored.get(input.id)?.consumedAt)) {
|
|
2366
|
+
throw new Error('agent input receipts change only through commitOutcome');
|
|
2367
|
+
}
|
|
2234
2368
|
const now = new Date().toISOString();
|
|
2235
2369
|
for (const input of run.agentInputs ?? []) {
|
|
2236
2370
|
const claim = input.inboxClaim;
|
|
@@ -2241,10 +2375,7 @@ export class RunStore extends EventEmitter {
|
|
|
2241
2375
|
throw new Error('live inbox receipt changed');
|
|
2242
2376
|
}
|
|
2243
2377
|
}
|
|
2244
|
-
|
|
2245
|
-
!agentInputs.some(input => input.id === openingContinuationInputId && input.deliveredAt)))
|
|
2246
|
-
throw new Error('opening agent input checkpoint changed');
|
|
2247
|
-
this.commitIndex(new Map([[id, { ...run, agentInputs, ...(openingContinuationInputId ? { continuationMessage: undefined } : {}) }]]));
|
|
2378
|
+
this.commitIndex(new Map([[id, { ...run, agentInputs }]]));
|
|
2248
2379
|
}
|
|
2249
2380
|
/** Reserve only unread conversation inputs in one durable index replacement. */
|
|
2250
2381
|
claimInboxInputs(runId, ids, claim) {
|
|
@@ -2292,8 +2423,7 @@ export class RunStore extends EventEmitter {
|
|
|
2292
2423
|
if (matching.some(input => input.deliveredAt || input.inboxClaim?.acknowledgedAt || input.inboxClaim.expiresAt <= at)) {
|
|
2293
2424
|
throw new Error('inbox receipt expired or displaced');
|
|
2294
2425
|
}
|
|
2295
|
-
this.
|
|
2296
|
-
? agentInputSchema.parse({ ...input, deliveredAt: at, inboxClaim: { ...input.inboxClaim, acknowledgedAt: at } }) : input) }]]));
|
|
2426
|
+
this.commitOutcome(runId, { kind: 'inbox-acknowledged', receiptId, at });
|
|
2297
2427
|
return 'acknowledged';
|
|
2298
2428
|
}
|
|
2299
2429
|
releaseInboxInputs(runId, receiptId, generation) {
|
|
@@ -2328,67 +2458,102 @@ export class RunStore extends EventEmitter {
|
|
|
2328
2458
|
return agentInputSchema.parse(unclaimed);
|
|
2329
2459
|
}) }]]));
|
|
2330
2460
|
}
|
|
2331
|
-
/**
|
|
2332
|
-
|
|
2461
|
+
/**
|
|
2462
|
+
* A wait and its wake input share one atomic index replacement (spec 2026-10-17-unified-waits).
|
|
2463
|
+
* Upserts `wait` by id into `waits` and clears `lastWaitsError`; `input` is appended once (an
|
|
2464
|
+
* input whose id is already queued stays as it is). Only an open or resolved wait is committed
|
|
2465
|
+
* here: settling is an outcome (`commitOutcome`). A new open wait
|
|
2466
|
+
* past `WAIT_LIMIT` open ones is refused, so the list stays bounded whatever the caller checked.
|
|
2467
|
+
*/
|
|
2468
|
+
commitWait(id, wait, input) {
|
|
2333
2469
|
const run = this.peek(id);
|
|
2334
2470
|
if (!run)
|
|
2335
|
-
throw new Error('missing
|
|
2336
|
-
const
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2471
|
+
throw new Error('missing wait target');
|
|
2472
|
+
const entry = waitSchema.parse(wait);
|
|
2473
|
+
if (entry.phase !== 'open' && entry.phase !== 'resolved')
|
|
2474
|
+
throw new Error('a settled wait is committed through its delivery or cancellation');
|
|
2475
|
+
const waits = run.waits ?? [];
|
|
2476
|
+
const known = waits.some((row) => row.id === entry.id);
|
|
2477
|
+
if (!known && entry.phase === 'open' && waits.filter((row) => row.phase === 'open').length >= WAIT_LIMIT) {
|
|
2478
|
+
throw new Error(`open wait limit reached (${WAIT_LIMIT})`);
|
|
2479
|
+
}
|
|
2480
|
+
const agentInput = input ? agentInputSchema.parse(input) : undefined;
|
|
2481
|
+
this.commitIndex(new Map([[id, { ...run, waits: known ? waits.map((row) => row.id === entry.id ? entry : row) : [...waits, entry], lastWaitsError: undefined,
|
|
2482
|
+
...(agentInput ? { agentInputs: run.agentInputs?.some((row) => row.id === agentInput.id)
|
|
2483
|
+
? run.agentInputs : [...(run.agentInputs ?? []), agentInput] } : {}) }]]));
|
|
2484
|
+
}
|
|
2485
|
+
/** A park that gives up its slot (hearsay-tools/cezarion#1235): the run and its step read
|
|
2486
|
+
* `waiting`, committed at once instead of on the debounced save, so a refused write throws with
|
|
2487
|
+
* nothing changed and the caller keeps the slot. */
|
|
2488
|
+
commitParked(id, stepId) {
|
|
2343
2489
|
const run = this.peek(id);
|
|
2344
|
-
if (!run
|
|
2490
|
+
if (!run)
|
|
2345
2491
|
return;
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
this.commitIndex(new Map([[id, { ...rest, ciWait: undefined, lastCiWait,
|
|
2349
|
-
...(run.agentInputs ? { agentInputs: run.agentInputs.filter(input => input.id !== ciWait.wakeId || input.deliveredAt) } : {}),
|
|
2350
|
-
...(acceptedHumanMessage ? { queuedMessages: [...(run.queuedMessages ?? []), queuedMessageSchema.parse(acceptedHumanMessage)] } : {}),
|
|
2351
|
-
}]]));
|
|
2492
|
+
this.commitIndex(new Map([[id, { ...run, status: 'waiting', activity: undefined,
|
|
2493
|
+
...(stepId ? { steps: run.steps.map((step) => step.id === stepId ? { ...step, status: 'waiting' } : step) } : {}) }]]));
|
|
2352
2494
|
}
|
|
2353
|
-
/**
|
|
2354
|
-
|
|
2495
|
+
/** A usage-limit appointment withdrawn, committed at once instead of on the debounced save
|
|
2496
|
+
* (hearsay-tools/cezarion#1236): a crash right after the withdrawal must not find it on disk,
|
|
2497
|
+
* or recovery re-arms work the user cancelled. A run held for its resume settles `failed`. */
|
|
2498
|
+
commitAutoResumeWithdrawal(id) {
|
|
2355
2499
|
const run = this.peek(id);
|
|
2356
|
-
if (!run
|
|
2500
|
+
if (!run || (run.autoResumeAt === undefined && run.autoResumeAttempts === undefined))
|
|
2357
2501
|
return;
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2502
|
+
const held = isUsageLimitHold(run);
|
|
2503
|
+
this.commitIndex(new Map([[id, { ...run, autoResumeAt: undefined, autoResumeAttempts: undefined,
|
|
2504
|
+
...(held ? { status: 'failed', finishedAt: new Date().toISOString() } : {}) }]]));
|
|
2505
|
+
}
|
|
2506
|
+
/** Legacy CI checkpoint over `commitWait`, kept until its callers move (unified waits, Task 4). */
|
|
2507
|
+
commitCiWait(id, wait, input) {
|
|
2508
|
+
this.commitWait(id, ciWaitEntry(ciWaitSchema.parse(wait)), input);
|
|
2362
2509
|
}
|
|
2363
|
-
/**
|
|
2364
|
-
|
|
2510
|
+
/** A human message beside a run's waits (hearsay-tools/cezarion#1235): accepted durably, and
|
|
2511
|
+
* every wait stays. A lifecycle opening becomes
|
|
2512
|
+
* the human's and keeps its wake text, so the message opens the continuation (and answers a
|
|
2513
|
+
* pending question) and the owed results still reach the agent. */
|
|
2514
|
+
commitHumanMessageBesideWaits(id, acceptedHumanMessage) {
|
|
2365
2515
|
const run = this.peek(id);
|
|
2366
|
-
if (!run
|
|
2367
|
-
throw new Error('
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
const message = acceptedHumanMessage ? queuedMessageSchema.parse(acceptedHumanMessage) : undefined;
|
|
2371
|
-
this.commitIndex(new Map([[id, { ...run, delegation: { ...delegation, lastWait: wait.phase === 'wake-pending'
|
|
2372
|
-
? reconcileWorkerWait(wait, [], new Date().toISOString())
|
|
2373
|
-
: { ...wait, phase: 'wake-pending', reason: wait.reason ?? 'cancelled', wakeId: wait.wakeId ?? wait.id } },
|
|
2374
|
-
// An adopted agent input belongs to its sender, even when it carries the wait receipt.
|
|
2375
|
-
...(run.agentInputs ? { agentInputs: run.agentInputs.filter(input => input.id !== wait.wakeId || input.deliveredAt || input.source === 'agent') } : {}),
|
|
2376
|
-
...(message ? {
|
|
2377
|
-
queuedMessages: [...(run.queuedMessages ?? []), message],
|
|
2378
|
-
...(run.continuationMessage ? { continuationMessage: {
|
|
2379
|
-
...run.continuationMessage, origin: 'human',
|
|
2380
|
-
text: run.continuationMessage.origin === 'lifecycle' ? '' : run.continuationMessage.text,
|
|
2381
|
-
} } : {}),
|
|
2382
|
-
} : {}),
|
|
2516
|
+
if (!run)
|
|
2517
|
+
throw new Error('missing queued message target');
|
|
2518
|
+
this.commitIndex(new Map([[id, { ...run, queuedMessages: [...(run.queuedMessages ?? []), queuedMessageSchema.parse(acceptedHumanMessage)],
|
|
2519
|
+
...(run.continuationMessage ? { continuationMessage: { ...run.continuationMessage, origin: 'human' } } : {}),
|
|
2383
2520
|
}]]));
|
|
2384
2521
|
}
|
|
2385
|
-
/**
|
|
2386
|
-
*
|
|
2387
|
-
|
|
2522
|
+
/**
|
|
2523
|
+
* The one write of a run outcome (hearsay-tools/cezarion#1805): an input or wait delivered, a wake
|
|
2524
|
+
* charged, a wait settled, a question answered, staged by `applyOutcomes` with what it depends on
|
|
2525
|
+
* (`deps`) and committed in one transaction, with every pending row. A crash keeps all of it or
|
|
2526
|
+
* none. Nothing changes: nothing is written. Needs no live session: it reads the stored record.
|
|
2527
|
+
*
|
|
2528
|
+
* A refused write throws with nothing changed. `bestEffort` is for a cleanup path, which must
|
|
2529
|
+
* never stop on it: it warns and applies the change in memory for the debounced save to retry.
|
|
2530
|
+
* A conversation request outcome is the other sanctioned write: `commitConversation`.
|
|
2531
|
+
*/
|
|
2532
|
+
commitOutcome(id, outcomes, deps = {}, opts = {}) {
|
|
2388
2533
|
const run = this.peek(id);
|
|
2389
2534
|
if (!run)
|
|
2390
|
-
|
|
2391
|
-
|
|
2535
|
+
return { committed: false, answered: [] };
|
|
2536
|
+
const staged = applyOutcomes(run, Array.isArray(outcomes) ? outcomes : [outcomes], deps);
|
|
2537
|
+
if (!staged.changed)
|
|
2538
|
+
return { committed: false, answered: [] };
|
|
2539
|
+
try {
|
|
2540
|
+
this.commitIndex(new Map([[id, staged.next]]));
|
|
2541
|
+
}
|
|
2542
|
+
catch (error) {
|
|
2543
|
+
if (!opts.bestEffort)
|
|
2544
|
+
throw error;
|
|
2545
|
+
console.warn(`[cez] outcome checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2546
|
+
const live = this.record(id);
|
|
2547
|
+
if (!live)
|
|
2548
|
+
return { committed: false, answered: staged.answered };
|
|
2549
|
+
for (const key of Object.keys(live))
|
|
2550
|
+
if (!(key in staged.next))
|
|
2551
|
+
delete live[key];
|
|
2552
|
+
Object.assign(live, staged.next);
|
|
2553
|
+
this.touch(live);
|
|
2554
|
+
return { committed: false, answered: staged.answered };
|
|
2555
|
+
}
|
|
2556
|
+
return { committed: true, answered: staged.answered };
|
|
2392
2557
|
}
|
|
2393
2558
|
/** An acknowledged inactive-root Finish must survive the async diff and restart. */
|
|
2394
2559
|
commitRootFinishIntent(id) {
|
|
@@ -2419,7 +2584,7 @@ export class RunStore extends EventEmitter {
|
|
|
2419
2584
|
const { finishRequestedAt: _intent, ...delegation } = run.delegation;
|
|
2420
2585
|
const finishedAt = new Date().toISOString();
|
|
2421
2586
|
this.commitIndex(new Map([[id, { ...run, delegation, status, finishedAt, currentStepId: undefined, autoResumeAttempts: undefined,
|
|
2422
|
-
activity: undefined,
|
|
2587
|
+
activity: undefined,
|
|
2423
2588
|
steps: run.steps.map(step => step.status === 'waiting' || step.status === 'running'
|
|
2424
2589
|
? { ...step, status: 'done', finishedAt: step.finishedAt ?? finishedAt } : step),
|
|
2425
2590
|
}]]));
|
|
@@ -2664,16 +2829,12 @@ export class RunStore extends EventEmitter {
|
|
|
2664
2829
|
return run;
|
|
2665
2830
|
}
|
|
2666
2831
|
/**
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
* One transaction carries the staged rows and every row a debounced save still owes, so the
|
|
2672
|
-
* database never holds a commit without the optimistic writes made before it. If it fails,
|
|
2673
|
-
* nothing changed anywhere: the held records, the database, the pending dirty rows and the
|
|
2674
|
-
* subscribers all see the store as it was.
|
|
2832
|
+
* Durably replace or delete staged rows together with pending dirty rows. Persist before
|
|
2833
|
+
* changing held objects or notifying subscribers. On failure retain all pending state; flush()
|
|
2834
|
+
* remains best-effort.
|
|
2675
2835
|
*/
|
|
2676
|
-
commitIndex(
|
|
2836
|
+
commitIndex(input, source) {
|
|
2837
|
+
const staged = this.prepareCompletionActions(withoutStaleResumes(input));
|
|
2677
2838
|
if (!this.db)
|
|
2678
2839
|
throw this.openFailure ?? new Error('runs database unavailable: nothing can be saved');
|
|
2679
2840
|
this.assertWritable(staged);
|
|
@@ -2687,8 +2848,13 @@ export class RunStore extends EventEmitter {
|
|
|
2687
2848
|
}
|
|
2688
2849
|
this.committing.add(id);
|
|
2689
2850
|
const current = this.held.get(id);
|
|
2690
|
-
if (current)
|
|
2851
|
+
if (current) {
|
|
2852
|
+
for (const key of Object.keys(current)) {
|
|
2853
|
+
if (!(key in next))
|
|
2854
|
+
delete current[key];
|
|
2855
|
+
}
|
|
2691
2856
|
Object.assign(current, next);
|
|
2857
|
+
}
|
|
2692
2858
|
else
|
|
2693
2859
|
this.held.set(id, next);
|
|
2694
2860
|
}
|
|
@@ -2706,6 +2872,107 @@ export class RunStore extends EventEmitter {
|
|
|
2706
2872
|
this.committing.delete(id);
|
|
2707
2873
|
this.sweepIfSettled(staged.keys());
|
|
2708
2874
|
}
|
|
2875
|
+
for (const [id, next] of staged)
|
|
2876
|
+
if (next?.archived)
|
|
2877
|
+
this.compressor.enqueue(id);
|
|
2878
|
+
}
|
|
2879
|
+
/** Completion and its intent/archive share the same committed rows, including root Finish. */
|
|
2880
|
+
prepareCompletionActions(input) {
|
|
2881
|
+
const staged = new Map(input);
|
|
2882
|
+
for (const [id, candidate] of input) {
|
|
2883
|
+
if (!candidate)
|
|
2884
|
+
continue;
|
|
2885
|
+
const previous = this.peek(id);
|
|
2886
|
+
let next = candidate;
|
|
2887
|
+
const changed = candidate.status === 'done' && !candidate.archived && (previous?.status !== 'done' ||
|
|
2888
|
+
Boolean(previous.archiveWhenDone) !== Boolean(candidate.archiveWhenDone) ||
|
|
2889
|
+
Boolean(previous.autoMergePr) !== Boolean(candidate.autoMergePr));
|
|
2890
|
+
if (changed && (candidate.archiveWhenDone || candidate.autoMergePr)) {
|
|
2891
|
+
next = { ...candidate, completionAction: { id: randomUUID(), phase: 'pending' } };
|
|
2892
|
+
staged.set(id, next);
|
|
2893
|
+
if (next.archiveWhenDone && !next.autoMergePr) {
|
|
2894
|
+
const blocker = this.completionArchiveBlocker(next);
|
|
2895
|
+
next.completionAction = { ...next.completionAction, phase: blocker ? 'blocked' : 'complete', ...(blocker ? { message: blocker } : {}) };
|
|
2896
|
+
if (!blocker)
|
|
2897
|
+
this.stageArchive(staged, next, true);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
else if (candidate.status !== 'done' || (!candidate.archiveWhenDone && !candidate.autoMergePr)) {
|
|
2901
|
+
if (candidate.completionAction)
|
|
2902
|
+
staged.set(id, { ...candidate, completionAction: undefined });
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
return staged;
|
|
2906
|
+
}
|
|
2907
|
+
completionArchiveBlocker(root) {
|
|
2908
|
+
const family = [root, ...this.listWorkersOf(root.id)];
|
|
2909
|
+
for (const run of family) {
|
|
2910
|
+
const facts = this.facts.peek(run.id);
|
|
2911
|
+
if (!facts || !['done', 'review', 'failed', 'cancelled'].includes(run.status) || run.waits?.some(wait => wait.phase === 'open') ||
|
|
2912
|
+
run.hasPendingPermission || facts.pendingAsk !== undefined)
|
|
2913
|
+
return 'Task or worker work still needs attention. Open the task or archive manually.';
|
|
2914
|
+
if (run.delegation?.role === 'invalid' || (run.delegation?.role === 'worker' && this.readWorkerExecution(run.id)?.phase !== 'complete'))
|
|
2915
|
+
return 'Worker execution has not settled. Open the worker or archive manually.';
|
|
2916
|
+
}
|
|
2917
|
+
return undefined;
|
|
2918
|
+
}
|
|
2919
|
+
/** One archive write for manual actions, merge observations, completion, and sweeps.
|
|
2920
|
+
* Archiving files history away; it never stops a process or removes a worktree. */
|
|
2921
|
+
stageArchive(staged, root, archived) {
|
|
2922
|
+
const ids = root.delegation?.role === 'worker' ? [root.id] :
|
|
2923
|
+
[root.id, ...new Set([...(this.db?.listIdsByParent(root.id) ?? []), ...this.listWorkersOf(root.id).map(run => run.id)])];
|
|
2924
|
+
const now = new Date().toISOString();
|
|
2925
|
+
for (const id of ids) {
|
|
2926
|
+
const member = staged.get(id) ?? this.peek(id);
|
|
2927
|
+
if (!member)
|
|
2928
|
+
continue;
|
|
2929
|
+
if (!archived) {
|
|
2930
|
+
this.compressor.cancel(id);
|
|
2931
|
+
restoreHistory(this.dataDir, id);
|
|
2932
|
+
}
|
|
2933
|
+
const next = { ...member, archiveBlocker: undefined };
|
|
2934
|
+
archiveFields(next, archived, now);
|
|
2935
|
+
staged.set(id, next);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
/** A bounded persisted queue also covers projects without a live RunManager. */
|
|
2939
|
+
listCompletionCandidates(afterSeq = 0, limit = 32) {
|
|
2940
|
+
return this.db?.listCompletionCandidates(afterSeq, limit) ?? [];
|
|
2941
|
+
}
|
|
2942
|
+
canCompleteAction(id, token) {
|
|
2943
|
+
const run = this.record(id);
|
|
2944
|
+
return Boolean(run && run.status === 'done' && !run.archived && (run.autoMergePr || run.archiveWhenDone) &&
|
|
2945
|
+
run.completionAction?.id === token && run.completionAction.phase !== 'complete' && !this.writeRefusal(id));
|
|
2946
|
+
}
|
|
2947
|
+
claimCompletionAction(id, token) {
|
|
2948
|
+
if (!this.canCompleteAction(id, token))
|
|
2949
|
+
return false;
|
|
2950
|
+
const run = this.record(id);
|
|
2951
|
+
this.assertWritable(new Map([[id, run]]));
|
|
2952
|
+
// A normal write may defer its claim until the transaction. A remote mutation
|
|
2953
|
+
// needs the committed claim now, and must not act on a stale row read before it.
|
|
2954
|
+
const claim = this.db?.getClaim(familyKey(run));
|
|
2955
|
+
return Boolean(claim && claim.session === this.owner?.session &&
|
|
2956
|
+
claim.generation === this.claimed.get(familyKey(run)) &&
|
|
2957
|
+
this.db?.get(id)?.revision === this.base.get(id)?.revision);
|
|
2958
|
+
}
|
|
2959
|
+
completeAction(id, token, phase, message) {
|
|
2960
|
+
const run = this.record(id);
|
|
2961
|
+
if (!run || !this.canCompleteAction(id, token))
|
|
2962
|
+
return false;
|
|
2963
|
+
if (run.completionAction?.phase === phase && run.completionAction.message === message)
|
|
2964
|
+
return true;
|
|
2965
|
+
const next = { ...run, completionAction: { id: token, phase, ...(message ? { message } : {}) } };
|
|
2966
|
+
const staged = new Map([[id, next]]);
|
|
2967
|
+
if (phase === 'complete' && next.archiveWhenDone) {
|
|
2968
|
+
const blocker = this.completionArchiveBlocker(next);
|
|
2969
|
+
if (blocker)
|
|
2970
|
+
next.completionAction = { id: token, phase: 'blocked', message: blocker };
|
|
2971
|
+
else
|
|
2972
|
+
this.stageArchive(staged, next, true);
|
|
2973
|
+
}
|
|
2974
|
+
this.commitIndex(staged);
|
|
2975
|
+
return true;
|
|
2709
2976
|
}
|
|
2710
2977
|
/**
|
|
2711
2978
|
* Refuse (throwing, with nothing changed) a commit that stages a quarantined run, or a run whose
|
|
@@ -2754,18 +3021,17 @@ export class RunStore extends EventEmitter {
|
|
|
2754
3021
|
delete run.referencedIssueNumberSeeded;
|
|
2755
3022
|
}
|
|
2756
3023
|
const normalized = { ...patch };
|
|
2757
|
-
if (normalized.
|
|
2758
|
-
normalized.
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
// The manager's timer re-checks the record before it fires, so a cleared field is enough.
|
|
2766
|
-
if (normalized.status && ['running', 'waiting', 'queued'].includes(normalized.status)) {
|
|
2767
|
-
normalized.autoResumeAt = undefined;
|
|
3024
|
+
if (normalized.pullRequestUrl !== undefined) {
|
|
3025
|
+
this.accumulatePullRequests(run, [run.pullRequestUrl, normalized.pullRequestUrl].flatMap((url) => {
|
|
3026
|
+
const number = refUrlNumber(url);
|
|
3027
|
+
if (number === undefined)
|
|
3028
|
+
return [];
|
|
3029
|
+
const parsed = runPullRequestSchema.safeParse({ number, url, source: 'created' });
|
|
3030
|
+
return parsed.success ? [parsed.data] : [];
|
|
3031
|
+
}));
|
|
2768
3032
|
}
|
|
3033
|
+
// Waits are not cleared here: RunManager cancels them explicitly when a run ends (spec
|
|
3034
|
+
// 2026-10-17-unified-waits), so a terminal patch never retires one as a side effect.
|
|
2769
3035
|
const archived = Object.prototype.hasOwnProperty.call(normalized, 'archived')
|
|
2770
3036
|
? Boolean(normalized.archived)
|
|
2771
3037
|
: undefined;
|
|
@@ -2776,7 +3042,22 @@ export class RunStore extends EventEmitter {
|
|
|
2776
3042
|
// Restore before mutating so a throw leaves status and archived unchanged (#818 S3).
|
|
2777
3043
|
if (archived === false)
|
|
2778
3044
|
restoreHistory(this.dataDir, id);
|
|
3045
|
+
// Opted-in completion and the handoff's records (the file shows only committed ones) take this
|
|
3046
|
+
// transaction. Default-off Finish keeps its best-effort write so a locked database cannot stop cleanup.
|
|
3047
|
+
if ((normalized.status === 'done' && (run.archiveWhenDone || run.autoMergePr)) || normalized.handoffRecords !== undefined ||
|
|
3048
|
+
normalized.archiveWhenDone !== undefined || normalized.autoMergePr !== undefined) {
|
|
3049
|
+
const next = { ...run, ...this.redactPatch(normalized) };
|
|
3050
|
+
if (archived !== undefined)
|
|
3051
|
+
archiveFields(next, archived, new Date().toISOString());
|
|
3052
|
+
this.commitIndex(new Map([[id, next]]));
|
|
3053
|
+
return this.record(id);
|
|
3054
|
+
}
|
|
2779
3055
|
Object.assign(run, this.redactPatch(normalized));
|
|
3056
|
+
// The usage-limit resume is the appointment of a `waiting` run (hearsay-tools/cezarion#1236),
|
|
3057
|
+
// so a run coming back to life or settling retires it. `commitIndex` applies the same rule to
|
|
3058
|
+
// every commit (`withoutStaleResumes`); this debounced path mutates the held record without it.
|
|
3059
|
+
if (run.status !== 'waiting')
|
|
3060
|
+
run.autoResumeAt = undefined;
|
|
2780
3061
|
if (normalized.task !== undefined)
|
|
2781
3062
|
this.resolveEditedTaskRefs(run);
|
|
2782
3063
|
if (archived !== undefined)
|
|
@@ -2798,7 +3079,7 @@ export class RunStore extends EventEmitter {
|
|
|
2798
3079
|
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates, run.task, referencedPrDeclaration(run), this.repoHandle);
|
|
2799
3080
|
}
|
|
2800
3081
|
if (run.referencedIssueCandidates !== undefined) {
|
|
2801
|
-
run.referencedIssueUrl =
|
|
3082
|
+
run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
|
|
2802
3083
|
const number = refUrlNumber(run.referencedIssueUrl);
|
|
2803
3084
|
if (run.referencedIssueNumberSeeded) {
|
|
2804
3085
|
// We may change only the number this janitor owns, including clearing an ambiguous one.
|
|
@@ -2893,6 +3174,123 @@ export class RunStore extends EventEmitter {
|
|
|
2893
3174
|
run.costUsd = run.steps.some((step) => step.costUsd !== undefined) ? cost : undefined;
|
|
2894
3175
|
this.touch(run);
|
|
2895
3176
|
}
|
|
3177
|
+
listArchiveOnMergeCandidates(afterSeq = 0, limit = 32) {
|
|
3178
|
+
return this.db?.listArchiveOnMergeCandidates(afterSeq, limit) ?? [];
|
|
3179
|
+
}
|
|
3180
|
+
/** Use unnormalized persisted evidence: cold reads must not turn interrupted work into safety. */
|
|
3181
|
+
archiveFamily(id, isActive) {
|
|
3182
|
+
const ids = [id, ...new Set([...(this.db?.listIdsByParent(id) ?? []), ...this.listWorkersOf(id).map(run => run.id)])];
|
|
3183
|
+
if (ids.length > 1025)
|
|
3184
|
+
return undefined;
|
|
3185
|
+
const family = [];
|
|
3186
|
+
const block = (message, runId = id) => { this.setArchiveBlocker(id, { message, runId }); return undefined; };
|
|
3187
|
+
for (const memberId of ids) {
|
|
3188
|
+
const row = this.db?.get(memberId);
|
|
3189
|
+
const raw = row ? JSON.parse(row.data) : this.held.get(memberId);
|
|
3190
|
+
const decoded = decodeRunRow(JSON.stringify(raw));
|
|
3191
|
+
if (!decoded)
|
|
3192
|
+
return block('Task history is unavailable. Open the task or archive it manually.', memberId);
|
|
3193
|
+
// Do not let the runtime schema's per-entry salvage hide malformed PR authority.
|
|
3194
|
+
if (raw && typeof raw === 'object' && 'pullRequests' in raw &&
|
|
3195
|
+
(!Array.isArray(raw.pullRequests) || raw.pullRequests.some(entry => !runPullRequestSchema.safeParse(entry).success)))
|
|
3196
|
+
return block('PR associations are invalid. Check the task PRs or archive manually.', memberId);
|
|
3197
|
+
const run = this.held.get(memberId) ?? decoded.run;
|
|
3198
|
+
if (row && !this.held.has(memberId))
|
|
3199
|
+
this.decodeCold(row); // records the synchronous write fence
|
|
3200
|
+
if (this.deleted.has(memberId) || this.quarantined.has(memberId) || this.writeRefusal(memberId))
|
|
3201
|
+
return block('Task history is owned elsewhere or cannot be changed. Retry after its owner stops.', memberId);
|
|
3202
|
+
if (this.pins.has(memberId) || isActive(memberId) || isLiveRecord(run) || row?.live ||
|
|
3203
|
+
!['review', 'done', 'failed', 'cancelled'].includes(run.status))
|
|
3204
|
+
return block('Task work is still active. Wait for it to finish or archive manually.', memberId);
|
|
3205
|
+
if (run.waits?.some(wait => wait.kind === 'human' && wait.phase === 'open') || run.hasPendingPermission)
|
|
3206
|
+
return block('Task needs an answer or permission. Open it to respond, or archive manually.', memberId);
|
|
3207
|
+
if (run.delegation?.role === 'invalid')
|
|
3208
|
+
return block('Worker ownership is invalid. Inspect the task or archive manually.', memberId);
|
|
3209
|
+
const facts = this.facts.peek(memberId);
|
|
3210
|
+
if (!facts || facts.pendingAsk !== undefined)
|
|
3211
|
+
return block('Task has an unanswered question or unread history. Open it to respond, or archive manually.', memberId);
|
|
3212
|
+
if (run.delegation?.role === 'worker' && this.readWorkerExecution(memberId)?.phase !== 'complete')
|
|
3213
|
+
return block('Worker execution has not settled. Open the worker or archive manually.', memberId);
|
|
3214
|
+
// Process and scratch cleanup evidence protects deletion, not reversible filing.
|
|
3215
|
+
family.push(run);
|
|
3216
|
+
}
|
|
3217
|
+
if (family[0]?.delegation?.role === 'worker' || family[0]?.archived)
|
|
3218
|
+
return undefined;
|
|
3219
|
+
return family;
|
|
3220
|
+
}
|
|
3221
|
+
/** Blockers are feedback, never archive authority or cleanup permission. */
|
|
3222
|
+
setArchiveBlocker(id, blocker) {
|
|
3223
|
+
const run = this.record(id);
|
|
3224
|
+
if (!run || run.archived || isDeepStrictEqual(run.archiveBlocker, blocker))
|
|
3225
|
+
return;
|
|
3226
|
+
this.commitIndex(new Map([[id, { ...run, archiveBlocker: blocker }]]));
|
|
3227
|
+
}
|
|
3228
|
+
archiveMergeBlocker(snapshot, missing, repoBase) {
|
|
3229
|
+
const url = `https://github.com/${missing.owner}/${missing.repo}/pull/${missing.number}`;
|
|
3230
|
+
const family = [snapshot.rootId, ...(this.db?.listIdsByParent(snapshot.rootId) ?? [])].flatMap(id => {
|
|
3231
|
+
const run = this.peek(id);
|
|
3232
|
+
return run ? [run] : [];
|
|
3233
|
+
});
|
|
3234
|
+
const member = family.find(run => archivePullRequests([run], repoBase)?.some(pr => pullRequestKey(pr) === pullRequestKey(missing)));
|
|
3235
|
+
this.setArchiveBlocker(snapshot.rootId, { message: `PR ${missing.owner}/${missing.repo}#${missing.number} is not confirmed merged. Check the PR association or archive manually.`,
|
|
3236
|
+
runId: member?.id ?? snapshot.rootId, pullRequestUrl: url });
|
|
3237
|
+
}
|
|
3238
|
+
hasArchiveOnMergeAssociations(id) {
|
|
3239
|
+
return this.db?.archiveFamilyHasPullRequests(id) ?? false;
|
|
3240
|
+
}
|
|
3241
|
+
async prepareArchiveOnMerge(id) {
|
|
3242
|
+
const ids = [id, ...(this.db?.listIdsByParent(id) ?? [])];
|
|
3243
|
+
if (ids.length > 1025)
|
|
3244
|
+
return;
|
|
3245
|
+
for (const memberId of ids)
|
|
3246
|
+
await this.facts.ready(memberId);
|
|
3247
|
+
}
|
|
3248
|
+
archiveOnMergeSnapshot(id, repoBase, isActive = () => false) {
|
|
3249
|
+
const family = this.archiveFamily(id, isActive);
|
|
3250
|
+
if (!family)
|
|
3251
|
+
return undefined;
|
|
3252
|
+
return this.archiveSnapshot(family, repoBase);
|
|
3253
|
+
}
|
|
3254
|
+
archiveSnapshot(family, repoBase) {
|
|
3255
|
+
const pullRequests = archivePullRequests(family, repoBase);
|
|
3256
|
+
if (!pullRequests) {
|
|
3257
|
+
this.setArchiveBlocker(family[0].id, { message: 'PR associations cannot be resolved. Check task and worker PRs, or archive manually.' });
|
|
3258
|
+
return undefined;
|
|
3259
|
+
}
|
|
3260
|
+
const fingerprint = archiveDigest(pullRequests);
|
|
3261
|
+
if (family[0].autoArchive?.fingerprint === fingerprint)
|
|
3262
|
+
return undefined;
|
|
3263
|
+
return { rootId: family[0].id, fingerprint, pullRequests, token: archiveDigest(family.map(({ archiveBlocker: _blocker, ...run }) => run)) };
|
|
3264
|
+
}
|
|
3265
|
+
/** No await between final revalidation and the fenced, all-or-nothing cascade. */
|
|
3266
|
+
archiveAfterMerge(snapshot, observations, repoBase, isActive = () => false) {
|
|
3267
|
+
const family = this.archiveFamily(snapshot.rootId, isActive);
|
|
3268
|
+
if (!family)
|
|
3269
|
+
return false;
|
|
3270
|
+
const current = this.archiveSnapshot(family, repoBase);
|
|
3271
|
+
if (!current || current.token !== snapshot.token || current.fingerprint !== snapshot.fingerprint)
|
|
3272
|
+
return false;
|
|
3273
|
+
const parsed = observations.map(observation => mergedPullRequestSchema.safeParse(observation));
|
|
3274
|
+
if (parsed.some(result => !result.success))
|
|
3275
|
+
return false;
|
|
3276
|
+
const merged = parsed.flatMap(result => result.success ? [result.data] : []);
|
|
3277
|
+
const byKey = new Map(merged.map(pr => [pullRequestKey(pr), pr]));
|
|
3278
|
+
if (byKey.size !== current.pullRequests.length || current.pullRequests.some(pr => !byKey.has(pullRequestKey(pr))))
|
|
3279
|
+
return false;
|
|
3280
|
+
const restoredAt = Math.max(0, ...family.map(run => run.manualUnarchivedAt ? Date.parse(run.manualUnarchivedAt) : 0));
|
|
3281
|
+
if (restoredAt && merged.every(pr => Date.parse(pr.mergedAt) <= restoredAt))
|
|
3282
|
+
return false;
|
|
3283
|
+
const now = new Date().toISOString();
|
|
3284
|
+
if (merged.some(pr => Date.parse(pr.mergedAt) > Date.parse(now)))
|
|
3285
|
+
return false;
|
|
3286
|
+
const staged = new Map();
|
|
3287
|
+
this.stageArchive(staged, family[0], true);
|
|
3288
|
+
staged.get(snapshot.rootId).autoArchive = { reason: 'pull-requests-merged', archivedAt: now, fingerprint: current.fingerprint, pullRequests: merged };
|
|
3289
|
+
this.commitIndex(staged);
|
|
3290
|
+
for (const id of staged.keys())
|
|
3291
|
+
this.compressor.enqueue(id);
|
|
3292
|
+
return true;
|
|
3293
|
+
}
|
|
2896
3294
|
setArchived(id, archived) {
|
|
2897
3295
|
const run = this.record(id);
|
|
2898
3296
|
if (!run)
|
|
@@ -2902,30 +3300,23 @@ export class RunStore extends EventEmitter {
|
|
|
2902
3300
|
}
|
|
2903
3301
|
/** Parent + owned workers; returns how many records flipped `archived`. */
|
|
2904
3302
|
applyArchivedCascade(run, archived) {
|
|
3303
|
+
const staged = new Map();
|
|
3304
|
+
this.stageArchive(staged, run, archived);
|
|
2905
3305
|
let changed = 0;
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
for (const { id } of this.listWorkersOf(run.id)) {
|
|
2912
|
-
const child = this.record(id);
|
|
2913
|
-
if (child && this.applyArchived(child, archived))
|
|
2914
|
-
changed++;
|
|
2915
|
-
}
|
|
3306
|
+
for (const [id, next] of staged) {
|
|
3307
|
+
if (next && this.peek(id)?.archived !== archived)
|
|
3308
|
+
changed++;
|
|
3309
|
+
if (next)
|
|
3310
|
+
next.completionAction = undefined; // an explicit filing/restoration retires pending work
|
|
2916
3311
|
}
|
|
3312
|
+
this.commitIndex(staged);
|
|
2917
3313
|
return changed;
|
|
2918
3314
|
}
|
|
2919
3315
|
applyArchived(run, archived) {
|
|
2920
3316
|
const changed = run.archived !== archived;
|
|
2921
3317
|
// Transcript first: a restore throw aborts with the record unchanged (#818 S3).
|
|
2922
3318
|
this.syncTranscriptForm(run.id, archived);
|
|
2923
|
-
run
|
|
2924
|
-
run.archivedAt = archived ? new Date().toISOString() : undefined;
|
|
2925
|
-
if (archived) {
|
|
2926
|
-
clearPendingAutoResume(run);
|
|
2927
|
-
clearPin(run);
|
|
2928
|
-
}
|
|
3319
|
+
archiveFields(run, archived, new Date().toISOString());
|
|
2929
3320
|
this.touch(run);
|
|
2930
3321
|
return changed;
|
|
2931
3322
|
}
|
|
@@ -3021,17 +3412,18 @@ export class RunStore extends EventEmitter {
|
|
|
3021
3412
|
/** Bulk-archive finished runs (#780). `isSweepable` decides which; `archived` counts every
|
|
3022
3413
|
* record that flipped (cascaded workers included), `ids` lists the top-level runs picked and
|
|
3023
3414
|
* `pinnedIds` the subset that carried a pin before `clearPin` dropped it. */
|
|
3024
|
-
archiveFinished(scope) {
|
|
3415
|
+
archiveFinished(scope, automation) {
|
|
3025
3416
|
let archived = 0;
|
|
3026
3417
|
// Snapshot first: cascade may archive still-live workers, mutating the held set, and the pin
|
|
3027
3418
|
// has to be read before `applyArchived` clears it. Runs not in memory are picked off their
|
|
3028
3419
|
// stored summary, which carries every field `isSweepable` reads.
|
|
3029
|
-
const picked = [...this.held.values()].filter((run) => isSweepable(run,
|
|
3420
|
+
const picked = [...this.held.values()].filter((run) => isSweepable({ ...run, automationId: automationIdOf(run) }, scope, automation))
|
|
3421
|
+
.map((run) => [run, this.listOrder(run)]);
|
|
3030
3422
|
for (const row of this.db?.listWhere("archived = 0 AND status IN ('done', 'failed', 'cancelled') AND parent_run_id IS NULL") ?? []) {
|
|
3031
3423
|
if (this.held.has(row.id) || this.deleted.has(row.id))
|
|
3032
3424
|
continue;
|
|
3033
3425
|
const summary = parseStoredSummary(row.summary);
|
|
3034
|
-
if (summary && isSweepable(summary, scope))
|
|
3426
|
+
if (summary && isSweepable(summary, scope, automation))
|
|
3035
3427
|
picked.push([summary, row]);
|
|
3036
3428
|
}
|
|
3037
3429
|
// Another process's runs are not this store's to archive (#779, plan step 3): they stay out.
|
|
@@ -3082,31 +3474,13 @@ export class RunStore extends EventEmitter {
|
|
|
3082
3474
|
this.touch(run);
|
|
3083
3475
|
return run;
|
|
3084
3476
|
}
|
|
3085
|
-
/**
|
|
3086
|
-
*
|
|
3087
|
-
*
|
|
3088
|
-
*
|
|
3089
|
-
*
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
* - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
|
|
3093
|
-
* (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
|
|
3094
|
-
* appointment to pick the work back up, so there is no outcome to have missed.
|
|
3095
|
-
*
|
|
3096
|
-
* Keeping the two rules identical is what makes the returned count the number the
|
|
3097
|
-
* cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
|
|
3098
|
-
* (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
|
|
3099
|
-
* out a usage limit was uncounted by the badge but stamped read by the sweep — and this
|
|
3100
|
-
* comment asserted an invariant the code no longer held.
|
|
3101
|
-
*
|
|
3102
|
-
* This rule lives in two languages of the same repo, which is why it has now drifted
|
|
3103
|
-
* once. The cockpit cannot import it (`packages/web` does not depend on the service, and
|
|
3104
|
-
* should not), so a single definition would have to move to `packages/contract` — the one
|
|
3105
|
-
* package both sides already import. Worth doing; deliberately not done here, because
|
|
3106
|
-
* widening the contract package's remit from "shapes" to "behavior" is a design change
|
|
3107
|
-
* that deserves its own review rather than riding along in a bug fix. Until then: EDIT
|
|
3108
|
-
* BOTH, and the case-table tests on either side are what catch you if you don't. */
|
|
3109
|
-
markAllRead() {
|
|
3477
|
+
/**
|
|
3478
|
+
* Stamp unread finished runs in bulk and return the count. Keep the rule aligned with cockpit
|
|
3479
|
+
* isUnread: done/failed, finished since seen, not archived; cancelled and usage-limit waiting
|
|
3480
|
+
* runs are excluded. `automation` narrows it to one origin, as in `archiveFinished`
|
|
3481
|
+
* (hearsay-tools/cezarion#1616); absent stamps both.
|
|
3482
|
+
*/
|
|
3483
|
+
markAllRead(automation) {
|
|
3110
3484
|
const now = new Date().toISOString();
|
|
3111
3485
|
let count = 0;
|
|
3112
3486
|
// Runs not in memory are picked by the same rule over their stored summary, then re-checked
|
|
@@ -3123,11 +3497,12 @@ export class RunStore extends EventEmitter {
|
|
|
3123
3497
|
for (const run of candidates) {
|
|
3124
3498
|
const unread = !run.archived &&
|
|
3125
3499
|
(run.status === 'done' || run.status === 'failed') &&
|
|
3126
|
-
!(run.status === 'failed' && run.autoResumeAt !== undefined) &&
|
|
3127
3500
|
run.finishedAt !== undefined &&
|
|
3128
3501
|
(run.seenAt === undefined || run.seenAt < run.finishedAt);
|
|
3129
3502
|
if (!unread)
|
|
3130
3503
|
continue;
|
|
3504
|
+
if (automation !== undefined && (automationIdOf(run) !== undefined) !== automation)
|
|
3505
|
+
continue;
|
|
3131
3506
|
run.seenAt = now;
|
|
3132
3507
|
this.touch(run);
|
|
3133
3508
|
count++;
|
|
@@ -3146,7 +3521,7 @@ export class RunStore extends EventEmitter {
|
|
|
3146
3521
|
// Scrub credentials before the event touches disk or the live wire (#427):
|
|
3147
3522
|
// tool-result output is persisted verbatim and served back over the API, so
|
|
3148
3523
|
// a secret in an agent's command output would otherwise land in `.ai/cezar/`.
|
|
3149
|
-
const full = this.redact({ ...event, seq, ts: new Date().toISOString() });
|
|
3524
|
+
const full = this.redact(defined({ ...event, seq, ts: new Date().toISOString() }));
|
|
3150
3525
|
// Sync append keeps event order without a write queue; local NDJSON
|
|
3151
3526
|
// appends at agent-event rates are effectively free.
|
|
3152
3527
|
if (!hasPlainHistory(this.dataDir, runId))
|
|
@@ -3155,9 +3530,7 @@ export class RunStore extends EventEmitter {
|
|
|
3155
3530
|
appendFileSync(this.eventsPath(runId), line, 'utf8');
|
|
3156
3531
|
this.facts.append(runId, full, Buffer.byteLength(line));
|
|
3157
3532
|
this.maybeEnqueueHistoryCompress(run);
|
|
3158
|
-
|
|
3159
|
-
this.syncHumanAskSummary(run))
|
|
3160
|
-
this.touch(run);
|
|
3533
|
+
this.liveItems.observe(runId, full, false);
|
|
3161
3534
|
this.emit('event', { runId, event: full });
|
|
3162
3535
|
// The janitor trick: agents print the PR URL after `gh pr create` — the
|
|
3163
3536
|
// first one spotted in the transcript becomes the run's PR link. Scans v1
|
|
@@ -3174,23 +3547,31 @@ export class RunStore extends EventEmitter {
|
|
|
3174
3547
|
const claim = eventCreationClaimFragments(full).join(' ');
|
|
3175
3548
|
if (haystack.length > 0) {
|
|
3176
3549
|
let changed = false;
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3550
|
+
// One associated URL per trusted creation claim; replayed v1/v2 reports merge by identity.
|
|
3551
|
+
const item = full.item;
|
|
3552
|
+
const toolOutput = item?.kind === 'tool' && typeof item.output === 'string' ? item.output : undefined;
|
|
3553
|
+
const created = createdPrUrls(claim, haystack, toolOutput);
|
|
3554
|
+
for (const url of created) {
|
|
3555
|
+
const number = refUrlNumber(url);
|
|
3556
|
+
const parsed = runPullRequestSchema.safeParse({ number, url, source: 'created' });
|
|
3557
|
+
if (!parsed.success)
|
|
3558
|
+
continue;
|
|
3559
|
+
changed = this.accumulatePullRequests(run, [parsed.data]) || changed;
|
|
3560
|
+
if (!run.pullRequestUrl) {
|
|
3561
|
+
run.pullRequestUrl = url;
|
|
3562
|
+
changed = true;
|
|
3189
3563
|
}
|
|
3190
|
-
|
|
3564
|
+
}
|
|
3565
|
+
if (created.length > 0) {
|
|
3566
|
+
const resolved = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
|
|
3567
|
+
if (resolved !== run.referencedPullRequestUrl) {
|
|
3568
|
+
run.referencedPullRequestUrl = resolved;
|
|
3191
3569
|
changed = true;
|
|
3192
3570
|
}
|
|
3193
3571
|
}
|
|
3572
|
+
else if (!run.pullRequestUrl && PR_URL_RE.test(haystack) && this.trackReferencedPrs(run, haystack)) {
|
|
3573
|
+
changed = true;
|
|
3574
|
+
}
|
|
3194
3575
|
// Issue links feed their own referenced tier regardless of PR state —
|
|
3195
3576
|
// a task that created a PR can still be ABOUT an issue
|
|
3196
3577
|
// (spec 2026-07-21-report-ref-discovery).
|
|
@@ -3242,7 +3623,7 @@ export class RunStore extends EventEmitter {
|
|
|
3242
3623
|
if (candidatesChanged)
|
|
3243
3624
|
run.referencedIssueCandidates = [...seen];
|
|
3244
3625
|
const prev = run.referencedIssueUrl;
|
|
3245
|
-
run.referencedIssueUrl =
|
|
3626
|
+
run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
|
|
3246
3627
|
let numberChanged = false;
|
|
3247
3628
|
if (run.markerRefs?.issue === undefined && ISSUE_URL_RE.test(seedHaystack)) {
|
|
3248
3629
|
if (run.referencedIssueUrl && run.issueNumber === undefined) {
|
|
@@ -3266,7 +3647,7 @@ export class RunStore extends EventEmitter {
|
|
|
3266
3647
|
/**
|
|
3267
3648
|
* Apply agent-declared reference markers (spec 2026-07-18-task-ref-markers).
|
|
3268
3649
|
* Marker values are authoritative for the display tier: they overwrite the
|
|
3269
|
-
* regex/namer numbers, and a declared PR re-resolves the referenced URL
|
|
3650
|
+
* regex/namer numbers unless the prompt identifies the originating issue, and a declared PR re-resolves the referenced URL
|
|
3270
3651
|
* against the candidate working set — including down to `undefined` when no
|
|
3271
3652
|
* candidate matches (a wrong chip is worse than no chip). The created tier
|
|
3272
3653
|
* (`pullRequestUrl`) is deliberately untouched.
|
|
@@ -3279,6 +3660,17 @@ export class RunStore extends EventEmitter {
|
|
|
3279
3660
|
const run = this.record(runId);
|
|
3280
3661
|
if (!run || (refs.pr === undefined && refs.issue === undefined))
|
|
3281
3662
|
return run;
|
|
3663
|
+
const numbers = [...new Set([...(run.markerRefs?.pr === undefined ? [] : [run.markerRefs.pr]),
|
|
3664
|
+
...(refs.prs ?? (refs.pr === undefined ? [] : [refs.pr]))])];
|
|
3665
|
+
if (numbers.length)
|
|
3666
|
+
this.accumulatePullRequests(run, numbers.flatMap((number) => {
|
|
3667
|
+
const matches = (run.referencedPrCandidates ?? []).filter((url) => refUrlNumber(url) === number &&
|
|
3668
|
+
(this.repoHandle ? isRepoScopedRef(url, run.task, this.repoHandle) : promptNamesRepo(url, run.task)));
|
|
3669
|
+
const url = matches.length === 1 ? matches[0] : matches.length === 0 && this.repoHandle
|
|
3670
|
+
? `https://github.com/${this.repoHandle.owner}/${this.repoHandle.name}/pull/${number}` : undefined;
|
|
3671
|
+
const parsed = runPullRequestSchema.safeParse({ number, ...(url ? { url } : {}), source: 'declared' });
|
|
3672
|
+
return parsed.success ? [parsed.data] : [];
|
|
3673
|
+
}));
|
|
3282
3674
|
run.markerRefs = {
|
|
3283
3675
|
...run.markerRefs,
|
|
3284
3676
|
...(refs.pr !== undefined ? { pr: refs.pr } : {}),
|
|
@@ -3291,28 +3683,53 @@ export class RunStore extends EventEmitter {
|
|
|
3291
3683
|
run.prNumber = refs.pr;
|
|
3292
3684
|
}
|
|
3293
3685
|
if (refs.issue !== undefined) {
|
|
3294
|
-
run.issueNumber = refs.issue;
|
|
3686
|
+
run.issueNumber = originatingIssue(run) ?? refs.issue;
|
|
3295
3687
|
delete run.referencedIssueNumberSeeded;
|
|
3296
3688
|
}
|
|
3297
3689
|
if (run.markerRefs.pr !== undefined) {
|
|
3298
3690
|
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
|
|
3299
3691
|
}
|
|
3300
3692
|
if (run.markerRefs.issue !== undefined) {
|
|
3301
|
-
run.referencedIssueUrl =
|
|
3693
|
+
run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
|
|
3302
3694
|
}
|
|
3303
3695
|
this.touch(run);
|
|
3304
3696
|
return run;
|
|
3305
3697
|
}
|
|
3698
|
+
/** Authoritative creation keeps the first scalar for publishing, plus every created identity. */
|
|
3699
|
+
recordCreatedPr(runId, url) {
|
|
3700
|
+
const run = this.record(runId);
|
|
3701
|
+
const parsed = runPullRequestSchema.safeParse({ number: refUrlNumber(url), url, source: 'created' });
|
|
3702
|
+
if (!run || !parsed.success)
|
|
3703
|
+
return run;
|
|
3704
|
+
let changed = this.accumulatePullRequests(run, [parsed.data]);
|
|
3705
|
+
if (!run.pullRequestUrl) {
|
|
3706
|
+
run.pullRequestUrl = url;
|
|
3707
|
+
changed = true;
|
|
3708
|
+
}
|
|
3709
|
+
if (changed)
|
|
3710
|
+
this.touch(run);
|
|
3711
|
+
return run;
|
|
3712
|
+
}
|
|
3713
|
+
accumulatePullRequests(run, entries) {
|
|
3714
|
+
const next = mergePullRequests(run.pullRequests, entries, this.repoHandle
|
|
3715
|
+
? `https://github.com/${this.repoHandle.owner}/${this.repoHandle.name}` : undefined);
|
|
3716
|
+
if (isDeepStrictEqual(next, run.pullRequests))
|
|
3717
|
+
return false;
|
|
3718
|
+
run.pullRequests = next;
|
|
3719
|
+
return true;
|
|
3720
|
+
}
|
|
3306
3721
|
/**
|
|
3307
3722
|
* Fan an event out to live subscribers WITHOUT writing it to the NDJSON
|
|
3308
3723
|
* file — the channel for coalesced `item.delta` flushes (protocol-v2
|
|
3309
3724
|
* performance guardrail: raw deltas never hit disk; replay = the persisted
|
|
3310
3725
|
* snapshots). Stamped with `seq`/`ts` like persisted lines so the live
|
|
3311
|
-
* wire keeps one ordering axis
|
|
3312
|
-
*
|
|
3726
|
+
* wire keeps one ordering axis. Finite polling coalesces active item content
|
|
3727
|
+
* into bounded in-memory snapshots on that clock; disk replay still has gaps
|
|
3728
|
+
* (dedup compares with `>`).
|
|
3313
3729
|
*/
|
|
3314
3730
|
emitEphemeral(runId, event) {
|
|
3315
3731
|
const full = this.redact({ ...event, seq: this.nextSeq(runId), ts: new Date().toISOString() });
|
|
3732
|
+
this.liveItems.observe(runId, full, true);
|
|
3316
3733
|
this.emit('event', { runId, event: full });
|
|
3317
3734
|
return full;
|
|
3318
3735
|
}
|
|
@@ -3350,19 +3767,89 @@ export class RunStore extends EventEmitter {
|
|
|
3350
3767
|
transcriptFacts(runId) {
|
|
3351
3768
|
return this.facts.get(runId) ?? emptyFacts();
|
|
3352
3769
|
}
|
|
3770
|
+
/** The run's transcript facts when they are known: undefined when the transcript exists but its
|
|
3771
|
+
* facts are not current or cannot be read, so a caller can tell "no ask" from "no evidence". */
|
|
3772
|
+
knownTranscriptFacts(runId) {
|
|
3773
|
+
return this.facts.peek(runId);
|
|
3774
|
+
}
|
|
3353
3775
|
hasProjection(runId, projectionId) {
|
|
3354
3776
|
return this.transcriptFacts(runId).projectionIds.includes(projectionId);
|
|
3355
3777
|
}
|
|
3356
3778
|
factsWarming = Promise.resolve();
|
|
3357
|
-
/**
|
|
3358
|
-
|
|
3779
|
+
/** Indexed family enumeration, including members not yet flushed to SQLite. */
|
|
3780
|
+
listFamilyRunIds(rootId) {
|
|
3781
|
+
const ids = new Set([rootId, ...(this.db?.listIdsByParent(rootId) ?? [])]);
|
|
3782
|
+
for (const [id, run] of this.held) {
|
|
3783
|
+
if (familyRootOf(run) === rootId)
|
|
3784
|
+
ids.add(id);
|
|
3785
|
+
else if (id !== rootId)
|
|
3786
|
+
ids.delete(id);
|
|
3787
|
+
}
|
|
3788
|
+
for (const id of this.deleted)
|
|
3789
|
+
ids.delete(id);
|
|
3790
|
+
return [...ids];
|
|
3791
|
+
}
|
|
3792
|
+
recoveryFactIds(familyRootId) {
|
|
3793
|
+
if (familyRootId !== undefined)
|
|
3794
|
+
return this.listFamilyRunIds(familyRootId);
|
|
3795
|
+
const ids = new Set();
|
|
3796
|
+
for (const [id, run] of this.held) {
|
|
3797
|
+
if (!isLiveRecord(run))
|
|
3798
|
+
continue;
|
|
3799
|
+
const root = familyRootOf(run);
|
|
3800
|
+
for (const member of root === undefined ? [id] : this.listFamilyRunIds(root))
|
|
3801
|
+
ids.add(member);
|
|
3802
|
+
}
|
|
3803
|
+
return [...ids];
|
|
3804
|
+
}
|
|
3805
|
+
/** Readiness owns no run snapshot across an await: refresh only the currently held owner. */
|
|
3806
|
+
async prepareTranscriptFacts(runIds) {
|
|
3807
|
+
for (const id of new Set(runIds)) {
|
|
3808
|
+
// Exact sidecars resolve immediately too: recovery must yield to timers/I/O between IDs.
|
|
3809
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
3810
|
+
await this.facts.ready(id);
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
prepareRecoveryFacts(familyRootId) {
|
|
3814
|
+
return this.prepareTranscriptFacts(this.recoveryFactIds(familyRootId));
|
|
3815
|
+
}
|
|
3816
|
+
/** Register IDs only here; the index yields between cache reads and off-loop rebuilds. */
|
|
3359
3817
|
warmTranscriptFacts() {
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3818
|
+
this.factsWarming = Promise.resolve().then(async () => {
|
|
3819
|
+
if (!this.db)
|
|
3820
|
+
return;
|
|
3821
|
+
const ids = new Set(this.recoveryFactIds());
|
|
3822
|
+
// hearsay-tools/cezarion#949: this microtask ends open()'s synchronous slice, and on a
|
|
3823
|
+
// large history the JSON scan for conversation roots plus one family query per root cost
|
|
3824
|
+
// as much again. Past one slice's worth of runs, yield before the scan and between root
|
|
3825
|
+
// batches; a small store (every unit test) still registers in this one microtask.
|
|
3826
|
+
const large = this.listRunIds().length > WARM_IDS_PER_SLICE;
|
|
3827
|
+
const turn = async () => {
|
|
3828
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
3829
|
+
return this.db !== null;
|
|
3830
|
+
};
|
|
3831
|
+
if (large && !await turn())
|
|
3832
|
+
return;
|
|
3833
|
+
const roots = this.listConversationRootIds();
|
|
3834
|
+
for (let i = 0; i < roots.length; i++) {
|
|
3835
|
+
if (large && i > 0 && i % WARM_IDS_PER_SLICE === 0 && !await turn())
|
|
3836
|
+
return;
|
|
3837
|
+
for (const id of this.listFamilyRunIds(roots[i]))
|
|
3838
|
+
ids.add(id);
|
|
3839
|
+
}
|
|
3840
|
+
if (large && !await turn())
|
|
3841
|
+
return;
|
|
3842
|
+
// Re-list after the yields: a run deleted meanwhile must not be registered again.
|
|
3843
|
+
const present = this.listRunIds();
|
|
3844
|
+
if (large) {
|
|
3845
|
+
const live = new Set(present);
|
|
3846
|
+
for (const id of ids)
|
|
3847
|
+
if (!live.has(id))
|
|
3848
|
+
ids.delete(id);
|
|
3849
|
+
}
|
|
3850
|
+
for (const id of present)
|
|
3851
|
+
ids.add(id);
|
|
3852
|
+
await this.facts.warm(ids);
|
|
3366
3853
|
}).catch(() => undefined);
|
|
3367
3854
|
}
|
|
3368
3855
|
/** Resolves when the open-time warm-up is done (tests). */
|
|
@@ -3379,18 +3866,6 @@ export class RunStore extends EventEmitter {
|
|
|
3379
3866
|
return [];
|
|
3380
3867
|
}
|
|
3381
3868
|
}
|
|
3382
|
-
/** The owned store's `refreshHumanAskSummary`, from the index, on the same rules: no history
|
|
3383
|
-
* (legacy runs) keeps the summary, an unreadable one requests attention. True when it changed. */
|
|
3384
|
-
syncHumanAskSummary(run) {
|
|
3385
|
-
const facts = this.facts.get(run.id);
|
|
3386
|
-
const next = !facts ? true
|
|
3387
|
-
: facts.bytes > 0 || facts.archive !== undefined ? facts.pendingAsk !== undefined
|
|
3388
|
-
: run.hasPendingHumanAsk === true;
|
|
3389
|
-
if (next === run.hasPendingHumanAsk)
|
|
3390
|
-
return false;
|
|
3391
|
-
run.hasPendingHumanAsk = next;
|
|
3392
|
-
return true;
|
|
3393
|
-
}
|
|
3394
3869
|
readEvents(runId) {
|
|
3395
3870
|
try {
|
|
3396
3871
|
const raw = readHistoryText(this.dataDir, runId);
|
|
@@ -3498,6 +3973,8 @@ export class RunStore extends EventEmitter {
|
|
|
3498
3973
|
}
|
|
3499
3974
|
/** `workerResourcesSafe` with the refusal's live PIDs (empty when no PID explains it). */
|
|
3500
3975
|
workerResourceHolders(id, generation, resourceId, opts = {}) {
|
|
3976
|
+
if (autosaveCleanupBlocker(this.dataDir, id))
|
|
3977
|
+
return [];
|
|
3501
3978
|
const run = this.peek(id);
|
|
3502
3979
|
const proof = this.readWorkerExecution(id);
|
|
3503
3980
|
if (run?.delegation?.role !== 'worker' || run.delegation.workspace.ownerRunId !== id ||
|
|
@@ -3540,6 +4017,8 @@ export class RunStore extends EventEmitter {
|
|
|
3540
4017
|
/** Cleanup can outlive its index row, but never its generation or terminal task intent.
|
|
3541
4018
|
* `cwds` is the scratch reprobe tick's shared `/proc` snapshot (hearsay-tools/cezarion#879). */
|
|
3542
4019
|
workerScratchResourcesSafe(id, generation, resourceId, cwds) {
|
|
4020
|
+
if (autosaveCleanupBlocker(this.dataDir, id))
|
|
4021
|
+
return false;
|
|
3543
4022
|
const run = this.peek(id), proof = this.readWorkerExecution(id);
|
|
3544
4023
|
if (proof?.phase !== 'complete' || proof.generation !== generation || proof.scratchCleanup?.resourceId !== resourceId ||
|
|
3545
4024
|
(run && ['queued', 'running', 'waiting'].includes(run.status)))
|
|
@@ -3726,6 +4205,8 @@ export class RunStore extends EventEmitter {
|
|
|
3726
4205
|
return proof?.phase === 'complete';
|
|
3727
4206
|
}
|
|
3728
4207
|
canDeleteRun(id) {
|
|
4208
|
+
if (autosaveCleanupBlocker(this.dataDir, id))
|
|
4209
|
+
return false;
|
|
3729
4210
|
const run = this.peek(id);
|
|
3730
4211
|
if (run?.delegation?.role === 'invalid')
|
|
3731
4212
|
return false;
|
|
@@ -3821,6 +4302,7 @@ export class RunStore extends EventEmitter {
|
|
|
3821
4302
|
rmSync(this.handoffPath(id), { force: true });
|
|
3822
4303
|
rmSync(this.imagesDir(id), { recursive: true, force: true });
|
|
3823
4304
|
removeArtifacts(this.dataDir, id);
|
|
4305
|
+
clearAutosaveCleanup(this.dataDir, id);
|
|
3824
4306
|
}
|
|
3825
4307
|
deleteRun(id) {
|
|
3826
4308
|
if (!this.canDeleteRun(id))
|
|
@@ -3830,6 +4312,8 @@ export class RunStore extends EventEmitter {
|
|
|
3830
4312
|
return false;
|
|
3831
4313
|
if (run?.delegation?.role === 'worker' || run?.delegation?.role === 'root')
|
|
3832
4314
|
return this.deleteDelegatedRun(id);
|
|
4315
|
+
if (run)
|
|
4316
|
+
clearAutosaveCleanup(this.dataDir, id);
|
|
3833
4317
|
const existed = run !== undefined;
|
|
3834
4318
|
this.held.delete(id);
|
|
3835
4319
|
if (existed) {
|
|
@@ -3891,6 +4375,7 @@ export class RunStore extends EventEmitter {
|
|
|
3891
4375
|
/** The held record is gone; the next save or commit deletes its row, fenced by `family`'s claim
|
|
3892
4376
|
* and by the revision this store last saw (kept in `base` until the delete is written). */
|
|
3893
4377
|
markDeleted(id, family) {
|
|
4378
|
+
this.liveItems.forget(id);
|
|
3894
4379
|
const read = this.coldBase.get(id);
|
|
3895
4380
|
if (!this.base.has(id) && read)
|
|
3896
4381
|
this.base.set(id, storedRow(read));
|
|
@@ -3989,7 +4474,10 @@ export class RunStore extends EventEmitter {
|
|
|
3989
4474
|
const encode = (run) => {
|
|
3990
4475
|
const encoded = encodeRawRecord(run, (this.base.get(run.id) ?? this.coldBase.get(run.id))?.extras);
|
|
3991
4476
|
extras.set(run.id, encoded.extras);
|
|
3992
|
-
|
|
4477
|
+
const projected = { ...run };
|
|
4478
|
+
restoreOriginatingIssue(projected);
|
|
4479
|
+
upserts.push({ ...encodeRunRow(run, encoded.data),
|
|
4480
|
+
issueAttributionCurrent: isDeepStrictEqual(projected, run) });
|
|
3993
4481
|
};
|
|
3994
4482
|
for (const [id, next] of staged) {
|
|
3995
4483
|
if (next)
|
|
@@ -4043,6 +4531,7 @@ export class RunStore extends EventEmitter {
|
|
|
4043
4531
|
this.base.set(row.id, { revision: commit.revisions.get(row.id), seq: commit.seqs.get(row.id), data: row.data, extras: extras.get(row.id) });
|
|
4044
4532
|
}
|
|
4045
4533
|
for (const id of deletes) {
|
|
4534
|
+
this.liveItems.forget(id);
|
|
4046
4535
|
this.base.delete(id);
|
|
4047
4536
|
this.deletedFamilies.delete(id);
|
|
4048
4537
|
this.removeOwedHistory(id);
|
|
@@ -4068,23 +4557,9 @@ export class RunStore extends EventEmitter {
|
|
|
4068
4557
|
return { owner: this.owner, claims, rows };
|
|
4069
4558
|
}
|
|
4070
4559
|
/**
|
|
4071
|
-
*
|
|
4072
|
-
*
|
|
4073
|
-
*
|
|
4074
|
-
* A conflict is UNEXPECTED when this store owned the row's family when it last read or wrote it:
|
|
4075
|
-
* a held or pending row, or one read inside a family it claimed. Another writer then broke the
|
|
4076
|
-
* claim. For each such row the original (what this store last saw), local (what it meant to
|
|
4077
|
-
* write, or that it meant to delete it) and current versions go to `run_conflicts` first. Only
|
|
4078
|
-
* once that evidence is stored does the row leave the pending set, so no retry rewrites it; this
|
|
4079
|
-
* store then refuses to write it again, and its held copy is replaced by the current row (or
|
|
4080
|
-
* dropped when deleted) and announced. That refusal lasts as long as this store: the explicit
|
|
4081
|
-
* recovery is a restart, which reads the row as it now stands, and the evidence row stays for a
|
|
4082
|
-
* person to compare. Other pending rows stay pending and the next save writes them.
|
|
4083
|
-
*
|
|
4084
|
-
* If the evidence cannot be stored, nothing changes: the rows stay pending, and the next write
|
|
4085
|
-
* meets the same conflict and tries again. A row read before its family was claimed may simply
|
|
4086
|
-
* have been finished by its previous owner in between: a stale read, not a conflict, so only the
|
|
4087
|
-
* write fails.
|
|
4560
|
+
* Handle a fenced-write conflict. Save unexpected conflict evidence before clearing pending
|
|
4561
|
+
* state or quarantining the row. If evidence persistence fails, retain pending writes. Other
|
|
4562
|
+
* rows remain writable; stale unclaimed reads only fail their write.
|
|
4088
4563
|
*/
|
|
4089
4564
|
handleConflicts(error, staged) {
|
|
4090
4565
|
const unexpected = error.conflicts.filter((conflict) => this.held.has(conflict.id) || this.deleted.has(conflict.id) || this.coldBase.get(conflict.id)?.owned === true);
|
|
@@ -4111,6 +4586,7 @@ export class RunStore extends EventEmitter {
|
|
|
4111
4586
|
}
|
|
4112
4587
|
unexpected.forEach((conflict, index) => {
|
|
4113
4588
|
const family = this.familyOf(conflict.id);
|
|
4589
|
+
this.liveItems.forget(conflict.id);
|
|
4114
4590
|
this.quarantined.add(conflict.id);
|
|
4115
4591
|
this.dirty.delete(conflict.id);
|
|
4116
4592
|
this.deleted.delete(conflict.id);
|
|
@@ -4174,6 +4650,9 @@ export class RunStore extends EventEmitter {
|
|
|
4174
4650
|
* absent. It saves nothing more, and a durable commit on it throws.
|
|
4175
4651
|
*/
|
|
4176
4652
|
close() {
|
|
4653
|
+
this.liveItems.clear();
|
|
4654
|
+
// Demand-bound feeds stop before storage disappears during project removal/shutdown.
|
|
4655
|
+
this.emit('closed');
|
|
4177
4656
|
this.compressor.stop();
|
|
4178
4657
|
this.facts.stop();
|
|
4179
4658
|
this.facts.flush();
|
|
@@ -4198,4 +4677,17 @@ export class RunStore extends EventEmitter {
|
|
|
4198
4677
|
this.db = null;
|
|
4199
4678
|
}
|
|
4200
4679
|
}
|
|
4680
|
+
function archiveDigest(value) {
|
|
4681
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
4682
|
+
}
|
|
4683
|
+
function archiveFields(run, archived, now) {
|
|
4684
|
+
run.archived = archived;
|
|
4685
|
+
run.archivedAt = archived ? now : undefined;
|
|
4686
|
+
if (archived) {
|
|
4687
|
+
clearPendingAutoResume(run);
|
|
4688
|
+
clearPin(run);
|
|
4689
|
+
}
|
|
4690
|
+
else
|
|
4691
|
+
run.manualUnarchivedAt = now;
|
|
4692
|
+
}
|
|
4201
4693
|
//# sourceMappingURL=store.js.map
|