@sema-agent/server 1.323.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 +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,28 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable `CheckpointStore` (core 1.67 durable-checkpoint / design/45) — SINGLE-FILE DUAL-DIALECT
|
|
3
|
+
* (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBCheckpointStore` /
|
|
4
|
+
* `PgCheckpointStore` class names survive as thin ctor subclasses so every consumer (store-backend.ts,
|
|
5
|
+
* local-checkpoint-store.ts, the chaos/ownership/blackbox suites) is untouched.
|
|
6
|
+
*
|
|
7
|
+
* This is the durable backend that makes a policy `ask` survive cross-replica/crash/time as a suspended
|
|
8
|
+
* task an operator can `resume` from any instance. It implements core's `CheckpointStore` (put create-once
|
|
9
|
+
* / get / atomic CAS resolve / reopen / reap), modeled on the proven approval-store CAS, plus two concerns
|
|
10
|
+
* the core interface doesn't own but the service does (DURABLE-CHECKPOINT-PLAN §4.4a):
|
|
11
|
+
* - `checkpoint_ctx` (sessionId-keyed): the service-side scenario rebuild inputs for `resume`'s taskConfig —
|
|
12
|
+
* core's `put(token, cp)` can't carry it (it's service "profile", not the generic core checkpoint).
|
|
13
|
+
* - operator-queue helpers (`listPending` / `findPendingTokenBySession`) that surface NON-secret fields and
|
|
14
|
+
* map a session → its pending token INTERNALLY — the capability token never leaves the service (token-as-auth).
|
|
15
|
+
*
|
|
16
|
+
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
17
|
+
* - `?` placeholders vs `$n` (and the OPTIONAL `AND rev = ?` / `AND rev = $5` OCC tail)
|
|
18
|
+
* - `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (session_id) DO UPDATE SET … EXCLUDED.col`
|
|
19
|
+
* - dup-key errno `1062` (ER_DUP_ENTRY) vs SQLSTATE `23505`
|
|
20
|
+
* - null-safe owner compare: `<=>` vs `IS NOT DISTINCT FROM` — AND the parameter arity that follows
|
|
21
|
+
* (`?` cannot be reused, so the TiDB deleteBySession binds sessionId TWICE; PG reuses `$1`)
|
|
22
|
+
* - affectedRows vs rowCount (via SqlDriver)
|
|
23
|
+
* - JSON binding: TiDB `JSON.stringify` verbatim vs PG `pgProtocolJsonStringify` + `$n::jsonb` casts.
|
|
24
|
+
* PG's envelope is LOSSLESS (codex R4-H1): tool_input is the operator's review surface and must agree
|
|
25
|
+
* with `checkpoint.pendingAction.args` at the NUL position — redact/truncate is the display contract,
|
|
26
|
+
* byte rewriting is not.
|
|
27
|
+
* - schema ownership: TiDB DDL in tidb-pool.ts, PG DDL centrally in pg-pool.ts (neither store creates tables).
|
|
28
|
+
*/
|
|
1
29
|
import { createHash } from "node:crypto";
|
|
2
30
|
import { CheckpointError, validatePendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
|
|
3
31
|
import { redactDeep } from "../trace/redact.js";
|
|
4
32
|
import { pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
5
33
|
import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
|
|
6
34
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
35
|
+
/** Cap the persisted/served pending-approval args so a pathological tool payload can't bloat the checkpoint row
|
|
36
|
+
* or the operator-queue response (the BFF renders this in an approval card; a few KB is plenty). */
|
|
7
37
|
const MAX_TOOL_INPUT_CHARS = 8192;
|
|
38
|
+
/**
|
|
39
|
+
* design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
|
|
40
|
+
* checkpoint's lifetime, stamped at put() into `terminal_at` INDEPENDENT of the per-approval `deadline`. The
|
|
41
|
+
* SLA-timer (D-D) does the fine, per-gate-kind resolve-deny; THIS coarse backstop ensures even a pending row
|
|
42
|
+
* with a NULL `deadline` (no approval TTL configured) is eventually GC'd if the SLA service dies — closing a
|
|
43
|
+
* forever-leak of a never-resolved suspension (the current `reap`/`reapExpired` only catch non-NULL deadlines).
|
|
44
|
+
* Generous (default 30d) so it never pre-empts a legitimately long-lived human/irreversible_ask gate;
|
|
45
|
+
* env-overridable. Clamped ≥1min so a misconfig can't expire live suspensions instantly.
|
|
46
|
+
*/
|
|
8
47
|
export const TERMINAL_BACKSTOP_MS = Math.max(60_000, Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) || 30 * 86_400_000);
|
|
9
|
-
|
|
48
|
+
/**
|
|
49
|
+
* design/80 D-D (adversarial fix): the crash-safe `terminal_at` backstop must fall STRICTLY AFTER any SLA
|
|
50
|
+
* `deadline`, never AT it. `terminal_at = max(createdAt+backstop, deadline)` made the two coincide whenever an
|
|
51
|
+
* operator tuned APPROVAL_TERMINAL_BACKSTOP_MS at/below the SLA — and reapExpired's terminal_at-branch (which
|
|
52
|
+
* has NO gate_kind filter) then abort-EXPIRED a human/irreversible_ask gate in the SAME tick the deny-sweep
|
|
53
|
+
* wanted to gracefully DENY it, racing it away. Adding this grace to the deadline term guarantees the deny-sweep
|
|
54
|
+
* at least this window of clean ticks before the absolute backstop can fire. Far smaller than the backstop, so
|
|
55
|
+
* it never meaningfully delays the eventual crash-safe GC.
|
|
56
|
+
*/
|
|
57
|
+
export const TERMINAL_GRACE_MS = 3_600_000; // exported for the LOCAL twin's read-time terminal_at derivation (anti-drift) // 1h — many reaper intervals of deny-sweep runway past the SLA deadline
|
|
58
|
+
/**
|
|
59
|
+
* The capability token IS the resume credential (token-as-auth) — anyone who reads it can impersonate a
|
|
60
|
+
* resume, so it must never reach the logs (which fan out to a log-aggregation pipeline). For the diagnostic
|
|
61
|
+
* resume-read-path log we emit a stable, non-reversible fingerprint instead, enough to correlate get↔resolve.
|
|
62
|
+
*/
|
|
10
63
|
export function tokenFingerprint(token) {
|
|
11
64
|
return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
12
65
|
}
|
|
66
|
+
/** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
|
|
67
|
+
* matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
|
|
68
|
+
* Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
|
|
13
69
|
export function boundedToolInput(args) {
|
|
14
70
|
if (args === undefined)
|
|
15
71
|
return null;
|
|
16
72
|
const redacted = redactDeep(args);
|
|
17
73
|
const json = JSON.stringify(redacted);
|
|
18
74
|
if (json === undefined)
|
|
19
|
-
return null;
|
|
75
|
+
return null; // non-serializable (e.g. a bare function) — nothing to surface
|
|
20
76
|
if (json.length > MAX_TOOL_INPUT_CHARS)
|
|
21
77
|
return { truncated: true, bytes: json.length };
|
|
22
78
|
return redacted;
|
|
23
79
|
}
|
|
24
|
-
const DUP_ENTRY = 1062;
|
|
80
|
+
const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
|
|
25
81
|
const PG_UNIQUE_VIOLATION = "23505";
|
|
82
|
+
/** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
|
|
26
83
|
export class SqlCheckpointStore {
|
|
27
84
|
db;
|
|
28
85
|
logger;
|
|
@@ -30,9 +87,12 @@ export class SqlCheckpointStore {
|
|
|
30
87
|
this.db = db;
|
|
31
88
|
this.logger = logger;
|
|
32
89
|
}
|
|
90
|
+
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
33
91
|
q(tidb, pg) {
|
|
34
92
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
35
93
|
}
|
|
94
|
+
/** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
|
|
95
|
+
* (R4-H1 — the operator's review surface must agree with the executed args at the NUL position). */
|
|
36
96
|
json(value, label) {
|
|
37
97
|
return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
|
|
38
98
|
}
|
|
@@ -41,9 +101,12 @@ export class SqlCheckpointStore {
|
|
|
41
101
|
? e?.errno === DUP_ENTRY
|
|
42
102
|
: e?.code === PG_UNIQUE_VIOLATION;
|
|
43
103
|
}
|
|
104
|
+
/** Create-once. core mints the token (`mintCheckpointToken`) and calls this during suspend. */
|
|
44
105
|
async put(token, cp) {
|
|
45
106
|
const pa = cp.pendingAction;
|
|
46
|
-
const version = checkpointVersionOf(cp);
|
|
107
|
+
const version = checkpointVersionOf(cp); // core helper: cp.version ?? 0 (1.67/1.68-era no-field = legacy 0)
|
|
108
|
+
// Persist the (redacted, bounded) tool args alongside so the operator queue can serve the approval-card
|
|
109
|
+
// payload without an N+1 trace.turns fetch. Stored as a stringified JSON column value.
|
|
47
110
|
const toolInput = boundedToolInput(pa?.args);
|
|
48
111
|
try {
|
|
49
112
|
await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at, terminal_at, gate_kind, bound_input_hash, risk_descriptor) " +
|
|
@@ -59,14 +122,15 @@ export class SqlCheckpointStore {
|
|
|
59
122
|
this.json(cp, "checkpoint"),
|
|
60
123
|
cp.deadline ?? null,
|
|
61
124
|
cp.createdAt,
|
|
62
|
-
Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS),
|
|
63
|
-
cp.gate?.kind ?? null,
|
|
64
|
-
pa?.boundInputHash ?? null,
|
|
65
|
-
((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate),
|
|
125
|
+
Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS), // D-1 §3 inv#3 backstop — STRICTLY after any SLA deadline (grace) so the terminal_at-branch never races the D-D deny-sweep, and never pre-empts an operator's longer TTL
|
|
126
|
+
cp.gate?.kind ?? null, // D-D SLA split: human/irreversible_ask deadline → resolve-deny; others → expire
|
|
127
|
+
pa?.boundInputHash ?? null, // D-1: the opaque hash the portal must echo on /decide (surfaced via listPending so the TOCTOU binding is reachable)
|
|
128
|
+
((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate), // riskDescriptor inbox: stamp core's INERT descriptor for triage-sort
|
|
66
129
|
]);
|
|
67
130
|
}
|
|
68
131
|
catch (e) {
|
|
69
132
|
if (this.isDupKey(e)) {
|
|
133
|
+
// a reused token would clobber a live suspension — never a silent overwrite (checkpoint-store contract)
|
|
70
134
|
throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
|
|
71
135
|
}
|
|
72
136
|
throw e;
|
|
@@ -77,22 +141,37 @@ export class SqlCheckpointStore {
|
|
|
77
141
|
const r = rows[0];
|
|
78
142
|
if (!r)
|
|
79
143
|
return null;
|
|
144
|
+
// Forward-compat guard: never hand a too-new checkpoint to a resume that can't
|
|
145
|
+
// parse it. Compare with `<= MAX` (never `=== CURRENT`) using core's exported constant so this
|
|
146
|
+
// stays aligned when core bumps the format. This durable-store get-path is the STRONGEST guard (protects a
|
|
147
|
+
// worker that doesn't check version itself); core's resume has a second pre-CAS guard.
|
|
80
148
|
const version = Number(r.version);
|
|
81
149
|
if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
82
150
|
throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
|
|
83
151
|
}
|
|
84
152
|
const cp = parseJson(r.checkpoint);
|
|
153
|
+
// the COLUMN status is authoritative (resolve/reap update it, not the blob) — reflect it on the returned cp.
|
|
85
154
|
cp.status = r.status;
|
|
155
|
+
// design/80 D-1: surface the reopen-by-reason + OCC fields off the AUTHORITATIVE columns (the blob is the
|
|
156
|
+
// suspend-time snapshot; resolve/reopen mutate only the columns). Core's resume reads these to validate an
|
|
157
|
+
// env_failed re-resume against the persisted winner, and to require the `rev` it observed is still live (OCC).
|
|
158
|
+
// resolvedOutcome is DERIVED from the stored full outcome (winnerFromOutcome → {boundCallId,decision,
|
|
159
|
+
// updatedInput?}), so it is preserved across a reopen for free (reopen leaves `outcome` untouched).
|
|
86
160
|
const outcomeRaw = parseJson(r.outcome);
|
|
87
161
|
if (outcomeRaw)
|
|
88
162
|
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
89
163
|
cp.reopenReason = r.reopen_reason ?? undefined;
|
|
90
|
-
cp.rev = r.rev == null ? 0 : Number(r.rev);
|
|
164
|
+
cp.rev = r.rev == null ? 0 : Number(r.rev); // legacy/pre-migration rows ⇒ 0 (the .d.ts "Absent ⇒ legacy 0")
|
|
165
|
+
// design/80 D-A: overlay the parked steer off the AUTHORITATIVE column (set by setPendingSteer AFTER the
|
|
166
|
+
// suspend-time blob was written) so core injects it on resume. The blob never carries it.
|
|
91
167
|
const steerRaw = parseJson(r.pending_steer);
|
|
92
168
|
if (steerRaw && cp.state)
|
|
93
169
|
cp.state.pendingSteer = steerRaw;
|
|
170
|
+
// Diagnostic: log what the REAL runner.resume store.get returns for the remote
|
|
171
|
+
// workspaceHandle (the isolated round-trip test can't prove the real-path get). undefined here ⇒ store/get
|
|
172
|
+
// drops it; present ⇒ core's 910→614 threading drops it. Cheap, on the resume read-path only.
|
|
94
173
|
this.logger?.info?.("checkpoint_get", {
|
|
95
|
-
tokenFp: tokenFingerprint(token),
|
|
174
|
+
tokenFp: tokenFingerprint(token), // NEVER the raw token — it is the resume credential (token-as-auth)
|
|
96
175
|
status: cp.status,
|
|
97
176
|
version,
|
|
98
177
|
hasWorkspaceHandle: !!cp.state?.workspaceHandle,
|
|
@@ -100,6 +179,25 @@ export class SqlCheckpointStore {
|
|
|
100
179
|
});
|
|
101
180
|
return cp;
|
|
102
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Atomic CAS: pending → resolved, scoped. Returns true for the single winner, false if already
|
|
184
|
+
* resolved/expired (AlreadyResolved → caller no-ops, never re-executes). `outcome` is persisted atomically
|
|
185
|
+
* with the flip (R20 #1 — a future v2 resumable-resume can recover a crash-after-CAS; v1 runner write-only).
|
|
186
|
+
*
|
|
187
|
+
* design/80 D-1 (atomicity): the winner-persist (the full `outcome` is stored; the winner
|
|
188
|
+
* {boundCallId,decision,updatedInput?} is derived from it on read) + the `rev` bump happen ATOMICALLY in the
|
|
189
|
+
* SAME UPDATE as the pending→resolved CAS — one statement, no get→write window. When the caller passes the
|
|
190
|
+
* `rev` it observed at get() (ResolveExpectation), the CAS additionally requires the LIVE rev to still equal
|
|
191
|
+
* it: ANY intervening resolve/reopen cycle bumps rev, so a cycle in the get→resolve window loses the CAS
|
|
192
|
+
* (returns false → core re-inspects → checkpoint.reopened_concurrently). Omitting `expect` keeps the legacy
|
|
193
|
+
* unconditional CAS (a first resume of a never-reopened checkpoint is unconstrained by rev).
|
|
194
|
+
* 🔴 reopen_reason = NULL on a winning resolve — parity with the core reference InMemoryCheckpointStore
|
|
195
|
+
* (MINOR-12: "a freshly-resolved row carries no stale reopen reason"). Without this, an env_failed reopen
|
|
196
|
+
* followed by a correct winner-replay resolve leaves reopen_reason='env_failed' stale on the now-resolved
|
|
197
|
+
* row; a later DUPLICATE resume of that token would hit core's pre-CAS env_failed winner-replay guard
|
|
198
|
+
* against the stale reason and throw checkpoint.reopen_revote (which the service classifies RETRIABLE →
|
|
199
|
+
* re-parks the run suspended) instead of the terminal checkpoint.already_resolved — stranding the run.
|
|
200
|
+
*/
|
|
103
201
|
async resolve(token, scope, outcome, expect) {
|
|
104
202
|
const params = [this.json(outcome, "checkpoint outcome"), Date.now(), token, scope];
|
|
105
203
|
if (expect)
|
|
@@ -109,35 +207,103 @@ export class SqlCheckpointStore {
|
|
|
109
207
|
(expect ? " AND rev = $5" : "")), params);
|
|
110
208
|
return res.affected === 1;
|
|
111
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Inverse of `resolve` (core `CheckpointStore.reopen`): CAS `resolved → pending`. Returns the CAS winner
|
|
212
|
+
* (true = was resolved → now pending again). Compensates a post-CAS env-restore failure
|
|
213
|
+
* (`resume.env_failed`): `resumeStream` consumes the checkpoint BEFORE `resumeVM`, so a restore failure
|
|
214
|
+
* would otherwise strand the suspended work as `resolved` + unfindable (`findPendingTokenBySession` only
|
|
215
|
+
* matches `pending`). Reopening lets a retry re-resume the SAME work.
|
|
216
|
+
*
|
|
217
|
+
* design/80 D-1 (reopen-by-reason): the `reason` distinguishes the two triggers (both fire at core's
|
|
218
|
+
* runtask.ts) so the re-resume can validate per reason (§3 inv #1) — `env_failed` = the action is still
|
|
219
|
+
* valid, the re-resume MUST replay the persisted winner (core rejects a different decision with
|
|
220
|
+
* `checkpoint.reopen_revote`); `tool_unavailable` = the action could not run and may now be stale, so a
|
|
221
|
+
* FRESH decision is allowed (P-7). We RECORD the reason on the row and bump `rev` (so a concurrent resume's
|
|
222
|
+
* resolve CAS loses — atomicity). 🔴 We DELIBERATELY leave the persisted `outcome` UNTOUCHED (NOT stale junk
|
|
223
|
+
* as a pre-D-1 reopen treated it): `resolvedOutcome` derives from it on get(), and an `env_failed` re-resume
|
|
224
|
+
* is validated against it — clearing it would defeat the winner-replay invariant.
|
|
225
|
+
*/
|
|
112
226
|
async reopen(token, scope, reason) {
|
|
113
227
|
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved'", "UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved'"), [reason, token, scope]);
|
|
114
228
|
return res.affected === 1;
|
|
115
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* design/80 D-A (durable steering): park a steer message on a STILL-PENDING checkpoint (last-writer-wins),
|
|
232
|
+
* surfaced via get() as `state.pendingSteer` and injected on resume. CAS on `status='pending'` (+ scope) so a
|
|
233
|
+
* steer NEVER lands on a resolved/expired checkpoint and NEVER touches status/the resolve path (§3 inv #4).
|
|
234
|
+
* Returns the CAS winner (`true` = landed on a live pending checkpoint; `false` = no pending row to steer).
|
|
235
|
+
*/
|
|
116
236
|
async setPendingSteer(token, scope, steer) {
|
|
117
|
-
|
|
237
|
+
// M2(接入审计 2026-07-08):契约把 validatePendingSteer 定为【每个 impl 内部】必跑的 fail-closed 不变式
|
|
238
|
+
// ("EVERY CheckpointStore.setPendingSteer impl runs [it] so the reject is IDENTICAL across the in-memory and
|
|
239
|
+
// durable backends")——core 自家 File/InMemory/Pg 三 impl 全有;此前这里裸写 UPDATE=靠调用方自觉(HTTP 层
|
|
240
|
+
// 三处前置校验只是碰巧全覆盖,下一个内部调用方不一定记得)。clean = 冻结 {text,trusted} 两字段拷贝
|
|
241
|
+
// (调用方对象上的多余字段绝不落库,契约同款)。
|
|
242
|
+
const clean = validatePendingSteer(steer); // throws CheckpointError(steering.invalid_content) — per-impl 一致拒绝
|
|
118
243
|
const res = await this.db.query(this.q("UPDATE checkpoint SET pending_steer = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET pending_steer = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [this.json(clean, "pending steer"), token, scope]);
|
|
119
244
|
return res.affected > 0;
|
|
120
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* CAS-expire a SINGLE checkpoint by token (core 1.71 `CheckpointStore.expire`, for `TaskStream.destroy`'s
|
|
248
|
+
* suspended-worker reap — core [R33] {delta}). `pending → expired`, returns the CAS winner (true = we
|
|
249
|
+
* expired it; false = already resolved/expired). Same UPDATE as `reap`, keyed by **token** not deadline.
|
|
250
|
+
* It races the SAME `pending` row as `resolve`, so the two are mutually exclusive: destroy() must `expire`
|
|
251
|
+
* (fence) BEFORE destroying the env — if a concurrent resume already won `resolve`, expire returns false and
|
|
252
|
+
* destroy leaves the env for that resume; if expire wins, the resume's resolve fails → never resume-to-dead-env.
|
|
253
|
+
* Uses `expired` (not a `resolve`-deny) so a CANCELLED checkpoint never pollutes resolved-count / outcome.
|
|
254
|
+
*/
|
|
121
255
|
async expire(token, scope) {
|
|
122
256
|
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [Date.now(), token, scope]);
|
|
123
257
|
return res.affected === 1;
|
|
124
258
|
}
|
|
259
|
+
/** Interface reap: CAS-expire pending checkpoints in `scope` past `cutoff`. Returns count. */
|
|
125
260
|
async reap(scope, cutoff) {
|
|
126
261
|
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE scope = ? AND status = 'pending' AND deadline IS NOT NULL AND deadline <= ?", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE scope = $2 AND status = 'pending' AND deadline IS NOT NULL AND deadline <= $3"), [Date.now(), scope, cutoff]);
|
|
127
262
|
return res.affected;
|
|
128
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* GLOBAL sweep for the service's per-replica TTL reaper (expiry isn't tenant-
|
|
266
|
+
* sensitive — only `resolve` is scoped). Idempotent across replicas (DB serializes; no election). Returns count.
|
|
267
|
+
* Called with `cutoff = Date.now()` (deadline/terminal_at are ABSOLUTE epoch-ms), so it expires any pending row
|
|
268
|
+
* whose per-approval `deadline` OR its design/80 D-1 §3-inv#3 `terminal_at` crash-safe backstop has passed —
|
|
269
|
+
* the latter closes the forever-leak of a pending row with a NULL `deadline` (no approval TTL was configured).
|
|
270
|
+
*
|
|
271
|
+
* design/80 D-D: the deadline-branch EXPIRES (≈ abort) every kind EXCEPT a tool-approval human/irreversible_ask
|
|
272
|
+
* gate — those are resolve-DENIED gracefully by the SLA deny-sweep (listExpiredApprovalGates →
|
|
273
|
+
* resumeCheckpoint(deny)), so expiring them here too would race + lose the graceful denial. 🔴 A durable
|
|
274
|
+
* AskUserQuestion ALSO mints gate.kind='human' (no question-specific kind in core) — but DENYING a question is
|
|
275
|
+
* incoherent (the model gets a "denied" tool-result, not an answer), so it is carved BACK INTO the expire path
|
|
276
|
+
* (COALESCE(tool_name,'')='AskUserQuestion') to abort-expire on timeout instead. Legacy rows (gate_kind NULL)
|
|
277
|
+
* stay on the expire path. The terminal_at-branch is the crash-safe backstop for ANY kind (incl. a human gate
|
|
278
|
+
* whose deny-resume keeps failing) — it always abort-expires past the absolute cap (which is now STRICTLY after
|
|
279
|
+
* the deadline, so it never races the deny-sweep at the deadline instant).
|
|
280
|
+
*/
|
|
129
281
|
async reapExpired(cutoff) {
|
|
130
282
|
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? " +
|
|
131
283
|
"WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= ?))", "UPDATE checkpoint SET status = 'expired', decided_at = $1 " +
|
|
132
284
|
"WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= $3))"), [Date.now(), cutoff, cutoff]);
|
|
133
285
|
return res.affected;
|
|
134
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* design/80 D-D (SLA-timer): the sessions of pending human/irreversible_ask checkpoints past their `deadline`
|
|
289
|
+
* (the per-approval SLA). The reaper resolve-DENIES each (re-resume with decision=deny → the model continues
|
|
290
|
+
* with the denial) — graceful, vs the abort that expire() gives resource_limit/needs_review. Excludes legacy
|
|
291
|
+
* rows (gate_kind NULL — they predate the split and fall through to reapExpired's expire path). LIMIT bounds
|
|
292
|
+
* the per-tick deny-resume work (each is a model run). Scope is read off the COMMITTED row (invariant #4).
|
|
293
|
+
* 🔴 EXCLUDES AskUserQuestion gates (which also mint gate.kind='human'): denying a question is incoherent —
|
|
294
|
+
* the model would get a "denied" tool-result instead of an answer — so a timed-out durable question falls to
|
|
295
|
+
* reapExpired's abort-expire path instead of this graceful-deny path.
|
|
296
|
+
*/
|
|
135
297
|
async listExpiredApprovalGates(cutoff, limit = 100) {
|
|
136
298
|
const { rows } = await this.db.query(this.q("SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
|
|
137
299
|
"AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?", "SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
|
|
138
300
|
"AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2"), [cutoff, limit]);
|
|
139
301
|
return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
|
|
140
302
|
}
|
|
303
|
+
// ─────────────────────── operator approval queue (non-secret; token stays internal) ───────────────────────
|
|
304
|
+
/** Pending checkpoints for the operator queue — NON-secret fields only (never the token). `taskId` is
|
|
305
|
+
* joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
|
|
306
|
+
* session claim, so the join is live for every pending row; null only in pathological windows). */
|
|
141
307
|
async listPending(scope) {
|
|
142
308
|
const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
|
|
143
309
|
"FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
|
|
@@ -151,21 +317,41 @@ export class SqlCheckpointStore {
|
|
|
151
317
|
scope: String(r.scope),
|
|
152
318
|
toolName: r.tool_name ?? null,
|
|
153
319
|
toolCallId,
|
|
320
|
+
// D-1: boundCallId === the pending tool call id; boundInputHash is the server-minted opaque hash. The
|
|
321
|
+
// portal echoes BOTH on /decide so the resume binds to the exact action shown (TOCTOU guard reachable).
|
|
154
322
|
boundCallId: toolCallId,
|
|
155
323
|
boundInputHash: r.bound_input_hash ?? null,
|
|
156
324
|
taskId: r.task_id ?? null,
|
|
325
|
+
// Both drivers return the JSON column already parsed; null for pre-migration rows. PG 库内恒干净
|
|
326
|
+
// (拒绝式)——直读即审阅面=执行面。
|
|
157
327
|
input: r.tool_input ?? null,
|
|
158
328
|
createdAt: Number(r.created_at),
|
|
159
329
|
deadline: r.deadline == null ? null : Number(r.deadline),
|
|
160
330
|
riskDescriptor: parseJson(r.risk_descriptor),
|
|
161
331
|
};
|
|
162
332
|
});
|
|
333
|
+
// riskDescriptor inbox: triage-sort by severity DESC (highest-risk asks first), then oldest-first within a
|
|
334
|
+
// tier. A row with no descriptor (pre-migration / non-gate) sorts to severity 0 (after the descriptored ones).
|
|
163
335
|
return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
164
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* design/80 #2 (scheduler seam, core 1.105): a scope's PENDING checkpoints as lightweight
|
|
339
|
+
* {@link CheckpointSummary} rows in ONE query — the cross-task triage view a supervisor scheduler needs
|
|
340
|
+
* (gate kind + risk severity + budget spent + deadline per suspended task), no N+1 `get`s. The projection is
|
|
341
|
+
* core's shared {@link summarizeCheckpoint} run over the persisted blob (the `checkpoint` column = the same full
|
|
342
|
+
* {@link Checkpoint} `get()` parses), so this stays byte-identical to core's InMemory/Pg/File impls. Order =
|
|
343
|
+
* created_at ASC; callers (the inbox/scheduler) sort by severity. The COLUMN `status` is authoritative (the blob
|
|
344
|
+
* is the suspend-time snapshot), so it overrides the blob's status before the summary is derived.
|
|
345
|
+
*
|
|
346
|
+
* LIMIT bounds the fan-out (review w16yqkkxv): a triage view never needs more than a few — 500 is a generous
|
|
347
|
+
* ceiling that still protects memory/latency if a scope ever accumulates pathologically many pending gates.
|
|
348
|
+
*/
|
|
165
349
|
async listByScope(scope) {
|
|
166
350
|
const { rows } = await this.db.query(this.q("SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at ASC LIMIT 500", "SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at ASC LIMIT 500"), [scope]);
|
|
167
351
|
const out = [];
|
|
168
352
|
for (const r of rows) {
|
|
353
|
+
// A corrupt/missing blob skips THAT row rather than crashing the whole scheduler view (review w16yqkkxv):
|
|
354
|
+
// unlike get() (a single-token read where a null blob is a hard error), a bulk triage list must degrade.
|
|
169
355
|
const cp = parseJson(r.checkpoint);
|
|
170
356
|
if (!cp)
|
|
171
357
|
continue;
|
|
@@ -174,18 +360,31 @@ export class SqlCheckpointStore {
|
|
|
174
360
|
}
|
|
175
361
|
return out;
|
|
176
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Map a session (the non-secret handle the operator decides by) → its single pending checkpoint token,
|
|
365
|
+
* INTERNALLY. A suspended session has exactly one pending checkpoint. `scope` is enforced so an operator
|
|
366
|
+
* can only resolve within its own tenant. Returns null if there is no pending checkpoint for that session/scope.
|
|
367
|
+
*/
|
|
177
368
|
async findPendingTokenBySession(sessionId, scope) {
|
|
178
369
|
const { rows } = scope
|
|
179
370
|
? await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND scope=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND scope=$2 AND status='pending' LIMIT 1"), [sessionId, scope])
|
|
180
371
|
: await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
|
|
181
372
|
return rows[0] ? String(rows[0].token) : null;
|
|
182
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* The owner SCOPE of a session's pending checkpoint (the multi-tenant key === the owner principal in the
|
|
376
|
+
* BFF/non-operator flow, the same key listPending filters by). For the /decide owner-gate: a non-operator
|
|
377
|
+
* caller may only decide a checkpoint in their OWN scope. Returns `undefined` (no pending row), `null`
|
|
378
|
+
* (anonymous/dev session — open), or the owner scope string.
|
|
379
|
+
*/
|
|
183
380
|
async peekPendingScope(sessionId) {
|
|
184
381
|
const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT scope FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
|
|
185
382
|
if (!rows[0])
|
|
186
383
|
return undefined;
|
|
187
384
|
return rows[0].scope ?? null;
|
|
188
385
|
}
|
|
386
|
+
// ─────────────────────── checkpoint_ctx (service rebuild inputs; sessionId-keyed) ───────────────────────
|
|
387
|
+
/** Upsert the resume rebuild inputs for a session (written at SUBMIT when durable mode is on). */
|
|
189
388
|
async putCtx(sessionId, ctx) {
|
|
190
389
|
await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES (?,?,?) " +
|
|
191
390
|
"ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at = VALUES(updated_at)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES ($1,$2::jsonb,$3) " +
|
|
@@ -195,6 +394,17 @@ export class SqlCheckpointStore {
|
|
|
195
394
|
const { rows } = await this.db.query(this.q("SELECT ctx FROM checkpoint_ctx WHERE session_id=?", "SELECT ctx FROM checkpoint_ctx WHERE session_id=$1"), [sessionId]);
|
|
196
395
|
return rows[0] ? parseJson(rows[0].ctx) : null;
|
|
197
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* GC ctx rows whose lifecycle is over (bound to the checkpoint, don't leak):
|
|
399
|
+
* a session with NO pending checkpoint, NO active run, AND not touched since `cutoff`.
|
|
400
|
+
*
|
|
401
|
+
* The extra `task_active` guard closes a multi-gate window: between a resume's CAS (pending→resolved) and
|
|
402
|
+
* the task hitting its NEXT gate (writing a new pending checkpoint), the session has no pending checkpoint —
|
|
403
|
+
* yet `putCtx` only stamps `updated_at` at submit, so the ctx could be older than `cutoff` and was being
|
|
404
|
+
* reaped, making the next resume 409 ("resume context missing"). `task_active` holds the session lock for
|
|
405
|
+
* the WHOLE submit→last-resume-complete span (running OR suspended), so excluding sessions with an active
|
|
406
|
+
* claim keeps the ctx exactly as long as a run still needs it, while still GC'ing it once the run is terminal.
|
|
407
|
+
*/
|
|
198
408
|
async reapCtx(cutoff) {
|
|
199
409
|
const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at < ? " +
|
|
200
410
|
"AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
|
|
@@ -203,6 +413,22 @@ export class SqlCheckpointStore {
|
|
|
203
413
|
"AND session_id NOT IN (SELECT session_id FROM task_active)"), [cutoff]);
|
|
204
414
|
return res.affected;
|
|
205
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* E21 (§0.5 session delete) — purge ALL checkpoint state for one session: the durable checkpoint rows
|
|
418
|
+
* (suspended-task gates) AND their service-side resume-context rows (checkpoint_ctx). Keyed on the
|
|
419
|
+
* `session_id` column both tables carry. Idempotent (returns the count of `checkpoint` rows removed).
|
|
420
|
+
* Unconditional by design: a session being deleted has no resumable future, so any pending gate is moot.
|
|
421
|
+
*
|
|
422
|
+
* `owner` is a single-DB-fleet defense-in-depth guard (adversarial-review MEDIUM). Neither table carries an
|
|
423
|
+
* owner column, so the guard threads through `session_meta.owner` via an EXISTS sub-select (null-safe compare:
|
|
424
|
+
* `<=>` on TiDB, `IS NOT DISTINCT FROM` on PG). The purge coordinator runs this BEFORE deleting session_meta,
|
|
425
|
+
* so the meta row (with its authoritative owner) is still present here — a wrong-owner caller's EXISTS fails
|
|
426
|
+
* and purges nothing. `owner=null` (legacy/dev, never threaded by the route) keeps the historical
|
|
427
|
+
* unconditional behaviour via the null-safe match.
|
|
428
|
+
*
|
|
429
|
+
* 🔴 Placeholder ARITY differs, not just the syntax: `?` cannot be reused, so the TiDB arm binds `sessionId`
|
|
430
|
+
* TWICE (row key + guard sub-select) while PG reuses `$1`.
|
|
431
|
+
*/
|
|
206
432
|
async deleteBySession(sessionId, owner) {
|
|
207
433
|
const ownerGuard = this.q("EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = ? AND sm.owner <=> ?)", "EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = $1 AND sm.owner IS NOT DISTINCT FROM $2)");
|
|
208
434
|
const params = this.db.dialect === "tidb" ? [sessionId, sessionId, owner] : [sessionId, owner];
|
|
@@ -211,11 +437,13 @@ export class SqlCheckpointStore {
|
|
|
211
437
|
return res.affected;
|
|
212
438
|
}
|
|
213
439
|
}
|
|
440
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
214
441
|
export class TiDBCheckpointStore extends SqlCheckpointStore {
|
|
215
442
|
constructor(pool, logger) {
|
|
216
443
|
super(mysqlDriver(pool), logger);
|
|
217
444
|
}
|
|
218
445
|
}
|
|
446
|
+
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
219
447
|
export class PgCheckpointStore extends SqlCheckpointStore {
|
|
220
448
|
constructor(pool, logger) {
|
|
221
449
|
super(pgDriver(pool), logger);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { TaskOutcome as CoreTaskOutcome } from "@sema-agent/core";
|
|
2
|
+
/** The one write verb every outcome sink (TiDB/Pg ledger adapters + this File twin) exposes to main.ts,
|
|
3
|
+
* plus the OPTIONAL read aggregate backing `GET /v1/outcomes` (P2) — present on the SQL ledgers (their
|
|
4
|
+
* existing per-(signature, model) summary); absent here (the JSONL file IS the local dataset — a dashboard
|
|
5
|
+
* reads the file, the endpoint 501s honestly). */
|
|
2
6
|
export interface OutcomeSink {
|
|
3
7
|
recordCore(o: CoreTaskOutcome): Promise<void>;
|
|
4
8
|
summary?(opts?: {
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/73 §1 consumption, LOCAL lane — the File twin of the TiDB/Pg outcome ledgers for the single-user /
|
|
3
|
+
* no-DB posture: append each core `TaskOutcome` fact VERBATIM to an owner-only JSONL ledger. Read-only v1
|
|
4
|
+
* (dashboard/backtest dataset — mirrors the SQL ledgers' read-only red line); no query surface here, the
|
|
5
|
+
* file IS the dataset. Non-load-bearing observability → no fsync (a crash may lose the tail fact; the run
|
|
6
|
+
* itself is unaffected — same posture as metrics).
|
|
7
|
+
*/
|
|
1
8
|
import { mkdirSync, appendFileSync } from "node:fs";
|
|
2
9
|
import { join } from "node:path";
|
|
3
10
|
export class FileOutcomeSink {
|
|
4
11
|
path;
|
|
5
12
|
constructor(root) {
|
|
6
13
|
const dir = join(root, "outcomes");
|
|
14
|
+
// 0o700 dir + 0o600 file: outcome facts carry model/taskSignature/oracle detail — operator-only, parity
|
|
15
|
+
// with the other File ledgers' posture.
|
|
7
16
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
8
17
|
this.path = join(dir, "outcomes.jsonl");
|
|
9
18
|
}
|
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
export declare class FileResumeAnchorStore {
|
|
2
2
|
private readonly dir;
|
|
3
|
+
/** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
|
|
3
4
|
private readonly bySession;
|
|
5
|
+
/** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
|
|
4
6
|
private readonly logs;
|
|
5
7
|
constructor(root: string);
|
|
8
|
+
/** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
|
|
6
9
|
private pathFor;
|
|
10
|
+
/** Replay every session log into the in-memory index on boot (torn-tail-safe via readJsonlRecords; last-wins upsert
|
|
11
|
+
* so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
|
|
12
|
+
* sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
|
|
13
|
+
* `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */
|
|
7
14
|
private hydrate;
|
|
15
|
+
/** Open (or reuse) the append log for a session. The file base is the SANITIZED id (matches hydrate's de-sanitize). */
|
|
8
16
|
private logFor;
|
|
17
|
+
/** Record/overwrite the anchor for one settled message boundary (idempotent upsert; parity with the SQL
|
|
18
|
+
* ON DUPLICATE/CONFLICT). Appends a durable record (fsync:true) THEN upserts the index — a crash after the
|
|
19
|
+
* fsync is recovered by hydrate (the record is on disk); a crash before it just loses an un-acked anchor. */
|
|
9
20
|
put(sessionId: string, eventId: string, entryId: string, owner: string | null): Promise<void>;
|
|
21
|
+
/** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id for TaskSpec.resumeAt; undefined = unknown
|
|
22
|
+
* anchor (the route 4xx's before handing core a bad id). owner null-safe guard = the SQL `owner <=> ?` defense-in-depth. */
|
|
10
23
|
resolve(sessionId: string, eventId: string, owner: string | null): Promise<string | undefined>;
|
|
24
|
+
/** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
|
|
25
|
+
* session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
|
|
26
|
+
* route already proved session ownership (the export gate), so no per-row owner filter here. Twin of the SQL stores. */
|
|
11
27
|
listBySession(sessionId: string): Promise<Array<{
|
|
12
28
|
eventId: string;
|
|
13
29
|
entryId: string;
|
|
14
30
|
owner: string | null;
|
|
15
31
|
}>>;
|
|
32
|
+
/** E21 (§0.5 delete) — purge ALL anchors for a session, scoped by session_id ALONE (the route already proved session
|
|
33
|
+
* ownership; a per-row owner guard would orphan rows stored under a divergent per-run principal). Idempotent (count).
|
|
34
|
+
* Closes + unlinks the session log so the dir does not leak a file, and drops the in-memory index entry. */
|
|
16
35
|
deleteBySession(sessionId: string): Promise<number>;
|
|
36
|
+
/** Release every open append-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
|
|
17
37
|
dispose(): void;
|
|
18
38
|
}
|
|
19
39
|
//# sourceMappingURL=file-resume-anchor-store.d.ts.map
|
|
@@ -1,23 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed resume-at anchor map (E18) — the DURABLE local twin of {@link MemoryResumeAnchorStore}, persisting the
|
|
3
|
+
* service-owned eventId→entryId translation across a process restart (P0.5 variant-1, clay 2026-06-26; the file-backed
|
|
4
|
+
* follow-on the Memory twin's header promised). Behaviour is byte-identical to the Memory store (put/resolve/
|
|
5
|
+
* deleteBySession + the §0.5/2c `listBySession` add); the only difference is durability.
|
|
6
|
+
*
|
|
7
|
+
* Persistence: one append-only JSONL log per session at `<root>/resume-anchors/<sanitizePathComponent(sessionId)>.jsonl`,
|
|
8
|
+
* each line `{eventId, entryId, owner, ts}`. `put` APPENDS (fsync:true — an anchor is a durable rewind boundary; a lost
|
|
9
|
+
* anchor silently breaks the next rewind), and the in-memory index is upserted last-wins so a re-stamped eventId
|
|
10
|
+
* overwrites (parity with the SQL ON DUPLICATE/CONFLICT). On construction the index is HYDRATED by replaying every
|
|
11
|
+
* session log via core's `readJsonlRecords` (torn-tail-safe: a crash mid-append drops only the partial last line, and a
|
|
12
|
+
* later `put` re-records it). `resolve`/`listBySession` read the in-memory index (O(1)/O(per-session)); `deleteBySession`
|
|
13
|
+
* unlinks the file + drops the index entry. owner null-safe guard (`(a??null)===(b??null)`) = the SQL `owner <=> ?`
|
|
14
|
+
* defense-in-depth twin. Single-process (the FileStorageBackend boot lock guarantees one writer per data dir), so there
|
|
15
|
+
* is no per-op lock — the in-process model is the premise (core fs-atomic BootLock).
|
|
16
|
+
*/
|
|
1
17
|
import { join } from "node:path";
|
|
2
18
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
19
|
import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
|
|
20
|
+
/** Null-safe owner equality — the JS twin of the SQL `owner <=> ?` guard (a legacy null-owner anchor matches owner=null). */
|
|
4
21
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
5
22
|
export class FileResumeAnchorStore {
|
|
6
23
|
dir;
|
|
24
|
+
/** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
|
|
7
25
|
bySession = new Map();
|
|
26
|
+
/** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
|
|
8
27
|
logs = new Map();
|
|
9
28
|
constructor(root) {
|
|
10
29
|
this.dir = join(root, "resume-anchors");
|
|
11
30
|
ensureDir(this.dir);
|
|
12
31
|
this.hydrate();
|
|
13
32
|
}
|
|
33
|
+
/** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
|
|
14
34
|
pathFor(sessionId) {
|
|
15
35
|
return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
|
|
16
36
|
}
|
|
37
|
+
/** Replay every session log into the in-memory index on boot (torn-tail-safe via readJsonlRecords; last-wins upsert
|
|
38
|
+
* so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
|
|
39
|
+
* sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
|
|
40
|
+
* `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */
|
|
17
41
|
hydrate() {
|
|
18
42
|
let names;
|
|
19
43
|
try {
|
|
20
|
-
names = readdirSync(this.dir);
|
|
44
|
+
names = readdirSync(this.dir); // a fresh root has the (just-ensured) empty dir → []
|
|
21
45
|
}
|
|
22
46
|
catch {
|
|
23
47
|
return;
|
|
@@ -25,19 +49,20 @@ export class FileResumeAnchorStore {
|
|
|
25
49
|
for (const name of names) {
|
|
26
50
|
if (!name.endsWith(".jsonl"))
|
|
27
51
|
continue;
|
|
28
|
-
const key = name.slice(0, -".jsonl".length);
|
|
52
|
+
const key = name.slice(0, -".jsonl".length); // the file base IS sanitizePathComponent(sessionId) = the index key
|
|
29
53
|
const records = readJsonlRecords(join(this.dir, name));
|
|
30
54
|
if (!records.length)
|
|
31
55
|
continue;
|
|
32
56
|
const m = this.bySession.get(key) ?? new Map();
|
|
33
57
|
for (const r of records) {
|
|
34
58
|
if (!r || typeof r.eventId !== "string" || typeof r.entryId !== "string")
|
|
35
|
-
continue;
|
|
36
|
-
m.set(r.eventId, { entryId: r.entryId, owner: r.owner ?? null });
|
|
59
|
+
continue; // skip a malformed record
|
|
60
|
+
m.set(r.eventId, { entryId: r.entryId, owner: r.owner ?? null }); // last-wins upsert (replay order = append order)
|
|
37
61
|
}
|
|
38
62
|
this.bySession.set(key, m);
|
|
39
63
|
}
|
|
40
64
|
}
|
|
65
|
+
/** Open (or reuse) the append log for a session. The file base is the SANITIZED id (matches hydrate's de-sanitize). */
|
|
41
66
|
logFor(sessionId) {
|
|
42
67
|
const key = sanitizePathComponent(sessionId);
|
|
43
68
|
let log = this.logs.get(key);
|
|
@@ -47,6 +72,9 @@ export class FileResumeAnchorStore {
|
|
|
47
72
|
}
|
|
48
73
|
return log;
|
|
49
74
|
}
|
|
75
|
+
/** Record/overwrite the anchor for one settled message boundary (idempotent upsert; parity with the SQL
|
|
76
|
+
* ON DUPLICATE/CONFLICT). Appends a durable record (fsync:true) THEN upserts the index — a crash after the
|
|
77
|
+
* fsync is recovered by hydrate (the record is on disk); a crash before it just loses an un-acked anchor. */
|
|
50
78
|
async put(sessionId, eventId, entryId, owner) {
|
|
51
79
|
const key = sanitizePathComponent(sessionId);
|
|
52
80
|
this.logFor(sessionId).append({ eventId, entryId, owner: owner ?? null, ts: new Date().toISOString() }, true);
|
|
@@ -57,10 +85,15 @@ export class FileResumeAnchorStore {
|
|
|
57
85
|
}
|
|
58
86
|
m.set(eventId, { entryId, owner });
|
|
59
87
|
}
|
|
88
|
+
/** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id for TaskSpec.resumeAt; undefined = unknown
|
|
89
|
+
* anchor (the route 4xx's before handing core a bad id). owner null-safe guard = the SQL `owner <=> ?` defense-in-depth. */
|
|
60
90
|
async resolve(sessionId, eventId, owner) {
|
|
61
91
|
const e = this.bySession.get(sanitizePathComponent(sessionId))?.get(eventId);
|
|
62
92
|
return e && ownerEq(e.owner, owner) ? e.entryId : undefined;
|
|
63
93
|
}
|
|
94
|
+
/** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
|
|
95
|
+
* session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
|
|
96
|
+
* route already proved session ownership (the export gate), so no per-row owner filter here. Twin of the SQL stores. */
|
|
64
97
|
async listBySession(sessionId) {
|
|
65
98
|
const m = this.bySession.get(sanitizePathComponent(sessionId));
|
|
66
99
|
if (!m)
|
|
@@ -70,6 +103,9 @@ export class FileResumeAnchorStore {
|
|
|
70
103
|
out.push({ eventId, entryId: e.entryId, owner: e.owner });
|
|
71
104
|
return out;
|
|
72
105
|
}
|
|
106
|
+
/** E21 (§0.5 delete) — purge ALL anchors for a session, scoped by session_id ALONE (the route already proved session
|
|
107
|
+
* ownership; a per-row owner guard would orphan rows stored under a divergent per-run principal). Idempotent (count).
|
|
108
|
+
* Closes + unlinks the session log so the dir does not leak a file, and drops the in-memory index entry. */
|
|
73
109
|
async deleteBySession(sessionId) {
|
|
74
110
|
const key = sanitizePathComponent(sessionId);
|
|
75
111
|
const removed = this.bySession.get(key)?.size ?? 0;
|
|
@@ -85,16 +121,19 @@ export class FileResumeAnchorStore {
|
|
|
85
121
|
rmSync(path, { force: true });
|
|
86
122
|
}
|
|
87
123
|
catch {
|
|
124
|
+
/* best-effort — a stale file is harmless (the index entry is already gone) */
|
|
88
125
|
}
|
|
89
126
|
}
|
|
90
127
|
return removed;
|
|
91
128
|
}
|
|
129
|
+
/** Release every open append-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
|
|
92
130
|
dispose() {
|
|
93
131
|
for (const log of this.logs.values()) {
|
|
94
132
|
try {
|
|
95
133
|
log.close();
|
|
96
134
|
}
|
|
97
135
|
catch {
|
|
136
|
+
/* best-effort */
|
|
98
137
|
}
|
|
99
138
|
}
|
|
100
139
|
this.logs.clear();
|