@sema-agent/server 1.322.0 → 2.0.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/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +59 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -1,9 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-image-pool BAKE store — SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场; the 4th collapse
|
|
3
|
+
* after background-agent / mailbox / task-list / roster). ONE implementation, TWO dialects; the
|
|
4
|
+
* historical `TiDBImageBake` / `PgImageBake` class names survive as thin ctor subclasses so every
|
|
5
|
+
* consumer (store-backend.ts, pg-pool.ts, the unit + real-DB suites) is untouched.
|
|
6
|
+
*
|
|
7
|
+
* A bake is the async, resumable, auto-registering execution of `build.sh` on the privileged build-host
|
|
8
|
+
* bake-runner. image-api (this, the unprivileged controller) owns the durable job row + the per-bake event
|
|
9
|
+
* log + the single-flight build lease; the runner CLAIMS a queued bake, runs build.sh, and POSTs each
|
|
10
|
+
* build.sh JSON line back to the ingest endpoint, which appends to the event log the SSE reader serves
|
|
11
|
+
* (run-trace mechanics verbatim: `(bakeId,seq)` PK, ++seq, getEvents(after), maxSeq, retainedFrom→416,
|
|
12
|
+
* heartbeat-not-persisted).
|
|
13
|
+
*
|
|
14
|
+
* This is a DEDICATED table set — NOT a reuse of task_run/task_event (FK-coupled to task_active + the run
|
|
15
|
+
* reaper + the /v1/runs list; co-mingling bakes — no session, no spend, no checkpoint — would leak non-task
|
|
16
|
+
* rows into reapStale/listRuns, a category error). The shape mirrors TiDBRunStore: durable idempotency via a
|
|
17
|
+
* unique-key CAS (like createRun), a conditional-UPDATE single-flight claim (like requestCancel), and a
|
|
18
|
+
* reaper twin.
|
|
19
|
+
*
|
|
20
|
+
* The split that makes the SSE reader correct (P2.3 MUST-FIX): the COARSE 4-value `status`
|
|
21
|
+
* (queued|running|done|failed) is what the reader polls for terminality; build.sh's 8-value `state`
|
|
22
|
+
* (PENDING→…→COMPLETE|FAILED|CANCELLED) is denormalized for the UI progress bar ONLY and never drives stream
|
|
23
|
+
* termination (a non-terminal PUSHING/VERIFYING state must NOT truncate the stream before `done`).
|
|
24
|
+
*
|
|
25
|
+
* ── Dialect deltas, kept EXPLICIT (never hidden behind an abstraction) ────────────────────────────────
|
|
26
|
+
* - `?` placeholders vs `$n` - `CAST(? AS JSON)` vs `$n::jsonb` (argv equality)
|
|
27
|
+
* - `INSERT IGNORE` vs `ON CONFLICT DO NOTHING` - affectedRows vs rowCount (via SqlDriver)
|
|
28
|
+
* - dup-key `ER_DUP_ENTRY` vs SQLSTATE `23505` - which UNIQUE collided: mysql2 `sqlMessage` text
|
|
29
|
+
* vs pg `err.constraint` (uq_line naming differs)
|
|
30
|
+
* - `BEGIN PESSIMISTIC` (TiDB current read) vs `BEGIN` (PG READ COMMITTED under the sentinel lock)
|
|
31
|
+
* - the ADMISSION statement itself differs (see createBakeIfIdle) — both are correct under the
|
|
32
|
+
* sentinel lock, and both are preserved verbatim because the real-DB suites are the oracle
|
|
33
|
+
* - PG-ONLY protocol-byte defenses: `pgSafeJsonStringify` on event payloads, and the setTerminal
|
|
34
|
+
* identity-fold (`invalidIdentity`) + `pgSanitizeText` on error/errorCode/tag. TiDB stores verbatim.
|
|
35
|
+
* - schema ownership: the TiDB DDL lives in tidb-pool.ts SCHEMA_STATEMENTS; the PG DDL is exported
|
|
36
|
+
* from HERE (PG_IMAGE_BAKE_SCHEMA + ensurePgImageBakeSchema) for pg-pool.ts + the integration test.
|
|
37
|
+
*/
|
|
1
38
|
import { uuidv7 } from "@sema-agent/core";
|
|
2
39
|
import { randomBytes } from "node:crypto";
|
|
3
40
|
import { pgSanitizeText, pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
|
|
4
41
|
import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
|
|
5
42
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
6
43
|
import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-contracts.js";
|
|
44
|
+
/** PG translation of the image_bake / image_bake_event / image_bake_lease DDL in tidb-pool.ts SCHEMA_STATEMENTS
|
|
45
|
+
* (JSON→JSONB, TINYINT→SMALLINT, DATETIME(3)→TIMESTAMPTZ(3), inline UNIQUE/KEY→named CONSTRAINT + CREATE INDEX).
|
|
46
|
+
* TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
|
|
47
|
+
* 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
|
|
48
|
+
* 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
|
|
49
|
+
* The uq_line UNIQUE is given an explicit name so a 23505 can be attributed to it via err.constraint. */
|
|
7
50
|
export const PG_IMAGE_BAKE_SCHEMA = [
|
|
8
51
|
`CREATE TABLE IF NOT EXISTS image_bake (
|
|
9
52
|
bake_id VARCHAR(64) NOT NULL,
|
|
@@ -56,20 +99,33 @@ export const PG_IMAGE_BAKE_SCHEMA = [
|
|
|
56
99
|
expires_at TIMESTAMPTZ(3) NOT NULL,
|
|
57
100
|
PRIMARY KEY (pool)
|
|
58
101
|
)`,
|
|
102
|
+
// Admission sentinel: ONE always-present row per builder pool, `SELECT … FOR UPDATE`-locked to SERIALIZE the
|
|
103
|
+
// createBakeIfIdle busy-check+insert (so the M4 single-flight admission is genuinely atomic, not a snapshot
|
|
104
|
+
// NOT EXISTS that two concurrent READ COMMITTED txns both pass — see createBakeIfIdle). Dialect-uniform with
|
|
105
|
+
// the TiDB twin so a future divergence FAILS LOUD in the shared real-DB suite.
|
|
59
106
|
`CREATE TABLE IF NOT EXISTS image_bake_admit (
|
|
60
107
|
pool VARCHAR(32) NOT NULL,
|
|
61
108
|
PRIMARY KEY (pool)
|
|
62
109
|
)`,
|
|
63
110
|
];
|
|
111
|
+
/** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
|
|
64
112
|
export async function ensurePgImageBakeSchema(pool, poolName = "standard") {
|
|
65
113
|
for (const stmt of PG_IMAGE_BAKE_SCHEMA)
|
|
66
114
|
await pool.query(stmt);
|
|
115
|
+
// Seed the always-present admission sentinel (idempotent) so createBakeIfIdle can FOR UPDATE-lock it.
|
|
67
116
|
await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
|
|
68
117
|
}
|
|
118
|
+
/** Dup-key detection — mysql2 names it `ER_DUP_ENTRY`, PG raises SQLSTATE 23505. */
|
|
69
119
|
function isDupKey(dialect, err) {
|
|
70
120
|
const code = err?.code;
|
|
71
121
|
return Boolean(err) && code === (dialect === "tidb" ? "ER_DUP_ENTRY" : "23505");
|
|
72
122
|
}
|
|
123
|
+
/** Which UNIQUE key did the dup collide on? The ATTRIBUTION CHANNEL differs by engine: mysql2 names the key in
|
|
124
|
+
* `sqlMessage` ("Duplicate entry '…' for key 'image_bake_event.uq_line'" vs "…'image_bake_event.PRIMARY'");
|
|
125
|
+
* PG names the violated constraint in `err.constraint` (the PK is auto-named `image_bake_event_pkey`, the line
|
|
126
|
+
* UNIQUE is explicitly named `uq_image_bake_event_line` so it is attributable at all).
|
|
127
|
+
* M2: a line-unique dup is a BENIGN at-least-once re-send (already stored → no-op); a `PRIMARY` (bake_id,seq)
|
|
128
|
+
* dup is a same-seq RACE the caller must RETRY (a fresh maxSeq+1), never swallow. */
|
|
73
129
|
function dupKeyName(dialect, err) {
|
|
74
130
|
if (dialect === "tidb") {
|
|
75
131
|
const msg = String(err?.sqlMessage ?? err?.message ?? "");
|
|
@@ -88,16 +144,21 @@ function dupKeyName(dialect, err) {
|
|
|
88
144
|
}
|
|
89
145
|
const INSERT_BAKE_COLS = "INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
90
146
|
"idem_key, cancel_requested, requested_by, created_at, updated_at) ";
|
|
147
|
+
/** Dual-dialect BAKE store. See the file header for the dialect-delta ledger. */
|
|
91
148
|
export class SqlImageBake {
|
|
92
149
|
db;
|
|
93
150
|
poolName;
|
|
94
|
-
constructor(db,
|
|
151
|
+
constructor(db,
|
|
152
|
+
/** The builder pool a lease serializes on (one docker daemon ⇒ one pool today). */
|
|
153
|
+
poolName = "standard") {
|
|
95
154
|
this.db = db;
|
|
96
155
|
this.poolName = poolName;
|
|
97
156
|
}
|
|
157
|
+
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
98
158
|
q(tidb, pg) {
|
|
99
159
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
100
160
|
}
|
|
161
|
+
/** The 12 bound values shared by both admission INSERTs (column order === INSERT_BAKE_COLS). */
|
|
101
162
|
insertParams(bakeId, input, now) {
|
|
102
163
|
return [
|
|
103
164
|
bakeId,
|
|
@@ -114,6 +175,12 @@ export class SqlImageBake {
|
|
|
114
175
|
now,
|
|
115
176
|
];
|
|
116
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Create a bake, queued. Durable submit-idempotency mirrors createRun's CAS: `idem_key` is UNIQUE, so a
|
|
180
|
+
* re-submit with the same key races to INSERT and the loser catches the dup-key, then re-reads + returns the
|
|
181
|
+
* EXISTING row (the live bake) instead of a second job. A null `idemKey` always inserts (no dedupe requested).
|
|
182
|
+
* Returns `{ created, bake }` so the handler can answer 202 either way (idempotent dup ⇒ created:false).
|
|
183
|
+
*/
|
|
117
184
|
async createBake(input) {
|
|
118
185
|
const bakeId = uuidv7();
|
|
119
186
|
const now = new Date();
|
|
@@ -133,6 +200,35 @@ export class SqlImageBake {
|
|
|
133
200
|
throw new Error("createBake: row vanished after insert");
|
|
134
201
|
return { created: true, bake };
|
|
135
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* M4 (§P2.1/§P2.7 — the 409-busy MUST): create a queued bake ATOMICALLY only when no other non-dryRun bake is
|
|
205
|
+
* in flight, so two concurrent keyless POSTs can NEVER both queue (the check-then-act in the submit handler —
|
|
206
|
+
* findActiveAny() then createBake() as two awaited round-trips — interleaves: both see null-busy, both insert).
|
|
207
|
+
* `uq_idem` only dedupes non-null idem_keys (a keyless submit is NULL = distinct), and the build lease is taken
|
|
208
|
+
* at CLAIM not submit, so neither backstops this — the admission itself must be serialized.
|
|
209
|
+
*
|
|
210
|
+
* A bare conditional INSERT (`INSERT … SELECT … WHERE NOT EXISTS(...)`) is NOT sufficient ON ITS OWN: under
|
|
211
|
+
* TiDB's default REPEATABLE READ (and PG's READ COMMITTED) two concurrent autocommit conditional inserts each
|
|
212
|
+
* evaluate NOT EXISTS against a snapshot that excludes the other's still-uncommitted row (PG takes no
|
|
213
|
+
* predicate/gap lock), so BOTH can pass and double-admit. The only backstop today is the build lease taken at
|
|
214
|
+
* CLAIM, so a double-admit degrades the 409-busy/202-attach contract rather than double-building — but the
|
|
215
|
+
* contract is the point.
|
|
216
|
+
*
|
|
217
|
+
* Fix: serialize the busy-check+insert on the per-pool admission sentinel (`image_bake_admit`) with
|
|
218
|
+
* `SELECT … FOR UPDATE` inside one transaction. The second admitter blocks on the sentinel row lock until the
|
|
219
|
+
* first commits, then sees the committed in-flight bake and inserts 0 → `created:false`, and the caller
|
|
220
|
+
* re-reads the active bake (findActiveByArgv for an idempotent attach, else findActiveAny) for the
|
|
221
|
+
* 202-attach / 409 body. A dryRun bypasses this path entirely (never takes the lease — parallel-safe) and uses
|
|
222
|
+
* plain createBake.
|
|
223
|
+
*
|
|
224
|
+
* 🔴 DIALECT DIVERGENCE (deliberate, both preserved verbatim — the real-DB suites are the oracle):
|
|
225
|
+
* - TiDB: `BEGIN PESSIMISTIC` + a separate `SELECT 1 … FOR UPDATE` busy-check, then a plain INSERT. The
|
|
226
|
+
* pessimistic txn is REQUIRED: TiDB's snapshot (REPEATABLE READ) txn would evaluate a NOT EXISTS against
|
|
227
|
+
* the txn-start snapshot and miss a rival's just-committed row EVEN under the sentinel lock.
|
|
228
|
+
* - PG: plain `BEGIN` + one `INSERT … SELECT … WHERE NOT EXISTS(...)`. READ COMMITTED takes a FRESH
|
|
229
|
+
* statement snapshot, so the NOT EXISTS run after the sentinel lock is granted already sees the committed
|
|
230
|
+
* rival — the conditional insert is atomic enough once serialized.
|
|
231
|
+
*/
|
|
136
232
|
async createBakeIfIdle(input) {
|
|
137
233
|
const bakeId = uuidv7();
|
|
138
234
|
const now = new Date();
|
|
@@ -142,8 +238,12 @@ export class SqlImageBake {
|
|
|
142
238
|
try {
|
|
143
239
|
if (this.db.dialect === "tidb") {
|
|
144
240
|
await conn.beginPessimistic();
|
|
241
|
+
// Lock the always-present per-pool admission sentinel so concurrent admissions serialize (defensive INSERT
|
|
242
|
+
// IGNORE first in case ensureSchema seeded a different poolName).
|
|
145
243
|
await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES (?)", [this.poolName]);
|
|
146
244
|
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
|
|
245
|
+
// Busy-check as a CURRENT read (FOR UPDATE) — under the sentinel lock this is a single serialized admitter,
|
|
246
|
+
// so it sees the prior admitter's committed in-flight bake (no stale-snapshot phantom).
|
|
147
247
|
const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
|
|
148
248
|
if (busy.rows.length === 0) {
|
|
149
249
|
await conn.query(INSERT_BAKE_COLS + "VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", params);
|
|
@@ -153,6 +253,7 @@ export class SqlImageBake {
|
|
|
153
253
|
}
|
|
154
254
|
else {
|
|
155
255
|
await conn.begin();
|
|
256
|
+
// Defensive upsert first in case ensureSchema seeded a different poolName; ON CONFLICT keeps it idempotent.
|
|
156
257
|
await conn.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [this.poolName]);
|
|
157
258
|
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
|
|
158
259
|
const res = await conn.query(INSERT_BAKE_COLS +
|
|
@@ -167,12 +268,15 @@ export class SqlImageBake {
|
|
|
167
268
|
await conn.rollback();
|
|
168
269
|
}
|
|
169
270
|
catch {
|
|
271
|
+
/* ignore */
|
|
170
272
|
}
|
|
171
273
|
throw err;
|
|
172
274
|
}
|
|
173
275
|
finally {
|
|
174
276
|
conn.release();
|
|
175
277
|
}
|
|
278
|
+
// 0 rows inserted ⇒ a non-dryRun bake is already in flight (the busy-check/NOT EXISTS matched under the
|
|
279
|
+
// sentinel lock) → busy.
|
|
176
280
|
if (!inserted)
|
|
177
281
|
return { created: false, bake: null };
|
|
178
282
|
const bake = await this.getBake(bakeId);
|
|
@@ -188,28 +292,55 @@ export class SqlImageBake {
|
|
|
188
292
|
const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = ? LIMIT 1`, `SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = $1 LIMIT 1`), [idemKey]);
|
|
189
293
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
190
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Find an in-flight (queued/running) bake with the identical normalized argv — so a re-submit WITHOUT an
|
|
297
|
+
* idempotency key still returns the live bake rather than starting a duplicate build (P2.1 "an in-flight
|
|
298
|
+
* identical-input bake returns the EXISTING row"). argv is the canonical normalized vector, so a JSON
|
|
299
|
+
* equality is the dedupe key. Newest first (the live one).
|
|
300
|
+
*/
|
|
191
301
|
async findActiveByArgv(argv) {
|
|
192
302
|
const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = CAST(? AS JSON) ` +
|
|
193
303
|
"ORDER BY created_at DESC LIMIT 1", `SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = $1::jsonb ` +
|
|
194
304
|
"ORDER BY created_at DESC LIMIT 1"), [JSON.stringify(argv)]);
|
|
195
305
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
196
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The OLDEST in-flight (queued/running) NON-dry-run bake — the single-flight at-submit guard (P2.7). Concurrency
|
|
309
|
+
* is 1 (one docker daemon), so a new submit while ANY non-dry-run bake is in flight is `409 busy` UNLESS it is
|
|
310
|
+
* the identical-argv one (then the caller attaches to it, idempotent). dryRun bakes never take the lease, so they
|
|
311
|
+
* are excluded here. Oldest first = the one that holds/will-hold the lease (the build the UI should attach to).
|
|
312
|
+
*/
|
|
197
313
|
async findActiveAny() {
|
|
198
314
|
const { rows } = await this.db.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
|
|
199
315
|
"ORDER BY created_at ASC LIMIT 1", []);
|
|
200
316
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
201
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* The OLDEST queued bake id — the runner's long-poll claim target (P2.12). dryRun bakes never take the lease, so
|
|
320
|
+
* they are still discoverable here (the runner resolves them with no docker), but ordering oldest-first gives a
|
|
321
|
+
* stable FIFO. Returns null when the queue is empty. The actual claim is a CAS (`claimBake`), so two runners
|
|
322
|
+
* racing this both see the id but only one wins the conditional UPDATE — no double-build.
|
|
323
|
+
*/
|
|
202
324
|
async findNextQueuedId() {
|
|
203
325
|
const { rows } = await this.db.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1", []);
|
|
204
326
|
return rows[0] ? String(rows[0].bake_id) : null;
|
|
205
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Single-flight CLAIM (P2.7) — the runner leases the next queued bake. Two CASes in one transaction:
|
|
330
|
+
* 1. flip the bake row `queued → running` (conditional UPDATE `WHERE status='queued'`, 1 row affected wins);
|
|
331
|
+
* 2. take the single-row `image_bake_lease` for the pool (INSERT, or steal an EXPIRED lease via CAS).
|
|
332
|
+
* Only the runner that wins BOTH gets the bake; a loser (someone else already claimed, or the lease is held by
|
|
333
|
+
* a live build) gets null. Mints + stores the per-bake `ingest_secret` (so a stale/rogue runner can't inject
|
|
334
|
+
* frames into another bake) and the initial `lease_until`. Returns the claimed row (with the secret) or null.
|
|
335
|
+
*/
|
|
206
336
|
async claimBake(bakeId, runnerId, leaseMs) {
|
|
207
337
|
const conn = await this.db.connect();
|
|
208
338
|
try {
|
|
209
339
|
await conn.begin();
|
|
210
340
|
const now = new Date();
|
|
211
341
|
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
212
|
-
const ingestSecret = randomBytes(24).toString("hex");
|
|
342
|
+
const ingestSecret = randomBytes(24).toString("hex"); // 48 hex chars ≤ VARCHAR(64)
|
|
343
|
+
// (1) claim the bake row: only a still-queued row can be claimed (single-flight on the row).
|
|
213
344
|
const bakeRes = await conn.query(this.q("UPDATE image_bake SET status='running', state='PENDING', runner_id=?, ingest_secret=?, " +
|
|
214
345
|
"lease_until=?, updated_at=? WHERE bake_id=? AND status='queued'", "UPDATE image_bake SET status='running', state='PENDING', runner_id=$1, ingest_secret=$2, " +
|
|
215
346
|
"lease_until=$3, updated_at=$4 WHERE bake_id=$5 AND status='queued'"), [runnerId, ingestSecret, leaseUntil, now, bakeId]);
|
|
@@ -217,11 +348,24 @@ export class SqlImageBake {
|
|
|
217
348
|
await conn.rollback();
|
|
218
349
|
return null;
|
|
219
350
|
}
|
|
351
|
+
// L1 (§P2.7): a dryRun is RESOLVE-ONLY (no docker) and MUST be parallel-safe — it NEVER takes the build lease.
|
|
352
|
+
// Claim the row alone (the row CAS above already single-flights it) and skip the pool-lease INSERT/steal CAS,
|
|
353
|
+
// so a dryRun can be claimed/resolved while a real build holds the lease (matching P2.1/P2.7/P2.14). The lease
|
|
354
|
+
// release in setTerminal/reapStaleBakes is a harmless no-op for a row that never took the lease. (Read dry_run
|
|
355
|
+
// on the same row we just claimed.)
|
|
220
356
|
const dry = await conn.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id=?", "SELECT dry_run FROM image_bake WHERE bake_id=$1"), [bakeId]);
|
|
221
357
|
if (Boolean(dry.rows[0]?.dry_run)) {
|
|
222
358
|
await conn.commit();
|
|
223
359
|
return (await this.getBake(bakeId)) ?? null;
|
|
224
360
|
}
|
|
361
|
+
// (2) take the pool lease (single-flight on the build host). 🔴 TiDB rejects a correlated sub-query that
|
|
362
|
+
// references the INSERT target inside ON DUPLICATE KEY UPDATE ("Can't find column …" — caught by the live
|
|
363
|
+
// bake, NOT the fake-pool unit test which never executes the SQL). So steal at APP-LEVEL: read the current
|
|
364
|
+
// lease, then INSERT (no holder) or a CAS-UPDATE (holder's lease EXPIRED **and** its bake no longer running).
|
|
365
|
+
// 🔴 H4(a): an `expires_at < now` lease whose holder is still `running` is an expired-but-LIVE build (a
|
|
366
|
+
// heartbeat gap inside the reap window) — stealing it would run TWO builds on one docker daemon, so the steal
|
|
367
|
+
// is gated on the holder being non-running (steal-window == reap-window; a vanished holder is also stealable).
|
|
368
|
+
// The CAS on (pool,bake_id,expires_at) makes the steal atomic against a concurrent refresh.
|
|
225
369
|
const leaseSel = await conn.query(this.q("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=?", "SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
|
|
226
370
|
const cur = leaseSel.rows[0];
|
|
227
371
|
if (!cur) {
|
|
@@ -231,7 +375,7 @@ export class SqlImageBake {
|
|
|
231
375
|
catch (err) {
|
|
232
376
|
if (isDupKey(this.db.dialect, err)) {
|
|
233
377
|
await conn.rollback();
|
|
234
|
-
return null;
|
|
378
|
+
return null; // a concurrent claim inserted the lease first — we lose.
|
|
235
379
|
}
|
|
236
380
|
throw err;
|
|
237
381
|
}
|
|
@@ -239,21 +383,22 @@ export class SqlImageBake {
|
|
|
239
383
|
else {
|
|
240
384
|
if (new Date(cur.expires_at).getTime() >= now.getTime()) {
|
|
241
385
|
await conn.rollback();
|
|
242
|
-
return null;
|
|
386
|
+
return null; // the lease is live (unexpired) — we lose.
|
|
243
387
|
}
|
|
244
388
|
const holderId = String(cur.bake_id ?? "");
|
|
245
389
|
const holder = await conn.query(this.q("SELECT status FROM image_bake WHERE bake_id=?", "SELECT status FROM image_bake WHERE bake_id=$1"), [holderId]);
|
|
246
390
|
const holderStatus = String(holder.rows[0]?.status ?? "");
|
|
247
391
|
if (holderStatus === "queued" || holderStatus === "running") {
|
|
248
392
|
await conn.rollback();
|
|
249
|
-
return null;
|
|
393
|
+
return null; // expired-but-LIVE build — never steal (would double-bake on one daemon).
|
|
250
394
|
}
|
|
251
395
|
const stealRes = await conn.query(this.q("UPDATE image_bake_lease SET bake_id=?, acquired_at=?, expires_at=? WHERE pool=? AND bake_id=? AND expires_at=?", "UPDATE image_bake_lease SET bake_id=$1, acquired_at=$2, expires_at=$3 WHERE pool=$4 AND bake_id=$5 AND expires_at=$6"), [bakeId, now, leaseUntil, this.poolName, holderId, cur.expires_at]);
|
|
252
396
|
if (stealRes.affected === 0) {
|
|
253
397
|
await conn.rollback();
|
|
254
|
-
return null;
|
|
398
|
+
return null; // a concurrent claim refreshed/stole the lease first — we lose.
|
|
255
399
|
}
|
|
256
400
|
}
|
|
401
|
+
// Double-check the lease row actually points at US (defends a same-instant race) before committing the claim.
|
|
257
402
|
const check = await conn.query(this.q("SELECT bake_id FROM image_bake_lease WHERE pool=?", "SELECT bake_id FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
|
|
258
403
|
if (String(check.rows[0]?.bake_id ?? "") !== bakeId) {
|
|
259
404
|
await conn.rollback();
|
|
@@ -267,6 +412,7 @@ export class SqlImageBake {
|
|
|
267
412
|
await conn.rollback();
|
|
268
413
|
}
|
|
269
414
|
catch {
|
|
415
|
+
/* ignore */
|
|
270
416
|
}
|
|
271
417
|
throw err;
|
|
272
418
|
}
|
|
@@ -274,11 +420,31 @@ export class SqlImageBake {
|
|
|
274
420
|
conn.release();
|
|
275
421
|
}
|
|
276
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Append one event. image-api is the SOLE ++seq writer: `seq = maxSeq(bakeId)+1`. The `(bakeId,line_ord)`
|
|
425
|
+
* UNIQUE is the at-least-once ingest dedupe (P2.10) — a runner re-sending a line it already acked races to
|
|
426
|
+
* INSERT and the loser is a NO-OP (the frame is already stored at its seq). Returns the seq written, or null
|
|
427
|
+
* when the line was a duplicate (already stored). A null `lineOrd` (image-api-synthesized frame:
|
|
428
|
+
* register/done/heartbeat-derived) never collides on the line-unique (NULLs are distinct in a UNIQUE index).
|
|
429
|
+
*
|
|
430
|
+
* seq allocation + the INSERT are NOT one atomic statement, so two concurrent appends could pick the same
|
|
431
|
+
* seq+1; the (bakeId,seq) PK then rejects the loser with a dup-key. M2: that PK collision is DISTINGUISHED from
|
|
432
|
+
* a benign line re-send (by the dup-key attribution channel) and RETRIED in a bounded loop with a fresh
|
|
433
|
+
* maxSeq+1 — never silently swallowed (which would drop a frame, incl. the terminal `done`, at a clean HTTP
|
|
434
|
+
* 200). A line dup is the at-least-once re-send no-op (returns null). The runner also serializes its per-bake
|
|
435
|
+
* pump (runner.ts), so the same-seq race is the rare burst case, not the norm.
|
|
436
|
+
*/
|
|
277
437
|
async appendEvent(bakeId, kind, data, opts = {}) {
|
|
278
438
|
const lineOrd = opts.lineOrd ?? null;
|
|
279
439
|
if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
|
|
280
|
-
return null;
|
|
440
|
+
return null; // fast dedupe pre-check
|
|
441
|
+
// PG-only: codex R14 — a `done` frame carrying a NUL in its error text would raise 22P05 and be LOST FOREVER
|
|
442
|
+
// (the terminal flip already cleared the ingest secret, so it cannot be retried). TiDB stores JSON verbatim.
|
|
281
443
|
const payload = data == null ? null : this.db.dialect === "tidb" ? JSON.stringify(data) : pgSafeJsonStringify(data);
|
|
444
|
+
// The PK race is a thundering-herd: K concurrent writers all read the same maxSeq, ONE wins the (bake_id,seq)
|
|
445
|
+
// INSERT, the rest re-read and retry — so the unluckiest writer can lose up to ~K races in a row. A small fixed
|
|
446
|
+
// cap (6) under-serves a realistic ingest burst and would THROW a frame that is merely contended, not lost.
|
|
447
|
+
// Use a generous cap plus jittered backoff so the herd disperses instead of lock-stepping onto the same seq.
|
|
282
448
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
283
449
|
const seq = (await this.maxSeq(bakeId)) + 1;
|
|
284
450
|
try {
|
|
@@ -289,8 +455,12 @@ export class SqlImageBake {
|
|
|
289
455
|
if (!isDupKey(this.db.dialect, err))
|
|
290
456
|
throw err;
|
|
291
457
|
const which = dupKeyName(this.db.dialect, err);
|
|
458
|
+
// the line-unique lost the race (the line was concurrently stored at its own seq) → a benign at-least-once
|
|
459
|
+
// duplicate, NOT an error and NOT a frame loss. A null lineOrd CANNOT collide on the line-unique.
|
|
292
460
|
if (which === "uq_line" && lineOrd !== null)
|
|
293
461
|
return null;
|
|
462
|
+
// PRIMARY/(bake_id,seq) → a real same-seq race; re-read maxSeq and retry (do NOT swallow — that would
|
|
463
|
+
// silently drop this frame). `other` (an unnamed dup driver) is treated conservatively as retryable.
|
|
294
464
|
await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
|
|
295
465
|
}
|
|
296
466
|
}
|
|
@@ -300,16 +470,19 @@ export class SqlImageBake {
|
|
|
300
470
|
const { rows } = await this.db.query(this.q("SELECT 1 FROM image_bake_event WHERE bake_id = ? AND line_ord = ? LIMIT 1", "SELECT 1 FROM image_bake_event WHERE bake_id = $1 AND line_ord = $2 LIMIT 1"), [bakeId, lineOrd]);
|
|
301
471
|
return rows.length > 0;
|
|
302
472
|
}
|
|
473
|
+
/** Highest event seq for a bake (0 if none) — the ++seq allocator + the resume cursor head. */
|
|
303
474
|
async maxSeq(bakeId) {
|
|
304
475
|
const { rows } = await this.db.query(this.q("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
|
|
305
476
|
const m = rows[0]?.m;
|
|
306
477
|
return m == null ? 0 : Number(m);
|
|
307
478
|
}
|
|
479
|
+
/** Earliest retained event seq (0 if none) → the SSE 416 boundary (a resume below this was evicted, P2.9). */
|
|
308
480
|
async retainedFrom(bakeId) {
|
|
309
481
|
const { rows } = await this.db.query(this.q("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
|
|
310
482
|
const m = rows[0]?.m;
|
|
311
483
|
return m == null ? 0 : Number(m);
|
|
312
484
|
}
|
|
485
|
+
/** Events with seq strictly greater than `afterSeq` (0 for the whole stream), ascending. */
|
|
313
486
|
async getEvents(bakeId, afterSeq) {
|
|
314
487
|
const { rows } = await this.db.query(this.q("SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = ? AND seq > ? ORDER BY seq ASC", "SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = $1 AND seq > $2 ORDER BY seq ASC"), [bakeId, afterSeq]);
|
|
315
488
|
return rows.map((r) => ({
|
|
@@ -321,12 +494,25 @@ export class SqlImageBake {
|
|
|
321
494
|
ts: iso(r.ts),
|
|
322
495
|
}));
|
|
323
496
|
}
|
|
497
|
+
/** Coarse 4-value lifecycle transition (the column the SSE reader polls for terminality). */
|
|
324
498
|
async setStatus(bakeId, status) {
|
|
325
499
|
await this.db.query(this.q("UPDATE image_bake SET status = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3"), [status, new Date(), bakeId]);
|
|
326
500
|
}
|
|
501
|
+
/** Denormalize build.sh's 8-value `state` for the UI progress bar (NEVER drives terminality, P2.3). */
|
|
327
502
|
async setState(bakeId, state) {
|
|
328
503
|
await this.db.query(this.q("UPDATE image_bake SET state = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET state = $1, updated_at = $2 WHERE bake_id = $3"), [state, new Date(), bakeId]);
|
|
329
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Terminal write (P2.10 step 4): flip the coarse `status` + persist all the terminal facts (digest/repo/ref/
|
|
507
|
+
* indexId/exitCode/error/errorCode/state/manifestSha/tag) the `done` frame and the poll row carry, AND release
|
|
508
|
+
* the pool lease in one transaction (a separate-statement release that failed would wedge the build host).
|
|
509
|
+
* Idempotent terminal write: only flips a NON-terminal row (`WHERE status IN ('queued','running')`) so a
|
|
510
|
+
* re-delivered terminal can't clobber the first outcome. Returns the UPDATE's affected count — 1 = THIS call won
|
|
511
|
+
* the queued/running→terminal transition; 0 = a terminal already exists (M3: the caller then SKIPS appending a
|
|
512
|
+
* contradictory second terminal `done` event, so at most one terminal `done` per bake matches the coarse row).
|
|
513
|
+
* 🔴 H3: null `ingest_secret` on the terminal flip so a terminal row can NEVER re-authenticate an ingest
|
|
514
|
+
* (a lagging/duplicate runner that still holds the secret can't inject frames into a settled bake).
|
|
515
|
+
*/
|
|
330
516
|
async setTerminal(bakeId, t) {
|
|
331
517
|
let affectedRows = 0;
|
|
332
518
|
await this.tx(async (conn) => {
|
|
@@ -338,6 +524,7 @@ export class SqlImageBake {
|
|
|
338
524
|
});
|
|
339
525
|
return affectedRows;
|
|
340
526
|
}
|
|
527
|
+
/** TiDB stores the terminal facts VERBATIM (no protocol-byte class to defend against). */
|
|
341
528
|
terminalParamsTidb(bakeId, t) {
|
|
342
529
|
return [
|
|
343
530
|
t.status,
|
|
@@ -355,6 +542,10 @@ export class SqlImageBake {
|
|
|
355
542
|
bakeId,
|
|
356
543
|
];
|
|
357
544
|
}
|
|
545
|
+
/** PG-ONLY protocol-byte handling. R20/R21:身份位(digest/manifestSha/repo/ref)带不可存字节=帧无效——
|
|
546
|
+
* **单一谓词**统管状态折叠与全部身份绑位(R21:只查两位会放 dirty repo/ref 的假 COMPLETE 过——终态单赢,
|
|
547
|
+
* 假成功不可逆)。事务必须提交(lease 释放/secret 清理=单飞池活性底线)。codex R13/R17-H2:NUL 错误文案
|
|
548
|
+
* 或脏 tag 若让终态事务回滚,lease 保持 = bake 永占单飞池,故文本位走 sanitize 而非拒绝。 */
|
|
358
549
|
terminalParamsPg(bakeId, t) {
|
|
359
550
|
const invalidIdentity = (t.digest != null && pgHasUnstorable(t.digest)) ||
|
|
360
551
|
(t.manifestSha != null && pgHasUnstorable(t.manifestSha)) ||
|
|
@@ -376,41 +567,79 @@ export class SqlImageBake {
|
|
|
376
567
|
bakeId,
|
|
377
568
|
];
|
|
378
569
|
}
|
|
570
|
+
/** Set the index_id once auto-register succeeds (P2.10 — kept distinct from the terminal flip so the `done`
|
|
571
|
+
* frame can carry indexId even when the terminal write retries). codex R24:non-terminal 门——并发 done
|
|
572
|
+
* 竞态下(本请求悬在 registerBuilding 时另一 done 已赢 setTerminal)绝不把 indexId 挂回终态行(R23 折叠
|
|
573
|
+
* 的 FAILED/null 身份行会被复写成「null 身份却带 index 绑定」的矛盾形)。返回是否真绑定。 */
|
|
379
574
|
async setIndexId(bakeId, indexId) {
|
|
380
575
|
const res = await this.db.query(this.q("UPDATE image_bake SET index_id = ?, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET index_id = $1, updated_at = $2 WHERE bake_id = $3 AND status IN ('queued','running')"), [indexId, new Date(), bakeId]);
|
|
381
576
|
return res.affected >= 1;
|
|
382
577
|
}
|
|
578
|
+
/** Heartbeat the lease forward (P2.7): the runner pushes `lease_until` (+ the lease row's `expires_at`) while
|
|
579
|
+
* its build child runs, so the reaper doesn't steal a LIVE build's lease. CAS on the runner owning the row +
|
|
580
|
+
* the bake still running. Returns whether it refreshed (false ⇒ lease lost/stolen — the runner should abort). */
|
|
383
581
|
async heartbeatLease(bakeId, runnerId, leaseMs) {
|
|
384
582
|
const now = new Date();
|
|
385
583
|
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
386
584
|
const res = await this.db.query(this.q("UPDATE image_bake SET lease_until = ?, updated_at = ? WHERE bake_id = ? AND runner_id = ? AND status = 'running'", "UPDATE image_bake SET lease_until = $1, updated_at = $2 WHERE bake_id = $3 AND runner_id = $4 AND status = 'running'"), [leaseUntil, now, bakeId, runnerId]);
|
|
387
585
|
if (res.affected < 1)
|
|
388
586
|
return false;
|
|
587
|
+
// L1: a dryRun never took the pool lease (claimBake skips it) — so it has NO `image_bake_lease` row to refresh.
|
|
588
|
+
// The bake-row refresh above is its only heartbeat; skip the pool-lease CAS so a healthy dryRun isn't aborted
|
|
589
|
+
// by an (always-zero) pool-lease no-op.
|
|
389
590
|
const dry = await this.db.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id = ?", "SELECT dry_run FROM image_bake WHERE bake_id = $1"), [bakeId]);
|
|
390
591
|
if (Boolean(dry.rows[0]?.dry_run))
|
|
391
592
|
return true;
|
|
593
|
+
// 🔴 H4(b): the pool-lease refresh is a REAL CAS — `WHERE pool=? AND bake_id=?` only matches while WE still
|
|
594
|
+
// own the lease row. If it was stolen to another bake, 0 rows affected → return false so the displaced runner
|
|
595
|
+
// sees leaseValid:false and aborts + kills its child (runner.ts). Without this guard the no-op UPDATE was
|
|
596
|
+
// silently treated as success and a displaced runner kept building (two builds, one daemon).
|
|
392
597
|
const leaseRes = await this.db.query(this.q("UPDATE image_bake_lease SET expires_at = ? WHERE pool = ? AND bake_id = ?", "UPDATE image_bake_lease SET expires_at = $1 WHERE pool = $2 AND bake_id = $3"), [leaseUntil, this.poolName, bakeId]);
|
|
393
598
|
return leaseRes.affected >= 1;
|
|
394
599
|
}
|
|
600
|
+
/** Request cooperative cancel (P2.11) — durable + cross-replica; the runner's heartbeat tick polls it and kills
|
|
601
|
+
* the build.sh process group. Only flags a non-terminal row (terminal → 0 affected = no-op). Returns whether a
|
|
602
|
+
* live bake was flagged (false ⇒ already terminal → the caller answers a no-op 202). */
|
|
395
603
|
async requestCancel(bakeId) {
|
|
396
604
|
const res = await this.db.query(this.q("UPDATE image_bake SET cancel_requested = 1, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')"), [new Date(), bakeId]);
|
|
397
605
|
return res.affected > 0;
|
|
398
606
|
}
|
|
607
|
+
/** Whether a cancel was requested (polled by the runner). */
|
|
399
608
|
async isCancelRequested(bakeId) {
|
|
400
609
|
const { rows } = await this.db.query(this.q("SELECT cancel_requested FROM image_bake WHERE bake_id = ?", "SELECT cancel_requested FROM image_bake WHERE bake_id = $1"), [bakeId]);
|
|
401
610
|
return Boolean(rows[0]?.cancel_requested);
|
|
402
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Reaper twin of TiDBRunStore.reapStale (P2.7/§P2.6): fail any `running` bake whose lease is STALE (a crashed
|
|
614
|
+
* runner stopped heartbeating), append a synthetic terminal `done{failed}` so SSE readers SETTLE (never hang),
|
|
615
|
+
* and force-release the pool lease. `olderThanMs` must exceed >2 lease-heartbeat intervals (≈3× the heartbeat)
|
|
616
|
+
* so a healthy slow build is not wrongly reaped. Returns the count reaped.
|
|
617
|
+
*
|
|
618
|
+
* M3: flip the row terminal FIRST (the CAS on `status='running'`), and append the synthetic `done{failed}` ONLY
|
|
619
|
+
* when the flip WON (1 row affected). A victim that recovered + COMPLETEd between the SELECT and the flip
|
|
620
|
+
* no-ops the CAS, so we leak NO contradictory `done{failed}` into its settled SSE log. A reader that polled the
|
|
621
|
+
* still-running row in the tiny pre-flip gap simply re-polls (streamSseLog re-fetches) and settles on the
|
|
622
|
+
* eventual terminal — the winner's `done` — rather than a spurious one.
|
|
623
|
+
*/
|
|
403
624
|
async reapStaleBakes(olderThanMs) {
|
|
404
625
|
const cutoff = new Date(Date.now() - olderThanMs);
|
|
405
626
|
const victims = await this.db.query(this.q("SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < ?) AND updated_at < ?", "SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < $1) AND updated_at < $2"), [cutoff, cutoff]);
|
|
406
627
|
let reaped = 0;
|
|
407
628
|
for (const v of victims.rows) {
|
|
408
629
|
const bakeId = String(v.bake_id);
|
|
630
|
+
// CAS the flip on still-running (a runner that recovered between the SELECT and here keeps its row).
|
|
631
|
+
// 🔴 H3: also null `ingest_secret` so the reaped (terminal) row can never re-authenticate an ingest — a
|
|
632
|
+
// lost-but-lagging runner that still holds the secret cannot inject frames into the dead bake's stream.
|
|
633
|
+
// codex R25: also null `index_id` atomically — a victim whose ingest died AFTER setIndexId bound but
|
|
634
|
+
// BEFORE setTerminal would otherwise keep a stale binding that contradicts the synthetic done below
|
|
635
|
+
// (indexId:null); the orphan `building` index row is lifecycle-tolerated (never published/selectable).
|
|
409
636
|
const res = await this.db.query(this.q("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
|
|
410
637
|
"ingest_secret=NULL, index_id=NULL, updated_at=? WHERE bake_id=? AND status='running'", "UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
|
|
411
638
|
"ingest_secret=NULL, index_id=NULL, updated_at=$1 WHERE bake_id=$2 AND status='running'"), [new Date(), bakeId]);
|
|
412
639
|
if (res.affected === 1) {
|
|
413
640
|
reaped += 1;
|
|
641
|
+
// M3: we WON the flip — only now append the synthetic terminal so a reader settles on a `done` that matches
|
|
642
|
+
// the coarse row. A recovered+COMPLETEd victim never reaches here (the CAS no-op'd), so no contradiction.
|
|
414
643
|
try {
|
|
415
644
|
await this.appendEvent(bakeId, "done", {
|
|
416
645
|
status: "FAILED",
|
|
@@ -424,12 +653,14 @@ export class SqlImageBake {
|
|
|
424
653
|
});
|
|
425
654
|
}
|
|
426
655
|
catch {
|
|
656
|
+
/* best-effort — the row is already terminal; a reader sees status!=running and re-fetches. */
|
|
427
657
|
}
|
|
428
658
|
await this.db.query(this.q("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", "DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2"), [this.poolName, bakeId]);
|
|
429
659
|
}
|
|
430
660
|
}
|
|
431
661
|
return reaped;
|
|
432
662
|
}
|
|
663
|
+
/** Run statements atomically; rolls back on failure (mirrors TiDBRunStore.tx). */
|
|
433
664
|
async tx(fn) {
|
|
434
665
|
const conn = await this.db.connect();
|
|
435
666
|
try {
|
|
@@ -442,6 +673,7 @@ export class SqlImageBake {
|
|
|
442
673
|
await conn.rollback();
|
|
443
674
|
}
|
|
444
675
|
catch {
|
|
676
|
+
/* ignore */
|
|
445
677
|
}
|
|
446
678
|
throw err;
|
|
447
679
|
}
|
|
@@ -450,11 +682,13 @@ export class SqlImageBake {
|
|
|
450
682
|
}
|
|
451
683
|
}
|
|
452
684
|
}
|
|
685
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
453
686
|
export class TiDBImageBake extends SqlImageBake {
|
|
454
687
|
constructor(pool, poolName = "standard") {
|
|
455
688
|
super(mysqlDriver(pool), poolName);
|
|
456
689
|
}
|
|
457
690
|
}
|
|
691
|
+
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
458
692
|
export class PgImageBake extends SqlImageBake {
|
|
459
693
|
constructor(pool, poolName = "standard") {
|
|
460
694
|
super(pgDriver(pool), poolName);
|
|
@@ -4,29 +4,75 @@ import { type ImageStatus, type ImageIndexEntry, type ImageIndexUpsert, type Ima
|
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
5
|
export type { ImageNestedBuildMode, ImageCapabilities, ImagePodContract, ImageStatus, ImageVisibility, ImageIndexEntry, ImageIndexUpsert, ImageViewer, ImageListFilter, ImageRow, } from "./store-contracts.js";
|
|
6
6
|
export { mapImageRow as mapRow } from "./store-contracts.js";
|
|
7
|
+
/** PG translation of the `sandbox_image_index` DDL in tidb-pool.ts (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
|
|
8
|
+
* TINYINT(1)→SMALLINT(布尔列 PG 映射归一,clay 批 2026-07-26 —— 两方言读回同为 number,写入 0/1,
|
|
9
|
+
* 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
|
|
7
10
|
export declare const PG_IMAGE_INDEX_SCHEMA: string[];
|
|
11
|
+
/** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
|
|
8
12
|
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
13
|
+
/** Dual-dialect INDEX store. See the file header for the dialect-delta ledger. */
|
|
9
14
|
export declare class SqlImageIndex {
|
|
10
15
|
protected readonly db: SqlDriver;
|
|
11
16
|
constructor(db: SqlDriver);
|
|
17
|
+
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
12
18
|
private q;
|
|
19
|
+
/** Stable index id = sha256(repo@digest) — identity is the digest, not the mutable tag. */
|
|
13
20
|
static idFor(repo: string, digest: string): string;
|
|
21
|
+
/** Register or update an index row (idempotent on repo+digest). Returns the row id. */
|
|
14
22
|
upsert(e: ImageIndexUpsert): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* STATUS-MONOTONIC register (IMAGE-API-DESIGN.md §P2.10 MUST-FIX) — the auto-register path the bake runner uses.
|
|
25
|
+
*
|
|
26
|
+
* `upsert` does an UNCONDITIONAL upsert: a re-ingested `done` (at-least-once from the runner) or a P2 bake of
|
|
27
|
+
* a digest P3 already PROMOTED would flip a live `published` row back to `building`, and `latestPublished`
|
|
28
|
+
* (`WHERE status='published'`) then DE-PUBLISHES a live, in-use image → sandbox-create 404s. This guarded path
|
|
29
|
+
* NEVER downgrades a `published` row: on a fresh row it inserts (`status:'building'` by default); on an
|
|
30
|
+
* existing row it updates the manifest projection + bumps `updated_at` but writes `status` only when the row
|
|
31
|
+
* isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
|
|
32
|
+
* lifecycle, never a regression. Returns the row id.
|
|
33
|
+
*
|
|
34
|
+
* Distinct from the unconditional `upsert` (kept for the seed + the operator `/v1/images/register` path); this
|
|
35
|
+
* is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
|
|
36
|
+
* node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
|
|
37
|
+
*
|
|
38
|
+
* The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
|
|
39
|
+
* defense: once a row is `published` and live, a re-register must NOT downgrade `status` NOR flip the
|
|
40
|
+
* columns `latestPublished` filters on. wq64gmm5e: guarding only `status` left `visibility`/`tenant_id`
|
|
41
|
+
* (and `signed`) overwritable, so a re-register with visibility=private/tenant or a different tenant_id
|
|
42
|
+
* silently dropped a live row out of every viewer's `latestPublished` — an effective de-publish. ALL FOUR
|
|
43
|
+
* (status/visibility/tenant_id/signed) are gated identically in BOTH dialects; non-published rows still
|
|
44
|
+
* take the freshest projection.
|
|
45
|
+
*/
|
|
15
46
|
registerBuilding(e: ImageIndexUpsert): Promise<string>;
|
|
16
47
|
getById(id: string): Promise<ImageIndexEntry | null>;
|
|
48
|
+
/** By immutable digest — what a running sandbox was built from. */
|
|
17
49
|
getByDigest(digest: string): Promise<ImageIndexEntry | null>;
|
|
50
|
+
/** The newest PUBLISHED, viewer-visible digest for a profile — the `select` resolution.
|
|
51
|
+
* 🔴 genuinely-divergent shape (not just SQL text): TiDB's `visibilityClauseTidb` needs no placeholder
|
|
52
|
+
* index (anonymous `?`); PG's `visibilityClausePg` threads a running `$n` index the caller continues
|
|
53
|
+
* from. Also the null-safe ORDER BY trick differs ((build_date IS NULL) vs NULLS LAST) — same semantics. */
|
|
18
54
|
latestPublished(profile: string, viewer: ImageViewer): Promise<ImageIndexEntry | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Paginated, viewer-scoped catalog query (the UI's list). Keyset cursor on (created_at, id) desc.
|
|
57
|
+
* 🔴 genuinely-divergent shape: the PG arm threads an explicit `$n` index through every optional predicate
|
|
58
|
+
* (node-pg has no anonymous placeholder); the TiDB arm just appends `?` in call order. Kept as two full
|
|
59
|
+
* branches rather than a shared placeholder-numbering helper (sql-driver.ts STEP 3/4) — a reviewer sees each
|
|
60
|
+
* dialect's real query shape, not a generated abstraction over it.
|
|
61
|
+
*/
|
|
19
62
|
list(filter: ImageListFilter): Promise<{
|
|
20
63
|
entries: ImageIndexEntry[];
|
|
21
64
|
nextCursor: string | null;
|
|
22
65
|
}>;
|
|
66
|
+
/** Lifecycle transition (atomic-publish: building→published, or →failed/deprecated). */
|
|
23
67
|
setStatus(id: string, status: ImageStatus, opts?: {
|
|
24
68
|
supersedes?: string | null;
|
|
25
69
|
}): Promise<void>;
|
|
26
70
|
}
|
|
71
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
27
72
|
export declare class TiDBImageIndex extends SqlImageIndex {
|
|
28
73
|
constructor(pool: MySqlPool);
|
|
29
74
|
}
|
|
75
|
+
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
30
76
|
export declare class PgImageIndex extends SqlImageIndex {
|
|
31
77
|
constructor(pool: PgPool);
|
|
32
78
|
}
|