@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,28 +1,14 @@
|
|
|
1
1
|
import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError, } from "@sema-agent/core";
|
|
2
2
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
3
3
|
import { isDupKeyError } from "./sql-errors.js";
|
|
4
|
-
/** 唯一键冲突的方言判别 —— 判据属主 = `sql-errors.ts`(A-032 P1-①:本站点原是全仓唯一的硬化形,
|
|
5
|
-
* 收编时把它**升格成属主**,其余六处按它对齐;`Reflect.get` 走原型链的理由见属主头注)。
|
|
6
|
-
* 只用来把并发登记撞 PK 翻成一句人话,不让驱动错误对象漏给调用方。 */
|
|
7
4
|
const isDupKey = isDupKeyError;
|
|
8
|
-
/** Per-org plane state (`connecting` / `unavailable` / `connected`). One row per org scope. */
|
|
9
5
|
export const SHARED_MEMORY_SCOPE_TABLE = "shared_memory_scope";
|
|
10
|
-
/** The model-facing store registry: one row per browsable library. */
|
|
11
6
|
export const SHARED_MEMORY_STORE_TABLE = "shared_memory_store";
|
|
12
|
-
/** The documents. One row per (store, path). */
|
|
13
7
|
export const SHARED_MEMORY_ENTRY_TABLE = "shared_memory_entry";
|
|
14
|
-
/** core's store-id rule, restated on the WRITE side: `/^[A-Za-z0-9_.-]{1,64}$/` and no `__` (the
|
|
15
|
-
* protocol table's namespace separator). A row failing it would be dropped from every listing — an id
|
|
16
|
-
* the model cannot echo back is worse than an absent one — so we refuse to store it in the first place. */
|
|
17
8
|
const STORE_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
18
|
-
/** core's org scope shape, shared with org-memory-admission. */
|
|
19
9
|
const ORG_SCOPE_RE = /^org:\S{1,185}$/;
|
|
20
|
-
/** core's FORBIDDEN_PATH_CHARS, restated. A path carrying any of these is dropped by core's listing gate. */
|
|
21
10
|
const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
|
|
22
|
-
/** core's DOCUMENT_SUFFIXES, restated. */
|
|
23
11
|
const DOCUMENT_SUFFIXES = [".md", ".txt", ".json", ".jsonl"];
|
|
24
|
-
/** The `path` column width. core's own bound is 1024 UTF-16 units; the column is the tighter of the two
|
|
25
|
-
* and is enforced here so an over-long path fails LOUDLY instead of being truncated by the engine. */
|
|
26
12
|
const PATH_MAX_CHARS = 512;
|
|
27
13
|
const DESCRIPTION_MAX_CHARS = 512;
|
|
28
14
|
const DEFAULT_MAX_LIST_LIMIT = 500;
|
|
@@ -36,11 +22,6 @@ export function assertScopeKey(scopeKey) {
|
|
|
36
22
|
throw new Error(`shared-memory scope key ${JSON.stringify(scopeKey)} is not usable: it must match /^org:\\S{1,185}$/`);
|
|
37
23
|
}
|
|
38
24
|
}
|
|
39
|
-
/**
|
|
40
|
-
* WRITE-side document-path admission. Deliberately at least as strict as core's READ-side listing gate
|
|
41
|
-
* (`isListablePath`): a row core would silently drop is refused here where an operator can see it. The
|
|
42
|
-
* ONE rule that is stricter on purpose is the non-BMP refusal — see the header's ORDERING section.
|
|
43
|
-
*/
|
|
44
25
|
export function assertDocumentPath(path) {
|
|
45
26
|
const refuse = (why) => {
|
|
46
27
|
throw new Error(`shared-memory document path ${JSON.stringify(path)} is not usable: ${why}`);
|
|
@@ -57,27 +38,17 @@ export function assertDocumentPath(path) {
|
|
|
57
38
|
const last = segments.at(-1);
|
|
58
39
|
if (last === undefined || !DOCUMENT_SUFFIXES.some((s) => last.endsWith(s)))
|
|
59
40
|
refuse(`the filename must end in ${DOCUMENT_SUFFIXES.join(", ")}`);
|
|
60
|
-
// Non-BMP refusal: SQL orders by code point, core's cursor progress check compares in UTF-16 code
|
|
61
|
-
// units, and the two disagree ONLY above the BMP. Refusing here keeps "the listing is complete" a
|
|
62
|
-
// structural fact instead of a probabilistic one.
|
|
63
41
|
for (const ch of path) {
|
|
64
42
|
if (ch.codePointAt(0) > 0xffff)
|
|
65
43
|
refuse("it carries a supplementary-plane character, which would make SQL ordering and core's UTF-16 cursor ordering disagree");
|
|
66
44
|
}
|
|
67
45
|
}
|
|
68
|
-
/** `path` values that start with `prefix` live in `[prefix, succ(prefix))`. `undefined` upper bound ⇒
|
|
69
|
-
* the range degenerates to `>= prefix`, which is still a legal SUPERSET (core re-filters), just wider. */
|
|
70
46
|
function prefixUpperBound(prefix) {
|
|
71
47
|
const lastUnit = prefix.charCodeAt(prefix.length - 1);
|
|
72
|
-
// Stop short of the surrogate block: incrementing into 0xD800 would build a lone surrogate, which is
|
|
73
|
-
// not encodable as UTF-8 and would be rejected by both drivers.
|
|
74
48
|
if (!Number.isFinite(lastUnit) || lastUnit >= 0xd7ff)
|
|
75
49
|
return undefined;
|
|
76
50
|
return prefix.slice(0, -1) + String.fromCharCode(lastUnit + 1);
|
|
77
51
|
}
|
|
78
|
-
/** `unavailable` beats `connecting` beats `connected`. A caller whose orgs are PARTLY down is told the
|
|
79
|
-
* plane is down rather than served a silently partial store set — a partial set is indistinguishable
|
|
80
|
-
* from "your team's library has these three documents", which is the failure this state exists to name. */
|
|
81
52
|
function foldPlaneState(states) {
|
|
82
53
|
let sawConnecting = false;
|
|
83
54
|
for (const state of states) {
|
|
@@ -93,8 +64,6 @@ function foldPlaneState(states) {
|
|
|
93
64
|
}
|
|
94
65
|
return sawConnecting ? "connecting" : "connected";
|
|
95
66
|
}
|
|
96
|
-
/** The stored word, re-read. An unknown word in the column is a corrupted/foreign write: refuse the
|
|
97
|
-
* whole snapshot rather than guess (this is the authorization/availability axis — no silent default). */
|
|
98
67
|
function planeStateOf(raw) {
|
|
99
68
|
const word = String(raw);
|
|
100
69
|
switch (word) {
|
|
@@ -115,12 +84,6 @@ function throwIfAborted(signal) {
|
|
|
115
84
|
const reason = signal.reason;
|
|
116
85
|
throw reason instanceof Error ? reason : new Error("shared-memory read aborted");
|
|
117
86
|
}
|
|
118
|
-
/**
|
|
119
|
-
* 回滚,但**绝不让回滚的失败顶掉主错**(codex 轮2 收尾建议)。裸 `await conn.rollback()` 放在 catch 里,
|
|
120
|
-
* 一旦连接已经坏掉(回滚本身抛),抛出去的就变成了回滚错误,真正的失败原因当场丢失 —— 那是排障时最贵的
|
|
121
|
-
* 一种信息损失。这里吞的只有**次要**错误,而且吞得出声(warn 带上主错);连接由调用方的 finally 归还,
|
|
122
|
-
* 池会在下次取用时体检它。
|
|
123
|
-
*/
|
|
124
87
|
async function rollbackPreservingError(conn, primary) {
|
|
125
88
|
try {
|
|
126
89
|
await conn.rollback();
|
|
@@ -130,16 +93,11 @@ async function rollbackPreservingError(conn, primary) {
|
|
|
130
93
|
}
|
|
131
94
|
}
|
|
132
95
|
function storeGone(storeId) {
|
|
133
|
-
// core folds `store_not_found` to "The memory store was not found — it may not be provisioned yet."
|
|
134
|
-
// The alternative (an empty listing) would read to the model as "your team's library is empty".
|
|
135
96
|
return new SharedMemoryStoreError("store_not_found", { message: `shared memory store ${JSON.stringify(storeId)} is no longer provisioned` });
|
|
136
97
|
}
|
|
137
98
|
function rowToStoreInfo(r) {
|
|
138
99
|
return { id: String(r.store_id), description: String(r.description), writable: Boolean(Number(r.writable)) };
|
|
139
100
|
}
|
|
140
|
-
/** 列举行。`sizeBytes` 取 `size_bytes` 列 —— 这是**展示用提示**,不是安全判据:该列独立可写,一个外来
|
|
141
|
-
* 写者可以让它与真实长度不符。任何**决定**(读帽)都必须现算 `OCTET_LENGTH(content)`(见 readDocument);
|
|
142
|
-
* 列举不现算是刻意的取舍:对一整窗 LONGTEXT 逐行算字节等于把每次列举变成全文扫描。 */
|
|
143
101
|
function rowToEntry(r) {
|
|
144
102
|
const updatedAt = strOrNull(r.updated_at_iso);
|
|
145
103
|
return {
|
|
@@ -148,10 +106,6 @@ function rowToEntry(r) {
|
|
|
148
106
|
...(updatedAt !== null ? { updatedAt } : {}),
|
|
149
107
|
};
|
|
150
108
|
}
|
|
151
|
-
/**
|
|
152
|
-
* The dual-dialect implementation. Every statement is written out in BOTH texts at its call site
|
|
153
|
-
* (design/158 A12 判据:方言差异必须显式) — the `q(tidbSql, pgSql)` helper never builds one text.
|
|
154
|
-
*/
|
|
155
109
|
export class SqlSharedMemoryStore {
|
|
156
110
|
db;
|
|
157
111
|
authorizer;
|
|
@@ -166,31 +120,17 @@ export class SqlSharedMemoryStore {
|
|
|
166
120
|
q(tidb, pg) {
|
|
167
121
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
168
122
|
}
|
|
169
|
-
// ───────────────────────────── the core-facing half (read only) ─────────────────────────────
|
|
170
|
-
/**
|
|
171
|
-
* ONE registry-binding snapshot per tool call, as core's seam requires: the state, the store identity
|
|
172
|
-
* set and each reader binding all come from this one call, so the set cannot change between "list the
|
|
173
|
-
* stores" and "resolve one". Document state stays live — which is why a store can still vanish between
|
|
174
|
-
* binding and read, the case {@link storeGone} names.
|
|
175
|
-
*/
|
|
176
123
|
async snapshot(ctx, opts) {
|
|
177
124
|
throwIfAborted(opts.signal);
|
|
178
125
|
const grant = await this.authorizer.resolve(ctx.principal);
|
|
179
126
|
if (grant.kind === "unavailable") {
|
|
180
|
-
// Host-authored reason, bounded and folded to one line by core. NOT "no stores" — see header.
|
|
181
127
|
return { state: "unavailable", message: `shared memory authorization is unavailable (${grant.reason})` };
|
|
182
128
|
}
|
|
183
129
|
const scopes = [...new Set(grant.scopes)];
|
|
184
130
|
if (scopes.length === 0)
|
|
185
131
|
return { state: "connected", stores: [] };
|
|
186
132
|
throwIfAborted(opts.signal);
|
|
187
|
-
// 🔴 codex 复审轮2 F3:盘状态与库登记必须来自**同一个数据库快照**。两条独立的池查询之间,一次
|
|
188
|
-
// 导入可以把 scope 翻成 connecting 再发布新库 —— 请求于是看到"老状态 connected + 新库",一个
|
|
189
|
-
// 数据库瞬间都不曾出现过的组合,而 connecting 这道门的全部意义正是拦住半截导入的库。
|
|
190
|
-
// 事务成本(一次连接借出 + 两次往返)由 core 每个工具调用取一次快照的频次承担;这是授权/可见性
|
|
191
|
-
// 面,一致性优先于省一次借出。
|
|
192
133
|
const { scopeRows, storeRows } = await this.readBinding(scopes);
|
|
193
|
-
// A scope with no row has never been provisioned — that is "connected with nothing in it", not a fault.
|
|
194
134
|
const folded = foldPlaneState(scopeRows.map((r) => planeStateOf(r.state)));
|
|
195
135
|
if (folded === "unavailable") {
|
|
196
136
|
const message = strOrNull(scopeRows.find((r) => planeStateOf(r.state) === "unavailable")?.message);
|
|
@@ -202,23 +142,10 @@ export class SqlSharedMemoryStore {
|
|
|
202
142
|
state: "connected",
|
|
203
143
|
stores: storeRows.map((r) => ({
|
|
204
144
|
info: rowToStoreInfo(r),
|
|
205
|
-
// The reader is PINNED to (store, scope) taken at binding time: a store re-homed to another org
|
|
206
|
-
// between snapshot and read cannot be reached through the stale binding (it reads as vanished).
|
|
207
145
|
reader: this.readerFor(String(r.store_id), String(r.scope_key)),
|
|
208
146
|
})),
|
|
209
147
|
};
|
|
210
148
|
}
|
|
211
|
-
/**
|
|
212
|
-
* 盘状态 + 库登记,**一个事务一个快照**(轮2 F3 修)。
|
|
213
|
-
*
|
|
214
|
-
* 🔴 A-010.11(两臂不对称,验真后修):此前 PG 臂显式抬隔离级别、**MySQL 臂只靠服务端默认值**,
|
|
215
|
-
* 注里写的是「MySQL/TiDB 的默认隔离级别就是 REPEATABLE READ ⇒ 天然同快照」。那句话对**默认配置**
|
|
216
|
-
* 为真,但 `transaction_isolation` 是可设的服务端/会话变量:一台跑 READ COMMITTED 的实例(托管
|
|
217
|
-
* MySQL 的厂商默认、中间层代理、运维手改)会把「一个事务一个快照」悄悄降成「每条语句各自取快照」,
|
|
218
|
-
* 而本方法**不会报任何错**——它只是读到一对撕裂的 scope/store 行。「靠默认值成立」不是结构保证。
|
|
219
|
-
* 两臂现在都走 {@link SqlTxConn.beginRepeatableRead}(语句文本与**摆放位置**的方言分歧写在那里:
|
|
220
|
-
* MySQL 必须在 BEGIN **之前**发、PG 必须在 BEGIN **之后**发)。
|
|
221
|
-
*/
|
|
222
149
|
async readBinding(scopes) {
|
|
223
150
|
const conn = await this.db.connect();
|
|
224
151
|
try {
|
|
@@ -242,14 +169,9 @@ export class SqlSharedMemoryStore {
|
|
|
242
169
|
read: (path, opts) => this.readDocument(storeId, scopeKey, path, opts),
|
|
243
170
|
};
|
|
244
171
|
}
|
|
245
|
-
/** One keyset window. Always the PAGED shape — a large library must never be materialized per call. */
|
|
246
172
|
async listDocuments(storeId, scopeKey, prefix, opts) {
|
|
247
173
|
throwIfAborted(opts.signal);
|
|
248
174
|
const limit = Math.max(1, Math.min(Math.floor(opts.limit ?? this.maxListLimit) || 1, this.maxListLimit));
|
|
249
|
-
// The predicate set is genuinely dynamic (cursor and prefix are each optional and independent), so
|
|
250
|
-
// the two texts are built from ONE clause list rather than written out twice — but the divergence
|
|
251
|
-
// stays visible: `ph()` is the only thing that differs, and both texts are logged side by side by
|
|
252
|
-
// the parity harness. Column list and ORDER BY are byte-identical on both dialects.
|
|
253
175
|
const clauses = [];
|
|
254
176
|
const params = [];
|
|
255
177
|
const bind = (value, clause) => {
|
|
@@ -257,27 +179,19 @@ export class SqlSharedMemoryStore {
|
|
|
257
179
|
const position = params.length;
|
|
258
180
|
clauses.push((ph) => clause(ph(position)));
|
|
259
181
|
};
|
|
260
|
-
// 🔴 codex 复审轮1 F4(真缺陷,已红先复现):列举必须**在主查询里**要求绑定时的 scope,不能只在
|
|
261
|
-
// 空结果时补查。原式只按 store_id 过滤 ⇒ 一个 id 被回收给另一个 org 之后,旧 reader 只要新属主有
|
|
262
|
-
// 文档就能列出对方的 path/大小/时间(正文读那条腿一直核 scope,所以这是一次纯元数据泄漏——更隐蔽)。
|
|
263
182
|
bind(scopeKey, (p) => `s.scope_key = ${p}`);
|
|
264
183
|
bind(storeId, (p) => `e.store_id = ${p}`);
|
|
265
|
-
// EXCLUSIVE lower bound — core's cursor vocabulary, unchanged.
|
|
266
184
|
if (opts.cursor !== undefined)
|
|
267
185
|
bind(opts.cursor, (p) => `e.path > ${p}`);
|
|
268
186
|
if (prefix !== undefined && prefix !== "" && prefix !== "/") {
|
|
269
187
|
bind(prefix, (p) => `e.path >= ${p}`);
|
|
270
188
|
const upper = prefixUpperBound(prefix);
|
|
271
|
-
// Absent upper bound ⇒ the range degenerates to `>= prefix`: a WIDER superset, which core
|
|
272
|
-
// re-filters. Never a miss, so this arm needs no compensating knob.
|
|
273
189
|
if (upper !== undefined)
|
|
274
190
|
bind(upper, (p) => `e.path < ${p}`);
|
|
275
191
|
}
|
|
276
192
|
const whereFor = (ph) => clauses.map((c) => c(ph)).join(" AND ");
|
|
277
193
|
const limitPosition = params.length + 1;
|
|
278
194
|
const from = `FROM ${SHARED_MEMORY_ENTRY_TABLE} e JOIN ${SHARED_MEMORY_STORE_TABLE} s ON s.store_id = e.store_id`;
|
|
279
|
-
// Fetch one past the window: the extra row IS the "more remain" evidence, so `nextCursor` is a fact
|
|
280
|
-
// rather than a guess (core consumes it as a HAS-MORE declaration).
|
|
281
195
|
const res = await this.db.query(this.q(`SELECT e.path AS path, e.size_bytes AS size_bytes, e.updated_at_iso AS updated_at_iso ${from} WHERE ${whereFor(() => "?")} ORDER BY e.path LIMIT ?`, `SELECT e.path AS path, e.size_bytes AS size_bytes, e.updated_at_iso AS updated_at_iso ${from} WHERE ${whereFor((n) => `$${n}`)} ORDER BY e.path LIMIT $${limitPosition}`), [...params, limit + 1]);
|
|
282
196
|
if (res.rows.length === 0 && !(await this.storeExists(storeId, scopeKey)))
|
|
283
197
|
throw storeGone(storeId);
|
|
@@ -286,21 +200,8 @@ export class SqlSharedMemoryStore {
|
|
|
286
200
|
const last = window.at(-1);
|
|
287
201
|
return { entries: window, ...(hasMore && last !== undefined ? { nextCursor: last.path } : {}) };
|
|
288
202
|
}
|
|
289
|
-
/** `null` = the document does not exist. Absence is a VALUE here; only a VANISHED STORE is an error. */
|
|
290
203
|
async readDocument(storeId, scopeKey, path, opts) {
|
|
291
204
|
throwIfAborted(opts.signal);
|
|
292
|
-
// One statement answers both questions: the LEFT JOIN keeps the store row even when the document is
|
|
293
|
-
// absent, so zero rows means the STORE is gone (error) and a row with a NULL path means the DOCUMENT
|
|
294
|
-
// is gone (value). Two separate SELECTs would leave a window where they disagree.
|
|
295
|
-
//
|
|
296
|
-
// 🔴 codex 轮1 F2b + 轮2 F1:`content` 走一层 CASE 门,超帽的行**在数据库里就不发字节** —— 否则一份
|
|
297
|
-
// LONGTEXT 级的文档每次读都要整个物化进进程内存,而它到了 core 那里横竖是被拒的。写面已经把帽挡在
|
|
298
|
-
// 入库(见 putDocument),这道是第二层:接管旧版/别的写者留下的行。
|
|
299
|
-
//
|
|
300
|
-
// 判据取 `OCTET_LENGTH(e.content)` 而**不是** `size_bytes` 列(轮2 修):那一列是独立可写的元数据,
|
|
301
|
-
// 没有任何约束把它绑在真实内容长度上。**低报**方向才是危险的那个 —— 一行"小 size_bytes + 巨大
|
|
302
|
-
// content"会让 CASE 门整个失效并把全文物化。真实字节数由引擎现算,元数据说什么都不影响安全判定
|
|
303
|
-
// (`size_bytes` 因此降级为纯展示用的列举提示,见 rowToEntry)。
|
|
304
205
|
const res = await this.db.query(this.q(`SELECT e.path AS path, OCTET_LENGTH(e.content) AS content_bytes, CASE WHEN OCTET_LENGTH(e.content) <= ? THEN e.content ELSE NULL END AS content, e.updated_at_iso AS updated_at_iso
|
|
305
206
|
FROM ${SHARED_MEMORY_STORE_TABLE} s
|
|
306
207
|
LEFT JOIN ${SHARED_MEMORY_ENTRY_TABLE} e ON e.store_id = s.store_id AND e.path = ?
|
|
@@ -314,25 +215,18 @@ export class SqlSharedMemoryStore {
|
|
|
314
215
|
if (strOrNull(row.path) === null)
|
|
315
216
|
return null;
|
|
316
217
|
if (Number(row.content_bytes) > SHARED_MEMORY_READ_CAP_BYTES) {
|
|
317
|
-
// A typed refusal, not a truncation and not a `null`: core folds `refused` with the host reason,
|
|
318
|
-
// and the model is told the document exists but cannot be served — which is true. The byte count
|
|
319
|
-
// in the message is the ENGINE's, so it is right even when the metadata column lies.
|
|
320
218
|
throw new SharedMemoryStoreError("refused", {
|
|
321
219
|
reason: "document_too_large",
|
|
322
220
|
message: `shared memory document ${JSON.stringify(path)} is ${String(row.content_bytes)} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte read cap`,
|
|
323
221
|
});
|
|
324
222
|
}
|
|
325
223
|
const updatedAt = strOrNull(row.updated_at_iso);
|
|
326
|
-
// Content is returned code-unit EXACT: no normalization, no scrubbing. core owns sanitizing, and a
|
|
327
|
-
// provider that pre-sanitized would make the model read different bytes than the library holds.
|
|
328
224
|
return { content: String(row.content), ...(updatedAt !== null ? { updatedAt } : {}) };
|
|
329
225
|
}
|
|
330
226
|
async storeExists(storeId, scopeKey) {
|
|
331
227
|
const res = await this.db.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ?`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2`), [storeId, scopeKey]);
|
|
332
228
|
return res.rows.length > 0;
|
|
333
229
|
}
|
|
334
|
-
// ───────────────────────── the DEPLOYMENT admin half (never model-facing) ─────────────────────────
|
|
335
|
-
/** Declare an org's plane state. Absent row ⇒ `connected` (a never-provisioned org simply has no stores). */
|
|
336
230
|
async putScopeState(scopeKey, state, message) {
|
|
337
231
|
assertScopeKey(scopeKey);
|
|
338
232
|
const now = this.now();
|
|
@@ -340,18 +234,6 @@ export class SqlSharedMemoryStore {
|
|
|
340
234
|
ON DUPLICATE KEY UPDATE state = VALUES(state), message = VALUES(message), updated_at_ms = VALUES(updated_at_ms)`, `INSERT INTO ${SHARED_MEMORY_SCOPE_TABLE} (scope_key, state, message, updated_at_ms) VALUES ($1, $2, $3, $4)
|
|
341
235
|
ON CONFLICT (scope_key) DO UPDATE SET state = EXCLUDED.state, message = EXCLUDED.message, updated_at_ms = EXCLUDED.updated_at_ms`), [scopeKey, state, message ?? null, now]);
|
|
342
236
|
}
|
|
343
|
-
/**
|
|
344
|
-
* Register (or re-describe) one browsable store.
|
|
345
|
-
*
|
|
346
|
-
* 🔴 codex 复审轮1 F1(真缺陷,已红先复现):**属主 org 不可变**。原式是一条 upsert,`scope_key` 跟着
|
|
347
|
-
* 被改写 —— 而文档行只按 `store_id` 编键、原地不动。于是一次撞 id 的导入(管理面手滑、重试、坏配置,
|
|
348
|
-
* 零竞态)就把 A 的整座库连内容一起交给了 B,同时 A 失去它。所以这里改成:已存在的 id 只允许它自己的
|
|
349
|
-
* org 更新描述/writable 位,换 org 一律响亮拒。真要迁库,那是一次需要显式审计的搬迁(先删再建,内容
|
|
350
|
-
* 由搬迁方决定重导还是丢弃),不能是一条 upsert 的副作用。
|
|
351
|
-
*
|
|
352
|
-
* 事务 + 行锁:属主判定与写入之间不留窗口(同一把锁 {@link deleteStore} 也拿)。并发到达的两个新建
|
|
353
|
-
* 会有一个撞 PK —— 那条也翻成同一句拒绝,不让驱动错误对象漏给调用方。
|
|
354
|
-
*/
|
|
355
237
|
async putStore(record) {
|
|
356
238
|
assertStoreId(record.storeId);
|
|
357
239
|
assertScopeKey(record.scopeKey);
|
|
@@ -359,11 +241,10 @@ export class SqlSharedMemoryStore {
|
|
|
359
241
|
throw new Error(`shared-memory store ${JSON.stringify(record.storeId)} description is ${record.description.length} chars, over the ${DESCRIPTION_MAX_CHARS} the column stores`);
|
|
360
242
|
}
|
|
361
243
|
const now = this.now();
|
|
362
|
-
// 冲突事实在事务里查明、在事务外抛:避免"分支里 rollback + catch 里再 rollback"的双回滚吞异常形。
|
|
363
244
|
let ownedBy;
|
|
364
245
|
const conn = await this.db.connect();
|
|
365
246
|
try {
|
|
366
|
-
await conn.begin();
|
|
247
|
+
await conn.begin();
|
|
367
248
|
const existing = await conn.query(this.q(`SELECT scope_key FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? FOR UPDATE`, `SELECT scope_key FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 FOR UPDATE`), [record.storeId]);
|
|
368
249
|
const owner = existing.rows[0] === undefined ? undefined : String(existing.rows[0].scope_key);
|
|
369
250
|
if (owner !== undefined && owner !== record.scopeKey) {
|
|
@@ -393,18 +274,6 @@ export class SqlSharedMemoryStore {
|
|
|
393
274
|
throw new Error(`shared-memory store id ${JSON.stringify(record.storeId)} is already owned by ${JSON.stringify(ownedBy)} — a store's org is IMMUTABLE (re-homing it would hand every document already written under it to ${JSON.stringify(record.scopeKey)}). Delete it explicitly first if a transfer is really intended.`);
|
|
394
275
|
}
|
|
395
276
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Write one document. Refuses when the store is not registered — an orphan document is unreachable
|
|
398
|
-
* through the read face, so accepting it would be a silent no-op the writer never learns about, AND a
|
|
399
|
-
* live landmine: whoever re-registers that id later inherits it.
|
|
400
|
-
*
|
|
401
|
-
* 🔴 codex 复审轮1 F2/F3 两修:
|
|
402
|
-
* · 帽在**入库**成立 —— 超 `SHARED_MEMORY_READ_CAP_BYTES` 的文档 core 横竖恒拒(从不截断),存下来
|
|
403
|
-
* 只是一行永远读不出来的死数据,却让每次读都要物化它。写面拒 = 那个帽在结构上为真。库里真有超大
|
|
404
|
-
* 文档的话,导入方分块——这是它自己就得做的事,因为模型面本来也读不到整份。
|
|
405
|
-
* · 存在性检查与写入**同事务同行锁**(与 {@link deleteStore} 争同一把):否则一次并发删除可以从检查
|
|
406
|
-
* 与插入之间穿过去,留下一行孤儿文档 —— 被删的内容在 id 复用时复活。
|
|
407
|
-
*/
|
|
408
277
|
async putDocument(record) {
|
|
409
278
|
assertStoreId(record.storeId);
|
|
410
279
|
assertScopeKey(record.scopeKey);
|
|
@@ -417,7 +286,7 @@ export class SqlSharedMemoryStore {
|
|
|
417
286
|
let unregistered = false;
|
|
418
287
|
const conn = await this.db.connect();
|
|
419
288
|
try {
|
|
420
|
-
await conn.begin();
|
|
289
|
+
await conn.begin();
|
|
421
290
|
const registered = await conn.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ? FOR UPDATE`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2 FOR UPDATE`), [record.storeId, record.scopeKey]);
|
|
422
291
|
if (registered.rows.length === 0) {
|
|
423
292
|
unregistered = true;
|
|
@@ -441,20 +310,6 @@ export class SqlSharedMemoryStore {
|
|
|
441
310
|
throw new Error(`shared-memory store ${JSON.stringify(record.storeId)} is not registered under ${JSON.stringify(record.scopeKey)} — either it was never created, or the id has since been recycled by another org (call putStore() first; a stale retry must NOT land in the new tenant's library)`);
|
|
442
311
|
}
|
|
443
312
|
}
|
|
444
|
-
/**
|
|
445
|
-
* Remove one document. Scope-fenced like {@link putDocument} (a stale delete must not reach a
|
|
446
|
-
* successor tenant's library). Returns whether a row was actually there.
|
|
447
|
-
*
|
|
448
|
-
* 🔴 A-010.14(验真后修):此前围栏是**两条独立语句** —— `assertOwnedBy()` 先查一次登记表,然后另
|
|
449
|
-
* 起一条 DELETE。它自称与 `putDocument` 对齐,但 `putDocument` 的检查与写是**同事务同行锁**
|
|
450
|
-
* (`FOR UPDATE`),而这里的两条语句之间有一道真窗:登记检查通过之后、DELETE 发出之前,另一个 org
|
|
451
|
-
* 完成 `deleteStore` + `putStore` 的 id 复用,这条 DELETE 就落进**继任租户**的库里删掉他们的文档。
|
|
452
|
-
* 那正是 `putDocument` 的注里写明要挡住的那一形。现在逐字照它:同一事务、对登记行 `FOR UPDATE`、
|
|
453
|
-
* 拿到锁之后再删。
|
|
454
|
-
*
|
|
455
|
-
* 「未登记 ⇒ 响亮抛」与「登记了但没这条文档 ⇒ 回 false」两件事仍然分家(调用方据此分支),所以
|
|
456
|
-
* 不能收成一条 `DELETE … WHERE EXISTS(…)`:那样 `affected === 0` 会把两种结局压成同一个读数。
|
|
457
|
-
*/
|
|
458
313
|
async deleteDocument(storeId, scopeKey, path) {
|
|
459
314
|
assertStoreId(storeId);
|
|
460
315
|
assertScopeKey(scopeKey);
|
|
@@ -462,7 +317,7 @@ export class SqlSharedMemoryStore {
|
|
|
462
317
|
let deleted = false;
|
|
463
318
|
const conn = await this.db.connect();
|
|
464
319
|
try {
|
|
465
|
-
await conn.begin();
|
|
320
|
+
await conn.begin();
|
|
466
321
|
const registered = await conn.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ? FOR UPDATE`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2 FOR UPDATE`), [storeId, scopeKey]);
|
|
467
322
|
if (registered.rows.length === 0) {
|
|
468
323
|
unregistered = true;
|
|
@@ -482,33 +337,17 @@ export class SqlSharedMemoryStore {
|
|
|
482
337
|
conn.release();
|
|
483
338
|
}
|
|
484
339
|
if (unregistered) {
|
|
485
|
-
// 文案逐字同 assertOwnedBy(调用方/测试按它对表);围栏的**执行面**改了,拒绝的**说法**没改。
|
|
486
340
|
throw new Error(`shared-memory store ${JSON.stringify(storeId)} is not registered under ${JSON.stringify(scopeKey)} — either it does not exist, or the id has since been recycled by another org`);
|
|
487
341
|
}
|
|
488
342
|
return deleted;
|
|
489
343
|
}
|
|
490
|
-
// 属主围栏的共用断言 `assertOwnedBy()` 曾住在这里(轮3 F1)。A-010.14 把它**唯一**的消费者
|
|
491
|
-
// (`deleteDocument`)改成「同事务 + FOR UPDATE」之后它就没有调用点了 —— 留着一个自带围栏语义却
|
|
492
|
-
// 无人调用的私有方法,下一个人会以为「围栏在这儿,照着调就行」,而它恰恰是被判定为不够强的那一版。
|
|
493
|
-
// 边界的如实说明随之搬进 `deleteDocument` / `putDocument`:两者**都不**覆盖同一个 org 内的
|
|
494
|
-
// "删了又建"代际重用 —— 那不是跨租户面,真要挡住需要一枚不可变的 generation token。
|
|
495
|
-
/**
|
|
496
|
-
* De-register a store AND its documents, ATOMICALLY.
|
|
497
|
-
*
|
|
498
|
-
* 🔴 codex 复审轮1 F3(真缺陷):原式是两条独立语句。崩在中间会留下「登记着但被悄悄清空」的库,而
|
|
499
|
-
* 并发的 {@link putDocument} 能从检查与插入之间穿过去留下孤儿行 —— 被删的内容会在这个 id 被重新登记时
|
|
500
|
-
* 复活。现在两条删除同事务,并且**先拿注册行的行锁**:写面争的是同一把锁,两者因此串行。
|
|
501
|
-
*
|
|
502
|
-
* 🔴 轮3 F1 同族:必须带 `scopeKey` 围栏。id 删后可复用,而一条**迟到的** deleteStore 若只按 id 寻址,
|
|
503
|
-
* 会把继任租户刚建好的库连内容一起抹掉 —— 一次歧义的重试变成一次跨租户的破坏性操作。
|
|
504
|
-
*/
|
|
505
344
|
async deleteStore(storeId, scopeKey) {
|
|
506
345
|
assertStoreId(storeId);
|
|
507
346
|
assertScopeKey(scopeKey);
|
|
508
347
|
let notOwned = false;
|
|
509
348
|
const conn = await this.db.connect();
|
|
510
349
|
try {
|
|
511
|
-
await conn.begin();
|
|
350
|
+
await conn.begin();
|
|
512
351
|
const owned = await conn.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ? FOR UPDATE`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2 FOR UPDATE`), [storeId, scopeKey]);
|
|
513
352
|
if (owned.rows.length === 0) {
|
|
514
353
|
notOwned = true;
|
|
@@ -532,13 +371,11 @@ export class SqlSharedMemoryStore {
|
|
|
532
371
|
}
|
|
533
372
|
}
|
|
534
373
|
}
|
|
535
|
-
/** MySQL-protocol (MySQL / TiDB) binding. */
|
|
536
374
|
export class TiDBSharedMemoryStore extends SqlSharedMemoryStore {
|
|
537
375
|
constructor(pool, authorizer, opts) {
|
|
538
376
|
super(mysqlDriver(pool), authorizer, opts);
|
|
539
377
|
}
|
|
540
378
|
}
|
|
541
|
-
/** PostgreSQL binding. */
|
|
542
379
|
export class PgSharedMemoryStore extends SqlSharedMemoryStore {
|
|
543
380
|
constructor(pool, authorizer, opts) {
|
|
544
381
|
super(pgDriver(pool), authorizer, opts);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
2
|
-
/** mysql2 hands back `[rows]` for a SELECT and `[ResultSetHeader]` for a write. */
|
|
3
2
|
function mysqlResult(r) {
|
|
4
3
|
if (Array.isArray(r))
|
|
5
4
|
return { rows: r, affected: 0 };
|
|
@@ -10,8 +9,6 @@ async function mysqlQuery(q, sql, params) {
|
|
|
10
9
|
const [r] = (await q.query(sql, params ?? []));
|
|
11
10
|
return mysqlResult(r);
|
|
12
11
|
}
|
|
13
|
-
/** MySQL-protocol (TiDB) driver. Transaction control uses mysql2's NATIVE methods — the historical
|
|
14
|
-
* call shape the fake-pool unit suites script around. */
|
|
15
12
|
export function mysqlDriver(pool) {
|
|
16
13
|
return {
|
|
17
14
|
dialect: "tidb",
|
|
@@ -24,8 +21,6 @@ export function mysqlDriver(pool) {
|
|
|
24
21
|
beginPessimistic: async () => {
|
|
25
22
|
await c.query("BEGIN PESSIMISTIC");
|
|
26
23
|
},
|
|
27
|
-
// Scope-less `SET TRANSACTION …` = next-transaction only, so it goes BEFORE the BEGIN and
|
|
28
|
-
// leaves the pooled connection's session default untouched for whoever gets it next.
|
|
29
24
|
beginRepeatableRead: async () => {
|
|
30
25
|
await c.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
|
|
31
26
|
await c.beginTransaction();
|
|
@@ -41,7 +36,6 @@ async function pgQuery(q, sql, params) {
|
|
|
41
36
|
const res = await q.query(sql, params ?? []);
|
|
42
37
|
return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
|
|
43
38
|
}
|
|
44
|
-
/** PostgreSQL driver. Transaction control is issued as STATEMENTS (node-pg has no native verbs). */
|
|
45
39
|
export function pgDriver(pool) {
|
|
46
40
|
return {
|
|
47
41
|
dialect: "pg",
|
|
@@ -56,8 +50,6 @@ export function pgDriver(pool) {
|
|
|
56
50
|
beginPessimistic: async () => {
|
|
57
51
|
await c.query("BEGIN");
|
|
58
52
|
},
|
|
59
|
-
// PG takes it INSIDE the transaction (and only before its first query) — the mirror image of
|
|
60
|
-
// the MySQL arm above. Same verb, deliberately different placement.
|
|
61
53
|
beginRepeatableRead: async () => {
|
|
62
54
|
await c.query("BEGIN");
|
|
63
55
|
await c.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
|
|
@@ -73,15 +65,6 @@ export function pgDriver(pool) {
|
|
|
73
65
|
},
|
|
74
66
|
};
|
|
75
67
|
}
|
|
76
|
-
/** design/158 redesign① — the "safe" JSON-column encoder DISPATCH (run-store/outcome-ledger's identical
|
|
77
|
-
* ternary, collected): TiDB stores `JSON.stringify` verbatim; PG goes through `pgSafeJsonStringify`'s
|
|
78
|
-
* protocol-byte defenses (a raw NUL/lone-surrogate would otherwise throw mid-write). Returns a 1-arg
|
|
79
|
-
* encoder; call sites invoke `dialectJsonEncoder(this.db.dialect)(value)` inline — the dispatch is the
|
|
80
|
-
* collected thing, not a cached field. */
|
|
81
68
|
export const dialectJsonEncoder = (dialect) => dialect === "tidb" ? JSON.stringify : pgSafeJsonStringify;
|
|
82
|
-
/** design/158 redesign① — the "protocol" JSON-column encoder DISPATCH (approval-store/checkpoint-store's
|
|
83
|
-
* identical ternary, collected): TiDB stores `JSON.stringify` verbatim (the `label` arg is ignored on that
|
|
84
|
-
* arm — TiDB has no protocol-byte envelope to name); PG goes through `pgProtocolJsonStringify(v, label)`,
|
|
85
|
-
* whose `label` names the field in its error text on an unstorable-byte reject. */
|
|
86
69
|
export const dialectProtocolJsonEncoder = (dialect) => dialect === "tidb" ? (v) => JSON.stringify(v) : pgProtocolJsonStringify;
|
|
87
70
|
//# sourceMappingURL=sql-driver.js.map
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
/** MySQL/TiDB `ER_DUP_ENTRY` 的数字 errno。 */
|
|
2
1
|
export const MYSQL_ER_DUP_ENTRY_ERRNO = 1062;
|
|
3
|
-
/** MySQL/TiDB `ER_DUP_ENTRY` 的字符串错误码(mysql2 的 `code`)。 */
|
|
4
2
|
export const MYSQL_ER_DUP_ENTRY_CODE = "ER_DUP_ENTRY";
|
|
5
|
-
/** PostgreSQL `unique_violation` 的 SQLSTATE。 */
|
|
6
3
|
export const PG_UNIQUE_VIOLATION_SQLSTATE = "23505";
|
|
7
|
-
/** 非对象(null / undefined / 字符串 / 数字)一律否——谓词自身绝不成为新的失败源。 */
|
|
8
4
|
function keyOf(err, key) {
|
|
9
5
|
if (err === null || typeof err !== "object")
|
|
10
6
|
return undefined;
|
|
11
7
|
return Reflect.get(err, key);
|
|
12
8
|
}
|
|
13
|
-
/** MySQL/TiDB 的唯一键冲突:`code === "ER_DUP_ENTRY"` **或** `errno === 1062`(同一件事的两种写法)。 */
|
|
14
9
|
export function isMysqlDupKeyError(err) {
|
|
15
10
|
return String(keyOf(err, "code")) === MYSQL_ER_DUP_ENTRY_CODE || Number(keyOf(err, "errno")) === MYSQL_ER_DUP_ENTRY_ERRNO;
|
|
16
11
|
}
|
|
17
|
-
/** PostgreSQL 的唯一键冲突:SQLSTATE `23505`(node-pg 挂在 `code` 上)。 */
|
|
18
12
|
export function isPgUniqueViolation(err) {
|
|
19
13
|
return String(keyOf(err, "code")) === PG_UNIQUE_VIOLATION_SQLSTATE;
|
|
20
14
|
}
|
|
21
|
-
/** 方言分派口——双方言店的调用形(`isDupKeyError(this.db.dialect, e)`)。 */
|
|
22
15
|
export function isDupKeyError(dialect, err) {
|
|
23
16
|
return dialect === "tidb" ? isMysqlDupKeyError(err) : isPgUniqueViolation(err);
|
|
24
17
|
}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend-NEUTRAL SQL string escaping (design/158 A3 沉底).
|
|
3
|
-
*
|
|
4
|
-
* Lives in its own leaf so no adapter has to load another dialect's adapter module to get a pure
|
|
5
|
-
* string transform (the PG stores used to runtime-import the TiDB tool-result store for exactly
|
|
6
|
-
* this one function). Zero dialect deps by construction — keep it that way.
|
|
7
|
-
*/
|
|
8
|
-
/** Escape SQL LIKE metacharacters (`\`, `%`, `_`) so a value is matched literally (default `\` escape char). */
|
|
9
1
|
export function escapeLike(s) {
|
|
10
2
|
return s.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
11
3
|
}
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared pure row-mapping helpers for the SQL store twins (design/158 S8 纯 helper 归位).
|
|
3
|
-
*
|
|
4
|
-
* These were duplicated verbatim across the pg-/tidb- store twin pairs; each store imports the flavor
|
|
5
|
-
* it was written against (usually under its historical local alias, e.g. `parseJsonStrict as
|
|
6
|
-
* parseJson`) so call sites and semantics are byte-identical to before.
|
|
7
|
-
*
|
|
8
|
-
* THREE parseJson flavors exist ON PURPOSE — they differ in what a corrupted/legacy JSON column
|
|
9
|
-
* reads back as (lenient → null / strict → throw / fallback value), and the TWO nullable date→ISO
|
|
10
|
-
* helpers differ in how a non-Date cell is rendered (String() passthrough vs `new Date()` re-parse).
|
|
11
|
-
* Do NOT collapse them into one.
|
|
12
|
-
*/
|
|
13
|
-
/** JSON column → value; a corrupted/legacy JSON column reads back as `null` instead of failing the
|
|
14
|
-
* read. Non-strings pass through (node-pg parses jsonb → object already). */
|
|
15
1
|
export function parseJsonLenient(v) {
|
|
16
2
|
if (v == null)
|
|
17
3
|
return null;
|
|
@@ -24,16 +10,11 @@ export function parseJsonLenient(v) {
|
|
|
24
10
|
return null;
|
|
25
11
|
}
|
|
26
12
|
}
|
|
27
|
-
/** JSON column → value; malformed JSON THROWS. For stores whose write arm keeps the column clean
|
|
28
|
-
* (R7 终形:协议面拒绝式,库内恒干净,直读). Non-strings pass through (node-pg parses jsonb →
|
|
29
|
-
* object already). */
|
|
30
13
|
export function parseJsonStrict(v) {
|
|
31
14
|
if (v == null)
|
|
32
15
|
return null;
|
|
33
16
|
return (typeof v === "string" ? JSON.parse(v) : v);
|
|
34
17
|
}
|
|
35
|
-
/** JSON column → value with a caller-supplied fallback for NULL/corrupted cells. Non-strings pass
|
|
36
|
-
* through (node-pg parses jsonb → object already; tolerate a string just in case). */
|
|
37
18
|
export function parseJsonOr(v, fallback) {
|
|
38
19
|
if (v == null)
|
|
39
20
|
return fallback;
|
|
@@ -46,20 +27,14 @@ export function parseJsonOr(v, fallback) {
|
|
|
46
27
|
return fallback;
|
|
47
28
|
}
|
|
48
29
|
}
|
|
49
|
-
/** NOT NULL datetime column → ISO string (drivers return Date; a string cell passes through via
|
|
50
|
-
* String() — NOT re-parsed). */
|
|
51
30
|
export function toIso(v) {
|
|
52
31
|
return v instanceof Date ? v.toISOString() : String(v);
|
|
53
32
|
}
|
|
54
|
-
/** Nullable twin of {@link toIso} (string cells still pass through verbatim). */
|
|
55
33
|
export function toIsoOrNull(v) {
|
|
56
34
|
if (v == null)
|
|
57
35
|
return null;
|
|
58
36
|
return v instanceof Date ? v.toISOString() : String(v);
|
|
59
37
|
}
|
|
60
|
-
/** Nullable datetime column → ISO string, RE-PARSING a string cell via `new Date()` (normalizes a
|
|
61
|
-
* raw DB datetime string like "2026-01-01 00:00:00" to ISO — unlike {@link toIsoOrNull}, which
|
|
62
|
-
* passes it through unchanged). */
|
|
63
38
|
export function normalizeIsoOrNull(v) {
|
|
64
39
|
if (v == null)
|
|
65
40
|
return null;
|