@sema-agent/server 1.323.0 → 2.0.1
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/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +995 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -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 +585 -1
- package/dist/config.d.ts +78 -2
- package/dist/config.js +874 -301
- 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 +530 -2129
- 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 +58 -2
- 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,7 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlobBackend — the content-addressed BYTE store for the durable FileSnapshotStore (E19 / 2c), DECOUPLED from the
|
|
3
|
+
* manifest. clay 2026-06-26: the conversation TRANSCRIPT stays DB-first; the ONLY thing that moves to an object store
|
|
4
|
+
* is the file-snapshot BLOBS (binary file content — a perf-suboptimal TiDB blob row; the tree bound is core's
|
|
5
|
+
* DEFAULT_SNAPSHOT_BOUNDS 256 MiB with NO per-file cap, hence the (a) SQL per-blob cap below). This seam
|
|
6
|
+
* lets the tidb/pg file-snapshot stores keep their MANIFEST in SQL (`snapshot_manifest`, the hot relPath→hash rows the
|
|
7
|
+
* transcript read NEVER touches) while routing the heavy bytes either to SQL (`snapshot_blob.bytes`, the DEFAULT — a
|
|
8
|
+
* MINIO-unset deployment is byte-identical to today) or to MinIO (the offload).
|
|
9
|
+
*
|
|
10
|
+
* THE SQL BLOB INDEX (clay 2026-06-26 design correction). EVEN under the MinIO backend, every blob keeps an INDEX row in
|
|
11
|
+
* `snapshot_blob` — `(blob_hash, byte_len, created_at)` with `bytes` NULL (the bytes live in MinIO). This is the load-
|
|
12
|
+
* bearing decision: ALL the reference-tracking (gcOrphanBlobs computes the orphan SET = `snapshot_blob` rows NOT IN
|
|
13
|
+
* `snapshot_manifest`) and the created_at GRACE WINDOW (delete-in-use protection) read that index, so they work
|
|
14
|
+
* IDENTICALLY for both backends. WITHOUT the index, a MinIO deployment would never GC its objects AND an E21 session
|
|
15
|
+
* purge would leave the file CONTENT in MinIO forever (a privacy/compliance breach). With it, a purged session's blobs
|
|
16
|
+
* become orphans (no manifest) and are EVENTUALLY collected by the async sweepOrphanBlobs reaper.
|
|
17
|
+
*
|
|
18
|
+
* ASYNC byte deletion (clay 2026-06-26): MinIO objects are NEVER deleted on the synchronous E21 purge / reap path (a
|
|
19
|
+
* synchronous external-store delete could block/fail the whole purge on a network blip). The purge deletes ONLY the SQL
|
|
20
|
+
* manifest rows → the blob becomes an orphan; the periodic `sweepOrphanBlobs` reaper (main.ts) deletes the MinIO objects
|
|
21
|
+
* + the index rows LATER, error-tolerantly (a single MinIO delete failure is skipped + retried next sweep) → bounded-
|
|
22
|
+
* window EVENTUAL CONSISTENCY. {@link BlobBackend.deleteBlobs} is therefore only ever called from the reaper.
|
|
23
|
+
*
|
|
24
|
+
* REUSE iron rule (core-service-boundary / no-stopgap): {@link SqlBlobBackend} EXTRACTS the `snapshot_blob` ops from the
|
|
25
|
+
* tidb/pg stores VERBATIM (the created_at-refreshing upsert, the SELECT, the grace-window orphan DELETE), so the SQL
|
|
26
|
+
* default path is byte-identical to before this refactor. {@link MinioBlobBackend} REUSES the existing zero-dependency
|
|
27
|
+
* {@link presignS3Url} SigV4 generator + `fetch` (the repo's no-SDK posture — OTLP + the k8s adapter are zero-dep too);
|
|
28
|
+
* it NEVER pulls in an S3 SDK, and it REUSES the SqlBlobBackend's index ops for the (blob_hash, byte_len, created_at)
|
|
29
|
+
* rows (the bytes-NULL index — no SQL drift between the two backends' reference-tracking).
|
|
30
|
+
*/
|
|
1
31
|
import { createHash } from "node:crypto";
|
|
2
32
|
import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
|
|
33
|
+
/** Orphan-blob GC grace (SHARED with the file-snapshot stores' BLOB_GC_GRACE_MS) — a blob is GC-eligible only if
|
|
34
|
+
* unreferenced AND untouched for this long; putBlob refreshes created_at on every touch (incl a dedup hit) so an
|
|
35
|
+
* in-flight snapshot's blobs are protected from delete-in-use. 1h ≫ any bounded snapshot. */
|
|
3
36
|
const BLOB_GC_GRACE_MS = 3_600_000;
|
|
37
|
+
/** (a)(clay 拍 a+c,2026-07-27)tidb 方言的默认 per-blob 帽 = **6 MiB,TiDB 自己的默认
|
|
38
|
+
* `txn-entry-size-limit`**。两堵墙真库实测(k3s8 tidb-test v7.5.1 默认配置,2026-07-27 探针):
|
|
39
|
+
* 7MiB=`entry too large`(entry 墙,默认 6291456)、31MiB=同、32MiB=`max_allowed_packet`(包墙,
|
|
40
|
+
* 64MiB ÷ ~2× 文本协议膨胀)——矮墙是 entry 的 6MiB,不是先前推测的包墙 ~31MiB。帽=诚实拒绝:
|
|
41
|
+
* 超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn 拿到可行动信息);**帽拒绝的
|
|
42
|
+
* 在默认配置下本来也落不进去 ⇒ 零回归**。调大过 entry 限/packet 的部署用 SNAPSHOT_BLOB_SQL_MAX_BYTES
|
|
43
|
+
* 放宽(两方言都生效)。**pg 方言默认不设帽**(扩展协议无这两堵墙,不人为降档)。D-1 附件战役同墙
|
|
44
|
+
* 教训:task-attachment-store.ts:19。 */
|
|
4
45
|
export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
|
|
46
|
+
/** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与
|
|
47
|
+
* transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */
|
|
5
48
|
export class BlobTooLargeError extends Error {
|
|
6
49
|
code = "blob_too_large_for_sql";
|
|
7
50
|
constructor(byteLen, capBytes) {
|
|
@@ -12,7 +55,11 @@ export class BlobTooLargeError extends Error {
|
|
|
12
55
|
this.name = "BlobTooLargeError";
|
|
13
56
|
}
|
|
14
57
|
}
|
|
58
|
+
/** Max concurrent MinIO object ops (HEAD/DELETE) per reap/sweep batch — bound the open sockets/presigns so a sweep with
|
|
59
|
+
* tens of thousands of orphans can't exhaust FDs / throttle MinIO (finding 22). The full hash set is processed in
|
|
60
|
+
* windows of this size; a slow/failed object never blocks the others (each is independently error-tolerant). */
|
|
15
61
|
const MINIO_OP_CONCURRENCY = 16;
|
|
62
|
+
/** Run `fn` over `items` with at most `limit` in flight at once (a tiny bounded-concurrency pool — no dependency). */
|
|
16
63
|
async function mapBounded(items, limit, fn) {
|
|
17
64
|
let i = 0;
|
|
18
65
|
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
@@ -23,15 +70,33 @@ async function mapBounded(items, limit, fn) {
|
|
|
23
70
|
});
|
|
24
71
|
await Promise.all(workers);
|
|
25
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* SQL BlobBackend — the `snapshot_blob` table ops EXTRACTED VERBATIM from the tidb/pg file-snapshot stores (the
|
|
75
|
+
* created_at-refreshing upsert, the bytes SELECT, the grace-window orphan DELETE→deleteBlobs, a has via SELECT). Two
|
|
76
|
+
* dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated
|
|
77
|
+
* real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct).
|
|
78
|
+
*
|
|
79
|
+
* NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the `NOT IN (SELECT … snapshot_manifest)` orphan
|
|
80
|
+
* filter (that reference check stays in the file-snapshot store, which computes the orphan hash SET from the manifest
|
|
81
|
+
* and hands it here). The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
|
|
82
|
+
* that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is
|
|
83
|
+
* harmless — the next sweep collects it).
|
|
84
|
+
*/
|
|
26
85
|
export class SqlBlobBackend {
|
|
27
86
|
dialect;
|
|
28
87
|
pool;
|
|
88
|
+
/** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */
|
|
29
89
|
capBytes;
|
|
30
90
|
constructor(dialect, pool, maxBytes) {
|
|
31
91
|
this.dialect = dialect;
|
|
32
92
|
this.pool = pool;
|
|
33
93
|
this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
|
|
34
94
|
}
|
|
95
|
+
/** The created_at-refreshing content-addressed upsert — VERBATIM the tidb/pg stores' `putBlobRow`. Refreshes
|
|
96
|
+
* created_at even on a dedup hit so a re-used/just-written blob is "touched=now" and GC-grace-protected.
|
|
97
|
+
* (a): an over-cap blob is REJECTED (typed {@link BlobTooLargeError}) BEFORE the driver ever sees the bytes —
|
|
98
|
+
* the alternative is an inscrutable packet-limit error deep in the driver, and on the HTTP face a 502 that
|
|
99
|
+
* invites the peer to retry a blob that can never land. */
|
|
35
100
|
async putBlob(hash, bytes) {
|
|
36
101
|
if (bytes.byteLength > this.capBytes)
|
|
37
102
|
throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
|
|
@@ -42,6 +107,8 @@ export class SqlBlobBackend {
|
|
|
42
107
|
await this.pool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES ($1,$2,$3,$4) ON CONFLICT (blob_hash) DO UPDATE SET created_at = EXCLUDED.created_at", [hash, bytes.byteLength, Buffer.from(bytes), new Date()]);
|
|
43
108
|
}
|
|
44
109
|
}
|
|
110
|
+
/** The bytes for `hash` (undefined if absent) — VERBATIM the tidb/pg stores' getBlob SELECT. A pool/query error
|
|
111
|
+
* PROPAGATES (a transient DB fault is loud, not a silent "missing blob" — parity with the MinIO backend's throw). */
|
|
45
112
|
async getBlob(hash) {
|
|
46
113
|
if (this.dialect === "tidb") {
|
|
47
114
|
const [rows] = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = ?", [hash]);
|
|
@@ -50,14 +117,22 @@ export class SqlBlobBackend {
|
|
|
50
117
|
const b = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = $1", [hash]);
|
|
51
118
|
return b.rows[0]?.bytes != null ? new Uint8Array(b.rows[0].bytes) : undefined;
|
|
52
119
|
}
|
|
120
|
+
/** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
|
|
53
121
|
async hasBlobs(hashes) {
|
|
54
122
|
return sqlHasBlobs(this.dialect, this.pool, hashes);
|
|
55
123
|
}
|
|
124
|
+
/** Byte-delete the given hashes (the orphan SET is computed by the caller from snapshot_manifest). Retains the
|
|
125
|
+
* grace-window guard so a just-written blob a concurrent in-flight snapshot needs is never delete-in-use — VERBATIM
|
|
126
|
+
* the tidb/pg stores' gcOrphanBlobs delete, minus the `NOT IN (SELECT … manifest)` clause (the caller already
|
|
127
|
+
* excluded referenced hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
|
|
56
128
|
async deleteBlobs(hashes) {
|
|
57
129
|
const deleted = await sqlDeleteIndexRowsPastGrace(this.dialect, this.pool, hashes);
|
|
58
130
|
return deleted.length;
|
|
59
131
|
}
|
|
60
132
|
}
|
|
133
|
+
/** SHARED index op — the subset of `hashes` present as a `snapshot_blob` row (the SQL INDEX). Used by SqlBlobBackend
|
|
134
|
+
* AND MinioBlobBackend (the index is the single source of presence truth for both — cheaper + consistent than a
|
|
135
|
+
* HEAD-per-hash on MinIO; the bytes-NULL MinIO index row still counts as present). */
|
|
61
136
|
async function sqlHasBlobs(dialect, pool, hashes) {
|
|
62
137
|
if (hashes.length === 0)
|
|
63
138
|
return new Set();
|
|
@@ -71,10 +146,16 @@ async function sqlHasBlobs(dialect, pool, hashes) {
|
|
|
71
146
|
const r = await pool.query(`SELECT blob_hash FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
|
|
72
147
|
return new Set(r.rows.map((row) => row.blob_hash));
|
|
73
148
|
}
|
|
149
|
+
/** SHARED index op(workspace 浏览面 #3,[1894]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len`
|
|
150
|
+
* 列(sqlHasBlobs 同款单 IN 查询;MinIO 后端的 bytes-NULL 索引行同样带 byte_len,两后端同源)。
|
|
151
|
+
* tree/file 面的 size 投影与「超限先拒后拉」都靠它——绝不为拿大小去 getBlob 整块字节。 */
|
|
74
152
|
export async function sqlBlobSizes(dialect, pool, hashes) {
|
|
75
153
|
if (hashes.length === 0)
|
|
76
154
|
return new Map();
|
|
77
155
|
const distinct = [...new Set(hashes)];
|
|
156
|
+
// 🔴 复审 F7:分块。workspace list 腿对每个快照各查一次全 manifest 的 unique hash——按 core 上限
|
|
157
|
+
// (maxFiles 10000)一次就是 1 万元素的 IN,`?limit=100` 则是 100 次这样的串行往返。PG 的 65535
|
|
158
|
+
// 参数协议墙虽未触到,但 1 万元素 IN 本身对计划器/网络包就是坏形。1000/块是保守值。
|
|
78
159
|
const CHUNK = 1000;
|
|
79
160
|
const out = new Map();
|
|
80
161
|
for (let i = 0; i < distinct.length; i += CHUNK) {
|
|
@@ -94,6 +175,10 @@ export async function sqlBlobSizes(dialect, pool, hashes) {
|
|
|
94
175
|
}
|
|
95
176
|
return out;
|
|
96
177
|
}
|
|
178
|
+
/** SHARED index op — DELETE the index rows for `hashes` that have passed the grace window (created_at < cutoff). Returns
|
|
179
|
+
* the hashes ACTUALLY deleted (so the MinIO backend deletes exactly the objects whose index rows it just removed — and
|
|
180
|
+
* a within-grace young orphan is left for a later sweep). Best-effort: a DB error is swallowed (under-delete is
|
|
181
|
+
* harmless; the next sweep retries). The grace guard (delete-in-use protection) is IDENTICAL across both backends. */
|
|
97
182
|
async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
98
183
|
if (hashes.length === 0)
|
|
99
184
|
return [];
|
|
@@ -101,6 +186,8 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
|
101
186
|
const cutoff = new Date(Date.now() - BLOB_GC_GRACE_MS);
|
|
102
187
|
if (dialect === "tidb") {
|
|
103
188
|
const ph = distinct.map(() => "?").join(",");
|
|
189
|
+
// MySQL/TiDB DELETE can't RETURNING — SELECT the grace-passed subset first, then DELETE it (the SELECT+DELETE
|
|
190
|
+
// race is benign: a row that flips referenced between them just isn't deleted = under-delete = harmless).
|
|
104
191
|
const grace = await pool
|
|
105
192
|
.query(`SELECT blob_hash FROM snapshot_blob WHERE created_at < ? AND blob_hash IN (${ph})`, [cutoff, ...distinct])
|
|
106
193
|
.then(([rows]) => rows.map((r) => String(r.blob_hash)))
|
|
@@ -119,10 +206,30 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
|
119
206
|
.catch(() => undefined);
|
|
120
207
|
return (r?.rows ?? []).map((row) => row.blob_hash);
|
|
121
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* MinIO BlobBackend — content-addressed bytes in an S3-compatible object store, via the zero-dependency
|
|
211
|
+
* {@link presignS3Url} SigV4 generator + `fetch` (NO S3 SDK; the repo's no-SDK posture). The object key is
|
|
212
|
+
* `keyPrefix + hash` (content-addressed → idempotent PUT, dedup across snapshots is automatic).
|
|
213
|
+
*
|
|
214
|
+
* THE SQL INDEX: the bytes go to MinIO, but a `snapshot_blob` INDEX row `(blob_hash, byte_len, created_at)` with bytes
|
|
215
|
+
* NULL is kept in SQL (same pool/dialect). This is what makes the orphan-reference computation + the created_at grace
|
|
216
|
+
* window + the §10 presence pre-check work identically to the SQL backend (see the file header). hasBlobs reads the
|
|
217
|
+
* index; putBlob PUTs then upserts the index (created_at-refresh on conflict); deleteBlobs deletes the grace-passed
|
|
218
|
+
* index rows AND the matching MinIO objects (called ONLY from the async sweep reaper).
|
|
219
|
+
*
|
|
220
|
+
* INTEGRITY (security-critical): getBlob VERIFIES sha256(bytes) === hash before returning. A content-addressed store's
|
|
221
|
+
* whole safety rests on bytes-match-hash; a mismatch means corruption (network) or POISONING (a hostile object written
|
|
222
|
+
* under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the
|
|
223
|
+
* importManifest content-address check the SQL stores already do.
|
|
224
|
+
*/
|
|
122
225
|
export class MinioBlobBackend {
|
|
123
226
|
cfg;
|
|
124
227
|
keyPrefix;
|
|
125
228
|
ttl;
|
|
229
|
+
/** The SQL index pool+dialect, or undefined for an index-LESS backend (the migration script + the unit suite, which
|
|
230
|
+
* exercise ONLY the object I/O — put/get/has-by-HEAD/delete — with no DB). When undefined, hasBlobs falls back to a
|
|
231
|
+
* HEAD-per-hash probe and deleteBlobs ignores the grace window (the script is a stop-the-world copy, not the live GC
|
|
232
|
+
* path). The file-snapshot stores ALWAYS pass the pool, so the live reference-tracking always has the index. */
|
|
126
233
|
indexPool;
|
|
127
234
|
indexDialect;
|
|
128
235
|
constructor(cfg, index) {
|
|
@@ -138,6 +245,8 @@ export class MinioBlobBackend {
|
|
|
138
245
|
return `${this.keyPrefix}${hash}`;
|
|
139
246
|
}
|
|
140
247
|
presign(hash, method) {
|
|
248
|
+
// presignS3Url only declares GET|PUT in its param type, but the SigV4 canonicalization is identical for any verb
|
|
249
|
+
// (host-only SignedHeaders, UNSIGNED-PAYLOAD) — HEAD/DELETE sign correctly with the same string math. Cast the verb.
|
|
141
250
|
return presignS3Url({
|
|
142
251
|
endpoint: this.cfg.endpoint,
|
|
143
252
|
bucket: this.cfg.bucket,
|
|
@@ -149,6 +258,14 @@ export class MinioBlobBackend {
|
|
|
149
258
|
expiresSec: this.ttl,
|
|
150
259
|
});
|
|
151
260
|
}
|
|
261
|
+
/** PUT the bytes under the content-addressed key, THEN upsert the SQL INDEX row (bytes NULL, created_at refresh on
|
|
262
|
+
* conflict — the SAME grace discipline as SqlBlobBackend, so a just-PUT blob an in-flight snapshot needs is touched=
|
|
263
|
+
* now and protected). Idempotent (same hash ⇒ overwrite-with-identical-bytes + created_at bump). A non-2xx PUT throws
|
|
264
|
+
* (an upload failure must surface — a silent drop would later read_failed on restore). The PUT happens BEFORE the
|
|
265
|
+
* index upsert so an index row never claims a blob the object store doesn't hold (an index row with no object would
|
|
266
|
+
* let hasBlobs report a phantom present; the reverse — an object with no index — is harmless and pruned by a bucket
|
|
267
|
+
* lifecycle if it ever happens). NOTE no post-PUT read-back verify on the LIVE path (perf): content-addressing
|
|
268
|
+
* catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */
|
|
152
269
|
async putBlob(hash, bytes) {
|
|
153
270
|
const url = this.presign(hash, "PUT");
|
|
154
271
|
const res = await fetch(url, { method: "PUT", body: bytes });
|
|
@@ -156,6 +273,9 @@ export class MinioBlobBackend {
|
|
|
156
273
|
const detail = await res.text().catch(() => "");
|
|
157
274
|
throw new Error(`MinioBlobBackend.putBlob ${hash}: HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
158
275
|
}
|
|
276
|
+
// Upsert the bytes-NULL INDEX row (byte_len + created_at) so the orphan/grace reference-tracking + presence checks
|
|
277
|
+
// see this blob. Same created_at-refresh-on-conflict as the SQL backend. (No index pool ⇒ object-only mode = the
|
|
278
|
+
// migration/unit path; the live stores always pass it.)
|
|
159
279
|
if (this.indexPool && this.indexDialect) {
|
|
160
280
|
if (this.indexDialect === "tidb") {
|
|
161
281
|
await this.indexPool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES (?,?,NULL,?) ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)", [hash, bytes.byteLength, new Date()]);
|
|
@@ -165,6 +285,12 @@ export class MinioBlobBackend {
|
|
|
165
285
|
}
|
|
166
286
|
}
|
|
167
287
|
}
|
|
288
|
+
/** GET the bytes for `hash`, then VERIFY content-address (sha256(bytes)===hash). THREE branches (finding 11):
|
|
289
|
+
* - 404 → undefined (genuinely ABSENT — the file-snapshot store treats undefined as a missing blob).
|
|
290
|
+
* - sha256 MISMATCH → undefined (corruption/poisoning; treat corrupt-as-absent, fail-CLOSED, NEVER wrong bytes).
|
|
291
|
+
* - a NON-2xx-NON-404 (5xx/throttle) OR a network/fetch REJECT → THROW (a TRANSIENT fault must fail LOUD so the
|
|
292
|
+
* restore/import surfaces an error, NOT silently drop a file as "missing"; the SQL backend likewise propagates a
|
|
293
|
+
* pool error). The snapshot store's restore() wraps applyManifest so this throw becomes a {ok:false} restore_failed. */
|
|
168
294
|
async getBlob(hash) {
|
|
169
295
|
const url = this.presign(hash, "GET");
|
|
170
296
|
let res;
|
|
@@ -175,19 +301,24 @@ export class MinioBlobBackend {
|
|
|
175
301
|
throw new Error(`MinioBlobBackend.getBlob ${hash}: transient fetch failure: ${err.message}`);
|
|
176
302
|
}
|
|
177
303
|
if (res.status === 404)
|
|
178
|
-
return undefined;
|
|
304
|
+
return undefined; // genuinely absent
|
|
179
305
|
if (!res.ok) {
|
|
180
306
|
const detail = await res.text().catch(() => "");
|
|
181
307
|
throw new Error(`MinioBlobBackend.getBlob ${hash}: transient HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
182
308
|
}
|
|
183
309
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
310
|
+
// content-address integrity: the bytes MUST hash to the key. A mismatch = corrupt/poisoned → undefined (absent), NOT wrong bytes.
|
|
184
311
|
if (createHash("sha256").update(buf).digest("hex") !== hash)
|
|
185
312
|
return undefined;
|
|
186
313
|
return buf;
|
|
187
314
|
}
|
|
315
|
+
/** The subset of `hashes` present — reads the SQL INDEX (cheaper than a HEAD-per-hash AND consistent with the SQL
|
|
316
|
+
* backend). Falls back to a presigned HEAD per hash ONLY in object-only mode (no index pool: the migration / unit
|
|
317
|
+
* path). The caller passes DISTINCT hashes (migration idempotency batch / reap orphan set / §10 presence pre-check). */
|
|
188
318
|
async hasBlobs(hashes) {
|
|
189
319
|
if (this.indexPool && this.indexDialect)
|
|
190
320
|
return sqlHasBlobs(this.indexDialect, this.indexPool, hashes);
|
|
321
|
+
// object-only mode (migration/unit): HEAD probe, bounded concurrency.
|
|
191
322
|
const distinct = [...new Set(hashes)];
|
|
192
323
|
const present = new Set();
|
|
193
324
|
await mapBounded(distinct, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
@@ -197,13 +328,22 @@ export class MinioBlobBackend {
|
|
|
197
328
|
present.add(hash);
|
|
198
329
|
}
|
|
199
330
|
catch {
|
|
331
|
+
/* treat an unreachable HEAD as absent — a false-absent only re-PUTs (idempotent) or re-checks; never a false-present */
|
|
200
332
|
}
|
|
201
333
|
});
|
|
202
334
|
return present;
|
|
203
335
|
}
|
|
336
|
+
/** D-1 孤儿对象 GC(clay 拍 2026-07-28):列举本后端 keyPrefix 下的全部对象(ListObjectsV2 分页,
|
|
337
|
+
* 零依赖 XML 取 <Key>/<LastModified>/<NextContinuationToken>)。**server 端 sweep 专用**——presign 的
|
|
338
|
+
* bucket-GET 列举能力绝不外发。响应非 2xx 即 throw(sweep 调用方自 catch:列举失败=本轮跳过,
|
|
339
|
+
* 绝不把「列不出来」当「没有孤儿」)。 */
|
|
204
340
|
async *listObjects() {
|
|
205
341
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
206
342
|
let token;
|
|
343
|
+
// 🔴 复审 F3:分页上限。异常/代理后端回环或恒定的 continuation-token 会让本循环无界翻页
|
|
344
|
+
// (实测可翻 5001 页仍不停),叠加维护 tick 就是逐轮堆积。超限 fail-loud —— 与本函数既有的
|
|
345
|
+
// 「列不出来 ≠ 没有孤儿」口径一致(宁可整轮失败也不给出残缺的「孤儿全集」)。
|
|
346
|
+
// 1000 页 × 1000 键/页 = 100 万对象,远超任何真实附件语料;真撞上=后端异常而非规模问题。
|
|
207
347
|
const MAX_PAGES = 1000;
|
|
208
348
|
let pages = 0;
|
|
209
349
|
do {
|
|
@@ -224,14 +364,15 @@ export class MinioBlobBackend {
|
|
|
224
364
|
if (!res.ok)
|
|
225
365
|
throw new Error(`listObjects: MinIO ${res.status}`);
|
|
226
366
|
const xml = await res.text();
|
|
367
|
+
// <Contents><Key>k</Key>…<LastModified>ISO</LastModified>…</Contents> —— 逐块取,键序无假设。
|
|
227
368
|
for (const m of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
|
|
228
369
|
const block = m[1];
|
|
229
370
|
const key = /<Key>([^<]*)<\/Key>/.exec(block)?.[1];
|
|
230
371
|
const lm = /<LastModified>([^<]*)<\/LastModified>/.exec(block)?.[1];
|
|
231
372
|
if (key === undefined || !key.startsWith(this.keyPrefix))
|
|
232
|
-
continue;
|
|
373
|
+
continue; // prefix 外键(理论不可达)跳过
|
|
233
374
|
const ms = lm ? Date.parse(lm) : NaN;
|
|
234
|
-
yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
375
|
+
yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 坏时间戳=按「新」处理(grace 保护,宁漏勿误删)
|
|
235
376
|
}
|
|
236
377
|
const truncated = /<IsTruncated>true<\/IsTruncated>/.test(xml);
|
|
237
378
|
token = truncated ? /<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(xml)?.[1] : undefined;
|
|
@@ -239,6 +380,8 @@ export class MinioBlobBackend {
|
|
|
239
380
|
throw new Error("listObjects: truncated response without NextContinuationToken");
|
|
240
381
|
} while (token !== undefined);
|
|
241
382
|
}
|
|
383
|
+
/** 单对象 HEAD 探针(复审 B-F1 的删前复查面):返回当前 LastModified(缺席=对象已不在)。
|
|
384
|
+
* put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */
|
|
242
385
|
async headObject(hash) {
|
|
243
386
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
244
387
|
const res = await f(this.presign(hash, "HEAD"), { method: "HEAD" });
|
|
@@ -246,17 +389,28 @@ export class MinioBlobBackend {
|
|
|
246
389
|
return undefined;
|
|
247
390
|
const lm = res.headers.get("last-modified");
|
|
248
391
|
const ms = lm ? Date.parse(lm) : NaN;
|
|
249
|
-
return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
392
|
+
return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 无/坏时间戳=按「新」处理(宁漏勿误删)
|
|
250
393
|
}
|
|
394
|
+
/** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge
|
|
395
|
+
* path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index
|
|
396
|
+
* rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp
|
|
397
|
+
* the backend reads), then delete the MinIO objects ONLY for those grace-passed hashes (so a just-PUT blob an
|
|
398
|
+
* in-flight snapshot needs is NOT delete-in-use). ERROR-TOLERANT + BOUNDED: each object delete is independent (a
|
|
399
|
+
* failure is skipped → retried next sweep), at most MINIO_OP_CONCURRENCY in flight (no FD/throttle storm). In
|
|
400
|
+
* object-only mode (no index pool: the migration/unit cleanup path) it deletes every given hash with no grace. */
|
|
251
401
|
async deleteBlobs(hashes) {
|
|
252
402
|
if (hashes.length === 0)
|
|
253
403
|
return 0;
|
|
404
|
+
// Determine which hashes are grace-passed AND remove their index rows (so the index no longer claims them present).
|
|
405
|
+
// Order: index-row DELETE FIRST, then the object DELETE — if the object delete fails, the orphan re-appears only as
|
|
406
|
+
// an object with no index, which a later sweep can't re-target; acceptable (under-delete is harmless, and a bucket
|
|
407
|
+
// lifecycle rule prunes index-less objects). Doing it index-first keeps hasBlobs honest immediately.
|
|
254
408
|
let toDelete;
|
|
255
409
|
if (this.indexPool && this.indexDialect) {
|
|
256
410
|
toDelete = await sqlDeleteIndexRowsPastGrace(this.indexDialect, this.indexPool, hashes);
|
|
257
411
|
}
|
|
258
412
|
else {
|
|
259
|
-
toDelete = [...new Set(hashes)];
|
|
413
|
+
toDelete = [...new Set(hashes)]; // object-only mode (migration/unit cleanup): no grace, delete all given
|
|
260
414
|
}
|
|
261
415
|
if (toDelete.length === 0)
|
|
262
416
|
return 0;
|
|
@@ -264,13 +418,16 @@ export class MinioBlobBackend {
|
|
|
264
418
|
await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
265
419
|
try {
|
|
266
420
|
const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE" });
|
|
421
|
+
// 🔴 复审 F4:统计**真实**删除数——index-less(附件)分支此前恒返回入参长度,MinIO 完全不可写
|
|
422
|
+
// 时指标/日志照报「清理成功」。2xx/404(已不在)都算删成功;其余=失败,不计数。
|
|
267
423
|
if (res.ok || res.status === 404)
|
|
268
424
|
okCount++;
|
|
269
425
|
}
|
|
270
426
|
catch {
|
|
427
|
+
/* error-tolerant: a failed/timed-out object delete is skipped → retried next sweep (eventual consistency) */
|
|
271
428
|
}
|
|
272
429
|
});
|
|
273
|
-
return okCount;
|
|
430
|
+
return okCount; // the index rows we removed = the work done (best-effort object deletes follow async)
|
|
274
431
|
}
|
|
275
432
|
}
|
|
276
433
|
//# sourceMappingURL=blob-backend.js.map
|
|
@@ -1,28 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-replica circuit-breaker state (core 1.38 `BreakerState`) — SINGLE-FILE DUAL-DIALECT
|
|
3
|
+
* (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBBreakerState` /
|
|
4
|
+
* `PgBreakerState` class names survive as thin ctor subclasses (src/plugins/tidb-breaker-state.ts +
|
|
5
|
+
* src/plugins/pg-breaker-state.ts are now re-export shims) so every consumer (store-backend.ts, the
|
|
6
|
+
* fake-pool unit suites, the real dual-DB integration suite) is untouched.
|
|
7
|
+
*
|
|
8
|
+
* core's `createCircuitBreakerBrain` defaults to a per-PROCESS Map → on a multi-replica deployment
|
|
9
|
+
* every replica re-discovers a provider outage independently (slow fleet-wide reaction). This backs
|
|
10
|
+
* the breaker with the shared `circuit_breaker` table so a trip on one replica is learned by the others.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT — `BreakerState.get/set` are **synchronous** (core calls them on the hot path of every
|
|
13
|
+
* brain invocation; a DB round-trip per call is untenable). So this is a **write-through cache**:
|
|
14
|
+
* - `get` / `set` operate on an in-process Map synchronously (so the local breaker is always correct);
|
|
15
|
+
* - `set` ALSO fires an async write-through to the shared table (best-effort; a DB hiccup must never
|
|
16
|
+
* break the request path — breaker state is advisory);
|
|
17
|
+
* - a periodic `refresh()` pulls the shared table back into the cache so this replica learns peers'
|
|
18
|
+
* trips. Consistency is therefore **eventual** (a replica may send a few extra requests before it
|
|
19
|
+
* learns a peer already opened the breaker — self-healing, never wrong in a dangerous direction).
|
|
20
|
+
*
|
|
21
|
+
* Strict per-call linearizable state would need an async `BreakerState` from core (a separate effort);
|
|
22
|
+
* for a consultative breaker, eventually-consistent write-through is the right trade (see core design/20).
|
|
23
|
+
*
|
|
24
|
+
* ── Dialect deltas, kept EXPLICIT (the ONLY place the two engines actually differ) ─────────────────
|
|
25
|
+
* - placeholders `?` vs `$n`
|
|
26
|
+
* - upsert form: `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (breaker_key) DO UPDATE SET
|
|
27
|
+
* … EXCLUDED.col`
|
|
28
|
+
* - schema ownership: TiDB DDL lives centrally in tidb-pool.ts; PG DDL is self-contained here
|
|
29
|
+
* (`PG_BREAKER_STATE_SCHEMA` / `ensureSchema`, re-exported off pg-breaker-state.ts) — disjoint table,
|
|
30
|
+
* so a standalone idempotent apply is safe (no central-schema shadowing), mirroring every other PG store.
|
|
31
|
+
* Everything else (write-through logic, the pending-guard race fix, refresh/prune, row→snapshot mapping,
|
|
32
|
+
* opened_at Date handling) is IDENTICAL — the SqlDriver seam (result unwrapping) is what let the row
|
|
33
|
+
* mapping collapse to one body; both drivers hand back the same `SqlRow` shape.
|
|
34
|
+
*
|
|
35
|
+
* Kept in lock-step with the real engines by test/pg-breaker-state-integration.test.ts (runs the SAME
|
|
36
|
+
* scenarios on BOTH real engines) and the cross-instance concurrency pin in
|
|
37
|
+
* test/file-stores-cross-instance-concurrency.test.ts (last-writer-wins overwrite semantics — a design
|
|
38
|
+
* question, not a defect; see that file's comment for the full reasoning).
|
|
39
|
+
*/
|
|
1
40
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
41
|
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
42
|
import type { BreakerSnapshot, BreakerState } from "@sema-agent/core";
|
|
4
43
|
import { type SqlDriver } from "./sql-driver.js";
|
|
44
|
+
/** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other
|
|
45
|
+
* stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives
|
|
46
|
+
* centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
|
|
5
47
|
export declare const PG_BREAKER_STATE_SCHEMA: string[];
|
|
48
|
+
/** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
|
|
6
49
|
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
50
|
+
/** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
|
|
7
51
|
export declare class SqlBreakerState implements BreakerState {
|
|
8
52
|
protected readonly db: SqlDriver;
|
|
53
|
+
/** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
|
|
9
54
|
private readonly onWriteFail?;
|
|
10
55
|
private cache;
|
|
11
56
|
private timer?;
|
|
57
|
+
/** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune
|
|
58
|
+
* these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core
|
|
59
|
+
* 1.40.0 council finding #5). */
|
|
12
60
|
private readonly pending;
|
|
13
61
|
private writeFailStreak;
|
|
14
|
-
constructor(db: SqlDriver,
|
|
62
|
+
constructor(db: SqlDriver,
|
|
63
|
+
/** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
|
|
64
|
+
onWriteFail?: ((streak: number) => void) | undefined);
|
|
65
|
+
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
15
66
|
private q;
|
|
16
67
|
get(key: string): BreakerSnapshot | undefined;
|
|
17
68
|
set(key: string, snap: BreakerSnapshot): void;
|
|
18
69
|
private writeThrough;
|
|
70
|
+
/**
|
|
71
|
+
* Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that
|
|
72
|
+
* vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet),
|
|
73
|
+
* else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent:
|
|
74
|
+
* a replica is at most one refresh interval behind a peer, never wrong in a data-losing way.
|
|
75
|
+
* `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter).
|
|
76
|
+
*
|
|
77
|
+
* SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()`
|
|
78
|
+
* (same precedent as image-bake-store-sql.ts's queued-bake lookup).
|
|
79
|
+
*/
|
|
19
80
|
refresh(): Promise<void>;
|
|
81
|
+
/** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
|
|
20
82
|
startRefresh(intervalMs?: number): this;
|
|
21
83
|
stop(): void;
|
|
22
84
|
}
|
|
85
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
23
86
|
export declare class TiDBBreakerState extends SqlBreakerState {
|
|
24
87
|
constructor(pool: MySqlPool, onWriteFail?: (streak: number) => void);
|
|
25
88
|
}
|
|
89
|
+
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
26
90
|
export declare class PgBreakerState extends SqlBreakerState {
|
|
27
91
|
constructor(pool: PgPool, onWriteFail?: (streak: number) => void);
|
|
28
92
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
|
+
/** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other
|
|
3
|
+
* stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives
|
|
4
|
+
* centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
|
|
2
5
|
export const PG_BREAKER_STATE_SCHEMA = [
|
|
3
6
|
`CREATE TABLE IF NOT EXISTS circuit_breaker (
|
|
4
7
|
breaker_key VARCHAR(190) NOT NULL,
|
|
@@ -9,21 +12,29 @@ export const PG_BREAKER_STATE_SCHEMA = [
|
|
|
9
12
|
PRIMARY KEY (breaker_key)
|
|
10
13
|
)`,
|
|
11
14
|
];
|
|
15
|
+
/** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
|
|
12
16
|
export async function ensureSchema(pool) {
|
|
13
17
|
for (const stmt of PG_BREAKER_STATE_SCHEMA)
|
|
14
18
|
await pool.query(stmt);
|
|
15
19
|
}
|
|
20
|
+
/** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
|
|
16
21
|
export class SqlBreakerState {
|
|
17
22
|
db;
|
|
18
23
|
onWriteFail;
|
|
19
24
|
cache = new Map();
|
|
20
25
|
timer;
|
|
26
|
+
/** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune
|
|
27
|
+
* these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core
|
|
28
|
+
* 1.40.0 council finding #5). */
|
|
21
29
|
pending = new Set();
|
|
22
30
|
writeFailStreak = 0;
|
|
23
|
-
constructor(db,
|
|
31
|
+
constructor(db,
|
|
32
|
+
/** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
|
|
33
|
+
onWriteFail) {
|
|
24
34
|
this.db = db;
|
|
25
35
|
this.onWriteFail = onWriteFail;
|
|
26
36
|
}
|
|
37
|
+
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
27
38
|
q(tidb, pg) {
|
|
28
39
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
29
40
|
}
|
|
@@ -31,9 +42,9 @@ export class SqlBreakerState {
|
|
|
31
42
|
return this.cache.get(key);
|
|
32
43
|
}
|
|
33
44
|
set(key, snap) {
|
|
34
|
-
this.cache.set(key, snap);
|
|
35
|
-
this.pending.add(key);
|
|
36
|
-
void this.writeThrough(key, snap);
|
|
45
|
+
this.cache.set(key, snap); // synchronous: the local breaker hot path sees it immediately
|
|
46
|
+
this.pending.add(key); // guard the in-flight write from a concurrent refresh
|
|
47
|
+
void this.writeThrough(key, snap); // async, best-effort; never throws into the caller
|
|
37
48
|
}
|
|
38
49
|
async writeThrough(key, snap) {
|
|
39
50
|
try {
|
|
@@ -46,18 +57,41 @@ export class SqlBreakerState {
|
|
|
46
57
|
try {
|
|
47
58
|
this.onWriteFail?.(0);
|
|
48
59
|
}
|
|
49
|
-
catch { }
|
|
50
|
-
}
|
|
60
|
+
catch { /* observability never breaks advisory state */ }
|
|
61
|
+
} // the new value is now in the DB → refresh may reconcile this key again
|
|
62
|
+
// KNOWN/ACCEPTED (audit B): two rapid set()s on the SAME key race their async writeThroughs — the
|
|
63
|
+
// older write can land last (no version column), so a later refresh() may briefly revert this
|
|
64
|
+
// replica to the older snapshot (e.g. re-close a just-opened breaker). Advisory state: the very
|
|
65
|
+
// next failure re-opens it locally (set() is synchronous on the hot path) and re-publishes. A
|
|
66
|
+
// monotonic guard would need versioned rows + CAS; not worth it for an advisory cache.
|
|
51
67
|
}
|
|
52
68
|
catch {
|
|
53
69
|
try {
|
|
54
70
|
this.onWriteFail?.(++this.writeFailStreak);
|
|
55
71
|
}
|
|
56
|
-
catch { }
|
|
72
|
+
catch { /* ditto */ } // LOW: previously a bare swallow
|
|
73
|
+
// Advisory state — swallow; a DB outage must not turn into a brain-call failure. Crucially we KEEP
|
|
74
|
+
// `pending` on failure: the DB still holds the OLD value, so releasing the guard here would let the
|
|
75
|
+
// next refresh() clobber this replica's just-set (correct) local value — silently resetting an open
|
|
76
|
+
// breaker toward a known-bad backend. The guard self-heals on the next successful set()/write.
|
|
57
77
|
}
|
|
58
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that
|
|
81
|
+
* vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet),
|
|
82
|
+
* else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent:
|
|
83
|
+
* a replica is at most one refresh interval behind a peer, never wrong in a data-losing way.
|
|
84
|
+
* `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter).
|
|
85
|
+
*
|
|
86
|
+
* SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()`
|
|
87
|
+
* (same precedent as image-bake-store-sql.ts's queued-bake lookup).
|
|
88
|
+
*/
|
|
59
89
|
async refresh() {
|
|
60
90
|
try {
|
|
91
|
+
// Snapshot the pending set BEFORE the read: a key whose write-through is in-flight when the SELECT starts may
|
|
92
|
+
// not be in the SELECT's snapshot yet, and its writeThrough can `pending.delete` DURING the await — so a
|
|
93
|
+
// check-after-read (`this.pending.has`) alone would let the stale row clobber the just-set local value. Skip a
|
|
94
|
+
// key pending NOW or at read-start (union) to actually close the set()-vs-refresh race (#5), not just narrow it.
|
|
61
95
|
const pendingAtStart = new Set(this.pending);
|
|
62
96
|
const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
|
|
63
97
|
const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
|
|
@@ -66,7 +100,7 @@ export class SqlBreakerState {
|
|
|
66
100
|
const key = String(r.breaker_key);
|
|
67
101
|
seen.add(key);
|
|
68
102
|
if (guarded(key))
|
|
69
|
-
continue;
|
|
103
|
+
continue; // don't clobber a locally-pending newer value (#5)
|
|
70
104
|
this.cache.set(key, {
|
|
71
105
|
phase: r.phase,
|
|
72
106
|
failures: Number(r.failures),
|
|
@@ -78,8 +112,10 @@ export class SqlBreakerState {
|
|
|
78
112
|
this.cache.delete(key);
|
|
79
113
|
}
|
|
80
114
|
catch {
|
|
115
|
+
// Keep the last-known cache on a transient read failure (fail static, don't blank the breaker).
|
|
81
116
|
}
|
|
82
117
|
}
|
|
118
|
+
/** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
|
|
83
119
|
startRefresh(intervalMs = 5_000) {
|
|
84
120
|
this.timer ??= setInterval(() => void this.refresh(), intervalMs);
|
|
85
121
|
this.timer.unref?.();
|
|
@@ -91,11 +127,13 @@ export class SqlBreakerState {
|
|
|
91
127
|
this.timer = undefined;
|
|
92
128
|
}
|
|
93
129
|
}
|
|
130
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
94
131
|
export class TiDBBreakerState extends SqlBreakerState {
|
|
95
132
|
constructor(pool, onWriteFail) {
|
|
96
133
|
super(mysqlDriver(pool), onWriteFail);
|
|
97
134
|
}
|
|
98
135
|
}
|
|
136
|
+
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
99
137
|
export class PgBreakerState extends SqlBreakerState {
|
|
100
138
|
constructor(pool, onWriteFail) {
|
|
101
139
|
super(pgDriver(pool), onWriteFail);
|