@wjarka/cezarion 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -46
- package/dist/agent-config/catalog.d.ts +6 -2
- package/dist/agent-config/catalog.js +78 -3
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/agent-config/model-settings/shared.js +6 -3
- package/dist/agent-config/model-settings/shared.js.map +1 -1
- package/dist/agent-config/models.js +4 -0
- package/dist/agent-config/models.js.map +1 -1
- package/dist/agent-config/validate.js +6 -0
- package/dist/agent-config/validate.js.map +1 -1
- package/dist/artifacts/cli.js +10 -2
- package/dist/artifacts/cli.js.map +1 -1
- package/dist/automations/coordinator.d.ts +7 -0
- package/dist/automations/coordinator.js +22 -2
- package/dist/automations/coordinator.js.map +1 -1
- package/dist/automations/github-poller.d.ts +4 -4
- package/dist/automations/github-poller.js.map +1 -1
- package/dist/automations/schedule-runner.d.ts +130 -0
- package/dist/automations/schedule-runner.js +326 -0
- package/dist/automations/schedule-runner.js.map +1 -0
- package/dist/automations/scheduler.d.ts +38 -7
- package/dist/automations/scheduler.js +90 -22
- package/dist/automations/scheduler.js.map +1 -1
- package/dist/automations/store.d.ts +80 -3
- package/dist/automations/store.js +208 -16
- package/dist/automations/store.js.map +1 -1
- package/dist/automations/task-template.d.ts +42 -6
- package/dist/automations/task-template.js +152 -24
- package/dist/automations/task-template.js.map +1 -1
- package/dist/automations/types.d.ts +142 -6
- package/dist/automations/types.js +64 -6
- package/dist/automations/types.js.map +1 -1
- package/dist/autosave-holders.js +8 -12
- package/dist/autosave-holders.js.map +1 -1
- package/dist/ci-wait/client.d.ts +8 -1
- package/dist/ci-wait/client.js +43 -1
- package/dist/ci-wait/client.js.map +1 -1
- package/dist/ci-wait/controller.d.ts +83 -4
- package/dist/ci-wait/controller.js +76 -11
- package/dist/ci-wait/controller.js.map +1 -1
- package/dist/ci-wait/errors.d.ts +7 -1
- package/dist/ci-wait/errors.js +36 -0
- package/dist/ci-wait/errors.js.map +1 -1
- package/dist/ci-wait/github.d.ts +2 -0
- package/dist/ci-wait/github.js +3 -1
- package/dist/ci-wait/github.js.map +1 -1
- package/dist/ci-wait/mcp.d.ts +2 -98
- package/dist/ci-wait/mcp.js +11 -25
- package/dist/ci-wait/mcp.js.map +1 -1
- package/dist/ci-wait/supervisor.d.ts +2 -1
- package/dist/ci-wait/supervisor.js +2 -1
- package/dist/ci-wait/supervisor.js.map +1 -1
- package/dist/ci-wait/tools.d.ts +307 -0
- package/dist/ci-wait/tools.js +63 -0
- package/dist/ci-wait/tools.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/contract/agent-config.d.ts +5 -0
- package/dist/contract/agent-profiles.d.ts +15 -0
- package/dist/contract/automation-schedule.d.ts +60 -0
- package/dist/contract/automations.d.ts +208 -36
- package/dist/contract/delegation.d.ts +96 -0
- package/dist/contract/discovery.d.ts +7 -0
- package/dist/contract/github.d.ts +1 -0
- package/dist/contract/health.d.ts +6 -0
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.js +1445 -745
- package/dist/contract/preview.d.ts +508 -0
- package/dist/contract/runs.d.ts +716 -15
- package/dist/contract/skills.d.ts +39 -0
- package/dist/contract/workflows.d.ts +6 -0
- package/dist/contract/workspace.d.ts +65 -0
- package/dist/contract/zoned-time.d.ts +35 -0
- package/dist/core/agent-env.d.ts +11 -0
- package/dist/core/agent-env.js +27 -5
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-profiles.js +7 -0
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-runner.d.ts +49 -1
- package/dist/core/agent-runner.js +2 -1
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/backend-detect.d.ts +3 -2
- package/dist/core/backend-detect.js +27 -1
- package/dist/core/backend-detect.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +1 -0
- package/dist/core/claude-cli-runner.js +69 -19
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.d.ts +1 -0
- package/dist/core/codex-app-server-runner.js +200 -25
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +7 -3
- package/dist/core/codex-app-server-transport.js +11 -9
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/cursor-acp-runner.d.ts +1 -0
- package/dist/core/cursor-acp-runner.js +24 -9
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/host-model-catalog.js +2 -0
- package/dist/core/host-model-catalog.js.map +1 -1
- package/dist/core/managed-session.js +2 -0
- package/dist/core/managed-session.js.map +1 -1
- package/dist/core/model-identity.js +2 -0
- package/dist/core/model-identity.js.map +1 -1
- package/dist/core/model-presets.js +3 -0
- package/dist/core/model-presets.js.map +1 -1
- package/dist/core/omp-model-catalog.d.ts +43 -0
- package/dist/core/omp-model-catalog.js +205 -0
- package/dist/core/omp-model-catalog.js.map +1 -0
- package/dist/core/omp-runner.d.ts +73 -0
- package/dist/core/omp-runner.js +990 -0
- package/dist/core/omp-runner.js.map +1 -0
- package/dist/core/omp-ui-mapper.d.ts +106 -0
- package/dist/core/omp-ui-mapper.js +882 -0
- package/dist/core/omp-ui-mapper.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +18 -4
- package/dist/core/opencode-server-runner.js +337 -96
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/opencode-session-error.d.ts +11 -0
- package/dist/core/opencode-session-error.js +26 -0
- package/dist/core/opencode-session-error.js.map +1 -0
- package/dist/core/opencode-ui-mapper.js +4 -36
- package/dist/core/opencode-ui-mapper.js.map +1 -1
- package/dist/core/pi-runner.d.ts +1 -0
- package/dist/core/pi-runner.js +75 -12
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/provider-auth.d.ts +9 -2
- package/dist/core/provider-auth.js +43 -6
- package/dist/core/provider-auth.js.map +1 -1
- package/dist/core/runner-factory.js +3 -0
- package/dist/core/runner-factory.js.map +1 -1
- package/dist/core/runner-model-catalog.js +1 -0
- package/dist/core/runner-model-catalog.js.map +1 -1
- package/dist/core/runner-stderr.d.ts +2 -0
- package/dist/core/runner-stderr.js +20 -0
- package/dist/core/runner-stderr.js.map +1 -0
- package/dist/core/session-process.d.ts +22 -0
- package/dist/core/session-process.js +202 -0
- package/dist/core/session-process.js.map +1 -0
- package/dist/core/ui-events.d.ts +1 -1
- package/dist/core/usage-limit.js +3 -60
- package/dist/core/usage-limit.js.map +1 -1
- package/dist/data-gitignore.d.ts +6 -0
- package/dist/data-gitignore.js +57 -0
- package/dist/data-gitignore.js.map +1 -0
- package/dist/delegation/capacity.d.ts +19 -0
- package/dist/delegation/capacity.js +34 -0
- package/dist/delegation/capacity.js.map +1 -0
- package/dist/delegation/cli.js +13 -4
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/conversations.d.ts +4 -2
- package/dist/delegation/conversations.js +8 -12
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/credentials.d.ts +2 -0
- package/dist/delegation/credentials.js +10 -0
- package/dist/delegation/credentials.js.map +1 -1
- package/dist/delegation/destroy-observation.d.ts +42 -0
- package/dist/delegation/destroy-observation.js +115 -0
- package/dist/delegation/destroy-observation.js.map +1 -0
- package/dist/delegation/execution-identity.d.ts +6 -0
- package/dist/delegation/execution-identity.js +1 -1
- package/dist/delegation/execution-identity.js.map +1 -1
- package/dist/delegation/policy.d.ts +3 -2
- package/dist/delegation/policy.js +7 -5
- package/dist/delegation/policy.js.map +1 -1
- package/dist/delegation/process-liveness.d.ts +57 -20
- package/dist/delegation/process-liveness.js +114 -92
- package/dist/delegation/process-liveness.js.map +1 -1
- package/dist/delegation/provision.js +6 -6
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/results.js +4 -2
- package/dist/delegation/results.js.map +1 -1
- package/dist/delegation/retry-backoff.d.ts +23 -0
- package/dist/delegation/retry-backoff.js +18 -0
- package/dist/delegation/retry-backoff.js.map +1 -0
- package/dist/delegation/routes.d.ts +64 -58
- package/dist/delegation/scratch-cleanup.d.ts +29 -0
- package/dist/delegation/scratch-cleanup.js +143 -0
- package/dist/delegation/scratch-cleanup.js.map +1 -0
- package/dist/delegation/service.d.ts +20 -1
- package/dist/delegation/service.js +233 -70
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +12 -6
- package/dist/delegation/workspace.d.ts +23 -4
- package/dist/delegation/workspace.js +147 -31
- package/dist/delegation/workspace.js.map +1 -1
- package/dist/git-worktree-lock-helper.js +8 -3
- package/dist/git-worktree-lock-helper.js.map +1 -1
- package/dist/git-worktree-lock.d.ts +6 -1
- package/dist/git-worktree-lock.js +10 -4
- package/dist/git-worktree-lock.js.map +1 -1
- package/dist/git-worktree-release.d.ts +20 -0
- package/dist/git-worktree-release.js +27 -0
- package/dist/git-worktree-release.js.map +1 -0
- package/dist/git-worktree.d.ts +8 -2
- package/dist/git-worktree.js +19 -2
- package/dist/git-worktree.js.map +1 -1
- package/dist/handoff.d.ts +2 -2
- package/dist/handoff.js +1 -1
- package/dist/index.js +109 -76
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +5 -1
- package/dist/paths.js.map +1 -1
- package/dist/preview/cdp.d.ts +13 -0
- package/dist/preview/cdp.js +69 -0
- package/dist/preview/cdp.js.map +1 -0
- package/dist/preview/chromium.d.ts +75 -0
- package/dist/preview/chromium.js +394 -0
- package/dist/preview/chromium.js.map +1 -0
- package/dist/preview/dev-server.d.ts +68 -0
- package/dist/preview/dev-server.js +341 -0
- package/dist/preview/dev-server.js.map +1 -0
- package/dist/preview/host.d.ts +154 -0
- package/dist/preview/host.js +681 -0
- package/dist/preview/host.js.map +1 -0
- package/dist/preview/registration.d.ts +63 -0
- package/dist/preview/registration.js +109 -0
- package/dist/preview/registration.js.map +1 -0
- package/dist/preview/session.d.ts +73 -0
- package/dist/preview/session.js +347 -0
- package/dist/preview/session.js.map +1 -0
- package/dist/preview/url.d.ts +6 -0
- package/dist/preview/url.js +17 -0
- package/dist/preview/url.js.map +1 -0
- package/dist/runs/agent-tmpdir-removal.d.ts +15 -0
- package/dist/runs/agent-tmpdir-removal.js +60 -0
- package/dist/runs/agent-tmpdir-removal.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +12 -1
- package/dist/runs/agent-tmpdir.js +123 -34
- package/dist/runs/agent-tmpdir.js.map +1 -1
- package/dist/runs/delegation-state.d.ts +5 -1
- package/dist/runs/delegation-state.js +9 -0
- package/dist/runs/delegation-state.js.map +1 -1
- package/dist/runs/event-history.d.ts +4 -4
- package/dist/runs/event-history.js +97 -117
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/history-compressor.d.ts +29 -0
- package/dist/runs/history-compressor.js +116 -0
- package/dist/runs/history-compressor.js.map +1 -0
- package/dist/runs/history-file.d.ts +36 -0
- package/dist/runs/history-file.js +463 -0
- package/dist/runs/history-file.js.map +1 -0
- package/dist/runs/human-ask-summary.js +18 -13
- package/dist/runs/human-ask-summary.js.map +1 -1
- package/dist/runs/legacy-index.d.ts +62 -0
- package/dist/runs/legacy-index.js +168 -0
- package/dist/runs/legacy-index.js.map +1 -0
- package/dist/runs/raw-record.d.ts +52 -0
- package/dist/runs/raw-record.js +145 -0
- package/dist/runs/raw-record.js.map +1 -0
- package/dist/runs/retention.d.ts +13 -6
- package/dist/runs/retention.js +42 -13
- package/dist/runs/retention.js.map +1 -1
- package/dist/runs/run-claims.d.ts +23 -0
- package/dist/runs/run-claims.js +24 -0
- package/dist/runs/run-claims.js.map +1 -0
- package/dist/runs/run-database.d.ts +385 -0
- package/dist/runs/run-database.js +930 -0
- package/dist/runs/run-database.js.map +1 -0
- package/dist/runs/run-index.d.ts +35 -9
- package/dist/runs/run-index.js +200 -25
- package/dist/runs/run-index.js.map +1 -1
- package/dist/runs/run-row.d.ts +44 -0
- package/dist/runs/run-row.js +84 -0
- package/dist/runs/run-row.js.map +1 -0
- package/dist/runs/run-search.d.ts +29 -0
- package/dist/runs/run-search.js +63 -0
- package/dist/runs/run-search.js.map +1 -0
- package/dist/runs/store-open-error.d.ts +24 -0
- package/dist/runs/store-open-error.js +61 -0
- package/dist/runs/store-open-error.js.map +1 -0
- package/dist/runs/store.d.ts +667 -32
- package/dist/runs/store.js +2153 -324
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/transcript-facts.d.ts +77 -0
- package/dist/runs/transcript-facts.js +302 -0
- package/dist/runs/transcript-facts.js.map +1 -0
- package/dist/runs/webhook.js +5 -4
- package/dist/runs/webhook.js.map +1 -1
- package/dist/runs/worker-execution.d.ts +22 -0
- package/dist/runs/worker-execution.js +30 -0
- package/dist/runs/worker-execution.js.map +1 -0
- package/dist/server/capabilities.d.ts +1 -0
- package/dist/server/capabilities.js +2 -0
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/cockpit-ownership.d.ts +8 -0
- package/dist/server/cockpit-ownership.js +19 -0
- package/dist/server/cockpit-ownership.js.map +1 -1
- package/dist/server/forge/github.d.ts +1 -4
- package/dist/server/forge/github.js +47 -23
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/git.js +1 -1
- package/dist/server/git.js.map +1 -1
- package/dist/server/open-in-app.js +1 -0
- package/dist/server/open-in-app.js.map +1 -1
- package/dist/server/preview-socket.d.ts +36 -0
- package/dist/server/preview-socket.js +126 -0
- package/dist/server/preview-socket.js.map +1 -0
- package/dist/server/project-context.d.ts +31 -4
- package/dist/server/project-context.js +73 -9
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/provider-action-gate.js +1 -0
- package/dist/server/provider-action-gate.js.map +1 -1
- package/dist/server/repo-branches.d.ts +4 -3
- package/dist/server/repo-branches.js +5 -4
- package/dist/server/repo-branches.js.map +1 -1
- package/dist/server/server.d.ts +4500 -2727
- package/dist/server/server.js +651 -202
- package/dist/server/server.js.map +1 -1
- package/dist/server/upgrade-router.d.ts +23 -0
- package/dist/server/upgrade-router.js +53 -0
- package/dist/server/upgrade-router.js.map +1 -0
- package/dist/server/validators.d.ts +6 -1
- package/dist/server/validators.js +4 -4
- package/dist/server/validators.js.map +1 -1
- package/dist/server/ws.d.ts +16 -16
- package/dist/server/ws.js +56 -54
- package/dist/server/ws.js.map +1 -1
- package/dist/skills-remote.d.ts +14 -0
- package/dist/skills-remote.js +91 -15
- package/dist/skills-remote.js.map +1 -1
- package/dist/task-cli/cli.js +40 -16
- package/dist/task-cli/cli.js.map +1 -1
- package/dist/task-cli/discovery.d.ts +4 -3
- package/dist/task-cli/discovery.js +4 -3
- package/dist/task-cli/discovery.js.map +1 -1
- package/dist/task-cli/http.d.ts +2 -0
- package/dist/task-cli/http.js +10 -4
- package/dist/task-cli/http.js.map +1 -1
- package/dist/task-cli/projections.d.ts +5 -2
- package/dist/task-cli/projections.js +13 -0
- package/dist/task-cli/projections.js.map +1 -1
- package/dist/task-cli/watch.d.ts +15 -2
- package/dist/task-cli/watch.js +31 -9
- package/dist/task-cli/watch.js.map +1 -1
- package/dist/ui-state.d.ts +4 -3
- package/dist/ui-state.js +8 -3
- package/dist/ui-state.js.map +1 -1
- package/dist/workflows/grouped-command.d.ts +31 -0
- package/dist/workflows/grouped-command.js +143 -0
- package/dist/workflows/grouped-command.js.map +1 -0
- package/dist/workflows/run.d.ts +223 -42
- package/dist/workflows/run.js +1316 -398
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/types.d.ts +12 -0
- package/dist/workflows/types.js +20 -0
- package/dist/workflows/types.js.map +1 -1
- package/dist/workspace/agent-accounts.d.ts +5 -1
- package/dist/workspace/agent-accounts.js +1 -0
- package/dist/workspace/agent-accounts.js.map +1 -1
- package/dist/workspace/agent-profiles.js +1 -1
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.d.ts +3 -1
- package/dist/workspace/config.js +1 -0
- package/dist/workspace/config.js.map +1 -1
- package/dist/worktree-setup.d.ts +85 -0
- package/dist/worktree-setup.js +164 -0
- package/dist/worktree-setup.js.map +1 -0
- package/package.json +7 -5
- package/scripts/benchmark-run-store-fixture.ts +428 -0
- package/scripts/benchmark-run-store.ts +743 -0
- package/scripts/benchmark-stuck-destroys.ts +143 -0
- package/scripts/mock-autonomous.mjs +11 -0
- package/scripts/mock-ci-tool.mjs +20 -5
- package/scripts/mock-claude.mjs +110 -1
- package/scripts/mock-codex-app-server.mjs +117 -2
- package/scripts/mock-codex-coalesce-steer.mjs +59 -0
- package/scripts/mock-codex-reverse-steer.mjs +130 -0
- package/scripts/mock-cursor-acp.mjs +75 -1
- package/scripts/mock-omp-rpc.mjs +605 -0
- package/scripts/mock-opencode-serve.mjs +312 -22
- package/scripts/mock-pi-rpc.mjs +109 -3
- package/scripts/mock-runner-crash.mjs +42 -0
- package/scripts/mock-silent-tail.mjs +78 -0
- package/scripts/mock-turn-messages.mjs +5 -0
- package/scripts/omp-ci-wait.mjs +29 -0
- package/scripts/omp-restrict-delegation.yml +4 -0
- package/scripts/pi-ci-wait.mjs +12 -9
- package/scripts/seed-archive-profile.ts +3 -3
- package/web/dist/assets/agent-accounts-CFOBMVHc.js +1 -0
- package/web/dist/assets/alert-dialog-BrpNUwMt.js +1 -0
- package/web/dist/assets/app-window-CSHwDX9N.js +1 -0
- package/web/dist/assets/button-C_rd7CPp.js +43 -0
- package/web/dist/assets/compare-variants-D6WXhvXA.js +1 -0
- package/web/dist/assets/diff-DrEARXTV.js +3 -0
- package/web/dist/assets/diff-controls-B1-j7nFg.js +1 -0
- package/web/dist/assets/{diff-view-DPn6bQ5W.js → diff-view-DvGLjdTk.js} +3 -3
- package/web/dist/assets/dropdown-menu-BegOZFrj.js +1 -0
- package/web/dist/assets/format-DpNe3S0A.js +1 -0
- package/web/dist/assets/git-toolbar-Cw4ljmhB.js +1 -0
- package/web/dist/assets/github-CfV5sqL0.js +1 -0
- package/web/dist/assets/{image-preview-DJ8cgb1Z.js → image-preview-FOEfqpZP.js} +1 -1
- package/web/dist/assets/index-DgWWlOLy.js +13 -0
- package/web/dist/assets/index-fBFnuvxu.css +1 -0
- package/web/dist/assets/{nav-row-styles-3j64kH5s.js → nav-row-styles-vp2JbhuJ.js} +1 -1
- package/web/dist/assets/play-CmVAj_fu.js +1 -0
- package/web/dist/assets/popover-BohP6WTx.js +1 -0
- package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +3 -0
- package/web/dist/assets/preview-state-vuMviq6q.js +1 -0
- package/web/dist/assets/prompt-templates-Cubdut_C.js +15 -0
- package/web/dist/assets/repo-git-Cf4t5PD0.js +1 -0
- package/web/dist/assets/{run-diff-5-RRdcVz.js → run-diff-BpkUW37Z.js} +3 -3
- package/web/dist/assets/run-header-68WHf9wV.js +1 -0
- package/web/dist/assets/skills-DiVIgKeU.js +1 -0
- package/web/dist/assets/square-terminal-B6RYYieR.js +1 -0
- package/web/dist/assets/status-dot-CqTJHSP9.js +1 -0
- package/web/dist/assets/{tab-link-K0hjzEXR.js → tab-link-B07Hu4QX.js} +1 -1
- package/web/dist/assets/task-changes--lQF6EYf.js +1 -0
- package/web/dist/assets/task-commits-Cx0qy4DN.js +1 -0
- package/web/dist/assets/task-files-D50s-gKW.js +2 -0
- package/web/dist/assets/task-github-item-DaqmSwYT.js +1 -0
- package/web/dist/assets/task-thread-BPQ64lui.js +7 -0
- package/web/dist/assets/{task-thread-Bw_nQuTx.css → task-thread-zNE2Q9E-.css} +1 -1
- package/web/dist/assets/textarea-D20O4Pw0.js +1 -0
- package/web/dist/assets/thread-state-DWcFMrvb.js +4 -0
- package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +1 -0
- package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +1 -0
- package/web/dist/assets/{utils-fzNfbPzA.js → utils-JsL3ZsJf.js} +1 -1
- package/web/dist/assets/workflows-jwG7uxqi.js +11 -0
- package/web/dist/assets/{zoomable-image-CDmZ_Rtl.js → zoomable-image-BJJ64KGc.js} +1 -1
- package/web/dist/cezarion-lockup-dark.svg +1 -13
- package/web/dist/cezarion-lockup-light.svg +1 -13
- package/web/dist/cezarion-mark-dark.svg +1 -12
- package/web/dist/cezarion-mark-light.svg +1 -12
- package/web/dist/index.html +16 -21
- package/web/dist/assets/agent-accounts-CU2ZDv24.js +0 -1
- package/web/dist/assets/alert-dialog-C-xYE_dg.js +0 -43
- package/web/dist/assets/chevron-right-BENZ-1ZN.js +0 -1
- package/web/dist/assets/collapsible-DD9J91Iv.js +0 -1
- package/web/dist/assets/compare-variants-5FmqpFSN.js +0 -1
- package/web/dist/assets/diff-controls-Bj0YrioE.js +0 -1
- package/web/dist/assets/diff-rE8rm75J2.js +0 -3
- package/web/dist/assets/diff-stat-BKDCSVfJ.js +0 -1
- package/web/dist/assets/dropdown-menu-Ch5BYQh_.js +0 -1
- package/web/dist/assets/format-dBiNDNVe.js +0 -1
- package/web/dist/assets/git-toolbar-ClVvf5iF.js +0 -1
- package/web/dist/assets/github-BkNOyour.css +0 -1
- package/web/dist/assets/github-DeZMeMwJ.js +0 -1
- package/web/dist/assets/github-item-detail-W5F6GVjF.js +0 -1
- package/web/dist/assets/github-sidebar-model-DcMu-IK2.js +0 -1
- package/web/dist/assets/highlighter-CdHqIwFr.js +0 -3
- package/web/dist/assets/index-CatemBEL.js +0 -8
- package/web/dist/assets/index-MvfD8bZJ.css +0 -1
- package/web/dist/assets/markdown-zdKeLIYA.js +0 -2
- package/web/dist/assets/mobile-run-bar-BCfmFi8V.js +0 -1
- package/web/dist/assets/prompt-templates-CBPp39IU.js +0 -15
- package/web/dist/assets/reference-chip-D6owncJT.js +0 -1
- package/web/dist/assets/repo-git-CtdkZFey.js +0 -1
- package/web/dist/assets/run-header-BcjoDi0W.js +0 -1
- package/web/dist/assets/sheet-ChnYVrp1.js +0 -1
- package/web/dist/assets/skills-C8YKp2vr.js +0 -1
- package/web/dist/assets/task-changes-BW8YdWG8.js +0 -1
- package/web/dist/assets/task-commits-q2kB2y6L.js +0 -1
- package/web/dist/assets/task-files-pPL6_3Cr.js +0 -2
- package/web/dist/assets/task-github-item-BXF5R52U.js +0 -1
- package/web/dist/assets/task-thread-DtkFL3EC.js +0 -10
- package/web/dist/assets/textarea-DxlkE-Yd.js +0 -1
- package/web/dist/assets/token-metrics-DIAjrPlM.js +0 -1
- package/web/dist/assets/use-desktop-CrDny2uG.js +0 -1
- package/web/dist/assets/use-submit-shortcut-KWJsC906.js +0 -1
- package/web/dist/assets/workflows-TmcPEj2K.js +0 -11
- /package/web/dist/assets/{lib-BQXq3kEf2.js → lib-BQXq3kEf.js} +0 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import { accessSync, closeSync, constants, existsSync, openSync, readSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
/**
|
|
5
|
+
* The transactional run store underneath `RunStore` (#779, plan step 2).
|
|
6
|
+
*
|
|
7
|
+
* One SQLite file per project, `<dataDir>/runs.db`, holding every run as a complete JSON record
|
|
8
|
+
* plus the handful of columns the store has to query without decoding that record. The module is
|
|
9
|
+
* deliberately schema-agnostic: it never imports `RunRecord`, never parses `data` or `summary`,
|
|
10
|
+
* and never decides what a column's value should be. The caller (RunStore's codec) computes them
|
|
11
|
+
* and hands over strings, so the record schema can change without this file noticing.
|
|
12
|
+
*
|
|
13
|
+
* Everything is synchronous, like the store it serves. Writes go through ONE entry point,
|
|
14
|
+
* `transaction()`, which commits a whole change set or nothing — the property `runs.json`'s
|
|
15
|
+
* debounced whole-file rewrite only approximated.
|
|
16
|
+
*
|
|
17
|
+
* Failure policy: every SQLite error leaves as a typed `RunDatabaseError` (`toRunDatabaseError`),
|
|
18
|
+
* and nothing here ever deletes, truncates or "repairs" a file. A corrupt database, WAL or SHM
|
|
19
|
+
* stays exactly where it is for the user (or a later recovery step) to inspect.
|
|
20
|
+
*/
|
|
21
|
+
export const RUNS_DB_FILE = 'runs.db';
|
|
22
|
+
/** `meta` key whose presence means the legacy `runs.json` import committed (or there was nothing
|
|
23
|
+
* to import): from then on this database is authoritative and `runs.json` is history. Written in
|
|
24
|
+
* the same transaction as the imported rows, so it can never describe a half-finished import. */
|
|
25
|
+
export const RUNS_IMPORT_COMPLETE_KEY = 'import-complete';
|
|
26
|
+
/** `PRAGMA user_version` of a database this build created. A higher number means a newer cezar
|
|
27
|
+
* wrote it; that is refused rather than read, because a newer schema may store what this one
|
|
28
|
+
* would silently drop on its next write. */
|
|
29
|
+
export const RUN_DATABASE_SCHEMA_VERSION = 2;
|
|
30
|
+
/**
|
|
31
|
+
* The oldest `user_version` the READ-ONLY reader can read as it stands (`openReadOnly`). It never
|
|
32
|
+
* migrates, so a project this build has not opened yet is still at the version an earlier build
|
|
33
|
+
* left; while every migration since only adds what readers can do without (indexes), that
|
|
34
|
+
* database stays readable rather than reading as empty. Raise it with any migration that changes
|
|
35
|
+
* a table or column a reader queries.
|
|
36
|
+
*/
|
|
37
|
+
export const RUN_DATABASE_READABLE_FROM = 1;
|
|
38
|
+
/** How long a statement waits on another connection's lock before failing as busy. Short on
|
|
39
|
+
* purpose: the store runs on the event loop, and a blocked write is a blocked cockpit. */
|
|
40
|
+
const BUSY_TIMEOUT_MS = 50;
|
|
41
|
+
/**
|
|
42
|
+
* Forward-only migrations: entry `i` takes a database from `user_version` i to i + 1. Never edit
|
|
43
|
+
* a shipped entry; append a new one and raise `RUN_DATABASE_SCHEMA_VERSION`.
|
|
44
|
+
*
|
|
45
|
+
* Changing `isLiveRecord` (run-row.ts) needs an entry that recomputes the `live` column for every
|
|
46
|
+
* row. That predicate is TypeScript over the decoded record, so such an entry cannot be SQL alone.
|
|
47
|
+
*/
|
|
48
|
+
const MIGRATIONS = [
|
|
49
|
+
`
|
|
50
|
+
-- Every column beyond id/data/summary/revision exists for a query the spec names. They are
|
|
51
|
+
-- copies of record fields (or of one predicate over the record), computed by the caller in the
|
|
52
|
+
-- same upsert as data and summary, so a row can never disagree with its own columns.
|
|
53
|
+
CREATE TABLE runs (
|
|
54
|
+
-- Insertion order, the rowid itself: given by a row's first insert (runs.json order on import,
|
|
55
|
+
-- then the order runs are first written) and kept by every upsert. Runs created in the same
|
|
56
|
+
-- millisecond list in this order, as they did when every list was a stable sort of the
|
|
57
|
+
-- in-memory map: recovery and the queue start tied runs in it.
|
|
58
|
+
seq INTEGER PRIMARY KEY,
|
|
59
|
+
id TEXT NOT NULL UNIQUE,
|
|
60
|
+
-- Newest-first order: GET /run-summaries, /workspace/runs-index, the cold newest-200 read,
|
|
61
|
+
-- and count-based history retention (the runs past the newest N).
|
|
62
|
+
created_at TEXT NOT NULL,
|
|
63
|
+
-- Worktree retention: most recently finished first (falls back to created_at).
|
|
64
|
+
finished_at TEXT,
|
|
65
|
+
-- The mark-all-read and archive-finished sweeps select finished statuses.
|
|
66
|
+
status TEXT NOT NULL,
|
|
67
|
+
-- History retention, mark-all-read and archive-finished skip or select archived runs (0/1).
|
|
68
|
+
archived INTEGER NOT NULL,
|
|
69
|
+
-- isLiveRecord() (run-row.ts): the rows RunStore.open loads into memory and recovers. Every
|
|
70
|
+
-- other row stays here until something reads it. Written with the row, never recomputed
|
|
71
|
+
-- (see the note above MIGRATIONS).
|
|
72
|
+
live INTEGER NOT NULL,
|
|
73
|
+
-- The workers of a parent (delegation.parentRunId): delegation families, archive cascades and
|
|
74
|
+
-- the "every worker" recovery passes, replacing whole-map scans.
|
|
75
|
+
parent_run_id TEXT,
|
|
76
|
+
-- The run an idempotent start already created (#504).
|
|
77
|
+
client_request_id TEXT,
|
|
78
|
+
-- The members of a parallel-variant group (spec 010).
|
|
79
|
+
group_id TEXT,
|
|
80
|
+
-- A materialized worktree directory (the path, unless retention reclaimed it): worktree
|
|
81
|
+
-- retention, the worktrees panel and the owned-worker resource collision check.
|
|
82
|
+
worktree_path TEXT,
|
|
83
|
+
-- The branch the run owns: branch cleanup, git-log attribution and the collision check.
|
|
84
|
+
branch TEXT,
|
|
85
|
+
-- Where that branch forked, which branch cleanup reads with it.
|
|
86
|
+
base_branch TEXT,
|
|
87
|
+
-- The commit that last wrote the row: one database-wide sequence (meta 'commit-seq'), so a
|
|
88
|
+
-- revision is never reused, not even by a row deleted and created again under the same id.
|
|
89
|
+
-- A writer compares it with the revision it last read to tell whether the row changed under it.
|
|
90
|
+
revision INTEGER NOT NULL,
|
|
91
|
+
-- The complete record JSON, opaque here.
|
|
92
|
+
data TEXT NOT NULL,
|
|
93
|
+
-- The record's toRunSummary() JSON, so list routes never decode data.
|
|
94
|
+
summary TEXT NOT NULL
|
|
95
|
+
) STRICT;
|
|
96
|
+
-- Newest first, ties in insertion order: the order every newest-first read asks for, so SQLite
|
|
97
|
+
-- walks the index instead of sorting the tie groups.
|
|
98
|
+
CREATE INDEX runs_created_at ON runs (created_at DESC, seq);
|
|
99
|
+
CREATE INDEX runs_status ON runs (status);
|
|
100
|
+
-- Partial indexes: each query asks for the rows that HAVE the value, and most rows do not.
|
|
101
|
+
CREATE INDEX runs_live ON runs (live) WHERE live = 1;
|
|
102
|
+
CREATE INDEX runs_parent_run_id ON runs (parent_run_id) WHERE parent_run_id IS NOT NULL;
|
|
103
|
+
CREATE INDEX runs_client_request_id ON runs (client_request_id) WHERE client_request_id IS NOT NULL;
|
|
104
|
+
CREATE INDEX runs_group_id ON runs (group_id) WHERE group_id IS NOT NULL;
|
|
105
|
+
CREATE INDEX runs_worktree_path ON runs (worktree_path) WHERE worktree_path IS NOT NULL;
|
|
106
|
+
CREATE INDEX runs_branch ON runs (branch) WHERE branch IS NOT NULL;
|
|
107
|
+
-- Private key/value metadata: import completion and the two sequences below.
|
|
108
|
+
CREATE TABLE meta (
|
|
109
|
+
key TEXT PRIMARY KEY,
|
|
110
|
+
value TEXT NOT NULL
|
|
111
|
+
) STRICT;
|
|
112
|
+
-- Cross-process ownership (#779, plan step 3): which RunStore may write a delegation family —
|
|
113
|
+
-- a root and its direct workers, or an ordinary run on its own. Only the holder writes the
|
|
114
|
+
-- family's rows, its NDJSON and its cleanup; everyone else reads them.
|
|
115
|
+
CREATE TABLE run_claims (
|
|
116
|
+
-- The family's root id: parent_run_id for a worker, the run's own id otherwise.
|
|
117
|
+
family TEXT PRIMARY KEY,
|
|
118
|
+
-- The RunStore holding it: a random id per open, so two stores in one process differ.
|
|
119
|
+
session TEXT NOT NULL,
|
|
120
|
+
-- That store's process and the process's start identity, so a reused pid is never taken for
|
|
121
|
+
-- the owner. start_token is NULL only where the platform cannot read one.
|
|
122
|
+
pid INTEGER NOT NULL,
|
|
123
|
+
start_token TEXT,
|
|
124
|
+
-- From meta 'claim-generation', bumped by every acquisition. Every write checks that its
|
|
125
|
+
-- family still carries this store's session and generation.
|
|
126
|
+
generation INTEGER NOT NULL
|
|
127
|
+
) STRICT;
|
|
128
|
+
CREATE INDEX run_claims_session ON run_claims (session);
|
|
129
|
+
-- Private recovery evidence: a write that found its row changed, created or deleted under it,
|
|
130
|
+
-- or its family's claim gone. Never served; kept for a person to inspect.
|
|
131
|
+
CREATE TABLE run_conflicts (
|
|
132
|
+
seq INTEGER PRIMARY KEY,
|
|
133
|
+
run_id TEXT NOT NULL,
|
|
134
|
+
detected_at TEXT NOT NULL,
|
|
135
|
+
session TEXT NOT NULL,
|
|
136
|
+
-- changed | created | deleted | claim-lost
|
|
137
|
+
reason TEXT NOT NULL,
|
|
138
|
+
-- The row as the writer last read or wrote it (the original); NULL for a row it created.
|
|
139
|
+
base_revision INTEGER,
|
|
140
|
+
base_data TEXT,
|
|
141
|
+
-- What the writer meant to store; NULL with local_deleted = 1 when it meant to delete it.
|
|
142
|
+
local_data TEXT,
|
|
143
|
+
local_deleted INTEGER NOT NULL,
|
|
144
|
+
-- The row as it stands; NULL when another writer deleted it.
|
|
145
|
+
current_revision INTEGER,
|
|
146
|
+
current_data TEXT
|
|
147
|
+
) STRICT;
|
|
148
|
+
`,
|
|
149
|
+
`
|
|
150
|
+
-- The run lists' window (#864): every unarchived run, plus the newest archived roots. Without
|
|
151
|
+
-- these, each half scans the whole table — about 30 ms at 20,000 runs, on every list read.
|
|
152
|
+
-- Indexes only: nothing a v1 reader reads changes, which is why RUN_DATABASE_READABLE_FROM stays 1.
|
|
153
|
+
CREATE INDEX runs_unarchived ON runs (created_at DESC, seq) WHERE archived = 0;
|
|
154
|
+
CREATE INDEX runs_archived_roots ON runs (archived, created_at DESC, seq) WHERE parent_run_id IS NULL;
|
|
155
|
+
`,
|
|
156
|
+
];
|
|
157
|
+
/** Every failure this module reports. `kind` is the discriminant; the subclasses exist so a
|
|
158
|
+
* caller can also `instanceof` the one case it handles. */
|
|
159
|
+
export class RunDatabaseError extends Error {
|
|
160
|
+
kind;
|
|
161
|
+
/** SQLite's primary result code, when the failure came from SQLite. */
|
|
162
|
+
sqliteCode;
|
|
163
|
+
constructor(kind, message, options = {}) {
|
|
164
|
+
super(message, { cause: options.cause });
|
|
165
|
+
this.name = new.target.name;
|
|
166
|
+
this.kind = kind;
|
|
167
|
+
this.sqliteCode = options.sqliteCode;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Another connection holds the lock past the busy timeout. Transient: retry later. */
|
|
171
|
+
export class RunDatabaseBusyError extends RunDatabaseError {
|
|
172
|
+
}
|
|
173
|
+
/** The file or its directory cannot be opened or written by this user. */
|
|
174
|
+
export class RunDatabasePermissionError extends RunDatabaseError {
|
|
175
|
+
}
|
|
176
|
+
/** The disk (or a page-count limit) has no room for the write. */
|
|
177
|
+
export class RunDatabaseDiskFullError extends RunDatabaseError {
|
|
178
|
+
}
|
|
179
|
+
/** The file is not a database, or a page is malformed. Never reset by this module. */
|
|
180
|
+
export class RunDatabaseCorruptError extends RunDatabaseError {
|
|
181
|
+
}
|
|
182
|
+
/** The database was written by a newer cezar whose schema this build does not know. */
|
|
183
|
+
export class RunDatabaseUnsupportedSchemaError extends RunDatabaseError {
|
|
184
|
+
found;
|
|
185
|
+
supported;
|
|
186
|
+
constructor(found, supported) {
|
|
187
|
+
super('unsupported-schema', `runs database schema ${found} is newer than this cezar supports (${supported})`);
|
|
188
|
+
this.found = found;
|
|
189
|
+
this.supported = supported;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** A fenced write found rows changed, created or deleted under it, or its claim gone. Nothing was
|
|
193
|
+
* written. `conflicts` names every such row of the change set, each with its current row. */
|
|
194
|
+
export class RunConflictError extends RunDatabaseError {
|
|
195
|
+
conflicts;
|
|
196
|
+
constructor(conflicts) {
|
|
197
|
+
super('conflict', `runs database: ${conflicts.map((c) => `${c.id} (${c.reason})`).join(', ')} changed under this writer`);
|
|
198
|
+
this.conflicts = conflicts;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// SQLite primary result codes (https://sqlite.org/rescode.html). node:sqlite reports the
|
|
202
|
+
// EXTENDED code in `errcode`; its low byte is the primary code.
|
|
203
|
+
const SQLITE_PERM = 3;
|
|
204
|
+
const SQLITE_BUSY = 5;
|
|
205
|
+
const SQLITE_LOCKED = 6;
|
|
206
|
+
const SQLITE_READONLY = 8;
|
|
207
|
+
const SQLITE_CORRUPT = 11;
|
|
208
|
+
const SQLITE_FULL = 13;
|
|
209
|
+
const SQLITE_CANTOPEN = 14;
|
|
210
|
+
const SQLITE_AUTH = 23;
|
|
211
|
+
const SQLITE_NOTADB = 26;
|
|
212
|
+
/**
|
|
213
|
+
* Map a node:sqlite error (`code: 'ERR_SQLITE_ERROR'`, numeric `errcode`) to its typed class.
|
|
214
|
+
* Anything that is not a SQLite error — a closed connection, a bad argument — passes through
|
|
215
|
+
* unchanged, because wrapping it would claim a database failure that did not happen.
|
|
216
|
+
*
|
|
217
|
+
* `path`, when given, lets SQLITE_CANTOPEN be told apart: it is a permission problem only when
|
|
218
|
+
* the directory, or the file itself, refuses this user. A missing directory, or a directory where
|
|
219
|
+
* the file should be, is not — and "check your permissions" would send the user the wrong way.
|
|
220
|
+
*/
|
|
221
|
+
export function toRunDatabaseError(error, path) {
|
|
222
|
+
if (error instanceof RunDatabaseError)
|
|
223
|
+
return error;
|
|
224
|
+
if (!(error instanceof Error) || error.code !== 'ERR_SQLITE_ERROR')
|
|
225
|
+
return error;
|
|
226
|
+
const errcode = error.errcode;
|
|
227
|
+
if (typeof errcode !== 'number')
|
|
228
|
+
return error;
|
|
229
|
+
const primary = errcode & 0xff;
|
|
230
|
+
const options = { sqliteCode: primary, cause: error };
|
|
231
|
+
const message = `runs database: ${error.message}`;
|
|
232
|
+
switch (primary) {
|
|
233
|
+
case SQLITE_BUSY:
|
|
234
|
+
case SQLITE_LOCKED:
|
|
235
|
+
return new RunDatabaseBusyError('busy', message, options);
|
|
236
|
+
case SQLITE_PERM:
|
|
237
|
+
case SQLITE_READONLY:
|
|
238
|
+
case SQLITE_AUTH:
|
|
239
|
+
return new RunDatabasePermissionError('permission', message, options);
|
|
240
|
+
case SQLITE_CANTOPEN:
|
|
241
|
+
return path !== undefined && accessDenied(path)
|
|
242
|
+
? new RunDatabasePermissionError('permission', message, options)
|
|
243
|
+
: new RunDatabaseError('other', message, options);
|
|
244
|
+
case SQLITE_FULL:
|
|
245
|
+
return new RunDatabaseDiskFullError('disk-full', message, options);
|
|
246
|
+
case SQLITE_CORRUPT:
|
|
247
|
+
case SQLITE_NOTADB:
|
|
248
|
+
return new RunDatabaseCorruptError('corrupt', message, options);
|
|
249
|
+
default:
|
|
250
|
+
return new RunDatabaseError('other', message, options);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Whether this user is refused read-write access to the database file, or (when the file does
|
|
254
|
+
* not exist yet) to the directory that would hold it. */
|
|
255
|
+
function accessDenied(path) {
|
|
256
|
+
const target = existsSync(path) ? path : dirname(path);
|
|
257
|
+
if (!existsSync(target))
|
|
258
|
+
return false;
|
|
259
|
+
try {
|
|
260
|
+
accessSync(target, constants.R_OK | constants.W_OK);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** The 16 bytes every SQLite database file starts with (https://sqlite.org/fileformat.html). */
|
|
268
|
+
const SQLITE_HEADER = Buffer.from('SQLite format 3\0', 'latin1');
|
|
269
|
+
/**
|
|
270
|
+
* Refuse a non-empty file that does not start with the SQLite header before SQLite sees it.
|
|
271
|
+
* SQLite would also report it as not a database, but closing that connection deletes the `-wal`
|
|
272
|
+
* and `-shm` files beside it — and those may be the only intact copy of recent commits. An empty
|
|
273
|
+
* file is a valid new database. Anything that is not a regular file is left to SQLite to refuse.
|
|
274
|
+
*/
|
|
275
|
+
function assertDatabaseHeader(path) {
|
|
276
|
+
if (!statSync(path, { throwIfNoEntry: false })?.isFile())
|
|
277
|
+
return;
|
|
278
|
+
const fd = openSync(path, 'r');
|
|
279
|
+
let header;
|
|
280
|
+
try {
|
|
281
|
+
header = Buffer.alloc(SQLITE_HEADER.length);
|
|
282
|
+
header = header.subarray(0, readSync(fd, header, 0, header.length, 0));
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
closeSync(fd);
|
|
286
|
+
}
|
|
287
|
+
if (header.length === 0 || header.equals(SQLITE_HEADER))
|
|
288
|
+
return;
|
|
289
|
+
throw new RunDatabaseCorruptError('corrupt', `runs database: ${path} is not a database`, { sqliteCode: SQLITE_NOTADB });
|
|
290
|
+
}
|
|
291
|
+
const ROW_COLUMNS = 'seq, id, created_at, finished_at, status, archived, live, parent_run_id, client_request_id, group_id, worktree_path, branch, base_branch, revision, data, summary';
|
|
292
|
+
/** Newest first, runs created in the same millisecond in insertion order (`seq`): the one order
|
|
293
|
+
* every list read uses. */
|
|
294
|
+
const NEWEST_FIRST = 'ORDER BY created_at DESC, seq';
|
|
295
|
+
/** The columns a list row reads, without the record. */
|
|
296
|
+
const SUMMARY_SELECT = 'SELECT seq, id, created_at, revision, summary FROM runs';
|
|
297
|
+
/** An archived run that is not an owned worker: what the run lists' archived window counts (#864). */
|
|
298
|
+
export const ARCHIVED_ROOT = 'archived = 1 AND parent_run_id IS NULL';
|
|
299
|
+
function toSummaryRow(row) {
|
|
300
|
+
return {
|
|
301
|
+
seq: row.seq,
|
|
302
|
+
id: row.id,
|
|
303
|
+
createdAt: row.created_at,
|
|
304
|
+
revision: row.revision,
|
|
305
|
+
summary: row.summary,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function toRunRow(row) {
|
|
309
|
+
return {
|
|
310
|
+
seq: row.seq,
|
|
311
|
+
id: row.id,
|
|
312
|
+
createdAt: row.created_at,
|
|
313
|
+
finishedAt: row.finished_at,
|
|
314
|
+
status: row.status,
|
|
315
|
+
archived: row.archived === 1,
|
|
316
|
+
live: row.live === 1,
|
|
317
|
+
parentRunId: row.parent_run_id,
|
|
318
|
+
clientRequestId: row.client_request_id,
|
|
319
|
+
groupId: row.group_id,
|
|
320
|
+
worktreePath: row.worktree_path,
|
|
321
|
+
branch: row.branch,
|
|
322
|
+
baseBranch: row.base_branch,
|
|
323
|
+
revision: row.revision,
|
|
324
|
+
data: row.data,
|
|
325
|
+
summary: row.summary,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function validateChanges(changes) {
|
|
329
|
+
const upserted = new Set();
|
|
330
|
+
for (const row of changes.upserts) {
|
|
331
|
+
if (upserted.has(row.id))
|
|
332
|
+
throw new TypeError(`run ${row.id} is upserted twice in one transaction`);
|
|
333
|
+
upserted.add(row.id);
|
|
334
|
+
}
|
|
335
|
+
for (const id of changes.deletes) {
|
|
336
|
+
if (upserted.has(id))
|
|
337
|
+
throw new TypeError(`run ${id} is both upserted and deleted in one transaction`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** `meta` key of the database-wide commit sequence every upsert stamps as its row's revision. */
|
|
341
|
+
const COMMIT_SEQ_KEY = 'commit-seq';
|
|
342
|
+
/** `meta` key of the sequence every claim acquisition takes its generation from. */
|
|
343
|
+
const CLAIM_GENERATION_KEY = 'claim-generation';
|
|
344
|
+
const CLAIM_COLUMNS = 'family, session, pid, start_token, generation';
|
|
345
|
+
function toRunClaim(row) {
|
|
346
|
+
return {
|
|
347
|
+
family: row.family,
|
|
348
|
+
session: row.session,
|
|
349
|
+
pid: row.pid,
|
|
350
|
+
startToken: row.start_token,
|
|
351
|
+
generation: row.generation,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
export class RunDatabase {
|
|
355
|
+
db;
|
|
356
|
+
path;
|
|
357
|
+
/** Set by the first statement that reports the file damaged (SQLITE_CORRUPT or NOTADB). */
|
|
358
|
+
damaged = false;
|
|
359
|
+
statements;
|
|
360
|
+
constructor(db, path) {
|
|
361
|
+
this.db = db;
|
|
362
|
+
this.path = path;
|
|
363
|
+
this.statements = {
|
|
364
|
+
get: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE id = ?`),
|
|
365
|
+
getMany: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE id IN (SELECT value FROM json_each(?))`),
|
|
366
|
+
listSummaries: db.prepare(`SELECT seq, id, created_at, revision, summary FROM runs ${NEWEST_FIRST} LIMIT ?`),
|
|
367
|
+
getSummaries: db.prepare(`${SUMMARY_SELECT} WHERE id IN (SELECT value FROM json_each(?))`),
|
|
368
|
+
listWindowSummaries: db.prepare(`${SUMMARY_SELECT} WHERE archived = 0 UNION ALL SELECT * FROM (${SUMMARY_SELECT} WHERE ${ARCHIVED_ROOT} ${NEWEST_FIRST} LIMIT ?) ${NEWEST_FIRST}`),
|
|
369
|
+
listRootWindowSummaries: db.prepare(`${SUMMARY_SELECT} WHERE archived = 0 AND parent_run_id IS NULL UNION ALL SELECT * FROM (${SUMMARY_SELECT} WHERE ${ARCHIVED_ROOT} ${NEWEST_FIRST} LIMIT ?) ${NEWEST_FIRST}`),
|
|
370
|
+
listAll: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs ${NEWEST_FIRST}`),
|
|
371
|
+
listLive: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE live = 1 ORDER BY seq`),
|
|
372
|
+
listByParent: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE parent_run_id = ? ${NEWEST_FIRST}`),
|
|
373
|
+
listWorkerIds: db.prepare('SELECT id FROM runs WHERE parent_run_id IS NOT NULL'),
|
|
374
|
+
listIdsByParent: db.prepare('SELECT id FROM runs WHERE parent_run_id = ?'),
|
|
375
|
+
findByClientRequestId: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE client_request_id = ? LIMIT 1`),
|
|
376
|
+
listByGroup: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE group_id = ?`),
|
|
377
|
+
listWithWorktree: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE worktree_path IS NOT NULL ORDER BY coalesce(finished_at, created_at) DESC, created_at DESC, seq`),
|
|
378
|
+
findResourceHolders: db.prepare('SELECT id FROM runs WHERE branch = :branch UNION SELECT id FROM runs WHERE worktree_path = :worktreePath'),
|
|
379
|
+
listIds: db.prepare('SELECT id FROM runs'),
|
|
380
|
+
has: db.prepare('SELECT 1 FROM runs WHERE id = ?'),
|
|
381
|
+
listRevisions: db.prepare('SELECT id, revision FROM runs ORDER BY id'),
|
|
382
|
+
getMeta: db.prepare('SELECT value FROM meta WHERE key = ?'),
|
|
383
|
+
upsert: db.prepare(`
|
|
384
|
+
INSERT INTO runs (id, created_at, finished_at, status, archived, live, parent_run_id, client_request_id, group_id,
|
|
385
|
+
worktree_path, branch, base_branch, revision, data, summary)
|
|
386
|
+
VALUES (:id, :createdAt, :finishedAt, :status, :archived, :live, :parentRunId, :clientRequestId, :groupId,
|
|
387
|
+
:worktreePath, :branch, :baseBranch, :revision, :data, :summary)
|
|
388
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
389
|
+
created_at = excluded.created_at, finished_at = excluded.finished_at, status = excluded.status,
|
|
390
|
+
archived = excluded.archived, live = excluded.live, parent_run_id = excluded.parent_run_id,
|
|
391
|
+
client_request_id = excluded.client_request_id, group_id = excluded.group_id,
|
|
392
|
+
worktree_path = excluded.worktree_path, branch = excluded.branch, base_branch = excluded.base_branch,
|
|
393
|
+
revision = excluded.revision, data = excluded.data, summary = excluded.summary
|
|
394
|
+
RETURNING seq`),
|
|
395
|
+
delete: db.prepare('DELETE FROM runs WHERE id = ?'),
|
|
396
|
+
setMeta: db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value'),
|
|
397
|
+
deleteMeta: db.prepare('DELETE FROM meta WHERE key = ?'),
|
|
398
|
+
nextSequence: db.prepare(`INSERT INTO meta (key, value) VALUES (?, '1')
|
|
399
|
+
ON CONFLICT (key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) RETURNING value`),
|
|
400
|
+
familyOf: db.prepare('SELECT coalesce(parent_run_id, id) AS family FROM runs WHERE id = ?'),
|
|
401
|
+
familyHasLive: db.prepare('SELECT 1 FROM runs WHERE live = 1 AND (id = :family OR parent_run_id = :family) LIMIT 1'),
|
|
402
|
+
getClaim: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims WHERE family = ?`),
|
|
403
|
+
getClaims: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims WHERE family IN (SELECT value FROM json_each(?))`),
|
|
404
|
+
listClaims: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims`),
|
|
405
|
+
putClaim: db.prepare(`INSERT INTO run_claims (family, session, pid, start_token, generation)
|
|
406
|
+
VALUES (:family, :session, :pid, :startToken, :generation)
|
|
407
|
+
ON CONFLICT (family) DO UPDATE SET session = excluded.session, pid = excluded.pid,
|
|
408
|
+
start_token = excluded.start_token, generation = excluded.generation`),
|
|
409
|
+
releaseClaims: db.prepare('DELETE FROM run_claims WHERE session = ? AND family IN (SELECT value FROM json_each(?))'),
|
|
410
|
+
releaseSessionClaims: db.prepare('DELETE FROM run_claims WHERE session = ?'),
|
|
411
|
+
listForeignClaimedIds: db.prepare(`SELECT runs.id AS id, run_claims.family AS family FROM run_claims JOIN runs
|
|
412
|
+
ON runs.id = run_claims.family OR runs.parent_run_id = run_claims.family WHERE run_claims.session <> ?`),
|
|
413
|
+
insertConflict: db.prepare(`INSERT INTO run_conflicts (run_id, detected_at, session, reason, base_revision, base_data,
|
|
414
|
+
local_data, local_deleted, current_revision, current_data)
|
|
415
|
+
VALUES (:runId, :detectedAt, :session, :reason, :baseRevision, :baseData, :localData, :localDeleted,
|
|
416
|
+
:currentRevision, :currentData) RETURNING seq`),
|
|
417
|
+
listConflicts: db.prepare('SELECT * FROM run_conflicts ORDER BY seq'),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Open (creating if absent) the database at `path` and bring its schema up to date. The
|
|
422
|
+
* directory must already exist; creating it is the caller's decision. On any failure the
|
|
423
|
+
* connection is closed and no existing data is deleted or rewritten — but a fresh path may be
|
|
424
|
+
* left holding an empty database (and its WAL), which the next open migrates. A reader that must
|
|
425
|
+
* create nothing uses `openReadOnly`.
|
|
426
|
+
*/
|
|
427
|
+
static open(path) {
|
|
428
|
+
let db;
|
|
429
|
+
try {
|
|
430
|
+
assertDatabaseHeader(path);
|
|
431
|
+
db = new DatabaseSync(path);
|
|
432
|
+
// busy_timeout first, so every later statement here (including the migration) waits on a
|
|
433
|
+
// concurrent opener instead of failing at once.
|
|
434
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
435
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
436
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
437
|
+
migrate(db);
|
|
438
|
+
return new RunDatabase(db, path);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
const typed = toRunDatabaseError(error, path);
|
|
442
|
+
if (db && typed instanceof RunDatabaseCorruptError)
|
|
443
|
+
closeLeavingFiles(db, path);
|
|
444
|
+
else if (db?.isOpen)
|
|
445
|
+
db.close();
|
|
446
|
+
throw typed;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Open an existing database for reading only: never creates the file, never migrates it, never
|
|
451
|
+
* changes journal mode. Answers `null` when there is nothing this build can read yet — no file,
|
|
452
|
+
* or a file below `RUN_DATABASE_READABLE_FROM` (schema 0 is what a failed first open leaves) —
|
|
453
|
+
* so the caller can fall back to whatever it read before the database existed. A database from
|
|
454
|
+
* `RUN_DATABASE_READABLE_FROM` up to this build's schema is read as it stands. A newer schema is
|
|
455
|
+
* still refused.
|
|
456
|
+
*
|
|
457
|
+
* SQLite may create the `-wal`/`-shm` coordination files beside the database; that is the price
|
|
458
|
+
* of seeing a live writer's commits. In a read-only directory with neither file present it
|
|
459
|
+
* cannot, and the open fails as permission denied — deliberately not worked around with
|
|
460
|
+
* `immutable=1`, which would read a stale snapshot without saying so.
|
|
461
|
+
*/
|
|
462
|
+
static openReadOnly(path) {
|
|
463
|
+
if (!existsSync(path))
|
|
464
|
+
return null;
|
|
465
|
+
let db;
|
|
466
|
+
try {
|
|
467
|
+
assertDatabaseHeader(path);
|
|
468
|
+
db = new DatabaseSync(path, { readOnly: true });
|
|
469
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
470
|
+
const found = readSchemaVersion(db);
|
|
471
|
+
if (found > RUN_DATABASE_SCHEMA_VERSION)
|
|
472
|
+
throw new RunDatabaseUnsupportedSchemaError(found, RUN_DATABASE_SCHEMA_VERSION);
|
|
473
|
+
if (found < RUN_DATABASE_READABLE_FROM) {
|
|
474
|
+
db.close();
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
return new RunDatabase(db, path);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
if (db?.isOpen)
|
|
481
|
+
db.close();
|
|
482
|
+
throw toRunDatabaseError(error, path);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
get(id) {
|
|
486
|
+
const row = this.run(() => this.statements.get.get(id));
|
|
487
|
+
return row ? toRunRow(row) : undefined;
|
|
488
|
+
}
|
|
489
|
+
/** The rows for `ids` that exist, in no particular order. */
|
|
490
|
+
getMany(ids) {
|
|
491
|
+
if (ids.length === 0)
|
|
492
|
+
return [];
|
|
493
|
+
return this.run(() => this.statements.getMany.all(JSON.stringify(ids))).map(toRunRow);
|
|
494
|
+
}
|
|
495
|
+
/** Summaries newest first (`created_at` descending, then insertion order). Archived runs are included,
|
|
496
|
+
* as every list route includes them. Without a limit, every row. */
|
|
497
|
+
listSummaries(options = {}) {
|
|
498
|
+
const limit = options.limit ?? -1;
|
|
499
|
+
return this.run(() => this.statements.listSummaries.all(limit)).map(toSummaryRow);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The run lists' window (#864), newest first: every unarchived row, plus the newest
|
|
503
|
+
* `archivedLimit` archived roots. Archived workers are never in it — no list renders them, and
|
|
504
|
+
* counting them is what let them push an older live root out of a newest-N window.
|
|
505
|
+
*/
|
|
506
|
+
listWindowSummaries(archivedLimit, options = {}) {
|
|
507
|
+
const statement = options.roots ? this.statements.listRootWindowSummaries : this.statements.listWindowSummaries;
|
|
508
|
+
return this.run(() => statement.all(archivedLimit)).map(toSummaryRow);
|
|
509
|
+
}
|
|
510
|
+
/** The summary rows of `ids` that exist, in no particular order. */
|
|
511
|
+
getSummaries(ids) {
|
|
512
|
+
if (ids.length === 0)
|
|
513
|
+
return [];
|
|
514
|
+
return this.run(() => this.statements.getSummaries.all(JSON.stringify(ids))).map(toSummaryRow);
|
|
515
|
+
}
|
|
516
|
+
/** Every root row (archived or not) whose stored summary contains each `prefilter` token,
|
|
517
|
+
* lowercased, newest first: the candidates of a run search (#864), which the caller still
|
|
518
|
+
* checks with `matchesRunQuery`. No tokens, every root. */
|
|
519
|
+
searchRootSummaries(prefilter) {
|
|
520
|
+
const rows = [];
|
|
521
|
+
this.visitRootSummaries(prefilter, (row) => { rows.push(row); return true; });
|
|
522
|
+
return rows;
|
|
523
|
+
}
|
|
524
|
+
/** The rows `searchRootSummaries` answers, one at a time, until `visit` answers false: a search
|
|
525
|
+
* that needs only its first matches stops reading there. */
|
|
526
|
+
visitRootSummaries(prefilter, visit) {
|
|
527
|
+
const where = ['parent_run_id IS NULL', ...prefilter.map(() => 'instr(lower(summary), ?) > 0')].join(' AND ');
|
|
528
|
+
this.run(() => {
|
|
529
|
+
for (const row of this.db.prepare(`${SUMMARY_SELECT} WHERE ${where} ${NEWEST_FIRST}`).iterate(...prefilter)) {
|
|
530
|
+
if (!visit(toSummaryRow(row)))
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/** Every row, newest first (`created_at` descending, then insertion order). */
|
|
536
|
+
listAll() {
|
|
537
|
+
return this.run(() => this.statements.listAll.all()).map(toRunRow);
|
|
538
|
+
}
|
|
539
|
+
/** The rows `RunStore.open` holds in memory: those whose `live` column is set, in insertion order. */
|
|
540
|
+
listLive() {
|
|
541
|
+
return this.run(() => this.statements.listLive.all()).map(toRunRow);
|
|
542
|
+
}
|
|
543
|
+
/** The workers of one parent, newest first. */
|
|
544
|
+
listByParent(parentId) {
|
|
545
|
+
return this.run(() => this.statements.listByParent.all(parentId)).map(toRunRow);
|
|
546
|
+
}
|
|
547
|
+
/** The ids of one parent's workers, in no particular order, without reading their records. */
|
|
548
|
+
listIdsByParent(parentId) {
|
|
549
|
+
return this.run(() => this.statements.listIdsByParent.all(parentId)).map((row) => row.id);
|
|
550
|
+
}
|
|
551
|
+
/** The id of every worker row, in no particular order. */
|
|
552
|
+
listWorkerIds() {
|
|
553
|
+
return this.run(() => this.statements.listWorkerIds.all()).map((row) => row.id);
|
|
554
|
+
}
|
|
555
|
+
findByClientRequestId(clientRequestId) {
|
|
556
|
+
const row = this.run(() => this.statements.findByClientRequestId.get(clientRequestId));
|
|
557
|
+
return row ? toRunRow(row) : undefined;
|
|
558
|
+
}
|
|
559
|
+
/** The members of one variant group, in no particular order. */
|
|
560
|
+
listByGroup(groupId) {
|
|
561
|
+
return this.run(() => this.statements.listByGroup.all(groupId)).map(toRunRow);
|
|
562
|
+
}
|
|
563
|
+
/** Rows with a materialized worktree, most recently finished (else created) first, then newest first. */
|
|
564
|
+
listWithWorktree() {
|
|
565
|
+
return this.run(() => this.statements.listWithWorktree.all()).map(toRunRow);
|
|
566
|
+
}
|
|
567
|
+
/** The rows matching `where` (by default, every row that owns a branch) as branch owners,
|
|
568
|
+
* with their summary, in no particular order. Same contract for `where` as `listWhere`. */
|
|
569
|
+
listBranchOwners(where = 'branch IS NOT NULL') {
|
|
570
|
+
const sql = `SELECT id, created_at, status, archived, branch, base_branch, summary FROM runs WHERE ${where}`;
|
|
571
|
+
return this.run(() => this.db.prepare(sql).all()).map((row) => ({
|
|
572
|
+
id: row.id,
|
|
573
|
+
createdAt: row.created_at,
|
|
574
|
+
status: row.status,
|
|
575
|
+
archived: row.archived === 1,
|
|
576
|
+
branch: row.branch,
|
|
577
|
+
baseBranch: row.base_branch,
|
|
578
|
+
summary: row.summary,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
/** The ids of the rows that own `branch` or hold a worktree at `worktreePath`. */
|
|
582
|
+
findResourceHolders(resource) {
|
|
583
|
+
return this.run(() => this.statements.findResourceHolders.all(resource)).map((row) => row.id);
|
|
584
|
+
}
|
|
585
|
+
/** Every id, in no particular order. */
|
|
586
|
+
listIds() {
|
|
587
|
+
return this.run(() => this.statements.listIds.all()).map((row) => row.id);
|
|
588
|
+
}
|
|
589
|
+
has(id) {
|
|
590
|
+
return this.run(() => this.statements.has.get(id)) !== undefined;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Rows matching `where`, newest first — the store's candidate filters (mark all read, archive
|
|
594
|
+
* finished, retention, the reference heal), which combine these columns with `json_extract`
|
|
595
|
+
* over `summary`. `where` is the caller's constant SQL; values go in `params`, never into it.
|
|
596
|
+
* `offset` skips the newest rows first (retention keeps the newest N).
|
|
597
|
+
*/
|
|
598
|
+
listWhere(where, params = [], options = {}) {
|
|
599
|
+
const sql = `SELECT ${ROW_COLUMNS} FROM runs WHERE ${where} ${NEWEST_FIRST} LIMIT -1 OFFSET ?`;
|
|
600
|
+
return this.run(() => this.db.prepare(sql).all(...params, options.offset ?? 0)).map(toRunRow);
|
|
601
|
+
}
|
|
602
|
+
/** The id and creation time of each row matching `where`, newest first, read off the
|
|
603
|
+
* `created_at` index without touching `data` or `summary` (history retention's ranking).
|
|
604
|
+
* Same contract for `where` as `listWhere`. */
|
|
605
|
+
listKeysWhere(where, params = []) {
|
|
606
|
+
const sql = `SELECT seq, id, created_at FROM runs WHERE ${where} ${NEWEST_FIRST}`;
|
|
607
|
+
return this.run(() => this.db.prepare(sql).all(...params))
|
|
608
|
+
.map((row) => ({ id: row.id, createdAt: row.created_at, seq: row.seq }));
|
|
609
|
+
}
|
|
610
|
+
/** Every id with its revision, ordered by id. */
|
|
611
|
+
listRevisions() {
|
|
612
|
+
return this.run(() => this.statements.listRevisions.all()).map((row) => ({
|
|
613
|
+
id: row.id,
|
|
614
|
+
revision: row.revision,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
getMeta(key) {
|
|
618
|
+
const row = this.run(() => this.statements.getMeta.get(key));
|
|
619
|
+
return row ? row.value : undefined;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* The only write of run rows. Deletes, upserts and metadata commit together or not at all: on
|
|
623
|
+
* any failure the transaction is rolled back and the error is rethrown typed, with the database
|
|
624
|
+
* exactly as it was. Every upserted row gets this commit's revision from the database-wide
|
|
625
|
+
* sequence. With a `fence`, nothing is written unless every claim and row is still as the writer
|
|
626
|
+
* last saw it (`RunConflictError` otherwise).
|
|
627
|
+
*/
|
|
628
|
+
transaction(changes) {
|
|
629
|
+
validateChanges(changes);
|
|
630
|
+
const revisions = new Map();
|
|
631
|
+
const seqs = new Map();
|
|
632
|
+
let claims = new Map();
|
|
633
|
+
let skipped = false;
|
|
634
|
+
this.run(() => {
|
|
635
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
636
|
+
try {
|
|
637
|
+
if (changes.onlyIfMetaAbsent !== undefined && this.statements.getMeta.get(changes.onlyIfMetaAbsent) !== undefined) {
|
|
638
|
+
skipped = true;
|
|
639
|
+
this.db.exec('ROLLBACK');
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (changes.fence)
|
|
643
|
+
claims = this.checkFence(changes.fence);
|
|
644
|
+
for (const id of changes.deletes)
|
|
645
|
+
this.statements.delete.run(id);
|
|
646
|
+
const revision = changes.upserts.length > 0 ? this.nextSequence(COMMIT_SEQ_KEY) : 0;
|
|
647
|
+
for (const row of changes.upserts) {
|
|
648
|
+
const inserted = this.statements.upsert.get({
|
|
649
|
+
id: row.id,
|
|
650
|
+
createdAt: row.createdAt,
|
|
651
|
+
finishedAt: row.finishedAt ?? null,
|
|
652
|
+
status: row.status,
|
|
653
|
+
archived: row.archived ? 1 : 0,
|
|
654
|
+
live: row.live ? 1 : 0,
|
|
655
|
+
parentRunId: row.parentRunId ?? null,
|
|
656
|
+
clientRequestId: row.clientRequestId ?? null,
|
|
657
|
+
groupId: row.groupId ?? null,
|
|
658
|
+
worktreePath: row.worktreePath ?? null,
|
|
659
|
+
branch: row.branch ?? null,
|
|
660
|
+
baseBranch: row.baseBranch ?? null,
|
|
661
|
+
revision,
|
|
662
|
+
data: row.data,
|
|
663
|
+
summary: row.summary,
|
|
664
|
+
});
|
|
665
|
+
revisions.set(row.id, revision);
|
|
666
|
+
seqs.set(row.id, inserted.seq);
|
|
667
|
+
}
|
|
668
|
+
for (const [key, value] of Object.entries(changes.meta ?? {})) {
|
|
669
|
+
if (value === null)
|
|
670
|
+
this.statements.deleteMeta.run(key);
|
|
671
|
+
else
|
|
672
|
+
this.statements.setMeta.run(key, value);
|
|
673
|
+
}
|
|
674
|
+
for (const [key, value] of Object.entries(changes.beforeCommit?.() ?? {}))
|
|
675
|
+
this.statements.setMeta.run(key, value);
|
|
676
|
+
this.db.exec('COMMIT');
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
rollback(this.db);
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
return { revisions, seqs, claims, skipped };
|
|
684
|
+
}
|
|
685
|
+
/** Inside a write transaction: take the claims the fence asks for, then throw `RunConflictError`
|
|
686
|
+
* naming every row of the fence that is not as its writer last saw it, before anything is
|
|
687
|
+
* written (the claims taken roll back with it). Returns the claims taken. */
|
|
688
|
+
checkFence(fence) {
|
|
689
|
+
const lost = new Set();
|
|
690
|
+
const taken = new Map();
|
|
691
|
+
for (const [family, expected] of fence.claims) {
|
|
692
|
+
const claim = this.statements.getClaim.get(family);
|
|
693
|
+
if ('generation' in expected) {
|
|
694
|
+
if (!claim || claim.session !== fence.owner.session || claim.generation !== expected.generation)
|
|
695
|
+
lost.add(family);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const unchanged = expected.take === null ? !claim
|
|
699
|
+
: claim?.session === expected.take.session && claim.generation === expected.take.generation;
|
|
700
|
+
if (!unchanged) {
|
|
701
|
+
lost.add(family);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const generation = this.nextSequence(CLAIM_GENERATION_KEY);
|
|
705
|
+
this.statements.putClaim.run({ family, ...fence.owner, generation });
|
|
706
|
+
taken.set(family, generation);
|
|
707
|
+
}
|
|
708
|
+
const conflicts = [];
|
|
709
|
+
for (const [id, expected] of fence.rows) {
|
|
710
|
+
const found = this.statements.get.get(id);
|
|
711
|
+
const current = found ? toRunRow(found) : undefined;
|
|
712
|
+
const reason = lost.has(expected.family) ? 'claim-lost'
|
|
713
|
+
: expected.revision === null ? (current ? 'created' : undefined)
|
|
714
|
+
: !current ? 'deleted' : current.revision !== expected.revision ? 'changed' : undefined;
|
|
715
|
+
if (reason)
|
|
716
|
+
conflicts.push({ id, reason, current });
|
|
717
|
+
}
|
|
718
|
+
if (conflicts.length > 0)
|
|
719
|
+
throw new RunConflictError(conflicts);
|
|
720
|
+
return taken;
|
|
721
|
+
}
|
|
722
|
+
/** Inside a write transaction: the next value of a database-wide sequence kept in `meta`. */
|
|
723
|
+
nextSequence(key) {
|
|
724
|
+
return Number(this.statements.nextSequence.get(key).value);
|
|
725
|
+
}
|
|
726
|
+
/** The family a stored run belongs to (`parent_run_id`, else its own id), or undefined. */
|
|
727
|
+
familyOf(id) {
|
|
728
|
+
const row = this.run(() => this.statements.familyOf.get(id));
|
|
729
|
+
return row ? row.family : undefined;
|
|
730
|
+
}
|
|
731
|
+
/** Whether any row of `family` (its root or a direct worker) is live. */
|
|
732
|
+
familyHasLive(family) {
|
|
733
|
+
return this.run(() => this.statements.familyHasLive.get({ family })) !== undefined;
|
|
734
|
+
}
|
|
735
|
+
getClaim(family) {
|
|
736
|
+
const row = this.run(() => this.statements.getClaim.get(family));
|
|
737
|
+
return row ? toRunClaim(row) : undefined;
|
|
738
|
+
}
|
|
739
|
+
/** The claims on `families` that exist, by family. */
|
|
740
|
+
getClaims(families) {
|
|
741
|
+
if (families.length === 0)
|
|
742
|
+
return new Map();
|
|
743
|
+
return new Map(this.run(() => this.statements.getClaims.all(JSON.stringify(families))).map((row) => [row.family, toRunClaim(row)]));
|
|
744
|
+
}
|
|
745
|
+
listClaims() {
|
|
746
|
+
return this.run(() => this.statements.listClaims.all()).map(toRunClaim);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Take the claims in `take` for `owner`, in one transaction. Each is taken only when its claim
|
|
750
|
+
* is still exactly what the caller judged it to be: absent (`expect: null`) or held by the dead
|
|
751
|
+
* owner it read (same session and generation) — so two takers racing for one family cannot both
|
|
752
|
+
* win. Returns the generation of each claim taken.
|
|
753
|
+
*/
|
|
754
|
+
takeClaims(owner, take) {
|
|
755
|
+
const taken = new Map();
|
|
756
|
+
if (take.length === 0)
|
|
757
|
+
return taken;
|
|
758
|
+
this.run(() => {
|
|
759
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
760
|
+
try {
|
|
761
|
+
for (const { family, expect } of take) {
|
|
762
|
+
const current = this.statements.getClaim.get(family);
|
|
763
|
+
const unchanged = expect === null ? !current
|
|
764
|
+
: current?.session === expect.session && current.generation === expect.generation;
|
|
765
|
+
if (!unchanged)
|
|
766
|
+
continue;
|
|
767
|
+
const generation = this.nextSequence(CLAIM_GENERATION_KEY);
|
|
768
|
+
this.statements.putClaim.run({ family, ...owner, generation });
|
|
769
|
+
taken.set(family, generation);
|
|
770
|
+
}
|
|
771
|
+
this.db.exec('COMMIT');
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
rollback(this.db);
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
return taken;
|
|
779
|
+
}
|
|
780
|
+
/** Drop `session`'s claims on `families`, or on everything it holds when omitted. */
|
|
781
|
+
releaseClaims(session, families) {
|
|
782
|
+
this.run(() => {
|
|
783
|
+
if (families === undefined)
|
|
784
|
+
this.statements.releaseSessionClaims.run(session);
|
|
785
|
+
else if (families.length > 0)
|
|
786
|
+
this.statements.releaseClaims.run(session, JSON.stringify(families));
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/** Every row whose family a session other than `session` claims, live or not, with that family. */
|
|
790
|
+
listForeignClaimedIds(session) {
|
|
791
|
+
return this.run(() => this.statements.listForeignClaimedIds.all(session)).map((row) => ({ id: row.id, family: row.family }));
|
|
792
|
+
}
|
|
793
|
+
/** Store conflict evidence in one transaction; returns each entry's `seq`. */
|
|
794
|
+
recordConflicts(session, entries) {
|
|
795
|
+
const detectedAt = new Date().toISOString();
|
|
796
|
+
const seqs = [];
|
|
797
|
+
this.run(() => {
|
|
798
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
799
|
+
try {
|
|
800
|
+
for (const entry of entries) {
|
|
801
|
+
seqs.push(this.statements.insertConflict.get({ ...entry, session, detectedAt, localDeleted: entry.localDeleted ? 1 : 0 }).seq);
|
|
802
|
+
}
|
|
803
|
+
this.db.exec('COMMIT');
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
rollback(this.db);
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return seqs;
|
|
811
|
+
}
|
|
812
|
+
/** Every stored conflict, oldest first (diagnostics and tests). */
|
|
813
|
+
listConflicts() {
|
|
814
|
+
return this.run(() => this.statements.listConflicts.all()).map((row) => ({
|
|
815
|
+
seq: row.seq,
|
|
816
|
+
runId: row.run_id,
|
|
817
|
+
detectedAt: row.detected_at,
|
|
818
|
+
session: row.session,
|
|
819
|
+
reason: row.reason,
|
|
820
|
+
baseRevision: row.base_revision,
|
|
821
|
+
baseData: row.base_data,
|
|
822
|
+
localData: row.local_data,
|
|
823
|
+
localDeleted: row.local_deleted === 1,
|
|
824
|
+
currentRevision: row.current_revision,
|
|
825
|
+
currentData: row.current_data,
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
/** The connection's effective settings, for diagnostics. */
|
|
829
|
+
pragmas() {
|
|
830
|
+
return this.run(() => ({
|
|
831
|
+
journalMode: this.db.prepare('PRAGMA journal_mode').get().journal_mode,
|
|
832
|
+
synchronous: this.db.prepare('PRAGMA synchronous').get().synchronous,
|
|
833
|
+
busyTimeoutMs: this.db.prepare('PRAGMA busy_timeout').get().timeout,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
/** Idempotent: closing a closed database does nothing. Once any statement found the file
|
|
837
|
+
* damaged, the close leaves every file exactly as it is (`closeLeavingFiles`). */
|
|
838
|
+
close() {
|
|
839
|
+
if (this.damaged)
|
|
840
|
+
closeLeavingFiles(this.db, this.path);
|
|
841
|
+
else if (this.db.isOpen)
|
|
842
|
+
this.db.close();
|
|
843
|
+
}
|
|
844
|
+
run(fn) {
|
|
845
|
+
try {
|
|
846
|
+
return fn();
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
const typed = toRunDatabaseError(error, this.path);
|
|
850
|
+
if (typed instanceof RunDatabaseCorruptError)
|
|
851
|
+
this.damaged = true;
|
|
852
|
+
throw typed;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/** Connections to a damaged database that could not be closed safely (`closeLeavingFiles`). */
|
|
857
|
+
const leftOpen = [];
|
|
858
|
+
/**
|
|
859
|
+
* Close `db` without letting SQLite touch the files of a damaged database. The last connection to
|
|
860
|
+
* close checkpoints the WAL into the database file and then deletes `-wal` and `-shm`: over a
|
|
861
|
+
* malformed page that rewrites the damaged file and removes what may be the only intact copy of
|
|
862
|
+
* the latest commits. A read-only connection opened first keeps a shared lock on the file, so the
|
|
863
|
+
* closing one is not the last and does neither; the read-only one cannot write, so its own close
|
|
864
|
+
* does neither either. Any failure on the way still closes `db` the same guarded way or not at all.
|
|
865
|
+
*/
|
|
866
|
+
function closeLeavingFiles(db, path) {
|
|
867
|
+
if (!db.isOpen)
|
|
868
|
+
return;
|
|
869
|
+
let guard;
|
|
870
|
+
try {
|
|
871
|
+
guard = new DatabaseSync(path, { readOnly: true });
|
|
872
|
+
// A read takes the shared lock; a WAL database keeps it for as long as the connection is open.
|
|
873
|
+
guard.prepare('PRAGMA user_version').get();
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Without a guard, leaving the connection open is the only close that changes nothing. Held
|
|
877
|
+
// here so garbage collection cannot close it either; the process lets it go when it exits.
|
|
878
|
+
guard?.close();
|
|
879
|
+
leftOpen.push(db);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
db.close();
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
guard.close();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function readSchemaVersion(db) {
|
|
890
|
+
return db.prepare('PRAGMA user_version').get().user_version;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Bring `db` to `RUN_DATABASE_SCHEMA_VERSION`, or refuse a newer one. A current database is only
|
|
894
|
+
* read, never written. The version is re-read inside the write transaction, so two processes
|
|
895
|
+
* opening a fresh file at once cannot both run the same migration.
|
|
896
|
+
*/
|
|
897
|
+
function migrate(db) {
|
|
898
|
+
const found = readSchemaVersion(db);
|
|
899
|
+
if (found > RUN_DATABASE_SCHEMA_VERSION)
|
|
900
|
+
throw new RunDatabaseUnsupportedSchemaError(found, RUN_DATABASE_SCHEMA_VERSION);
|
|
901
|
+
if (found === RUN_DATABASE_SCHEMA_VERSION)
|
|
902
|
+
return;
|
|
903
|
+
db.exec('BEGIN IMMEDIATE');
|
|
904
|
+
try {
|
|
905
|
+
let version = readSchemaVersion(db);
|
|
906
|
+
if (version > RUN_DATABASE_SCHEMA_VERSION)
|
|
907
|
+
throw new RunDatabaseUnsupportedSchemaError(version, RUN_DATABASE_SCHEMA_VERSION);
|
|
908
|
+
for (; version < RUN_DATABASE_SCHEMA_VERSION; version++)
|
|
909
|
+
db.exec(MIGRATIONS[version]);
|
|
910
|
+
db.exec(`PRAGMA user_version = ${RUN_DATABASE_SCHEMA_VERSION}`);
|
|
911
|
+
db.exec('COMMIT');
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
rollback(db);
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/** Undo an open transaction, if there is one. Never throws: the caller is already handling the
|
|
919
|
+
* error that got it here, and a failed rollback must not replace that error with its own. SQLite
|
|
920
|
+
* rolls back by itself on FULL, IOERR and BUSY, which is why "no transaction" is not a failure. */
|
|
921
|
+
function rollback(db) {
|
|
922
|
+
try {
|
|
923
|
+
if (db.isTransaction)
|
|
924
|
+
db.exec('ROLLBACK');
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
// The connection is closed or broken; whatever the transaction held is gone with it.
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
//# sourceMappingURL=run-database.js.map
|