@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,27 +1,11 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
2
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
3
3
|
import { isDupKeyError } from "./sql-errors.js";
|
|
4
|
-
/**
|
|
5
|
-
* 幂等键的入店卫生门(属主兜底轮 F2,三 twin 同判——SQL 双方言与 InMemory 都从这里过)。
|
|
6
|
-
*
|
|
7
|
-
* 🔴 为什么必须在店门口拒而不是「存什么比什么」:TiDB/MySQL 的 VARCHAR 等值与 UNIQUE 检查带
|
|
8
|
-
* PAD SPACE 语义(utf8mb4_bin 也一样)——`'retry'` 与 `'retry '` 在那侧是**同一个键**,而 PG 与
|
|
9
|
-
* InMemory 的严格字符串比较把它们当两个键 ⇒ 同一份调用方代码在不同后端拿到不同的
|
|
10
|
-
* `idempotency_conflict`/回放结果。键是调用方铸的 opaque 值,首尾空白没有任何合法语义,
|
|
11
|
-
* 在边界上 fail-loud 拒掉,方言分歧面整个消失(比把列改 VARBINARY 更窄、且对三形同时成立)。
|
|
12
|
-
*/
|
|
13
4
|
export function assertIdempotencyKeyShape(key) {
|
|
14
5
|
if (key.length === 0 || key.length > 255 || key.trim() !== key) {
|
|
15
6
|
throw new Error(`invalid idempotency key: must be 1-255 chars with no leading/trailing whitespace (got ${JSON.stringify(key.slice(0, 32))}${key.length > 32 ? "…" : ""}, length ${key.length})`);
|
|
16
7
|
}
|
|
17
8
|
}
|
|
18
|
-
/**
|
|
19
|
-
* #151 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
|
|
20
|
-
*
|
|
21
|
-
* 具名类而不是「按 message 文本判别」:错误文案是给人看的,拿它当控制流 = 上游改一句人话下游静默失效
|
|
22
|
-
* (#90 工程规范 v3 门②)。调用方(开流 preamble)对它的处置与其余读错误一样 —— fail-open、零卡、一次
|
|
23
|
-
* warn —— 所以它今天没有专门的分支;类在这里是为了让**将来**想分「超时 vs 店真报错」的消费点有得分。
|
|
24
|
-
*/
|
|
25
9
|
export class ApprovalAskReadAbortedError extends Error {
|
|
26
10
|
constructor(where) {
|
|
27
11
|
super(`approval-ask read ${where} aborted by caller`);
|
|
@@ -31,14 +15,6 @@ export class ApprovalAskReadAbortedError extends Error {
|
|
|
31
15
|
function abortedReadError(where) {
|
|
32
16
|
return new ApprovalAskReadAbortedError(where);
|
|
33
17
|
}
|
|
34
|
-
/**
|
|
35
|
-
* 把一次在飞的读与调用方的 `AbortSignal` 竞速(#151 车3 刀 3b)。
|
|
36
|
-
*
|
|
37
|
-
* 🔴 **它做到什么、做不到什么**(诚实边界,同 {@link ApprovalAskStore.listPendingBySession} 的注):
|
|
38
|
-
* 本仓的 `SqlDriver` 没有取消 seam,所以这里能做的是「让**调用方**不再被吊住」,不是「让**库**停下来」。
|
|
39
|
-
* 输掉竞速的那条 promise 仍在飞——它的 rejection 必须被接住(否则变成 unhandled rejection),它的成功
|
|
40
|
-
* 结果被丢弃(纯读,零副作用)。监听器用完即摘(`once` + finally 清理),不给长命 signal 攒 listener。
|
|
41
|
-
*/
|
|
42
18
|
async function withReadSignal(op, signal, where) {
|
|
43
19
|
if (!signal)
|
|
44
20
|
return op;
|
|
@@ -47,8 +23,8 @@ async function withReadSignal(op, signal, where) {
|
|
|
47
23
|
onAbort = () => reject(abortedReadError(where));
|
|
48
24
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
49
25
|
});
|
|
50
|
-
aborted.catch(() => undefined);
|
|
51
|
-
void op.catch(() => undefined);
|
|
26
|
+
aborted.catch(() => undefined);
|
|
27
|
+
void op.catch(() => undefined);
|
|
52
28
|
try {
|
|
53
29
|
return await Promise.race([op, aborted]);
|
|
54
30
|
}
|
|
@@ -57,56 +33,8 @@ async function withReadSignal(op, signal, where) {
|
|
|
57
33
|
signal.removeEventListener("abort", onAbort);
|
|
58
34
|
}
|
|
59
35
|
}
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
// DDL
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
63
36
|
export const APPROVAL_ASK_TABLE = "approval_ask";
|
|
64
37
|
export const APPROVAL_BATCH_TABLE = "approval_batch";
|
|
65
|
-
/**
|
|
66
|
-
* TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
|
|
67
|
-
* `SCHEMA_STATEMENTS` 用 `...TIDB_APPROVAL_ASK_STATEMENTS` 展开它们,于是这两张表随中央
|
|
68
|
-
* `ensureSchema` 一起在 **named-lock 的那条 `conn`** 上建 —— 把一个 pool 级 ensure 函数塞进
|
|
69
|
-
* `SCHEMA_STATEMENTS` 的执行循环里做不到这点(会绕开 DDL 串行化),所以真源必须是**语句文本**。
|
|
70
|
-
* 下面的 `ensureTiDBApprovalAskSchema` 保留为遍历这个数组的薄壳(集成测试直接调它建表,
|
|
71
|
-
* 不必为了建两张表去拉起整套中央 schema)。
|
|
72
|
-
*
|
|
73
|
-
* SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
|
|
74
|
-
* (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
|
|
75
|
-
*
|
|
76
|
-
* ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
|
|
77
|
-
* 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
|
|
78
|
-
*
|
|
79
|
-
* 收窄(有硬上限背书,证据即上限本身):
|
|
80
|
-
* · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
|
|
81
|
-
* 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
|
|
82
|
-
* · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
|
|
83
|
-
* `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
|
|
84
|
-
* · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
|
|
85
|
-
* 与 `session_meta`/`task_run` 同宽。
|
|
86
|
-
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts` 的同名导出常量,
|
|
87
|
-
* 刻意不钉行号 —— 旧注写的 `security.ts:350` 已随该文件编辑漂走)硬拒更长者;
|
|
88
|
-
* 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
|
|
89
|
-
* · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
|
|
90
|
-
* 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
|
|
91
|
-
* 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
|
|
92
|
-
*
|
|
93
|
-
* 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
|
|
94
|
-
* task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
|
|
95
|
-
* `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
|
|
96
|
-
* 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
|
|
97
|
-
* ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
|
|
98
|
-
*
|
|
99
|
-
* 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
|
|
100
|
-
* · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
|
|
101
|
-
* **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
|
|
102
|
-
* `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
|
|
103
|
-
* 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
|
|
104
|
-
* (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
|
|
105
|
-
* · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
|
|
106
|
-
* 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
|
|
107
|
-
* · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
|
|
108
|
-
* 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
|
|
109
|
-
*/
|
|
110
38
|
export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
111
39
|
`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
|
|
112
40
|
batch_id VARCHAR(64) NOT NULL,
|
|
@@ -168,8 +96,6 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
168
96
|
KEY idx_ask_state (state)
|
|
169
97
|
) COLLATE utf8mb4_bin`,
|
|
170
98
|
];
|
|
171
|
-
/** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
|
|
172
|
-
* (那里展开了同一份数组),本函数留给只需要这两张表的集成测试。 */
|
|
173
99
|
export async function ensureTiDBApprovalAskSchema(pool) {
|
|
174
100
|
for (const stmt of TIDB_APPROVAL_ASK_STATEMENTS)
|
|
175
101
|
await pool.query(stmt);
|
|
@@ -223,8 +149,6 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
223
149
|
CONSTRAINT uq_approval_ask_idem UNIQUE (task_id, idempotency_key)
|
|
224
150
|
)`);
|
|
225
151
|
await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASK_TABLE} (task_id, state)`);
|
|
226
|
-
// 车3 §5.1:sync 腿开流重放读口(listPendingBySession)的支撑索引 —— MySQL twin 的
|
|
227
|
-
// `KEY idx_ask_session_state (session_id, state)` 同形。
|
|
228
152
|
await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASK_TABLE} (session_id, state)`);
|
|
229
153
|
await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASK_TABLE} (batch_id)`);
|
|
230
154
|
await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASK_TABLE} (state)`);
|
|
@@ -234,10 +158,6 @@ const ASK_COLS = "ask_id, task_id, source_task_id, session_id, owner, batch_id,
|
|
|
234
158
|
"gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, " +
|
|
235
159
|
"created_at_ms, updated_at_ms";
|
|
236
160
|
const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
|
|
237
|
-
/** JSON 列的读出:**本层只做存取,不解释**——返回类型即 `unknown`,把「这是什么形」的责任留给消费方。
|
|
238
|
-
* 两个 JSON 列的 schema 属主都不在本车:`decision_actor` = design/171 ActorAssertion 形(车4 回决端点
|
|
239
|
-
* 消费,届时按宪法 [2704] 立 schema 后 safeParse);`card_json` = 172 §3.1 呈卡帧载荷(车3 wire 面属主)。
|
|
240
|
-
* 故此处刻意不铸内域类型(无 as),持久层对这两列保持透明字节存取语义。 */
|
|
241
161
|
function parseJsonColumn(v) {
|
|
242
162
|
return JSON.parse(String(v));
|
|
243
163
|
}
|
|
@@ -292,8 +212,6 @@ function mapBatchRow(r) {
|
|
|
292
212
|
updatedAtMs: Number(r.updated_at_ms),
|
|
293
213
|
};
|
|
294
214
|
}
|
|
295
|
-
/** 动态 SET 列清单(`transitionAsk`/`resolveProvisional` 用):只收 patch 里真出现的字段,`decisionActor`
|
|
296
|
-
* 按内容面 JSON 编码器序列化。`updatedAtMs` 不在这里——调用方把它拼进固定尾列(所有转移恒推进它)。 */
|
|
297
215
|
function patchColumns(patch, json) {
|
|
298
216
|
const out = [];
|
|
299
217
|
if (patch.decision !== undefined)
|
|
@@ -316,14 +234,9 @@ function patchColumns(patch, json) {
|
|
|
316
234
|
out.push(["provisional", patch.provisional ? 1 : 0]);
|
|
317
235
|
return out;
|
|
318
236
|
}
|
|
319
|
-
/** 第 n 个(1-based)绑定参数的占位符文本。TiDB 用位置序 `?`(n 被忽略);PG 需要显式编号 `$n`——
|
|
320
|
-
* 两种转移(transitionAsk/resolveProvisional)的列集合按 patch 内容动态变化,固定的两段式 SQL 文本
|
|
321
|
-
* 写不出来,因此占位符本身也必须按方言现场生成(与 background-agent-store-sql.ts 的 guard 占位符
|
|
322
|
-
* 构造同精神:方言差异仍然显式——分支在这一个函数里,不是被某个跨方言的抽象吞掉)。 */
|
|
323
237
|
function ph(dialect, n) {
|
|
324
238
|
return dialect === "tidb" ? "?" : `$${n}`;
|
|
325
239
|
}
|
|
326
|
-
/** 双方言 `ApprovalAskStore`。见文件头注的 CAS 铁则 + 方言差异清单。 */
|
|
327
240
|
export class SqlApprovalAskStore {
|
|
328
241
|
db;
|
|
329
242
|
constructor(db) {
|
|
@@ -335,44 +248,24 @@ export class SqlApprovalAskStore {
|
|
|
335
248
|
json(value) {
|
|
336
249
|
return dialectJsonEncoder(this.db.dialect)(value);
|
|
337
250
|
}
|
|
338
|
-
/** 唯一键冲突判别 —— 判据属主 = `sql-errors.ts`(A-032 P1-①)。`decideAsk` 用它把 `(task_id,
|
|
339
|
-
* idempotency_key)` 撞键翻成 typed 的 `idempotency_conflict` 臂,不让驱动错误对象漏给消费层;
|
|
340
|
-
* 旧形 tidb 臂只认 errno,只带 `code` 的驱动错误会漏判成"真障碍"。 */
|
|
341
251
|
isDupKey(e) {
|
|
342
252
|
return isDupKeyError(this.db.dialect, e);
|
|
343
253
|
}
|
|
344
|
-
/**
|
|
345
|
-
* 🔴 失败臂的回读**必须走当前这条 `conn`**,不许调 `this.getAsk`/`this.getBatch`(codex 复审 F1,
|
|
346
|
-
* 2026-08-06 真缺陷,池大小=1 的钉上实测挂死 25s)。原因:那两个读口走**池级** `this.db.query`,而
|
|
347
|
-
* 此刻本方法还攥着自己的事务连接(要到 `finally` 才 release)——于是「回读在等一条新连接,而唯一
|
|
348
|
-
* 那条连接要等回读返回才释放」= 确定性死锁。池大于 1 时它退化成「同时走到失败臂的并发数 ≥ 池大小
|
|
349
|
-
* 就整池饿死」,是同一个缺陷的更难复现形,不是另一个问题。
|
|
350
|
-
*
|
|
351
|
-
* 调用点纪律:**先 `conn.rollback()` 再调这两个**。回滚之后连接回到 autocommit,这条 SELECT 自成
|
|
352
|
-
* 一个事务 ⇒ 读到的是最新已提交视图(正是失败臂要如实上报的东西),不受本事务快照的影响——两方言
|
|
353
|
-
* 在这一点上同义,不必再纠结 InnoDB 读视图与 TiDB start_ts 的差别。
|
|
354
|
-
*/
|
|
355
254
|
async getAskOn(conn, askId) {
|
|
356
255
|
const { rows } = await conn.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [askId]);
|
|
357
256
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
358
257
|
}
|
|
359
|
-
/** {@link getAskOn} 的批侧同形(同一条纪律:失败臂回读走本连接)。 */
|
|
360
258
|
async getBatchOn(conn, batchId) {
|
|
361
259
|
const { rows } = await conn.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = $1`), [batchId]);
|
|
362
260
|
return rows[0] ? mapBatchRow(rows[0]) : null;
|
|
363
261
|
}
|
|
364
262
|
async ensureAsk(row) {
|
|
365
263
|
const now = row.createdAtMs;
|
|
366
|
-
/** 本次 ask 插入的 affected 行数(1 = 真插入 / 0 = 幂等命中)——判别位的**唯一**来源,见
|
|
367
|
-
* {@link EnsureAskResult}。两方言各自的语句形不同(`INSERT IGNORE` vs `ON CONFLICT DO NOTHING`),
|
|
368
|
-
* 但 affected 的语义是同一个,`SqlDriver` 已把 mysql2 的 `affectedRows` 与 pg 的 `rowCount` 归一。 */
|
|
369
264
|
let insertedRows = 0;
|
|
370
265
|
const conn = await this.db.connect();
|
|
371
266
|
try {
|
|
372
267
|
await conn.begin();
|
|
373
268
|
await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
|
|
374
|
-
// 🔴 `idempotency_key` **不在 INSERT 列清单里**(车4 §12-E 双写者收口):该列已归回决专用,只有
|
|
375
|
-
// `decideAsk` 赢 CAS 时写它,铸行恒 NULL。留在这里的 NULL 字面量就是这条纪律的落笔处。
|
|
376
269
|
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
377
270
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
378
271
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
@@ -400,7 +293,6 @@ export class SqlApprovalAskStore {
|
|
|
400
293
|
await conn.rollback();
|
|
401
294
|
}
|
|
402
295
|
catch {
|
|
403
|
-
/* ignore */
|
|
404
296
|
}
|
|
405
297
|
throw err;
|
|
406
298
|
}
|
|
@@ -410,12 +302,9 @@ export class SqlApprovalAskStore {
|
|
|
410
302
|
const out = await this.getAsk(row.askId);
|
|
411
303
|
if (!out)
|
|
412
304
|
throw new Error(`ensureAsk: row vanished after insert (askId=${row.askId})`);
|
|
413
|
-
// 判别位取**提交那一刻**引擎报的 affected,不是事后回读推断的:回读发生在提交之后,那时并发的
|
|
414
|
-
// 第二个调用可能已经改过行,任何「从行上反推谁插的」都是又一次猜。
|
|
415
305
|
return { row: out, inserted: insertedRows > 0 };
|
|
416
306
|
}
|
|
417
307
|
async transitionAsk(askId, from, to, patch) {
|
|
418
|
-
// machine 层先判(非法转移=编程错误,throw 而非返回 false——设计定稿 §4 原话)。
|
|
419
308
|
if (!canAskTransition(from, to)) {
|
|
420
309
|
throw new Error(`approval-ask-store.transitionAsk: illegal ask transition ${from} -> ${to} (askId=${askId})`);
|
|
421
310
|
}
|
|
@@ -427,33 +316,26 @@ export class SqlApprovalAskStore {
|
|
|
427
316
|
const setSql = cols.map((c) => `${c} = ${ph(dialect, ++n)}`).join(", ");
|
|
428
317
|
const askIdPh = ph(dialect, ++n);
|
|
429
318
|
const fromPh = ph(dialect, ++n);
|
|
430
|
-
// `rev = rev + 1`(恒变列,MySQL 同值 UPDATE affectedRows=0 坑——见文件头注 CAS 铁则②)。
|
|
431
319
|
const res = await this.db.query(`UPDATE ${APPROVAL_ASK_TABLE} SET ${setSql}, rev = rev + 1 WHERE ask_id = ${askIdPh} AND state = ${fromPh}`, [...vals, askId, from]);
|
|
432
320
|
return res.affected === 1;
|
|
433
321
|
}
|
|
434
322
|
async decideAsk(askId, batchId, decision) {
|
|
435
323
|
if (decision.idempotencyKey != null)
|
|
436
|
-
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
324
|
+
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
437
325
|
const now = Date.now();
|
|
438
326
|
const conn = await this.db.connect();
|
|
439
327
|
try {
|
|
440
328
|
await conn.begin();
|
|
441
|
-
// ① 批必须还 OPEN(自增写 rev 保证 affected 语义在 CLIENT_FOUND_ROWS 关闭下仍=1;=0 ⇒ 批已封)。
|
|
442
329
|
const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = ? WHERE batch_id = ? AND state = 'OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = $1 WHERE batch_id = $2 AND state = 'OPEN'`), [now, batchId]);
|
|
443
330
|
if (batchRes.affected !== 1) {
|
|
444
331
|
await conn.rollback();
|
|
445
332
|
return { ok: false, reason: "batch_closed", row: await this.getAskOn(conn, askId) };
|
|
446
333
|
}
|
|
447
|
-
|
|
448
|
-
// 分两步写会开一个「已 DECIDED 但键还没落」的窗口,同 key 重试在窗口内读不到键 ⇒ 会去打第二次
|
|
449
|
-
// CAS(输)再报 409,把一次合法回放变成冲突。同一 UPDATE ⇒ 键与终态同生共死。
|
|
450
|
-
let askRes; // 显式标注:`let x;` 是 evolving-any,本仓按强类型用
|
|
334
|
+
let askRes;
|
|
451
335
|
try {
|
|
452
336
|
askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=?, decision_actor=?, decision_note=?, decided_at_ms=?, idempotency_key=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=$1, decision_actor=$2, decision_note=$3, decided_at_ms=$4, idempotency_key=$5, rev=rev+1, updated_at_ms=$6 WHERE ask_id=$7 AND state='STREAM_PENDING'`), [decision.decision, decision.decisionActor == null ? null : this.json(decision.decisionActor), decision.decisionNote ?? null, now, decision.idempotencyKey ?? null, now, askId]);
|
|
453
337
|
}
|
|
454
338
|
catch (err) {
|
|
455
|
-
// `(task_id, idempotency_key)` 撞键 = 同一 key 已被本 task 内**另一条** ask 的回决占用。这是调用方
|
|
456
|
-
// 键管理的问题(不是 CAS 竞争),翻成 typed 臂交给端点译 409——方言 errno/SQLSTATE 不出店。
|
|
457
339
|
if (this.isDupKey(err)) {
|
|
458
340
|
await conn.rollback();
|
|
459
341
|
return { ok: false, reason: "idempotency_conflict", row: await this.getAskOn(conn, askId) };
|
|
@@ -471,7 +353,6 @@ export class SqlApprovalAskStore {
|
|
|
471
353
|
await conn.rollback();
|
|
472
354
|
}
|
|
473
355
|
catch {
|
|
474
|
-
/* ignore */
|
|
475
356
|
}
|
|
476
357
|
throw err;
|
|
477
358
|
}
|
|
@@ -488,39 +369,23 @@ export class SqlApprovalAskStore {
|
|
|
488
369
|
const conn = await this.db.connect();
|
|
489
370
|
try {
|
|
490
371
|
await conn.begin();
|
|
491
|
-
// ⓪ 锁序不变量(真双库 §6-4b 死锁实抓,2026-08-05):凡同一事务触碰批行+ask 行者,**一律批行先**
|
|
492
|
-
// (decideAsk/bindBatch/abortBatch 皆批先;expireAsk 原是 ask 先 ⇒ 与 decideAsk 构成 AB-BA,
|
|
493
|
-
// TiDB 悲观锁当场 `Deadlock found`)。这里用无谓词的 rev 自增先取批行锁作序列化点——
|
|
494
|
-
// 带 state 谓词的转移放后面(谓词不符时部分引擎不留锁,序列化点会失效)。ask CAS 输时整个
|
|
495
|
-
// 事务回滚,这次自增一并撤销,无痕。
|
|
496
372
|
const lockRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=?`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=$1`), [batchId]);
|
|
497
373
|
if (lockRes.affected !== 1) {
|
|
498
|
-
await conn.rollback();
|
|
374
|
+
await conn.rollback();
|
|
499
375
|
return { won: false, voidedSiblings: [] };
|
|
500
376
|
}
|
|
501
|
-
// ① 中选:ask STREAM_PENDING → PARKING(单赢者;输 = 已被回决/取消,整体放弃)。
|
|
502
377
|
const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=$1 WHERE ask_id=$2 AND state='STREAM_PENDING'`), [now, askId]);
|
|
503
378
|
if (askRes.affected !== 1) {
|
|
504
379
|
await conn.rollback();
|
|
505
380
|
return { won: false, voidedSiblings: [] };
|
|
506
381
|
}
|
|
507
|
-
// ② 批 OPEN → ROUTING_UNBOUND(已是 ROUTING_UNBOUND = 兄弟先到,affected=0 也算成功续走③——不检查
|
|
508
|
-
// affected,批状态无论如何这一刻都已经是 ROUTING_UNBOUND 或更前进)。
|
|
509
382
|
await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
|
|
510
|
-
// ③ 兄弟撤卡:PG 用 RETURNING 单语句拿名单;TiDB 无 RETURNING,同事务先 SELECT 后 UPDATE。
|
|
511
383
|
let voidedSiblings;
|
|
512
384
|
if (this.db.dialect === "pg") {
|
|
513
385
|
const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
|
|
514
386
|
voidedSiblings = res.rows.map((r) => String(r.ask_id));
|
|
515
387
|
}
|
|
516
388
|
else {
|
|
517
|
-
// 🔴 `FOR UPDATE` + 按选出的 id 列表更新(车5 同病修,与 `bindBatch` 的 PARKING 臂逐字同形——
|
|
518
|
-
// 那里的完整论证见该处注):裸 SELECT 是**非锁定读**,它与随后那条宽谓词 UPDATE 不是同一个行集
|
|
519
|
-
// (`transitionAsk`/`decideAsk` 不走批锁,可以在两条语句之间把某个兄弟搬进/搬出 STREAM_PENDING)。
|
|
520
|
-
// 搬进的会被 UPDATE 改成 VOID 却**不在**返回名单里(撤卡帧漏发 ⇒ 壳上留一张已作废的卡),搬出的
|
|
521
|
-
// 会在名单里却没被改(撤卡帧误发)。PG 的 RETURNING 与 InMemory 都返回「真正被改的那一组」,
|
|
522
|
-
// 所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。锁序:本方法已先取批行锁(步⓪),
|
|
523
|
-
// 此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
|
|
524
389
|
const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
|
|
525
390
|
voidedSiblings = sel.rows.map((r) => String(r.ask_id));
|
|
526
391
|
if (voidedSiblings.length > 0) {
|
|
@@ -539,7 +404,6 @@ export class SqlApprovalAskStore {
|
|
|
539
404
|
await conn.rollback();
|
|
540
405
|
}
|
|
541
406
|
catch {
|
|
542
|
-
/* ignore */
|
|
543
407
|
}
|
|
544
408
|
throw err;
|
|
545
409
|
}
|
|
@@ -552,44 +416,25 @@ export class SqlApprovalAskStore {
|
|
|
552
416
|
const conn = await this.db.connect();
|
|
553
417
|
try {
|
|
554
418
|
await conn.begin();
|
|
555
|
-
// bind-once:批 ROUTING_UNBOUND → ROUTING_BOUND + bound_ask_id(WHERE bound_ask_id IS NULL 是第二道
|
|
556
|
-
// 保险——state 谓词已经把并发者挡在外面,这里双保险防「同一批被 bind 两次」哪怕未来状态机被误改)。
|
|
557
419
|
const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=?, rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=$1, rev=rev+1, updated_at_ms=$2 WHERE batch_id=$3 AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`), [askId, now, batchId]);
|
|
558
420
|
if (batchRes.affected !== 1) {
|
|
559
|
-
// 判别式的信息来源 = 批行的真实态。🔴 回读**放在 rollback 之后、走池上的独立读**,不在本事务内读:
|
|
560
|
-
// TiDB 的事务快照按 start_ts 取,同事务内的普通 SELECT 可能读到 BEGIN 时刻的旧值(InnoDB 的读视图
|
|
561
|
-
// 规则又与它不同)——两方言在这一点上不等价,而本车恰恰要靠这个值分 ABORTED / ROUTING_BOUND 两条
|
|
562
|
-
// **处置相反**的臂。事务已回滚(本事务除了那条 affected=0 的 CAS 什么也没写),池上重读是干净的
|
|
563
|
-
// 已提交视图,双方言同义。
|
|
564
421
|
await conn.rollback();
|
|
565
422
|
const cur = await this.getBatchOn(conn, batchId);
|
|
566
423
|
if (!cur)
|
|
567
424
|
return { ok: false, batchState: "MISSING" };
|
|
568
425
|
return { ok: false, batchState: cur.state, ...(cur.boundAskId !== null ? { boundAskId: cur.boundAskId } : {}) };
|
|
569
426
|
}
|
|
570
|
-
// 中选 ask PARKING → PARKED(带 gate 三件)。
|
|
571
427
|
const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=?, gate_bound_call_id=?, gate_bound_input_hash=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='PARKING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=$1, gate_bound_call_id=$2, gate_bound_input_hash=$3, rev=rev+1, updated_at_ms=$4 WHERE ask_id=$5 AND state='PARKING'`), [gate.gateToken, gate.gateBoundCallId ?? null, gate.gateBoundInputHash ?? null, now, askId]);
|
|
572
428
|
if (askRes.affected !== 1) {
|
|
573
429
|
await conn.rollback();
|
|
574
|
-
// ask 不在 PARKING(被别的竞争者收走)⇒ 整事务回滚,批**没有**被绑上。如实报批的回滚后真值:
|
|
575
|
-
// 它仍是 ROUTING_UNBOUND(上一步的 CAS 谓词已经证明了这一点,连同本次自增一起撤销)。
|
|
576
430
|
return { ok: false, batchState: "ROUTING_UNBOUND" };
|
|
577
431
|
}
|
|
578
|
-
// 其余 PARKING 兄弟 → VOID,**名单随成功臂回传**(车5 §8 A-4):撤卡帧的 `askIds` 就是它。
|
|
579
|
-
// 读法与 expireAsk 逐字同形:PG `RETURNING` 单语句;TiDB 无 RETURNING ⇒ 同事务先 SELECT 后 UPDATE。
|
|
580
432
|
let voidedSiblings;
|
|
581
433
|
if (this.db.dialect === "pg") {
|
|
582
434
|
const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
|
|
583
435
|
voidedSiblings = res.rows.map((r) => String(r.ask_id));
|
|
584
436
|
}
|
|
585
437
|
else {
|
|
586
|
-
// 🔴 `FOR UPDATE` + 按选出的 id 逐条更新(codex 复审 F2,2026-08-06):裸 SELECT 是非锁定读,
|
|
587
|
-
// 它与随后那条宽谓词 UPDATE **不是**同一个行集——`transitionAsk` 不走批锁,可以在两条语句之间
|
|
588
|
-
// 把某个兄弟搬进/搬出 PARKING。搬进的会被 UPDATE 改成 VOID 却**不在**返回名单里(撤卡帧漏发 ⇒
|
|
589
|
-
// 壳上留一张已作废的卡),搬出的会在名单里却没被改(撤卡帧误发)。PG 的 RETURNING 与 InMemory
|
|
590
|
-
// 都返回「真正被改的那一组」,所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。
|
|
591
|
-
// 修法:锁定读把行集钉死,再只更新这批 id ⇒ 返回名单恒等于被改集合,三形同义。
|
|
592
|
-
// 锁序:本方法已先取批行锁(上面那条 CAS),此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
|
|
593
438
|
const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='PARKING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
|
|
594
439
|
voidedSiblings = sel.rows.map((r) => String(r.ask_id));
|
|
595
440
|
if (voidedSiblings.length > 0) {
|
|
@@ -605,7 +450,6 @@ export class SqlApprovalAskStore {
|
|
|
605
450
|
await conn.rollback();
|
|
606
451
|
}
|
|
607
452
|
catch {
|
|
608
|
-
/* ignore */
|
|
609
453
|
}
|
|
610
454
|
throw err;
|
|
611
455
|
}
|
|
@@ -623,17 +467,12 @@ export class SqlApprovalAskStore {
|
|
|
623
467
|
await conn.rollback();
|
|
624
468
|
return [];
|
|
625
469
|
}
|
|
626
|
-
// 只撤 STREAM_PENDING(设计定稿字面:「整批 STREAM_PENDING→VOID」)——已 DECIDED 的兄弟不受影响
|
|
627
|
-
// (§3.0 原话),PARKING/PARKED/DENIED 同样不在这条 UPDATE 的谓词里,保持不动。
|
|
628
470
|
let voided;
|
|
629
471
|
if (this.db.dialect === "pg") {
|
|
630
472
|
const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId]);
|
|
631
473
|
voided = res.rows.map((r) => String(r.ask_id));
|
|
632
474
|
}
|
|
633
475
|
else {
|
|
634
|
-
// 🔴 同 `expireAsk`/`bindBatch` 的锁定读改造(车5 同病修):非锁定读的名单与宽谓词 UPDATE 的
|
|
635
|
-
// 行集不是同一个 ⇒ 撤卡帧漏发/误发,且分歧只在 MySQL 方言上成立。锁序:批行锁(上一条 CAS)
|
|
636
|
-
// 已取,此处再取 ask 行锁。
|
|
637
476
|
const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' FOR UPDATE`, [batchId]);
|
|
638
477
|
voided = sel.rows.map((r) => String(r.ask_id));
|
|
639
478
|
if (voided.length > 0) {
|
|
@@ -649,7 +488,6 @@ export class SqlApprovalAskStore {
|
|
|
649
488
|
await conn.rollback();
|
|
650
489
|
}
|
|
651
490
|
catch {
|
|
652
|
-
/* ignore */
|
|
653
491
|
}
|
|
654
492
|
throw err;
|
|
655
493
|
}
|
|
@@ -657,17 +495,6 @@ export class SqlApprovalAskStore {
|
|
|
657
495
|
conn.release();
|
|
658
496
|
}
|
|
659
497
|
}
|
|
660
|
-
/**
|
|
661
|
-
* 🔴 排序键是 `updated_at_ms`(车5 §8 D-4;codex 三轮补抓:原先按 `created_at_ms` 排,`deferReconcile`
|
|
662
|
-
* 就是**空转**的)。这条读口与 `deferReconcile` 是一对:收敛器每轮取前 `limit` 条,判不出终局的行用
|
|
663
|
-
* `deferReconcile` 推 `updated_at_ms` 把自己排到队尾,下一轮新行才轮得到。若这里仍按建行时间排,
|
|
664
|
-
* 推 `updated_at_ms` 对顺序毫无影响 ⇒ 一旦 `limit` 被若干长驻 PARKING 行占满,每轮扫到的永远是同一批,
|
|
665
|
-
* 后来的审批被无限饿死(§5 旋钮那句「超出下轮接着扫」也就不成立)。
|
|
666
|
-
* 次级键 `created_at_ms` 只为**确定性**:同毫秒的行不至于在两次扫描间乱序(分页/限流下的稳定序)。
|
|
667
|
-
*
|
|
668
|
-
* 🔴 配套纪律(§9 C5):任何 TTL 判据一律量 immutable 的 `createdAtMs`/`expiresAtMs`,**绝不量
|
|
669
|
-
* `updatedAtMs`**——它被队列轮转每轮刷新,量它的 TTL 永不到期。
|
|
670
|
-
*/
|
|
671
498
|
async listByState(state, limit) {
|
|
672
499
|
const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = ? ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = $1 ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT $2`), [state, limit]);
|
|
673
500
|
return rows.map(mapAskRow);
|
|
@@ -681,8 +508,6 @@ export class SqlApprovalAskStore {
|
|
|
681
508
|
async listPendingBySession(sessionId, owner, signal) {
|
|
682
509
|
if (signal?.aborted === true)
|
|
683
510
|
throw abortedReadError("listPendingBySession");
|
|
684
|
-
// 租户门的两段谓词文本(见接口注):`owner = ?` 对 NULL 恒 UNKNOWN ⇒ 无租户身份的部署形必须走
|
|
685
|
-
// `owner IS NULL`,否则这条读口在那些部署上恒回零行(一条静默失效的重放面,比报错更难发现)。
|
|
686
511
|
const { rows } = await withReadSignal(this.db.query(owner === null
|
|
687
512
|
? this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`)
|
|
688
513
|
: this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
|
|
@@ -695,20 +520,15 @@ export class SqlApprovalAskStore {
|
|
|
695
520
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
696
521
|
}
|
|
697
522
|
async getByIdempotencyKey(taskId, key) {
|
|
698
|
-
assertIdempotencyKeyShape(key);
|
|
699
|
-
// 谓词跟 UNIQUE `(task_id, idempotency_key)` 逐字对齐 ⇒ 至多一行,且天然带租户门(见接口注)。
|
|
523
|
+
assertIdempotencyKeyShape(key);
|
|
700
524
|
const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND idempotency_key = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND idempotency_key = $2`), [taskId, key]);
|
|
701
525
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
702
526
|
}
|
|
703
527
|
async deferReconcile(askId, expectedState, expectedRev, nowMs) {
|
|
704
|
-
// 纯 touch:**不碰 state**(见接口注——同态转移在状态机表上无出边,这里刻意走另一扇门),只推
|
|
705
|
-
// `updated_at_ms` 与恒变的 `rev`。`AND state=? AND rev=?` 双谓词 = 两副本同扫时的单赢者门。
|
|
706
528
|
const res = await this.db.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = ?, rev = rev + 1 WHERE ask_id = ? AND state = ? AND rev = ?`, `UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = $1, rev = rev + 1 WHERE ask_id = $2 AND state = $3 AND rev = $4`), [nowMs, askId, expectedState, expectedRev]);
|
|
707
529
|
return res.affected === 1;
|
|
708
530
|
}
|
|
709
531
|
async resolveProvisional(askId, from, to, patch) {
|
|
710
|
-
// 🔴 故意不经 canAskTransition:这是设计定稿 §3.0/§4 明说的「provisional 行的版本化补偿」——终态→终态
|
|
711
|
-
// 的例外通道,机器表把终态定义为「无出边」,这里走的是另一扇门(WHERE provisional=1 AND state=from)。
|
|
712
532
|
const dialect = this.db.dialect;
|
|
713
533
|
const dyn = patchColumns(patch, (v) => this.json(v));
|
|
714
534
|
const cols = ["state", "provisional", ...dyn.map(([c]) => c), "updated_at_ms"];
|
|
@@ -730,13 +550,11 @@ export class SqlApprovalAskStore {
|
|
|
730
550
|
return rows[0] ? mapBatchRow(rows[0]) : null;
|
|
731
551
|
}
|
|
732
552
|
}
|
|
733
|
-
/** MySQL-protocol(TiDB)绑定。 */
|
|
734
553
|
export class TiDBApprovalAskStore extends SqlApprovalAskStore {
|
|
735
554
|
constructor(pool) {
|
|
736
555
|
super(mysqlDriver(pool));
|
|
737
556
|
}
|
|
738
557
|
}
|
|
739
|
-
/** PostgreSQL 绑定。 */
|
|
740
558
|
export class PgApprovalAskStore extends SqlApprovalAskStore {
|
|
741
559
|
constructor(pool) {
|
|
742
560
|
super(pgDriver(pool));
|
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-session per-toolName approval exemption ("本会话不再询问",
|
|
3
|
-
* design: sema-internal server/docs/design/APPROVAL-SESSION-EXEMPTION.md).
|
|
4
|
-
*
|
|
5
|
-
* SEMANTICS (the boundary that keeps this an APPROVAL MEMORY, not a policy loosen): a granted row makes the
|
|
6
|
-
* F4 ask-gate SKIP the human ask for that (sessionId, canonicalToolName) — it is consulted ONLY after
|
|
7
|
-
* `APPROVAL_DENY` and `APPROVAL_NEVER_AUTO` (both always win), and never touches the session-policy layer
|
|
8
|
-
* (tighten-only stays intact). Keyed in CANONICAL tool-name space (the same rename-smuggling discipline as
|
|
9
|
-
* the require/deny sets). Lifecycle = session-bound: E21 session DELETE purges via deleteBySession.
|
|
10
|
-
*
|
|
11
|
-
* Four twins behind one interface (the resume-anchor pattern): TiDB/PG = durable cross-replica SQL rows
|
|
12
|
-
* (idempotent upsert on the (session_id, tool_name) PK); File = per-session JSONL append log (fsync — a
|
|
13
|
-
* lost grant re-asks, but a torn log must not FABRICATE one), hydrated on boot, torn-tail-safe; Memory =
|
|
14
|
-
* tests / ephemeral. Consumers treat store FAILURE as fail-closed (ask anyway) — enforced at the call site.
|
|
15
|
-
*/
|
|
16
1
|
import { join } from "node:path";
|
|
17
2
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
18
3
|
import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
|
|
19
|
-
// ─── TiDB / MySQL twin ───────────────────────────────────────────────────────────────────────────────────
|
|
20
4
|
export class TiDBApprovalExemptionStore {
|
|
21
5
|
pool;
|
|
22
6
|
constructor(pool) {
|
|
@@ -24,8 +8,7 @@ export class TiDBApprovalExemptionStore {
|
|
|
24
8
|
}
|
|
25
9
|
async grant(sessionId, toolName, grantedBy) {
|
|
26
10
|
await this.pool.query("INSERT INTO approval_exemption (session_id, tool_name, granted_by, created_at) VALUES (?,?,?,?) " +
|
|
27
|
-
"ON DUPLICATE KEY UPDATE granted_by = VALUES(granted_by)",
|
|
28
|
-
[sessionId, toolName, grantedBy, new Date()]);
|
|
11
|
+
"ON DUPLICATE KEY UPDATE granted_by = VALUES(granted_by)", [sessionId, toolName, grantedBy, new Date()]);
|
|
29
12
|
}
|
|
30
13
|
async has(sessionId, toolName) {
|
|
31
14
|
const [rows] = await this.pool.query("SELECT 1 FROM approval_exemption WHERE session_id = ? AND tool_name = ?", [sessionId, toolName]);
|
|
@@ -48,7 +31,6 @@ export class TiDBApprovalExemptionStore {
|
|
|
48
31
|
return r.affectedRows;
|
|
49
32
|
}
|
|
50
33
|
}
|
|
51
|
-
// ─── PG twin (dialect twin of the TiDB store — same contract, $n placeholders + ON CONFLICT) ─────────────
|
|
52
34
|
export class PgApprovalExemptionStore {
|
|
53
35
|
pool;
|
|
54
36
|
constructor(pool) {
|
|
@@ -85,7 +67,6 @@ export class PgApprovalExemptionStore {
|
|
|
85
67
|
return res.rowCount ?? 0;
|
|
86
68
|
}
|
|
87
69
|
}
|
|
88
|
-
// ─── Memory twin (tests / ephemeral) ─────────────────────────────────────────────────────────────────────
|
|
89
70
|
export class MemoryApprovalExemptionStore {
|
|
90
71
|
bySession = new Map();
|
|
91
72
|
async grant(sessionId, toolName, grantedBy) {
|
|
@@ -129,7 +110,6 @@ export class FileApprovalExemptionStore {
|
|
|
129
110
|
pathFor(sessionId) {
|
|
130
111
|
return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
|
|
131
112
|
}
|
|
132
|
-
/** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
|
|
133
113
|
hydrate() {
|
|
134
114
|
let names;
|
|
135
115
|
try {
|
|
@@ -211,13 +191,9 @@ export class FileApprovalExemptionStore {
|
|
|
211
191
|
log.close();
|
|
212
192
|
this.logs.delete(key);
|
|
213
193
|
}
|
|
214
|
-
// 🔴 unlink BEFORE dropping the index, and FAIL LOUD if the file survives: a
|
|
215
|
-
// swallowed rm would let hydrate() resurrect purged exemptions after a restart — an E21 right-to-delete
|
|
216
|
-
// violation with PRIVILEGE attached (auto-approve rights), unlike the inert anchor-store twin. Throwing
|
|
217
|
-
// here aborts the caller's session purge before it deletes the session metadata (fail-loud, retryable).
|
|
218
194
|
const path = this.pathFor(sessionId);
|
|
219
195
|
try {
|
|
220
|
-
rmSync(path, { force: true });
|
|
196
|
+
rmSync(path, { force: true });
|
|
221
197
|
}
|
|
222
198
|
catch (e) {
|
|
223
199
|
if (existsSync(path))
|
|
@@ -226,14 +202,12 @@ export class FileApprovalExemptionStore {
|
|
|
226
202
|
this.bySession.delete(key);
|
|
227
203
|
return removed;
|
|
228
204
|
}
|
|
229
|
-
/** Release every open append-log fd (LocalBackend.close → graceful-restart re-open). */
|
|
230
205
|
dispose() {
|
|
231
206
|
for (const log of this.logs.values()) {
|
|
232
207
|
try {
|
|
233
208
|
log.close();
|
|
234
209
|
}
|
|
235
210
|
catch {
|
|
236
|
-
/* best-effort */
|
|
237
211
|
}
|
|
238
212
|
}
|
|
239
213
|
this.logs.clear();
|