@sema-agent/server 1.322.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +59 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -6,18 +6,31 @@ import { exportSession, exportSessionManifest, importSession, classifySyncRelati
|
|
|
6
6
|
import { isUuidV7, isUuidShape } from "../../security.js";
|
|
7
7
|
import { sendJson, sendError, msg } from "../send.js";
|
|
8
8
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
9
|
+
/** 2c session-sync PUT /sync/blobs/:hash per-blob upload cap. A snapshot blob is ONE file's bytes, bounded by core's
|
|
10
|
+
* DEFAULT_SNAPSHOT_BOUNDS (the whole tree ≤ 64 MiB), so a single blob can never exceed the per-snapshot total — this
|
|
11
|
+
* cap (= 64 MiB, ≥ that bound) is the reject-not-OOM ceiling: the blob body is read with a running size guard that
|
|
12
|
+
* 413s the instant it is exceeded (NEVER buffered unboundedly), the §5 "reject, not OOM" requirement. Larger than
|
|
13
|
+
* MAX_BODY (8 MiB, the JSON cap) on purpose — a blob is binary file content, not a JSON request. */
|
|
9
14
|
const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
/** 2c session-sync P1d-β: the per-LINE cap on the Phase-B NDJSON entry stream — ONE entry must fit in this (the §1.5
|
|
16
|
+
* ruling: a session_event row is ≤~20KB because core offloads >20K tool results, so a few MiB is a generous ceiling).
|
|
17
|
+
* A line exceeding it → 413 (reject, not OOM): a single oversized entry would otherwise force an unbounded line buffer. */
|
|
10
18
|
const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
/** 2c session-sync P1d-β: the Phase-B entry batch size — entries are fed to `appendBatch` in chunks of this many, so
|
|
20
|
+
* in-flight memory is O(batch), not O(session). 500 mirrors the PULL stream's keyset page size (one bounded txn). */
|
|
11
21
|
const SYNC_IMPORT_BATCH = 500;
|
|
22
|
+
/** 64 lowercase-hex chars = a sha256 content-address (the blob hash + the GET :hash param shape). */
|
|
12
23
|
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
13
24
|
export function createSessionSyncLocal() {
|
|
14
25
|
const pendingStagings = new Map();
|
|
15
26
|
const importLeases = new Map();
|
|
27
|
+
/** 租约续期 —— entries 上传腿每收一批就调,所以大会话的长上传不会被误判成陈旧。 */
|
|
16
28
|
const touchImportLease = (sessionId) => {
|
|
17
29
|
const cur = importLeases.get(sessionId);
|
|
18
30
|
if (cur)
|
|
19
31
|
importLeases.set(sessionId, { ...cur, at: Date.now() });
|
|
20
32
|
};
|
|
33
|
+
/** Drop a finished/aborted staging from the in-process maps + release its per-session import lease. Idempotent. */
|
|
21
34
|
const cleanupStaging = (stagingId) => {
|
|
22
35
|
const p = pendingStagings.get(stagingId);
|
|
23
36
|
if (p)
|
|
@@ -31,13 +44,25 @@ export async function handleSessionSync(req, res, url, ctx) {
|
|
|
31
44
|
await handleSessionSyncBody(req, res, url, ctx, miss);
|
|
32
45
|
return !miss.fell;
|
|
33
46
|
}
|
|
47
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
|
|
34
48
|
async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
35
49
|
const { deps } = ctx;
|
|
36
50
|
const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite } = ctx.helpers;
|
|
37
51
|
const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
|
|
52
|
+
// ── 2c session-sync (P1d) — the cloud as a SYNC PEER (docs/DESIGN-session-sync.md §15 + §5–§10) ──────────────
|
|
53
|
+
// The real 2c topology is TWO PROCESSES (the local shell's local backend + this cloud service); these routes let
|
|
54
|
+
// the local peer PULL a session's whole state out of the cloud or PUSH its own in. exportSession/importSession
|
|
55
|
+
// (src/session-sync.ts) fan out across ALL four session subsystems (entries + fileSnapshot + sessionPolicy +
|
|
56
|
+
// resumeAnchor) — they need the WHOLE backend (deps.backend), not the individual stores. Every route is
|
|
57
|
+
// owner-gated like fork/delete (§9): a PULL 404s a non-owner (no exfiltration oracle); a PUSH re-stamps the
|
|
58
|
+
// imported owner to the AUTHENTICATED principal (the bundle's owner is ignored). 501 when no durable session
|
|
59
|
+
// store is wired (parity with fork: capability says yes ⟺ the route resolves).
|
|
38
60
|
{
|
|
39
61
|
const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|snapshots\/([^/]+)\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
|
|
40
62
|
if (sm) {
|
|
63
|
+
// Durable-store gate (501) FIRST is wrong for an auth oracle, so AUTH-FIRST below mirrors fork/delete; but the
|
|
64
|
+
// backend is needed even to know the owner, so we gate on it up front and rely on AUTH being checked before
|
|
65
|
+
// any store touch. ownerOf is the existence/authz primitive every branch uses.
|
|
41
66
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
42
67
|
if (!deps.backend || !ownerOf) {
|
|
43
68
|
sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
|
|
@@ -47,32 +72,44 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
47
72
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
48
73
|
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
49
74
|
return;
|
|
50
|
-
}
|
|
75
|
+
} // auth-first
|
|
51
76
|
const sessionId = safeDecode(sm[1]);
|
|
77
|
+
// uuid ANY version — a v4-keyed legacy shell session must be pushable (it was
|
|
78
|
+
// creatable via the lenient submit face); crafted-id classes (LIKE metachars, over-long) still shut out.
|
|
52
79
|
if (sessionId === null || !isUuidShape(sessionId)) {
|
|
53
80
|
sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
|
|
54
81
|
return;
|
|
55
82
|
}
|
|
83
|
+
// Two owner gates, BOTH 404 a foreign session with no existence oracle:
|
|
84
|
+
// • PULL (read the cloud's session): the session MUST exist (undefined⇒404) AND the caller must own it.
|
|
85
|
+
// • PUSH/PLAN (write into / plan against the cloud): own-or-FRESH — `undefined` (no such session yet, the
|
|
86
|
+
// caller is about to import it) OR the caller's own ⇒ allowed; a DIFFERENT owner ⇒ 404.
|
|
56
87
|
const owner = await ownerOf(sessionId);
|
|
57
88
|
const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
|
|
58
89
|
const pullGateOk = () => {
|
|
59
90
|
if (owner === undefined) {
|
|
60
91
|
sendError(res, 404, "not_found.session", "session not found");
|
|
61
92
|
return false;
|
|
62
|
-
}
|
|
93
|
+
} // must exist to pull
|
|
63
94
|
if (!ownsOrFleet(owner)) {
|
|
64
95
|
sendError(res, 404, "not_found.session", "session not found");
|
|
65
96
|
return false;
|
|
66
|
-
}
|
|
97
|
+
} // no oracle
|
|
67
98
|
return true;
|
|
68
99
|
};
|
|
69
100
|
const pushGateOk = () => {
|
|
70
101
|
if (owner !== undefined && !ownsOrFleet(owner)) {
|
|
71
102
|
sendError(res, 404, "not_found.session", "session not found");
|
|
72
103
|
return false;
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
104
|
+
} // foreign → no oracle
|
|
105
|
+
return true; // undefined (fresh, caller will import) OR own/fleet
|
|
75
106
|
};
|
|
107
|
+
// GET …/sync/manifest — PULL: export the MANIFEST (entry IDS + per-snapshot relPath→blobHash + policy +
|
|
108
|
+
// anchors + leafId), NO entry payloads and NO blob bytes (P1d-α §1.5/§5/§15: bounded memory — the entries
|
|
109
|
+
// stream separately via …/sync/entries). The local peer diffs entryIds → classifySyncRelationshipByIds, then
|
|
110
|
+
// pulls the entries (NDJSON) + only the blobHashes it lacks. exportSessionManifest returns null ONLY for "no
|
|
111
|
+
// such session" (the owner gate above already proved existence+ownership, so a null here is a raced delete →
|
|
112
|
+
// 404).
|
|
76
113
|
if (sm[2] === "manifest" && req.method === "GET") {
|
|
77
114
|
if (!pullGateOk())
|
|
78
115
|
return;
|
|
@@ -80,13 +117,22 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
80
117
|
if (out === null) {
|
|
81
118
|
sendError(res, 404, "not_found.session", "session not found");
|
|
82
119
|
return;
|
|
83
|
-
}
|
|
120
|
+
} // raced delete after the gate
|
|
84
121
|
sendJson(res, 200, { manifest: out.manifest });
|
|
85
122
|
return;
|
|
86
123
|
}
|
|
124
|
+
// GET …/sync/entries[?afterSeq=N] — PULL the entry PAYLOADS as an NDJSON stream (P1d-α §1.5/§5/§15). The store
|
|
125
|
+
// seam keyset-PAGES the durable log (one pooled query per batch → bounded server memory; a slow client can't
|
|
126
|
+
// pin a connection), and we apply HTTP backpressure (await drain on a full socket buffer) so a slow consumer
|
|
127
|
+
// can't OOM the server either. The frame: a {"__sync":"begin",…} header line, then one JSON entry per line,
|
|
128
|
+
// then a {"__sync":"end","count":N} TRAILER line — a MISSING trailer signals a truncated stream to the client.
|
|
87
129
|
if (sm[2] === "entries" && req.method === "GET") {
|
|
88
130
|
if (!pullGateOk())
|
|
89
131
|
return;
|
|
132
|
+
// afterSeq (optional resume cursor): a non-negative integer; reject anything else as a 400 (never a NaN
|
|
133
|
+
// cursor). It is the BACKEND-NATIVE durable `seq` cursor (yield entries whose seq > afterSeq) — a resume is
|
|
134
|
+
// always against the SAME backend, so the seq base (durable `seq` column vs. local 0-based index) needn't be
|
|
135
|
+
// portable across backends.
|
|
90
136
|
const afterSeqRaw = new URL(req.url ?? "", "http://x").searchParams.get("afterSeq");
|
|
91
137
|
let afterSeq;
|
|
92
138
|
if (afterSeqRaw !== null) {
|
|
@@ -101,13 +147,16 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
101
147
|
if (!stream) {
|
|
102
148
|
sendError(res, 501, "capability.session_store_required", "session entry streaming requires a durable session store");
|
|
103
149
|
return;
|
|
104
|
-
}
|
|
150
|
+
} // method absent
|
|
105
151
|
const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
|
|
106
152
|
if (it === null) {
|
|
107
153
|
sendError(res, 404, "not_found.session", "session not found");
|
|
108
154
|
return;
|
|
109
|
-
}
|
|
155
|
+
} // raced delete after the gate
|
|
156
|
+
// Headers committed — past this point we cannot change the status; a mid-stream error just ends the body
|
|
157
|
+
// WITHOUT the trailer, which the client reads as truncation.
|
|
110
158
|
res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
|
|
159
|
+
// backpressure-aware write: if the socket buffer is full (write returns false), await 'drain' before the next.
|
|
111
160
|
const write = async (line) => { if (!res.write(line))
|
|
112
161
|
await once(res, "drain"); };
|
|
113
162
|
let count = 0;
|
|
@@ -117,15 +166,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
117
166
|
await write(`${JSON.stringify(entry)}\n`);
|
|
118
167
|
count++;
|
|
119
168
|
}
|
|
120
|
-
await write(`${JSON.stringify({ __sync: "end", count })}\n`);
|
|
169
|
+
await write(`${JSON.stringify({ __sync: "end", count })}\n`); // trailer — its ABSENCE = truncated stream
|
|
121
170
|
res.end();
|
|
122
171
|
}
|
|
123
172
|
catch (e) {
|
|
173
|
+
// The iterable (or a write) threw mid-stream. Headers are already sent, so we can't 4xx/5xx — just END the
|
|
174
|
+
// body. The MISSING trailer tells the client the stream was truncated. Log it for the operator.
|
|
124
175
|
deps.logger?.error?.("sync entries stream failed", { sessionId, count, error: msg(e) });
|
|
125
176
|
res.end();
|
|
126
177
|
}
|
|
127
178
|
return;
|
|
128
179
|
}
|
|
180
|
+
// GET …/sync/snapshots/:key/blobs/:hash — PULL one content-addressed blob's RAW bytes. The SCOPE CHECK is the
|
|
181
|
+
// security boundary: a content-addressed blob is shared across sessions, so we must NOT let an owner fetch an
|
|
182
|
+
// arbitrary hash (another tenant's file content). We require the requested :hash to be a value in THIS
|
|
183
|
+
// session's snapshot `:key` manifest — a session owner may only fetch blobs ITS OWN snapshots reference.
|
|
129
184
|
if (sm[4] !== undefined && req.method === "GET") {
|
|
130
185
|
if (!pullGateOk())
|
|
131
186
|
return;
|
|
@@ -141,6 +196,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
141
196
|
}
|
|
142
197
|
const fs = deps.fileSnapshotStore;
|
|
143
198
|
const manifest = await fs?.exportManifest?.(sessionId, key);
|
|
199
|
+
// null manifest (no such snapshot) OR the hash is not referenced by it ⇒ 404 (the scope boundary — never a
|
|
200
|
+
// blind content-addressed read of an unscoped hash).
|
|
144
201
|
if (!manifest || ![...manifest.values()].includes(hash)) {
|
|
145
202
|
sendError(res, 404, "not_found.blob", "blob not found");
|
|
146
203
|
return;
|
|
@@ -154,6 +211,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
154
211
|
res.end(Buffer.from(bytes));
|
|
155
212
|
return;
|
|
156
213
|
}
|
|
214
|
+
// PUT …/sync/blobs/:hash — PUSH one content-addressed blob INTO the cloud's snapshot store, ahead of the bundle
|
|
215
|
+
// import (§5 streamed negotiation: the local peer GETs the manifest, then PUTs only the blobs the cloud lacks).
|
|
216
|
+
// own-or-FRESH gate. Verify sha256(body)===:hash (the content-address is the integrity guarantee; a transfer
|
|
217
|
+
// crosses the net) BEFORE storing. The body is read with a PER-BLOB cap (reject-not-OOM).
|
|
157
218
|
if (sm[3] !== undefined && req.method === "PUT") {
|
|
158
219
|
if (!pushGateOk())
|
|
159
220
|
return;
|
|
@@ -167,8 +228,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
167
228
|
sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable snapshot store");
|
|
168
229
|
return;
|
|
169
230
|
}
|
|
170
|
-
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
|
|
231
|
+
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES); // 413s mid-stream if it exceeds the cap (no unbounded buffer)
|
|
232
|
+
// (a): SQL-default blob store cap — 413 typed, NOT the 502 the store's never-throw result would become
|
|
233
|
+
// (502 = "retry"; an over-cap blob can never land, so retrying it forever is the wrong contract).
|
|
171
234
|
if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
|
|
235
|
+
// 复审 D-F1/B-F2:归一漏网(多行体,旧门正则扫不到)——A6 迁移到 sendError 后此站点由
|
|
236
|
+
// errorCode 必填位置参数保结构(不再靠源级反射门兜底)。
|
|
172
237
|
sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`, { code: "blob_too_large_for_sql" });
|
|
173
238
|
return;
|
|
174
239
|
}
|
|
@@ -176,17 +241,29 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
176
241
|
sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
|
|
177
242
|
return;
|
|
178
243
|
}
|
|
244
|
+
// core 1.142.0: putBlob is a fail-closed never-throw seam — a backend write error (e.g. a MinIO non-2xx PUT)
|
|
245
|
+
// is {ok:false}, NOT a thrown 500. Surface it as 502 so the local peer retries rather than treating the bundle
|
|
246
|
+
// blob as durably stored when it isn't.
|
|
179
247
|
const put = await fs.putBlob(hash, new Uint8Array(body));
|
|
180
248
|
if (!put.ok) {
|
|
181
249
|
sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
|
|
182
250
|
return;
|
|
183
251
|
}
|
|
184
|
-
res.writeHead(204).end();
|
|
252
|
+
res.writeHead(204).end(); // no body
|
|
185
253
|
return;
|
|
186
254
|
}
|
|
255
|
+
// POST …/sync/import — PHASE A (PUSH, P1d-β §15.2): a SMALL JSON body — `{entryIds, snapshots, policy, anchors,
|
|
256
|
+
// resolution?}` — NO entries (they stream in Phase B). Classify (§7), pre-check blob presence (§10), guard the
|
|
257
|
+
// §4 import-vs-import lease + the active-run window, then mint a stagingId, open a StagingHandle, and park the
|
|
258
|
+
// Phase-A metadata. Returns `200 {stagingId, relation}`. The entries-inline whole-bundle import is GONE (the
|
|
259
|
+
// §1.5 OOM offender) — Phase B streams the log into the shadow id so server memory is bounded by batch.
|
|
187
260
|
if (sm[2] === "import" && req.method === "POST") {
|
|
188
261
|
if (!pushGateOk())
|
|
189
262
|
return;
|
|
263
|
+
// sessionMirror 观测面(裁定 (c),论证在 ServiceDeps.sessionMirrorRuling 注释):Phase A
|
|
264
|
+
// 到达 = 一次镜像 push 落到本 server 的时点信号。该 principal 若处 org 治理态(required=true),记
|
|
265
|
+
// audit 日志 + metric,把「治理生效」与「到达流」在运营面对上——这是 server 侧唯一诚实可观测的面
|
|
266
|
+
// (没到达的会话 server 看不见,真执法在壳)。fire-and-forget + 双层吞错:观测绝不拖慢/拖垮 sync。
|
|
190
267
|
if (deps.sessionMirrorRuling && !scope.fleetWide && scope.gateOwner !== null) {
|
|
191
268
|
const auditPrincipal = scope.gateOwner;
|
|
192
269
|
void deps.sessionMirrorRuling(auditPrincipal).then((mirror) => {
|
|
@@ -208,6 +285,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
208
285
|
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
209
286
|
return;
|
|
210
287
|
}
|
|
288
|
+
// Shape validation (§10): entryIds must be a string[]; snapshots an array of {key:string, manifest:array};
|
|
289
|
+
// policy/anchors arrays. A malformed shape → 400 (never a 500 from a downstream TypeError).
|
|
211
290
|
if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
|
|
212
291
|
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
213
292
|
return;
|
|
@@ -222,19 +301,56 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
222
301
|
return;
|
|
223
302
|
}
|
|
224
303
|
const resolution = body.resolution;
|
|
304
|
+
// 🔴 方案 B(黑板 [1701]→[1702]→[1707] 三端对齐):**可选** `logDigest` = 源端整条日志的内容摘要
|
|
305
|
+
// (core 1.414 `sessionLogDigest`)。老客户端不送 ⇒ 行为与此前逐字相同。坏形**不拒**,当作没送
|
|
306
|
+
// (core 口径:摘要不符/不可比永远不得变成 error / 409 / 拒绝同步 —— 它只意味着"多干一趟活")。
|
|
225
307
|
const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
|
|
226
308
|
const entryIds = body.entryIds;
|
|
227
309
|
const snapshots = body.snapshots;
|
|
228
310
|
const policy = body.policy;
|
|
229
311
|
const anchors = body.anchors;
|
|
312
|
+
// §7 classify (BEFORE issuing a stagingId): dst current ids → classifySyncRelationshipByIds. A fork/stale
|
|
313
|
+
// without overwrite-dst → 409 (no stream needed). `identical` → 200 {relation:"identical"} (no Phase B; the
|
|
314
|
+
// snapshot/policy/anchor replay is idempotent and not strictly needed — the dst already holds this log; the
|
|
315
|
+
// shell may re-push with no consequence). NB the AUTHORITATIVE re-classify is in commit() (the TOCTOU close).
|
|
230
316
|
const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
|
|
231
317
|
const relA = classifySyncRelationshipByIds(entryIds, dstIds);
|
|
232
318
|
if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
|
|
233
319
|
sendError(res, 409, "conflict", "sync conflict", { relation: relA });
|
|
234
320
|
return;
|
|
235
321
|
}
|
|
322
|
+
// 🔴 内容判据(方案 B 的 Phase A 半场):`identical` 只说明 **id 集合**相等。源端内容在 Phase A 还没上传,
|
|
323
|
+
// 所以这里只能靠调用方送来的 `logDigest`;拿目的端自己的日志算一次摘要跟它比。
|
|
324
|
+
// ⚠️ 目的端全量读**只在这条路上做**(要短路、且调用方送了摘要)—— 成本只花在它能改变结论的地方。
|
|
325
|
+
// 三态:相等 ⇒ 可以短路;不等 ⇒ **不短路**(照常开 staging 走完整同步);判不可比(scheme 不同/
|
|
326
|
+
// 对端没送)⇒ 同样**不短路**,退回完整同步 = 安全方向。
|
|
236
327
|
let payloadVerified = false;
|
|
237
328
|
let digestSaysDiffer = false;
|
|
329
|
+
// ── 这条路依赖的那条**性质**,以及它为什么由依赖下限 + 一条行为测试守着(不是由版本名判断守着)──
|
|
330
|
+
// 本路要求:**同一份日志,内存形与 JSON 往返形必须同摘要**。因为它比的正是「客户端从**内存**算的
|
|
331
|
+
// `logDigest`」对「server 从**已持久化**(JSON 往返过)的日志算的摘要」。
|
|
332
|
+
// `sessionLogDigest` 的 **v1** 不满足它:v1 走引擎那个规范化序列化器,**刻意**区分 `{a: undefined}`
|
|
333
|
+
// 与 `{}`,而 core 自己成规模产 own-key=undefined 的 entry(清除标签、无 data 的 custom entry)
|
|
334
|
+
// ⇒ 两形必然不等 ⇒ **永不短路**,却看起来像在工作(症状只是「Phase B 总在跑」,查不出原因):
|
|
335
|
+
// 纯成本、零收益,还多一次目的端全量读。core 黑板 [1709] 明确「别接 v1」,1.415.0 改算法并升 scheme v1→v2。
|
|
336
|
+
//
|
|
337
|
+
// 🔴 我一开始写的是 `SESSION_LOG_DIGEST_SCHEME !== "sema-log-v1"` 这道**名字门**。升上 1.415 后
|
|
338
|
+
// `tsc` 直接判它恒真(core 把 scheme 收成了字面量类型)—— 而这提醒了一件更要紧的事:名字门本来就
|
|
339
|
+
// **守不住这条性质**。它只认识"v1 这个名字",对将来任何一个同样破坏往返稳定性的 v3 一样放行。
|
|
340
|
+
// ⇒ 改成两道**真守得住**的:
|
|
341
|
+
// ① **依赖下限** `@sema-agent/core: ^1.416.0`。⚠️ package.json 是 JSON、写不了旁注,所以"这条下限是
|
|
342
|
+
// 承重的、不是随手升的"记在两处:**本注释** + `CHANGELOG.md` 1.277.0/1.277.1 两条。
|
|
343
|
+
// 降回更低的下限,tsc 不会有任何抱怨 —— 拦它的是 ② 那两道行为门。
|
|
344
|
+
// ② **行为门**(都在 `test/sync-content-digest.test.ts`),两条**方向相反**、缺一不可:
|
|
345
|
+
// · **往返稳定**:同一份含 own-key=undefined 的日志,内存形与 JSON 往返形必须同摘要。
|
|
346
|
+
// 它坏 ⇒ **永不短路**(纯成本,安全方向)。core 摘要 v1 不满足它(v1 刻意区分 `{a:undefined}`
|
|
347
|
+
// 与 `{}`,而 core 成规模产这种 entry),黑板 [1709] 明确「别接 v1」。
|
|
348
|
+
// · 🔴 **防撞**:内容不同 ⇒ 摘要必须不同,**按深度枚举**(坏值与真实内容同级)。
|
|
349
|
+
// 它坏 ⇒ **错误短路** = 静默丢数据,方向与上面相反、后果严重得多。core 摘要 **v2** 不满足它
|
|
350
|
+
// (逐键降级只做在 entry 顶层,而真实内容在深度 ≥2 ⇒ 一个 bigint 吃掉同级全部内容),
|
|
351
|
+
// 1.416.0 / v3 修复(黑板 [1714])。
|
|
352
|
+
// 两条钉的都是**性质**、不是版本名 —— 任何 scheme 再破坏其一都会红。
|
|
353
|
+
// (教训同形于 core 的 RB-149:源码/名字级判据挡的是写法,不是行为。)
|
|
238
354
|
if (relA.relation === "identical" && srcLogDigest !== undefined) {
|
|
239
355
|
const dstEntries = await deps.sessionStorage.exportEntries?.(sessionId).catch(() => null) ?? null;
|
|
240
356
|
const dstDigest = dstEntries === null ? undefined : sessionLogDigest(dstEntries);
|
|
@@ -244,10 +360,16 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
244
360
|
else
|
|
245
361
|
digestSaysDiffer = true;
|
|
246
362
|
}
|
|
363
|
+
// 不可比 ⇒ 两个标志都保持 false ⇒ 走下面「不短路」那一支(退回完整同步)
|
|
247
364
|
if (!payloadVerified)
|
|
248
365
|
digestSaysDiffer = true;
|
|
249
366
|
}
|
|
250
367
|
if (relA.relation === "identical" && !digestSaysDiffer) {
|
|
368
|
+
// 对抗复查 A-2(CRITICAL):identical 不能裸短路——若上一轮 commit 后 snapshot/policy/anchor 重放
|
|
369
|
+
// 失败(422),entries 已落库,重试的 Phase A 就落在这里;裸 200 会把「对话已同步但文件树缺失」
|
|
370
|
+
// 焊死成永久分裂(Phase B 的重放永远不再运行)。identical 时把附随物幂等重放(snapshot=
|
|
371
|
+
// content-addressed importManifest,已存在=no-op;policy/anchor put=幂等),失败=同款 422,
|
|
372
|
+
// 客户端 re-PUT blob 再重试——自愈闭环。
|
|
251
373
|
const fss = deps.fileSnapshotStore;
|
|
252
374
|
if (fss?.importManifest && fss.getBlob && snapshots.length > 0) {
|
|
253
375
|
const getBlobA = (h) => fss.getBlob(h);
|
|
@@ -267,13 +389,23 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
267
389
|
if (deps.resumeAnchorStore)
|
|
268
390
|
for (const a of anchors)
|
|
269
391
|
await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
|
|
392
|
+
// 🔴 措辞诚实化(2026-07-25,深挖第一轮 B2/B3/B4/G1):`identical` 的判据是 **entry id 集合相等**,
|
|
393
|
+
// 分类器(`classifySyncRelationshipByIds`)**从不看载荷/父子结构** —— 而 entry id 是 uuidv7、
|
|
394
|
+
// **不是内容寻址**,所以「同 id 异载荷」结构上完全可能。此时 entries 被**整段跳过**,客户端拿到 200,
|
|
395
|
+
// 目的端却仍是它自己那份内容(探针用真 HTTP 复现过:本地改过内容再 push,云端保持原样、200)。
|
|
396
|
+
// 现在把判据写进响应(`basis`)并显式声明**未验证载荷**,这样客户端不会把它读成"内容已同步"。
|
|
397
|
+
// ⚠️ 这只是止谎,**不是修**:真修要 Phase A 能拿到内容摘要(Phase A 只收 entryIds,载荷在 Phase B),
|
|
398
|
+
// 那是跨仓 wire 契约改动,已带证据上黑板与 cli/core 对齐。别把 `basis` 当成问题已解决。
|
|
270
399
|
sendJson(res, 200, {
|
|
271
400
|
relation: "identical",
|
|
401
|
+
// 判据如实:调用方送了可比摘要且相等 ⇒ 内容**真的**验过;否则仍只是 id 集合相等。
|
|
272
402
|
basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
|
|
273
403
|
payloadVerified,
|
|
274
404
|
});
|
|
275
405
|
return;
|
|
276
406
|
}
|
|
407
|
+
// §4 active-run guard (don't drop a live append) + per-session import lease (don't let two staged imports race
|
|
408
|
+
// to commit the same session). Both 409.
|
|
277
409
|
const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
278
410
|
if (activeTaskId) {
|
|
279
411
|
sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
|
|
@@ -281,22 +413,34 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
281
413
|
}
|
|
282
414
|
const heldLease = importLeases.get(sessionId);
|
|
283
415
|
if (heldLease) {
|
|
416
|
+
// 🔴 数值守卫而不是直接乘:任何**早于这个字段**的 config 对象(旧部署配置、精简的测试 harness)
|
|
417
|
+
// 会让它是 `undefined` ⇒ `undefined * 1000 = NaN` ⇒ `NaN > 0` 为假 ⇒ **接管静默不发生**,
|
|
418
|
+
// 自愈通道在不报错的情况下消失。实测撞到过(一份最小 config 的探针 harness)。缺省取与 config
|
|
419
|
+
// 同一个默认值(600s),而不是 0 —— 0 的语义是"明确关掉",不该由"漏配"来选中它。
|
|
284
420
|
const staleSecRaw = deps.config.syncImportLeaseStaleSec;
|
|
285
421
|
const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
|
|
286
422
|
const staleMs = staleSec * 1000;
|
|
287
423
|
const ageMs = Date.now() - heldLease.at;
|
|
288
424
|
if (staleMs > 0 && ageMs >= staleMs) {
|
|
425
|
+
// 接管:上一个持有者已经 staleMs 没有任何活动(既没上传 entries 也没 commit)⇒ 判它死了。
|
|
426
|
+
// 先清掉它的 staging(否则那份 staged 行会留成孤儿),再让本次 Phase A 正常继续。
|
|
289
427
|
deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId, ageMs, staleMs, stagingId: heldLease.stagingId });
|
|
290
428
|
cleanupStaging(heldLease.stagingId);
|
|
291
|
-
importLeases.delete(sessionId);
|
|
429
|
+
importLeases.delete(sessionId); // cleanupStaging 只在 pendingStagings 里还有它时才删租约,这里兜底
|
|
292
430
|
}
|
|
293
431
|
else {
|
|
432
|
+
// 错误文案点明**会自愈**:此前的措辞让客户端以为是永久状态,而它其实等一会儿就能重试。
|
|
294
433
|
const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
|
|
295
434
|
sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
|
|
296
435
|
+ (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
|
|
297
436
|
return;
|
|
298
437
|
}
|
|
299
438
|
}
|
|
439
|
+
// §10 blob-presence pre-check (BEFORE staging): every referenced blobHash must be present in the cloud's OWN
|
|
440
|
+
// snapshot store (blobs were pre-PUT). A missing one → 422 (PUT it, then retry) — fail FAST so we never open a
|
|
441
|
+
// staging that can only fail at commit. Uses the store's hasBlobs (a single SQL INDEX probe — finding 12) NOT
|
|
442
|
+
// a full getBlob GET-per-hash (under MinIO that would download + re-hash every ≤64 MiB object just to test
|
|
443
|
+
// presence). Feature-detect hasBlobs; if a store lacks it, fall back to getBlob (parity for older stores).
|
|
300
444
|
const fss = deps.fileSnapshotStore;
|
|
301
445
|
const canImportSnapshots = typeof fss?.importManifest === "function";
|
|
302
446
|
if (canImportSnapshots) {
|
|
@@ -322,8 +466,15 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
322
466
|
}
|
|
323
467
|
}
|
|
324
468
|
}
|
|
469
|
+
// importingPrincipal (§9) + operatorOk (E6 tighten-only) — mirror the v1 fixes; applied at commit (policy replay).
|
|
325
470
|
const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
|
|
326
471
|
const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
|
|
472
|
+
// Mint the stagingId TOKEN (the route supplies it, NOT the store) + open the handle + take the lease + park
|
|
473
|
+
// the metadata. Phase B drives it; abandoned ones are reaped (durable rows) / dropped on restart. The token is
|
|
474
|
+
// a SHORT random hex (8 bytes = 16 chars), NOT a full uuidv7: the staging id is `${sessionId}#stg-${token}` and
|
|
475
|
+
// session_event.session_id is VARCHAR(64) — a 36-char uuidv7 session id + "#stg-" (5) leaves 23 chars, so a
|
|
476
|
+
// 16-char token fits (and is collision-resistant for the few in-flight stagings of ONE session — the lease
|
|
477
|
+
// already serializes them per session). randomBytes is unguessable (it rides the URL the client gets back).
|
|
327
478
|
const token = randomBytes(8).toString("hex");
|
|
328
479
|
const handle = deps.sessionStorage.beginImportStaging(sessionId, token);
|
|
329
480
|
const stagingId = handle.stagingId;
|
|
@@ -335,6 +486,11 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
335
486
|
sendJson(res, 200, { stagingId, relation: relA.relation });
|
|
336
487
|
return;
|
|
337
488
|
}
|
|
489
|
+
// POST …/sync/import/:stagingId/entries — PHASE B (PUSH, P1d-β §15.3): stream the entry log as an NDJSON request
|
|
490
|
+
// body into the staged shadow id (bounded memory — line buffer + batch), driving core's STREAMING import gate
|
|
491
|
+
// ({@link StreamingImportValidator}) ONE entry per line, then `finish()` at stream end (exactly-one-root +
|
|
492
|
+
// leaf-resolvable) → commit (the atomic swap) → replay snapshots/policy/anchors. Re-POSTing the same stagingId is
|
|
493
|
+
// idempotent (appendBatch dedups; the commit re-runs). sm[6] = the stagingId.
|
|
338
494
|
if (sm[6] !== undefined && req.method === "POST") {
|
|
339
495
|
const stagingId = safeDecode(sm[6]);
|
|
340
496
|
if (stagingId === null) {
|
|
@@ -342,6 +498,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
342
498
|
return;
|
|
343
499
|
}
|
|
344
500
|
const pending = pendingStagings.get(stagingId);
|
|
501
|
+
// A wrong/foreign/already-finished stagingId → 404 (no oracle). It must also belong to THIS url's session and
|
|
502
|
+
// to the caller (the principal that opened it), unless the caller is fleet-wide (ops on-behalf).
|
|
345
503
|
if (!pending || pending.realSessionId !== sessionId) {
|
|
346
504
|
sendError(res, 404, "not_found.staging", "staging not found");
|
|
347
505
|
return;
|
|
@@ -350,15 +508,24 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
350
508
|
sendError(res, 404, "not_found.staging", "staging not found");
|
|
351
509
|
return;
|
|
352
510
|
}
|
|
353
|
-
const getBlob = (h) => deps.fileSnapshotStore.getBlob(h);
|
|
511
|
+
const getBlob = (h) => deps.fileSnapshotStore.getBlob(h); // cloud's OWN store
|
|
354
512
|
const canImportSnapshots = typeof deps.fileSnapshotStore?.importManifest === "function";
|
|
513
|
+
// ── Stream the NDJSON request body → core's StreamingImportValidator per line → batched appendBatch (bounded
|
|
514
|
+
// memory: line buffer + batch). The streaming validator IS the single invariant gate (unique ids /
|
|
515
|
+
// parent-before-child / structural refs / one-root / leaf-resolvable) — core 1.143.0 — so the service
|
|
516
|
+
// no longer re-states ANY of those rules (no ad-hoc Tier-1 pre-check, no Tier-2 read-back-and-re-validate;
|
|
517
|
+
// the invariant gate belongs to core and the service does not duplicate it). `step()` throws SessionError("invalid_session") on a per-entry violation;
|
|
518
|
+
// `finish()` throws on the end-of-stream invariants. The route keeps ONLY the TRANSPORT-level guards (the
|
|
519
|
+
// per-line byte cap → 413, JSON parse → 400, and the object/string-id shape so a `null`/non-object line maps
|
|
520
|
+
// to a typed 422 instead of an unmapped TypeError); these are NOT session-tree invariants. ──
|
|
355
521
|
const validator = new StreamingImportValidator();
|
|
356
|
-
let lineBuf = "";
|
|
522
|
+
let lineBuf = ""; // the partial-line accumulator (bytes counted against the per-line cap)
|
|
357
523
|
let pendingBatch = [];
|
|
358
524
|
const flush = async () => { if (pendingBatch.length) {
|
|
359
525
|
await pending.handle.appendBatch(pendingBatch);
|
|
360
526
|
pendingBatch = [];
|
|
361
527
|
} };
|
|
528
|
+
// Map a transport-level violation to a typed 4xx WITHOUT committing (the handle's staged rows are abandoned → reaped).
|
|
362
529
|
class PrecheckError extends Error {
|
|
363
530
|
status;
|
|
364
531
|
constructor(status, msg) {
|
|
@@ -366,16 +533,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
366
533
|
this.status = status;
|
|
367
534
|
}
|
|
368
535
|
}
|
|
369
|
-
const streamedIds = new Set();
|
|
536
|
+
const streamedIds = new Set(); // A-3: bind the actual stream to Phase A's declaration
|
|
370
537
|
const onEntry = (e) => {
|
|
371
538
|
if (!e || typeof e !== "object" || typeof e.id !== "string")
|
|
372
539
|
throw new PrecheckError(422, "malformed entry (missing id)");
|
|
373
540
|
streamedIds.add(e.id);
|
|
374
|
-
validator.step(e);
|
|
541
|
+
validator.step(e); // core's invariant gate, one entry at a time (oldest-first) — throws SessionError on a tree violation
|
|
375
542
|
};
|
|
543
|
+
// 对抗复查 A-7(HIGH):per-chunk Buffer.toString("utf8") mangles a MULTI-BYTE character that
|
|
544
|
+
// straddles a network chunk boundary (each half decodes to U+FFFD; the JSON often stays valid →
|
|
545
|
+
// the payload is SILENTLY rewritten — CJK content is the common casualty). StringDecoder holds the
|
|
546
|
+
// partial sequence across chunks — the standard Node fix.
|
|
376
547
|
const utf8 = new StringDecoder("utf8");
|
|
377
548
|
try {
|
|
378
549
|
for await (const chunk of req) {
|
|
550
|
+
// 租约续期(见 importLeases 旁注):上传本身就是活性证据 —— 大会话的长上传不该被判成"持有者死了"。
|
|
379
551
|
touchImportLease(pending.realSessionId);
|
|
380
552
|
lineBuf += utf8.write(chunk);
|
|
381
553
|
if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
|
|
@@ -385,7 +557,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
385
557
|
const line = lineBuf.slice(0, nl).trim();
|
|
386
558
|
lineBuf = lineBuf.slice(nl + 1);
|
|
387
559
|
if (line.length === 0)
|
|
388
|
-
continue;
|
|
560
|
+
continue; // tolerate blank lines / a trailing newline
|
|
389
561
|
let e;
|
|
390
562
|
try {
|
|
391
563
|
e = JSON.parse(line);
|
|
@@ -399,7 +571,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
399
571
|
await flush();
|
|
400
572
|
}
|
|
401
573
|
}
|
|
402
|
-
|
|
574
|
+
// any trailing partial line (no final newline) is still a complete entry to honor.
|
|
575
|
+
lineBuf += utf8.end(); // flush a dangling partial sequence (A-7)
|
|
403
576
|
const tail = lineBuf.trim();
|
|
404
577
|
if (tail.length > 0) {
|
|
405
578
|
let e;
|
|
@@ -413,12 +586,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
413
586
|
pendingBatch.push(e);
|
|
414
587
|
}
|
|
415
588
|
await flush();
|
|
589
|
+
// End-of-stream invariants (exactly-one-root + leaf-resolvable) — finish() throws SessionError on violation.
|
|
416
590
|
validator.finish();
|
|
591
|
+
// 对抗复查 A-3(HIGH):the streamed tree must BE the Phase-A-declared one — same count, same id set.
|
|
592
|
+
// Without this, a caller could open a staging with ids=A (snapshots/anchors parked against A) and
|
|
593
|
+
// stream a different-but-valid tree B to commit; the classification and parked artifacts would
|
|
594
|
+
// silently reference the wrong log. Set equality (order is core-validated separately).
|
|
417
595
|
if (streamedIds.size !== pending.declaredEntryIds.length || !pending.declaredEntryIds.every((id) => streamedIds.has(id))) {
|
|
418
596
|
throw new PrecheckError(422, "streamed entries do not match the Phase-A declared entryIds");
|
|
419
597
|
}
|
|
420
598
|
}
|
|
421
599
|
catch (e) {
|
|
600
|
+
// A transport-level PrecheckError carries its own status; a core SessionError("invalid_session") (a step()
|
|
601
|
+
// or finish() invariant violation) maps to 422. Either way: drop the staged rows + the lease/map entry up
|
|
602
|
+
// front (a malformed stream won't be retried under THIS stagingId; the client re-opens Phase A). The reaper
|
|
603
|
+
// is the backstop if the abort itself fails. Anything else re-throws to the outer handler.
|
|
422
604
|
if (e instanceof PrecheckError) {
|
|
423
605
|
await pending.handle.abort().catch(() => undefined);
|
|
424
606
|
cleanupStaging(stagingId);
|
|
@@ -431,10 +613,20 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
431
613
|
sendError(res, 422, "invalid_entries", "staged session log failed validation");
|
|
432
614
|
return;
|
|
433
615
|
}
|
|
616
|
+
// 对抗复查 A-4(HIGH):a NON-validation exception (storage blip, dropped connection) used to re-throw
|
|
617
|
+
// with the lease + pendingStagings entry INTACT — the outer handler 500s, and every later Phase A for
|
|
618
|
+
// this session 409s `import_in_flight` until the process restarts (the DB reaper can't clear the
|
|
619
|
+
// in-memory Set). Clean up here too: abort the staged rows (best-effort; reaper backstops) + release
|
|
620
|
+
// the lease, so the client's retry re-opens Phase A instead of wedging. Then re-throw for the 500.
|
|
434
621
|
await pending.handle.abort().catch(() => undefined);
|
|
435
622
|
cleanupStaging(stagingId);
|
|
436
623
|
throw e;
|
|
437
624
|
}
|
|
625
|
+
// 对抗复查 A-6(HIGH,route 半件):re-check the ACTIVE-RUN guard right before commit — Phase A's check
|
|
626
|
+
// can be minutes stale by the time the stream finishes, and an overwrite-dst commit would purge a log a
|
|
627
|
+
// LIVE run is appending to. (The owner half lives in the store commit's in-txn re-classify + FOR UPDATE;
|
|
628
|
+
// a residual sub-second TOCTOU between this check and the txn remains — documented, not load-bearing for
|
|
629
|
+
// correctness since the commit txn serializes against appends via FOR UPDATE.)
|
|
438
630
|
const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
|
|
439
631
|
if (activeAtCommit) {
|
|
440
632
|
await pending.handle.abort().catch(() => undefined);
|
|
@@ -442,8 +634,13 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
442
634
|
sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
|
|
443
635
|
return;
|
|
444
636
|
}
|
|
637
|
+
// ── commit (the atomic swap) → replay snapshots/policy/anchors → cache evict → release lease ──
|
|
445
638
|
try {
|
|
446
639
|
const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
|
|
640
|
+
// ① snapshots — content-addressed import (idempotent). importManifest never throws → a {ok:false} is a
|
|
641
|
+
// fail-closed 422 (a blob the Phase-A presence check couldn't catch — corrupted in transit). Note: the
|
|
642
|
+
// ENTRIES are already committed (the swap), so a snapshot failure here means the conversation synced but a
|
|
643
|
+
// file tree did not — we surface 422 so the client re-PUTs the blob and re-POSTs (idempotent).
|
|
447
644
|
if (canImportSnapshots) {
|
|
448
645
|
const importManifest = deps.fileSnapshotStore.importManifest.bind(deps.fileSnapshotStore);
|
|
449
646
|
for (const snap of pending.snapshots) {
|
|
@@ -455,25 +652,32 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
455
652
|
}
|
|
456
653
|
}
|
|
457
654
|
}
|
|
655
|
+
// ② policy — replay each record; the E6 tighten-only gate applies unless the importer is a verified operator.
|
|
458
656
|
const policyStore = deps.sessionPolicyStore;
|
|
459
657
|
if (policyStore)
|
|
460
658
|
for (const rec of pending.policy)
|
|
461
659
|
await policyStore.putRules(sessionId, rec.principal, rec.rules, { operator: pending.operatorOk });
|
|
660
|
+
// ③ anchors — owner RE-KEYED to the importing principal (§9).
|
|
462
661
|
const anchorStore = deps.resumeAnchorStore;
|
|
463
662
|
if (anchorStore)
|
|
464
663
|
for (const a of pending.anchors)
|
|
465
664
|
await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
|
|
665
|
+
// The commit wrote durable state via the RAW store, bypassing the CachingSessionStore wrapper → evict the warm handle.
|
|
466
666
|
deps.sessionStorage.evict?.(sessionId);
|
|
467
667
|
cleanupStaging(stagingId);
|
|
468
668
|
sendJson(res, 200, { relation: committed.relation });
|
|
469
669
|
}
|
|
470
670
|
catch (e) {
|
|
671
|
+
// A commit-time conflict (the in-txn re-classify flipped to fork/stale, TOCTOU close) rolled the swap back →
|
|
672
|
+
// the staging rows still exist → abort them; the client must restart from Phase A with a fresh resolution.
|
|
471
673
|
if (e instanceof SyncConflictError) {
|
|
472
674
|
await pending.handle.abort().catch(() => undefined);
|
|
473
675
|
cleanupStaging(stagingId);
|
|
474
676
|
sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
|
|
475
677
|
return;
|
|
476
678
|
}
|
|
679
|
+
// A SessionPolicyError happens AFTER the swap committed (the policy replay is post-commit) → the conversation
|
|
680
|
+
// synced, only the loosen is refused (fail-closed). The staging rows were already renamed away (abort no-op).
|
|
477
681
|
if (e instanceof SessionPolicyError) {
|
|
478
682
|
cleanupStaging(stagingId);
|
|
479
683
|
sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
|
|
@@ -484,6 +688,11 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
484
688
|
}
|
|
485
689
|
return;
|
|
486
690
|
}
|
|
691
|
+
// POST …/sync/plan — DRY-RUN: classify what a PUSH of the LOCAL peer's log (body.entryIds, the OTHER process's
|
|
692
|
+
// entry-id list) WOULD do against the cloud's current log, WITHOUT writing. The same-process planSync(both
|
|
693
|
+
// backends) doesn't map the HTTP topology (one peer is remote) → the route classifies by ids: the client ids
|
|
694
|
+
// are the SOURCE (oldest-first), the cloud's current entries are the DST. own-or-FRESH gate (a fresh cloud
|
|
695
|
+
// session ⇒ the relation is `fresh`).
|
|
487
696
|
if (sm[2] === "plan" && req.method === "POST") {
|
|
488
697
|
if (!pushGateOk())
|
|
489
698
|
return;
|
|
@@ -499,11 +708,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
499
708
|
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
500
709
|
return;
|
|
501
710
|
}
|
|
502
|
-
const dst = await deps.sessionStorage?.exportEntries?.(sessionId);
|
|
711
|
+
const dst = await deps.sessionStorage?.exportEntries?.(sessionId); // the cloud's current log (null if fresh)
|
|
503
712
|
const relation = classifySyncRelationshipByIds(body.entryIds, dst ? dst.map((e) => e.id) : null);
|
|
504
713
|
sendJson(res, 200, relation);
|
|
505
714
|
return;
|
|
506
715
|
}
|
|
716
|
+
// A matched sync path but a wrong METHOD (e.g. POST /manifest) → 404 (no method-specific 405 elsewhere).
|
|
507
717
|
sendError(res, 404, "not_found.route", "not found");
|
|
508
718
|
return;
|
|
509
719
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/158 A9 域模块 —— 会话列表 / 发件链接列表。
|
|
3
|
+
*
|
|
4
|
+
* 路由:`GET /v1/sessions`(CC /resume picker,keyset 分页)、`GET /v1/sendfile-links`。
|
|
5
|
+
* owner-scope 与 trace 列表同一权威模型(system/traceToken=fleet-wide,principal=钉住本人)。本域零可变状态。
|
|
6
|
+
*/
|
|
1
7
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
8
|
import type { RouteCtx } from "../route-ctx.js";
|
|
3
9
|
export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|