@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,103 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable `FileSnapshotStore` (E19 rewind-files) — SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场).
|
|
3
|
-
* ONE implementation, TWO dialects; the historical `TiDBFileSnapshotStore` / `PgFileSnapshotStore` class
|
|
4
|
-
* names survive as thin ctor subclasses so every consumer (store-backend.ts, the migration script, the
|
|
5
|
-
* contract/integration/minio-equivalence suites) is untouched.
|
|
6
|
-
*
|
|
7
|
-
* Cross-replica working-tree snapshots keyed by `(scope=sessionId, key=SessionTreeEntry.id)`, the SAME
|
|
8
|
-
* entryId anchor as E18 resume-at. Used for (a) rewind (restore a PAST turn's tree in the same env) and
|
|
9
|
-
* (b) the seamless local↔cloud work-artifact transfer (2c: a snapshot synced to the cloud, restored in the
|
|
10
|
-
* other environment — conversation + files in lockstep).
|
|
11
|
-
*
|
|
12
|
-
* The env-operating + security-critical half (the tree walk + the symlink-neutralizing restore
|
|
13
|
-
* convergence) is core's `captureManifest`/`applyManifest` — REUSED verbatim (re-implementing risks
|
|
14
|
-
* re-introducing the symlink-escape / fail-closed bugs core's dual-review fixed). This store provides ONLY
|
|
15
|
-
* the durable persistence: content-addressed blobs (sha256 → bytes, deduped across all snapshots, via the
|
|
16
|
-
* shared {@link BlobBackend} seam — SQL by default, MinIO when configured) + a per-`(scope,key)` manifest
|
|
17
|
-
* (relPath → blobHash) that stays in SQL either way (`snapshot_manifest`).
|
|
18
|
-
*
|
|
19
|
-
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
20
|
-
* - `?` placeholders vs `$n` on every `snapshot_manifest` statement (exists-check, insert, selects, the
|
|
21
|
-
* scoped/keyed deletes) — every statement stays written out at the call site via `q()`, never a single
|
|
22
|
-
* text with a numbering loop.
|
|
23
|
-
* - result unwrapping (`rows`/`affected`) and transaction control (mysql2 native begin/commit/rollback vs
|
|
24
|
-
* PG statement form) go through the shared `SqlDriver`/`SqlTxConn` seam (sql-driver.ts) — same
|
|
25
|
-
* normalization the other A12 collapses use; NOT a store-specific concern.
|
|
26
|
-
* - the default {@link BlobBackend} (`SqlBlobBackend`) and `blobSizes` (`sqlBlobSizes`) take the RAW pool
|
|
27
|
-
* + a `"tidb"|"pg"` dialect string directly (blob-backend.ts predates/sits outside this seam — it is a
|
|
28
|
-
* sibling module, not part of this collapse), so this store keeps BOTH the `SqlDriver` (for its own
|
|
29
|
-
* `snapshot_manifest` statements) and the raw native pool (to hand to blob-backend.ts unchanged).
|
|
30
|
-
* - in-flight guard key separator: the TiDB twin used `\x00`, the PG twin used a plain space — NOT a
|
|
31
|
-
* dialect requirement (this key never leaves the process or touches SQL), just accidental drift.
|
|
32
|
-
* Harmonized to `\x00` (can't collide with a normal scope/key string) — zero observable behavior change
|
|
33
|
-
* either way, since the key only dedups concurrent calls on THIS replica within THIS process.
|
|
34
|
-
* - `gcOrphanBlobs`'s orphan-SET query text is IDENTICAL for both dialects (no placeholders) — kept as one
|
|
35
|
-
* shared statement, not a `q()` pair.
|
|
36
|
-
*/
|
|
37
1
|
import { captureManifest, applyManifest, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
38
2
|
import { createHash } from "node:crypto";
|
|
39
3
|
import { SqlBlobBackend, sqlBlobSizes } from "./blob-backend.js";
|
|
40
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
41
|
-
/** Dual-dialect durable FileSnapshotStore. See the file header for the dialect-delta ledger. */
|
|
42
5
|
export class SqlFileSnapshotStore {
|
|
43
6
|
db;
|
|
44
7
|
pool;
|
|
45
8
|
bounds;
|
|
46
|
-
/** In-flight create-once guard (per process) — mirrors InMemoryFileSnapshotStore: a concurrent 2nd
|
|
47
|
-
* snapshot/import of the same (scope,key) on THIS replica is a no-op, not a re-capture. CROSS-REPLICA dup
|
|
48
|
-
* is HARMLESS here: the key is an immutable SessionTreeEntry.id (one settled turn → one fixed tree), so
|
|
49
|
-
* two replicas capture the SAME relPath→hash rows → restore's Map dedups them identically. (A
|
|
50
|
-
* snapshot_header(scope,snap_key) claim row would serialize one winner + drop dup rows — a follow-on
|
|
51
|
-
* hardening for if same-session-multi-replica ever happens; v1 is single-replica affinity so it doesn't.) */
|
|
52
9
|
inFlight = new Set();
|
|
53
|
-
/** The content-addressed BYTE store (clay 2026-06-26 object-store offload). DEFAULT = the SqlBlobBackend
|
|
54
|
-
* over THIS pool — so a MINIO-unset deployment is byte-identical to before this refactor (the
|
|
55
|
-
* `snapshot_blob` table). When MinIO is configured the StoreBackend passes a MinioBlobBackend, moving ONLY
|
|
56
|
-
* the heavy bytes off-DB; the MANIFEST (snapshot_manifest) + the orphan-reference computation STAY in SQL. */
|
|
57
10
|
blobs;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* sink it is silently dropped. Optional observer set by main.ts (metrics face) — never throws into the
|
|
61
|
-
* seam. codex F8: the type admits an ASYNC sink (`void | Promise<void>`) — the call site swallows a
|
|
62
|
-
* rejected promise too (a bare try/catch only caught sync throws, so an async observer's rejection
|
|
63
|
-
* surfaced as an unhandledRejection and could pierce the never-throw posture process-wide). */
|
|
64
|
-
onSkippedVanished = undefined; // explicit initializer: the key must EXIST on the instance (main.ts feature-detects via `in`)
|
|
65
|
-
constructor(db,
|
|
66
|
-
/** The RAW native pool — handed to blob-backend.ts (SqlBlobBackend / sqlBlobSizes), which sits OUTSIDE
|
|
67
|
-
* the SqlDriver seam and dialect-switches on its own `"tidb"|"pg"` string. Not used for this class's OWN
|
|
68
|
-
* `snapshot_manifest` statements (those go through `db`). */
|
|
69
|
-
pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
11
|
+
onSkippedVanished = undefined;
|
|
12
|
+
constructor(db, pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
70
13
|
this.db = db;
|
|
71
14
|
this.pool = pool;
|
|
72
15
|
this.bounds = bounds;
|
|
73
16
|
this.blobs = blobBackend ?? new SqlBlobBackend(this.db.dialect, this.pool);
|
|
74
17
|
}
|
|
75
|
-
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
76
18
|
q(tidb, pg) {
|
|
77
19
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
78
20
|
}
|
|
79
|
-
/** THE ONE content-addressed blob write — the SINGLE source `snapshot()`, `importManifest()`, and the
|
|
80
|
-
* `putBlob` seam all funnel through (no drift across the three writers). Routes the bytes through the
|
|
81
|
-
* BlobBackend (SQL default or MinIO); the SqlBlobBackend's upsert REFRESHES `created_at` even on a dedup
|
|
82
|
-
* hit so a re-used/just-written blob is "touched=now" and GC-grace-protected until a manifest row commits
|
|
83
|
-
* (closes the delete-in-use race). */
|
|
84
21
|
async putBlobRow(hash, bytes) {
|
|
85
22
|
await this.blobs.putBlob(hash, bytes);
|
|
86
23
|
}
|
|
87
|
-
/** 2c session-sync PUSH side — upload ONE content-addressed blob to the cloud's snapshot store AHEAD of a
|
|
88
|
-
* bundle import (§5 streamed blob negotiation: the local peer PUTs only the blobs the cloud is missing,
|
|
89
|
-
* then commits the bundle via importSession, whose getBlob reads the cloud's OWN store). A SERVICE method
|
|
90
|
-
* (NOT on core's FileSnapshotStore interface — core was asked, non-blocking; the route feature-detects
|
|
91
|
-
* `typeof fs.putBlob`). Reuses {@link putBlobRow} VERBATIM (the SAME created_at-refreshing upsert
|
|
92
|
-
* importManifest uses), so a PUT and a concurrent reap can't race: the just-PUT blob's created_at=now
|
|
93
|
-
* keeps it inside the GC grace window until the import references it from a manifest. Idempotent
|
|
94
|
-
* (content-addressed). The route verifies sha256(bytes)===hash BEFORE this. core 1.142.0 made this a
|
|
95
|
-
* fail-closed never-throw seam (Promise<FileSnapshotResult>): a BlobBackend write error (e.g. a
|
|
96
|
-
* MinioBlobBackend non-2xx PUT) is caught + encoded as `restore_failed`, NEVER thrown out of the seam. */
|
|
97
24
|
async putBlob(hash, bytes) {
|
|
98
|
-
// M3(接入审计 2026-07-08):sha256 自校验是契约的【store 级】强制不变量("VERIFIES content-address
|
|
99
|
-
// integrity … a mismatch → read_failed, nothing stored")——不外包给调用方(HTTP 路由前置校验保留=双保险;
|
|
100
|
-
// 绕过路由的内部新调用方不再能用错配 hash 污染 content-addressed store)。镜像 core InMemory 参考实现。
|
|
101
25
|
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
102
26
|
return { ok: false, error: { code: "read_failed", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
|
|
103
27
|
}
|
|
@@ -106,35 +30,20 @@ export class SqlFileSnapshotStore {
|
|
|
106
30
|
return { ok: true };
|
|
107
31
|
}
|
|
108
32
|
catch (err) {
|
|
109
|
-
// A WRITE/upload failure. core's FileSnapshotError code enum has no dedicated write code; `restore_failed` is the
|
|
110
|
-
// code core's OWN File backend uses for a putBlob PERSIST failure (vs `read_failed` = content-address integrity),
|
|
111
|
-
// so we match it + make the MESSAGE explicit that this is a write/upload failure (finding 19).
|
|
112
33
|
return { ok: false, error: { code: "restore_failed", message: `putBlob ${hash} write/upload failed: ${err.message}` } };
|
|
113
34
|
}
|
|
114
35
|
}
|
|
115
|
-
/** CREATE-ONCE: an entryId's tree is immutable, so a 2nd snapshot for an existing key is a no-op {ok:true}. */
|
|
116
36
|
async snapshot(scope, key, env, root, signal) {
|
|
117
|
-
const flightKey = `${scope}\x00${key}`;
|
|
37
|
+
const flightKey = `${scope}\x00${key}`;
|
|
118
38
|
if (this.inFlight.has(flightKey))
|
|
119
39
|
return { ok: true };
|
|
120
40
|
this.inFlight.add(flightKey);
|
|
121
41
|
try {
|
|
122
|
-
// M4(接入审计):create-once 前置 has() 探针必须在 try 【内】——DB 瞬态错(连接池抖动)曾从这里
|
|
123
|
-
// reject 逃逸 never-throw seam,击穿 core 引擎的裸 await。
|
|
124
42
|
if (await this.has(scope, key))
|
|
125
43
|
return { ok: true };
|
|
126
|
-
// captureManifest walks `root` via the env, hands each file's bytes to putBlob (content-addressed dedup), and
|
|
127
|
-
// returns relPath→hash. Fail-CLOSED: on an enumerate/read/bound error we DON'T persist a partial manifest.
|
|
128
|
-
// NEVER-THROW: captureManifest invokes the putBlob callback UN-try/caught, so a throwing BlobBackend (a MinIO
|
|
129
|
-
// non-2xx PUT / network blip — far likelier than a DB error) would escape this never-throw seam. Wrap the whole
|
|
130
|
-
// capture: a putBlob/MinIO throw → {ok:false, restore_failed}, the contract every caller (E19 auto-snapshot, the
|
|
131
|
-
// rewind/2c path) relies on (findings 4/9). The already-PUT distinct blobs are orphans (no manifest references
|
|
132
|
-
// them) → collected by the async sweepOrphanBlobs reaper.
|
|
133
44
|
let cap;
|
|
134
45
|
try {
|
|
135
46
|
cap = await captureManifest(env, root, this.bounds, signal, async (hash, bytes) => {
|
|
136
|
-
// The shared content-addressed blob write (created_at-refreshing upsert) — so a re-used old blob this in-flight
|
|
137
|
-
// snapshot needs is "touched" to now and protected by the GC grace window until its manifest row commits.
|
|
138
47
|
await this.putBlobRow(hash, bytes);
|
|
139
48
|
});
|
|
140
49
|
}
|
|
@@ -143,21 +52,16 @@ export class SqlFileSnapshotStore {
|
|
|
143
52
|
}
|
|
144
53
|
if (!cap.ok)
|
|
145
54
|
return { ok: false, error: cap.error };
|
|
146
|
-
// Observability tail ([805]③a): surface tolerated-vanished counts (metric sink; observer errors swallowed —
|
|
147
|
-
// an observer must never break the never-throw snapshot seam).
|
|
148
55
|
if (cap.skippedVanished > 0 && this.onSkippedVanished) {
|
|
149
|
-
// codex F8: Promise.resolve(...) absorbs an ASYNC sink's rejection (else it's an unhandledRejection);
|
|
150
|
-
// the outer try still absorbs a SYNC throw. Observer errors never break the never-throw seam either way.
|
|
151
56
|
try {
|
|
152
57
|
void Promise.resolve(this.onSkippedVanished(cap.skippedVanished)).catch(() => { });
|
|
153
58
|
}
|
|
154
|
-
catch {
|
|
59
|
+
catch { }
|
|
155
60
|
}
|
|
156
|
-
// Persist the manifest atomically (re-check has() under the txn so a concurrent winner isn't duplicated).
|
|
157
61
|
await this.tx(async (conn) => {
|
|
158
62
|
const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
159
63
|
if (exists.rows[0])
|
|
160
|
-
return;
|
|
64
|
+
return;
|
|
161
65
|
for (const [relPath, hash] of cap.value) {
|
|
162
66
|
await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
|
|
163
67
|
}
|
|
@@ -165,49 +69,21 @@ export class SqlFileSnapshotStore {
|
|
|
165
69
|
return { ok: true };
|
|
166
70
|
}
|
|
167
71
|
catch (err) {
|
|
168
|
-
// The manifest-persist tx (or has()) raised — encode it (never throw out of the seam), parity with importManifest.
|
|
169
72
|
return { ok: false, error: { code: "restore_failed", message: `snapshot persist failed: ${err.message}` } };
|
|
170
73
|
}
|
|
171
74
|
finally {
|
|
172
75
|
this.inFlight.delete(flightKey);
|
|
173
76
|
}
|
|
174
77
|
}
|
|
175
|
-
/** 2c session-sync IMPORT side — the inverse of `exportManifest`/`getBlob`: persist a snapshot's content
|
|
176
|
-
* (manifest + content-addressed blobs) that was EXPORTED from ANOTHER backend (the local→cloud transfer: a src
|
|
177
|
-
* snapshot's manifest+blobs land here as a NEW `(scope,key)`). Unlike `snapshot()` this does NOT walk an env — the
|
|
178
|
-
* manifest is given; bytes come from `srcGetBlob(blobHash)`. Mirrors core's InMemory/File `importManifest` fail-closed
|
|
179
|
-
* matrix verbatim (core 1.141.0). Reuses snapshot()'s EXACT putBlob (created_at-refreshing upsert, GC-grace-safe)
|
|
180
|
-
* + manifest-tx (re-check under txn) patterns.
|
|
181
|
-
*
|
|
182
|
-
* NEVER-throws + FAIL-CLOSED (the matrix core's dual-review fixed, point-named to the durable twins):
|
|
183
|
-
* - CREATE-ONCE: an entryId's tree is immutable → an existing OR in-flight `(scope,key)` is a no-op `{ok:true}`.
|
|
184
|
-
* - `srcGetBlob` is a CROSS-BACKEND fetch (remote/IPC) that CAN REJECT (timeout/ECONNRESET/query error) — caught and
|
|
185
|
-
* encoded as `read_failed`, NEVER thrown out of the seam.
|
|
186
|
-
* - a MISSING blob (`bytes===undefined`) → `read_failed`.
|
|
187
|
-
* - content-address INTEGRITY: `sha256(bytes) !== hash` → `read_failed` (a 2c transfer can cross net/IPC; a corrupt
|
|
188
|
-
* blob stored under a mismatched hash would let a later restore silently write WRONG bytes — verify, don't trust).
|
|
189
|
-
* - ANY blob failure → EARLY-RETURN `{ok:false}` and the manifest tx is NEVER run, so `has()` stays false (no
|
|
190
|
-
* half-snapshot). The DISTINCT blobs already staged before the failing one are orphans (no manifest references
|
|
191
|
-
* them) — their snapshot_blob INDEX rows are reaped (and, under MinIO, the objects deleted) by the async grace-
|
|
192
|
-
* window sweepOrphanBlobs reaper; they never make this `(scope,key)` visible. */
|
|
193
78
|
async importManifest(scope, key, manifest, srcGetBlob) {
|
|
194
|
-
// flightKey 分隔符统一 \x00(与 snapshot 一致;此前 pg 双胞用空格=两方法 in-flight 互斥键空间分裂,
|
|
195
|
-
// 并发 snapshot/import 同一 (scope,key) 不互斥——create-once tx 兜底故无损坏,但键空间应当同一)。
|
|
196
79
|
const flightKey = `${scope}\x00${key}`;
|
|
197
80
|
if (this.inFlight.has(flightKey))
|
|
198
|
-
return { ok: true };
|
|
81
|
+
return { ok: true };
|
|
199
82
|
this.inFlight.add(flightKey);
|
|
200
83
|
try {
|
|
201
|
-
// M4(接入审计):同 snapshot——create-once 前置 has() 探针在 try 内,DB 瞬态错不逃逸 never-throw seam。
|
|
202
84
|
if (await this.has(scope, key))
|
|
203
85
|
return { ok: true };
|
|
204
|
-
// STAGE every DISTINCT blob first (content-addressed: fetch each hash once). Any failure here EARLY-RETURNS before
|
|
205
|
-
// the manifest tx runs → fail-closed: no manifest row ⇒ has() false ⇒ never a half-snapshot a restore would
|
|
206
|
-
// silently truncate. Upsert REFRESHES created_at (incl dedup hits) → blobs touched=now, GC-grace-protected.
|
|
207
86
|
for (const hash of new Set(manifest.values())) {
|
|
208
|
-
// D2(试剂盒揪出,RB-361 同源):路径逃逸型 hash 在任何 fetch **之前**拒,文案逐字同 core
|
|
209
|
-
// File/InMemory 后端(消费方按文案分支不劈叉);SQL 侧 hash 虽只是字符串键,但 srcGetBlob
|
|
210
|
-
// 是跨后端回调(2c 传输面可能远端),可疑值不外递。
|
|
211
87
|
if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
|
|
212
88
|
return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
|
|
213
89
|
}
|
|
@@ -216,23 +92,19 @@ export class SqlFileSnapshotStore {
|
|
|
216
92
|
bytes = await srcGetBlob(hash);
|
|
217
93
|
}
|
|
218
94
|
catch (err) {
|
|
219
|
-
// NEVER throw out of the seam — a rejecting cross-backend fetch is `read_failed` (parity with InMemory/File).
|
|
220
95
|
return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
|
|
221
96
|
}
|
|
222
97
|
if (!bytes)
|
|
223
98
|
return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
|
|
224
|
-
// Content-address integrity — verify ONCE here (the durable backend doesn't blindly trust the transferred bytes).
|
|
225
99
|
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
226
100
|
return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fails content-address integrity` } };
|
|
227
101
|
}
|
|
228
|
-
await this.putBlobRow(hash, bytes);
|
|
102
|
+
await this.putBlobRow(hash, bytes);
|
|
229
103
|
}
|
|
230
|
-
// Commit the manifest atomically ONLY after every distinct blob is staged+verified (re-check has() under the txn so
|
|
231
|
-
// a concurrent winner isn't duplicated) — the SAME tx pattern as snapshot().
|
|
232
104
|
await this.tx(async (conn) => {
|
|
233
105
|
const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
234
106
|
if (exists.rows[0])
|
|
235
|
-
return;
|
|
107
|
+
return;
|
|
236
108
|
for (const [relPath, hash] of manifest) {
|
|
237
109
|
await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
|
|
238
110
|
}
|
|
@@ -240,19 +112,13 @@ export class SqlFileSnapshotStore {
|
|
|
240
112
|
return { ok: true };
|
|
241
113
|
}
|
|
242
114
|
catch (err) {
|
|
243
|
-
// A persist (manifest tx) fs/db error must NOT throw out of the never-throw seam either — encode it. `restore_failed`
|
|
244
|
-
// mirrors core's File backend's outer catch (the manifest-PERSIST failure code, distinct from the fetch-side read_failed).
|
|
245
115
|
return { ok: false, error: { code: "restore_failed", message: `import persist failed: ${err.message}` } };
|
|
246
116
|
}
|
|
247
117
|
finally {
|
|
248
118
|
this.inFlight.delete(flightKey);
|
|
249
119
|
}
|
|
250
120
|
}
|
|
251
|
-
/** Converge `root` back to the snapshot (applyManifest: PHASE-1 remove symlinks/strays, PHASE-2 write captured). */
|
|
252
121
|
async restore(scope, key, env, root, signal) {
|
|
253
|
-
// never-throw seam 全覆盖(L5,接入审计):入口 SELECT 也在 try 内——DB 层异常编码为 restore_failed。
|
|
254
|
-
// applyManifest calls getBlob UN-try/caught; a TRANSIENT MinIO fault THROWS from getBlob (so a restore fails LOUD,
|
|
255
|
-
// not silently-as-missing — finding 11). The same wrap turns that throw into {ok:false, restore_failed}.
|
|
256
122
|
try {
|
|
257
123
|
const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
|
|
258
124
|
if (rows.length === 0)
|
|
@@ -268,40 +134,25 @@ export class SqlFileSnapshotStore {
|
|
|
268
134
|
const { rows } = await this.db.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
269
135
|
return Boolean(rows[0]);
|
|
270
136
|
}
|
|
271
|
-
/** 2c session-sync: enumerate every snapshot key (entryId) in a scope — the SAME enumeration reap()
|
|
272
|
-
* does (`SELECT DISTINCT snap_key`), exposed so 2c can export a session's full set of file snapshots. */
|
|
273
137
|
async listKeys(scope) {
|
|
274
138
|
const { rows } = await this.db.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
275
139
|
return rows.map((r) => String(r.snap_key));
|
|
276
140
|
}
|
|
277
|
-
/** 2c session-sync: the manifest (relPath → blobHash) for one snapshot — to EXPORT its content to another
|
|
278
|
-
* backend byte-for-byte (paired with getBlob for content-addressed transfer). Reads the STORED manifest rows (never
|
|
279
|
-
* re-hashes). 0 rows = no snapshot (a real snapshot always has ≥1 manifest row) → null (parity with InMemory). */
|
|
280
141
|
async exportManifest(scope, key) {
|
|
281
142
|
const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
|
|
282
143
|
if (rows.length === 0)
|
|
283
|
-
return null;
|
|
144
|
+
return null;
|
|
284
145
|
return new Map(rows.map((r) => [String(r.rel_path), String(r.blob_hash)]));
|
|
285
146
|
}
|
|
286
|
-
/** 2c session-sync: the content-addressed bytes for `hash` (undefined if absent) — reads via the BlobBackend
|
|
287
|
-
* (SQL default or MinIO, where getBlob also verifies content-address integrity). */
|
|
288
147
|
async getBlob(hash) {
|
|
289
148
|
return this.blobs.getBlob(hash);
|
|
290
149
|
}
|
|
291
|
-
/** The subset of `hashes` already present in the blob store — reads the SQL INDEX (one IN(...) for BOTH backends),
|
|
292
|
-
* so the §10 import blob-presence pre-check is a cheap index probe, NOT N full-object getBlob GETs (finding 12). */
|
|
293
150
|
async hasBlobs(hashes) {
|
|
294
151
|
return this.blobs.hasBlobs(hashes);
|
|
295
152
|
}
|
|
296
|
-
/** workspace 浏览面(#3,[1894]②):per-blob 字节数——读 SQL INDEX 的 byte_len(单 IN 查询;
|
|
297
|
-
* MinIO 后端的索引行同带 byte_len,两后端同源)。tree 的 size 投影 + file 面「超限先拒后拉」用。 */
|
|
298
153
|
async blobSizes(hashes) {
|
|
299
154
|
return sqlBlobSizes(this.db.dialect, this.pool, hashes);
|
|
300
155
|
}
|
|
301
|
-
/** GC: drop every snapshot in `scope` whose key is NOT in keepKeys. Drops ONLY the SQL manifest rows synchronously —
|
|
302
|
-
* the now-unreferenced blobs become ORPHANS (their snapshot_blob INDEX rows survive) and are collected LATER by the
|
|
303
|
-
* async sweepOrphanBlobs reaper (NEVER a synchronous external-store delete that could block/fail this reap on a MinIO
|
|
304
|
-
* blip — clay 2026-06-26 async-purge design). */
|
|
305
156
|
async reap(scope, keepKeys) {
|
|
306
157
|
return this.tx(async (conn) => {
|
|
307
158
|
const { rows: keys } = await conn.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
@@ -312,24 +163,10 @@ export class SqlFileSnapshotStore {
|
|
|
312
163
|
return drop.length;
|
|
313
164
|
});
|
|
314
165
|
}
|
|
315
|
-
/** E21 purge — drop ALL snapshots for a session (every key). Drops ONLY the SQL manifest rows synchronously → the
|
|
316
|
-
* session is immediately gone + its blobs become orphans; the actual MinIO/SQL BYTE deletion is the async
|
|
317
|
-
* sweepOrphanBlobs reaper's job (bounded-window EVENTUAL CONSISTENCY satisfies the E21 privacy/compliance "really
|
|
318
|
-
* delete the bytes" requirement WITHOUT a fragile synchronous object-store delete on the purge path). Scoped by sessionId. */
|
|
319
166
|
async deleteBySession(scope) {
|
|
320
167
|
const { affected } = await this.db.query(this.q("DELETE FROM snapshot_manifest WHERE scope = ?", "DELETE FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
321
168
|
return affected;
|
|
322
169
|
}
|
|
323
|
-
/** Content-addressed blobs are shared across snapshots → GC only those NO manifest references. The orphan-hash SET is
|
|
324
|
-
* computed from the `snapshot_blob` INDEX in SQL (reference tracking stays in the DB — works for BOTH the SQL backend
|
|
325
|
-
* AND MinIO, whose index rows are kept bytes-NULL); the BlobBackend's deleteBlobs applies the grace window (a just-
|
|
326
|
-
* written blob a concurrent in-flight snapshot needs is never delete-in-use) and, for MinIO, deletes the objects.
|
|
327
|
-
* Returns the count ACTUALLY deleted (the grace-passed subset deleteBlobs reports — NOT the pre-grace orphan count,
|
|
328
|
-
* finding 13). Called ONLY from the async sweepOrphanBlobs reaper (never the synchronous purge path).
|
|
329
|
-
*
|
|
330
|
-
* The orphan-SET query text is IDENTICAL for both dialects (no placeholders) — a single shared statement, not a `q()`
|
|
331
|
-
* pair. A query error degrades to "zero orphans this sweep" (both twins already treated it that way — the reaper
|
|
332
|
-
* retries next tick, so under-collecting once is harmless; never surfaced as a hard failure). */
|
|
333
170
|
async gcOrphanBlobs() {
|
|
334
171
|
const { rows } = await this.db
|
|
335
172
|
.query("SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)")
|
|
@@ -337,16 +174,11 @@ export class SqlFileSnapshotStore {
|
|
|
337
174
|
const orphans = rows.map((r) => String(r.blob_hash));
|
|
338
175
|
if (orphans.length === 0)
|
|
339
176
|
return 0;
|
|
340
|
-
return this.blobs.deleteBlobs(orphans);
|
|
177
|
+
return this.blobs.deleteBlobs(orphans);
|
|
341
178
|
}
|
|
342
|
-
/** 2c session-sync DoS bound + the ASYNC byte-GC for reap/deleteBySession orphans — GLOBALLY GC grace-passed
|
|
343
|
-
* orphan blobs (standalone PUTs never imported AND the blobs a purge/reap left orphaned). The driver is the main.ts
|
|
344
|
-
* reaper (scheduled, error-tolerant: a per-object MinIO delete failure is skipped + retried next sweep → bounded-
|
|
345
|
-
* window eventual consistency). Reuses {@link gcOrphanBlobs} VERBATIM; returns the count actually deleted this sweep. */
|
|
346
179
|
async sweepOrphanBlobs() {
|
|
347
180
|
return this.gcOrphanBlobs();
|
|
348
181
|
}
|
|
349
|
-
/** Run statements atomically; rolls back on failure. */
|
|
350
182
|
async tx(fn) {
|
|
351
183
|
const conn = await this.db.connect();
|
|
352
184
|
try {
|
|
@@ -360,7 +192,6 @@ export class SqlFileSnapshotStore {
|
|
|
360
192
|
await conn.rollback();
|
|
361
193
|
}
|
|
362
194
|
catch {
|
|
363
|
-
/* ignore */
|
|
364
195
|
}
|
|
365
196
|
throw err;
|
|
366
197
|
}
|
|
@@ -369,13 +200,11 @@ export class SqlFileSnapshotStore {
|
|
|
369
200
|
}
|
|
370
201
|
}
|
|
371
202
|
}
|
|
372
|
-
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
373
203
|
export class TiDBFileSnapshotStore extends SqlFileSnapshotStore {
|
|
374
204
|
constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
375
205
|
super(mysqlDriver(pool), pool, blobBackend, bounds);
|
|
376
206
|
}
|
|
377
207
|
}
|
|
378
|
-
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
379
208
|
export class PgFileSnapshotStore extends SqlFileSnapshotStore {
|
|
380
209
|
constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
381
210
|
super(pgDriver(pool), pool, blobBackend, bounds);
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ForkRoutingSessionStore — the subRunner's session backing after core 1.350's fork 终修([1364]①/[1367]①).
|
|
3
|
-
*
|
|
4
|
-
* 问题形:core 的 `hostSessionFork` 在 HOST runner 的店里 fork,fork 子任务带 `requireExistingSession:true`
|
|
5
|
-
* 在 opts.runner(subRunner)上执行——subRunner 拆店时子任务查不到刚 fork 出的会话,响亮失败
|
|
6
|
-
* `resume.session_not_found`(cli 真机复现)。
|
|
7
|
-
*
|
|
8
|
-
* 但把 host durable 店**整个**给 subRunner(第一版修)被 codex 判两 high:普通子任务(council 镜头/
|
|
9
|
-
* team/子代理)会把每次运行都铸成 committed durable 会话——sweepStagingSessions 只扫**无 session_meta**
|
|
10
|
-
* 的 staging 弃置行,committed 子会话没有 reaper=无界增长;且进 owner 过滤的会话枚举(零 run 的内部
|
|
11
|
-
* 会话污染 /v1/sessions)。
|
|
12
|
-
*
|
|
13
|
-
* 此包装店按**会话的真居所**路由,两边语义都保住:
|
|
14
|
-
* - `acquire(id, {requireExisting})`(fork 子 + retained-child resume 的形)→ **host 店优先**,miss 落
|
|
15
|
-
* transient(retained 瞬态子的 resume 仍走私店);两边都 miss=抛 host 侧错误(fail-loud 原语义)。
|
|
16
|
-
* - `acquire()`(无 id=普通子任务铸新会话)→ **transient**(throwaway 姿势保留:council/team 子会话
|
|
17
|
-
* 不落 durable、不进枚举、TTL 自清)。
|
|
18
|
-
* - `acquire(id)`(带 id 不 requireExisting)→ 已有路由按路由走;未知 id 落 transient(claim-create
|
|
19
|
-
* 绝不打到 durable 店——那是 authorizer 的职权)。
|
|
20
|
-
* - `fork(sourceId)` → 源在哪店在哪店 fork(路由未知时 host 先试,null 再 transient);新 id 记路由。
|
|
21
|
-
* - `list`/`size`/`dispose` = transient 侧(host 店归 main runner 所有;不双列、不双清)。
|
|
22
|
-
*
|
|
23
|
-
* 路由表 LRU 上限 {@link ROUTE_MAX}:满额逐最老(refresh-on-use);被逐后下次 `acquire` 按上述规则重derive
|
|
24
|
-
* (requireExisting 的 host-first 重试仍落对店),中途 touch/noteTaskRun 缺路由=落 transient(no-op 级)。
|
|
25
|
-
*/
|
|
26
1
|
import { SessionError } from "@sema-agent/core";
|
|
27
2
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
28
3
|
const ROUTE_MAX = 8192;
|
|
@@ -30,36 +5,20 @@ export class ForkRoutingSessionStore {
|
|
|
30
5
|
host;
|
|
31
6
|
transient;
|
|
32
7
|
route = new Map();
|
|
33
|
-
/** pin 时的店归属——unpin 恒回同店(LRU 逐出不拆对,见 pin/unpin)。 */
|
|
34
8
|
pinnedAt = new Map();
|
|
35
|
-
/** [1593] 晋升名册(探针 Finding A 修):已 promoteToHost 的 id **永久**钉 host——路由表会被 LRU
|
|
36
|
-
* 逐出,而私店的 forget 只清 entries 缓存行、底层 repo 行不清且 sweep 扫不到(core session-store.js
|
|
37
|
-
* 亲读),逐出后 transient-first 的 requireExisting 会把陈旧快照端回给 revive。本集不逐出(上界=
|
|
38
|
-
* parked 子代数,天然小);pick/acquire 首查。 */
|
|
39
9
|
promoted = new Set();
|
|
40
|
-
/** #292 placement 半场:placed id 名册(与 promoted 同姿势——不随路由 LRU 逐出)。placed 会话的
|
|
41
|
-
* 真居所恒=host(分区在 durable 店),任何形的 acquire/touch/release 都不得落 transient——
|
|
42
|
-
* 否则 transient 的 claim-create 会铸出同 id 影子会话,requireExisting 的 transient-first 顺序
|
|
43
|
-
* 会让影子遮蔽真转录(F-7 顺序对 placed 反向成害)。 */
|
|
44
10
|
placed = new Set();
|
|
45
|
-
/** #292:host 店的 placements 声明透传(声明制——包装层不 duck-type,host 声明了才武装 placed
|
|
46
|
-
* 路由;host 不声明时本店全行为与 5.47 前逐字节同)。 */
|
|
47
11
|
placements;
|
|
48
|
-
/** #292:分区枚举透传(host 有才有——恒在的空实现会把「不能枚举」谎报成「没有 placed 会话」)。 */
|
|
49
12
|
listPlaced;
|
|
50
13
|
constructor(host, transient) {
|
|
51
14
|
this.host = host;
|
|
52
15
|
this.transient = transient;
|
|
53
|
-
// #292 声明/枚举面:host 声明什么透传什么(transient 永不声明 placements——分区住 durable 店)。
|
|
54
16
|
if (host.placements !== undefined)
|
|
55
17
|
this.placements = host.placements;
|
|
56
18
|
const hostListPlaced = host.listPlaced;
|
|
57
19
|
if (typeof hostListPlaced === "function") {
|
|
58
20
|
this.listPlaced = (kind, o) => hostListPlaced.call(host, kind, o);
|
|
59
21
|
}
|
|
60
|
-
// backlog(hasSessionFork 恒真):两 backing 都无 fork 面时,包装店的 fork 方法**摘除**——core 的
|
|
61
|
-
// fork 门(hasSessionFork(opts.runner.sessions))才能诚实报 fork.unavailable,而不是放行后折叠成
|
|
62
|
-
// 误导性的「source not found」。现三 backing 均有 fork,此臂为接入无 fork durable backend 备。
|
|
63
22
|
if (host.fork === undefined && transient.fork === undefined) {
|
|
64
23
|
this.fork = undefined;
|
|
65
24
|
}
|
|
@@ -75,16 +34,12 @@ export class ForkRoutingSessionStore {
|
|
|
75
34
|
}
|
|
76
35
|
pick(id) {
|
|
77
36
|
if (this.promoted.has(id) || this.placed.has(id))
|
|
78
|
-
return this.host;
|
|
37
|
+
return this.host;
|
|
79
38
|
const r = this.route.get(id);
|
|
80
39
|
if (r !== undefined)
|
|
81
|
-
this.remember(id, r);
|
|
40
|
+
this.remember(id, r);
|
|
82
41
|
return r === "host" ? this.host : this.transient;
|
|
83
42
|
}
|
|
84
|
-
/** #292:host 声明分区时的冷缓存判位探针。判据=host `requireExisting` acquire 的 `SessionMetadata.placement`
|
|
85
|
-
* (5.48 additive,File/SQL/Ttl 三形都带——比 `placementOf` 面更普适:Ttl 缓存层不透传 placementOf)。
|
|
86
|
-
* placed 命中把 acquired 会话一并交回(调用方正需要它时零二次 acquire);非 placed/absent 的探针
|
|
87
|
-
* acquire 一律 **forget** 收尾([4743] 义务①:验存在的 acquire 禁 release——release(placed)=真删)。 */
|
|
88
43
|
async hostPlacedProbe(id) {
|
|
89
44
|
if (this.placed.has(id)) {
|
|
90
45
|
const acquired = await this.host.acquire(id, { requireExisting: true });
|
|
@@ -97,9 +52,6 @@ export class ForkRoutingSessionStore {
|
|
|
97
52
|
acquired = await this.host.acquire(id, { requireExisting: true });
|
|
98
53
|
}
|
|
99
54
|
catch (err) {
|
|
100
|
-
// not_found=常态(绝大多数 id 非 placed),静默折 absent;**真故障**(host 不可达/读错)同样折
|
|
101
|
-
// absent 但计数留痕(#191 门②:F 类兜底必走 recordFailOpen)——放行的是 transient 腿在 host
|
|
102
|
-
// 故障窗内保持独立可用,代价与恢复路径见 tag 注册表。
|
|
103
55
|
if (!(err instanceof SessionError && err.code === "not_found")) {
|
|
104
56
|
recordFailOpen("server.fork-routing.placed-probe-host-error", err instanceof Error ? err.message : String(err));
|
|
105
57
|
}
|
|
@@ -110,7 +62,7 @@ export class ForkRoutingSessionStore {
|
|
|
110
62
|
this.placed.add(id);
|
|
111
63
|
return { state: "placed", acquired };
|
|
112
64
|
}
|
|
113
|
-
await this.host.forget?.(id);
|
|
65
|
+
await this.host.forget?.(id);
|
|
114
66
|
return { state: "unplaced" };
|
|
115
67
|
}
|
|
116
68
|
async acquire(sessionId, opts) {
|
|
@@ -119,9 +71,6 @@ export class ForkRoutingSessionStore {
|
|
|
119
71
|
this.remember(a.sessionId, "transient");
|
|
120
72
|
return a;
|
|
121
73
|
}
|
|
122
|
-
// #292 建会话入分区形(受信 spawner:带 placement、不 requireExisting)——分区住 host durable 店;
|
|
123
|
-
// host 不声明 placements 时按旧行为落 transient(core 铸造门本就不会给这种部署铸 placement,
|
|
124
|
-
// 这臂只是防御性一致)。
|
|
125
74
|
if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
|
|
126
75
|
const a = await this.host.acquire(sessionId, opts);
|
|
127
76
|
this.placed.add(sessionId);
|
|
@@ -129,8 +78,6 @@ export class ForkRoutingSessionStore {
|
|
|
129
78
|
return a;
|
|
130
79
|
}
|
|
131
80
|
if (this.promoted.has(sessionId) || this.placed.has(sessionId)) {
|
|
132
|
-
// [1593] 晋升名册:恒走 host——绝不能让 transient-first 把 forget 后仍活在私店 repo 里的陈旧
|
|
133
|
-
// 快照端给 revive(探针 Finding A)。placed 名册同规则(#292:影子遮蔽真转录的反向 F-7)。
|
|
134
81
|
const a = await this.host.acquire(sessionId, opts);
|
|
135
82
|
this.remember(sessionId, "host");
|
|
136
83
|
return a;
|
|
@@ -142,19 +89,13 @@ export class ForkRoutingSessionStore {
|
|
|
142
89
|
return a;
|
|
143
90
|
}
|
|
144
91
|
if (opts?.requireExisting) {
|
|
145
|
-
// PAIR-REVIEW F-7(codex 过审):**transient-first**——retained 瞬态子 resume 直达私店;fork 子恒不在
|
|
146
|
-
// transient(miss)→ fallback host 命中,语义不变。顺序反转封住 F-7 组合链:同 id 在两店并存时
|
|
147
|
-
// (child uuid 被同租户误提交到主 API 而 host 店 claim-create 出空壳),真子会话(transient)赢,
|
|
148
|
-
// 空壳不遮蔽。双 miss 抛 host 侧错误(响亮,fail-closed 到「真不存在」)。
|
|
149
|
-
// #292 例外:transient 命中且 host 声明分区时,还要过一次 placed 判位——同 id 若在 host 是 placed
|
|
150
|
-
// 会话,transient 那份是影子(claim-create 铸的),真转录必须赢(F-7 顺序对 placed 反向成害)。
|
|
151
92
|
let transientErr;
|
|
152
93
|
try {
|
|
153
94
|
const a = await this.transient.acquire(sessionId, opts);
|
|
154
95
|
if (this.placements?.subagent !== undefined) {
|
|
155
96
|
const probe = await this.hostPlacedProbe(sessionId);
|
|
156
97
|
if (probe.state === "placed" && probe.acquired !== undefined) {
|
|
157
|
-
await this.transient.forget?.(sessionId);
|
|
98
|
+
await this.transient.forget?.(sessionId);
|
|
158
99
|
this.remember(sessionId, "host");
|
|
159
100
|
return probe.acquired;
|
|
160
101
|
}
|
|
@@ -167,8 +108,6 @@ export class ForkRoutingSessionStore {
|
|
|
167
108
|
}
|
|
168
109
|
try {
|
|
169
110
|
const a = await this.host.acquire(sessionId, opts);
|
|
170
|
-
// 顺手:host 端回的会话若带 placement 元组,入名册(后续零探针直达)。只在声明分区的部署上读
|
|
171
|
-
// (非声明形零行为差),且 meta 读失败不阻断 acquire(名册只是省探针的缓存,不是判定)。
|
|
172
111
|
if (this.placements?.subagent !== undefined) {
|
|
173
112
|
try {
|
|
174
113
|
const meta = await a.session.getMetadata();
|
|
@@ -176,8 +115,6 @@ export class ForkRoutingSessionStore {
|
|
|
176
115
|
this.placed.add(sessionId);
|
|
177
116
|
}
|
|
178
117
|
catch (err) {
|
|
179
|
-
// 名册只是省探针的缓存,不是判定——缺一条=该 id 下次多付一次 hostPlacedProbe,正确性零影响;
|
|
180
|
-
// 计数留痕(#191 门②)让「meta 一直读失败」的环境病在遥测显形。
|
|
181
118
|
recordFailOpen("server.fork-routing.placed-roster-meta-unreadable", err instanceof Error ? err.message : String(err));
|
|
182
119
|
}
|
|
183
120
|
}
|
|
@@ -189,16 +126,13 @@ export class ForkRoutingSessionStore {
|
|
|
189
126
|
throw hostErr;
|
|
190
127
|
}
|
|
191
128
|
}
|
|
192
|
-
// 带 id 但不要求已存在:claim-create 只允许落 transient(durable 店的 claim-create 是 authorizer 的职权)。
|
|
193
|
-
// #292 claim closure(义务⑥过墙形):id 若已是 host 分区的 placed 会话,claim-create 落 transient 会
|
|
194
|
-
// 铸同 id 影子——委托给 host claim 形,让其 placement_refused 响亮拒(绝不静默铸影子)。
|
|
195
129
|
if (this.placements?.subagent !== undefined) {
|
|
196
130
|
const probe = await this.hostPlacedProbe(sessionId);
|
|
197
131
|
if (probe.state === "placed") {
|
|
198
|
-
await this.host.forget?.(sessionId);
|
|
199
|
-
const a = await this.host.acquire(sessionId, opts);
|
|
132
|
+
await this.host.forget?.(sessionId);
|
|
133
|
+
const a = await this.host.acquire(sessionId, opts);
|
|
200
134
|
this.remember(sessionId, "host");
|
|
201
|
-
return a;
|
|
135
|
+
return a;
|
|
202
136
|
}
|
|
203
137
|
}
|
|
204
138
|
const a = await this.transient.acquire(sessionId, opts);
|
|
@@ -212,16 +146,11 @@ export class ForkRoutingSessionStore {
|
|
|
212
146
|
return this.pick(sessionId).release(sessionId);
|
|
213
147
|
}
|
|
214
148
|
forget(sessionId) {
|
|
215
|
-
// PAIR-REVIEW F-8:forget=显式移除 → 路由项同步清(死 id 不再占 LRU 槽挤活路由);release 保留
|
|
216
|
-
// 路由(retained 子 resume 会再 acquire,清了只是多付一次 derive)。
|
|
217
149
|
const target = this.pick(sessionId);
|
|
218
150
|
this.route.delete(sessionId);
|
|
219
151
|
return target.forget?.(sessionId);
|
|
220
152
|
}
|
|
221
153
|
pin(sessionId) {
|
|
222
|
-
// backlog 潜伏钉(pin/unpin 路由不对称):pin 时 refresh 路由近度(remember 已在 pick 内做)+把
|
|
223
|
-
// pinned id 记入独立集合——unpin 优先按 pin 时的店走,LRU 逐出不再能把 pin/unpin 拆到两家店
|
|
224
|
-
// (host 店永久 pinned 不可回收的潜伏形就此封死)。
|
|
225
154
|
const r = this.route.get(sessionId);
|
|
226
155
|
if (r !== undefined)
|
|
227
156
|
this.pinnedAt.set(sessionId, r);
|
|
@@ -241,23 +170,6 @@ export class ForkRoutingSessionStore {
|
|
|
241
170
|
list() {
|
|
242
171
|
return this.transient.list?.() ?? Promise.resolve([]);
|
|
243
172
|
}
|
|
244
|
-
/**
|
|
245
|
-
* [1593] 子代 park 晋升:把住在 transient(TTL)店的子会话**整树拷贝**进 host durable 店,然后
|
|
246
|
-
* 让该 id 从此路由到 host——`ensureChildSessionDurable` 的「主动晋升」半场(1.263 的只查不迁形在
|
|
247
|
-
* 拆店生产形下恒否决 park:子会话从不在 host 店,core 收 veto 即 expire checkpoint + settle failed,
|
|
248
|
-
* cli 真机 14ms~2s expired 即此)。
|
|
249
|
-
*
|
|
250
|
-
* 三步缺一不可:
|
|
251
|
-
* 1. 拷贝:transient 读整树(getEntries)+ 源 leaf → host 店 acquire(id)(durable 店的 acquire
|
|
252
|
-
* 缺行即建行)→ storage.appendEntry 逐条 verbatim(重复 id 容忍=幂等续迁:上次晋升半途崩的
|
|
253
|
-
* 残留行跳过)→ setLeafId(源 leaf)(分支树以源为准,不信 append 推进的 leaf)。
|
|
254
|
-
* 2. transient.forget(id):`requireExisting` 的 acquire 是 **transient-first**(F-7 裁定)——私店
|
|
255
|
-
* 副本不移除,revive 会续写进 1h TTL 副本,durable park 名存实亡。
|
|
256
|
-
* 3. 路由记 host(此后 touch/pin/release 全走 host;pinnedAt 若记着 transient 一并改写,
|
|
257
|
-
* unpin 不落错店)。
|
|
258
|
-
*
|
|
259
|
-
* 已在 host(路由或真行)⇒ 幂等直返。两店都无此会话 ⇒ 抛(调用方=park veto,诚实 fail-closed)。
|
|
260
|
-
*/
|
|
261
173
|
async promoteToHost(sessionId) {
|
|
262
174
|
if (this.promoted.has(sessionId))
|
|
263
175
|
return;
|
|
@@ -270,26 +182,17 @@ export class ForkRoutingSessionStore {
|
|
|
270
182
|
src = await this.transient.acquire(sessionId, { requireExisting: true });
|
|
271
183
|
}
|
|
272
184
|
catch {
|
|
273
|
-
// 私店无此会话:可能已在 host(直查确认=幂等),否则如实抛(host 侧错误响亮上浮)
|
|
274
185
|
const probe = await this.host.acquire(sessionId, { requireExisting: true });
|
|
275
|
-
// #292 preflight 纪律([4743] 义务①):**只为验存在**的 acquire 收尾一律 forget —— host 店的
|
|
276
|
-
// release 对 placed 会话是真删除(core 5.48 义务 4),用它给一次探活收尾就是拿删除当探针。
|
|
277
186
|
await this.host.forget?.(probe.sessionId);
|
|
278
187
|
this.promoted.add(sessionId);
|
|
279
188
|
this.remember(sessionId, "host");
|
|
280
189
|
return;
|
|
281
190
|
}
|
|
282
|
-
// ⚠️ 私店源在此**不 release**:TtlSessionStore.release 对非 pinned 会话=整删(F3 教训)——拷贝
|
|
283
|
-
// 失败时删源=毁掉仍在跑/仍可赎回的子会话。成功路径由下方 forget(纯 entries 移除,无 repo 删)收尾;
|
|
284
|
-
// 失败路径源原样留活,veto 如实上浮。
|
|
285
191
|
const entries = await src.session.getEntries();
|
|
286
192
|
const leaf = await src.session.getLeafId();
|
|
287
193
|
const dst = await this.host.acquire(sessionId);
|
|
288
194
|
try {
|
|
289
195
|
const storage = dst.session.getStorage();
|
|
290
|
-
// 探针 Finding B 修:core 各 storage 的 appendEntry 对重复 id **不抛不查**(storage-base 亲读)
|
|
291
|
-
// ——靠 try/catch 实现幂等是空想,断续晋升会把同 id 双写成脏树(且脏树会被 import-validate 的
|
|
292
|
-
// 重复 id 门拒)。真幂等 = 先读 dst 现存 id 集,set-diff 后只补缺。
|
|
293
196
|
const existing = new Set((await dst.session.getEntries()).map((e) => e.id));
|
|
294
197
|
for (const entry of entries) {
|
|
295
198
|
if (existing.has(entry.id))
|
|
@@ -302,18 +205,12 @@ export class ForkRoutingSessionStore {
|
|
|
302
205
|
throw new Error(`session "${sessionId}" promotion verification failed (leaf mismatch after import)`);
|
|
303
206
|
}
|
|
304
207
|
finally {
|
|
305
|
-
// #292:晋升写完只需**丢引用**(内容必须留下,那正是晋升的目的)⇒ forget,不是 release
|
|
306
|
-
// (同上:release 对 placed 会话已是真删除,而这条 finally 连失败路径都会走)。
|
|
307
208
|
await this.host.forget?.(sessionId);
|
|
308
209
|
}
|
|
309
210
|
try {
|
|
310
211
|
await this.transient.forget?.(sessionId);
|
|
311
212
|
}
|
|
312
213
|
catch {
|
|
313
|
-
// forget 失败不阻断晋升。⚠️ forget 本就只清私店的 entries 缓存行——底层 repo 行不清、sweep 也
|
|
314
|
-
// 扫不到(探针 Finding A 取证),陈旧快照在私店**永生**;对 revive 的真防线是上面的晋升名册
|
|
315
|
-
// (promoted 集恒指 host,不随路由 LRU 逐出),forget 只是缩内存的 best-effort。transient.pinned
|
|
316
|
-
// 可能残留该 id(forget 不清 pin 集)——unpin 已随 pinnedAt 改写落 host,残留仅是空集合项。
|
|
317
214
|
}
|
|
318
215
|
if (this.pinnedAt.get(sessionId) === "transient")
|
|
319
216
|
this.pinnedAt.set(sessionId, "host");
|
|
@@ -340,7 +237,7 @@ export class ForkRoutingSessionStore {
|
|
|
340
237
|
return this.transient.size;
|
|
341
238
|
}
|
|
342
239
|
dispose() {
|
|
343
|
-
return this.transient.dispose();
|
|
240
|
+
return this.transient.dispose();
|
|
344
241
|
}
|
|
345
242
|
}
|
|
346
243
|
//# sourceMappingURL=fork-routing-session-store.js.map
|