@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
package/dist/adoption/quiesce.js
CHANGED
|
@@ -1,69 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A-010.18 —— 收编弧跑动期间的**进程内静默闸**(design/183 I1「引擎先停」在本进程里的执法面)。
|
|
3
|
-
*
|
|
4
|
-
* ── 病 ──────────────────────────────────────────────────────────────────────────────────────────
|
|
5
|
-
* 183 I1 写的是「发起收编之前必须先把引擎停掉」。那是一条**运维前提**,而本进程此前对它**零执法**:
|
|
6
|
-
* `POST /v1/adoption` 收下请求、弧开始逐表 UPDATE 的同时,本副本的维护 tick 照常每几秒跑一轮,
|
|
7
|
-
* 而那一轮里好几条腿写的正是被迁的那些表。最锋利的一条是**审批收敛器**(`approval-reconciler.ts`):
|
|
8
|
-
* 它按 `owner` 读 PARKING/STREAM_PENDING 行并 CAS 结算。收编把 `approval_ask.owner` 从 A 改成 B 的
|
|
9
|
-
* 那一瞬,收敛器手里可能正握着按**旧** owner 读出来的一批行 —— 结算下去就是把一只本该 PARKED 的 ask
|
|
10
|
-
* 写成 **DENIED**,而 DENIED 是终局:收编回滚不了它,重跑也追认不回来。
|
|
11
|
-
*
|
|
12
|
-
* ── 修 ──────────────────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
* 弧跑动期间把维护 tick 整轮让开。判据不是「挑几条腿避让」而是**整轮**:
|
|
14
|
-
* ① 维护 tick 是 best-effort 的周期动作(每条腿都幂等、都能等下一轮),让开几轮零代价;而逐腿判
|
|
15
|
-
* 「这条腿碰不碰被迁的表」会立刻变成第二份需要与 `REBIND_LEGS` 同步的表 —— 那正是本仓反复吃过亏的
|
|
16
|
-
* 「两份判据必然漂」形(漏判一条新腿是**静默**的:它照写不误)。
|
|
17
|
-
* ② 弧本身是毫秒级的几条 UPDATE,库里常态在飞行数为 0 —— 让开的窗极短。
|
|
18
|
-
*
|
|
19
|
-
* ── 射程边界(如实登记,不假称已解)──────────────────────────────────────────────────────────────
|
|
20
|
-
* 两条边界,都写在明处:
|
|
21
|
-
* ① **进程内**。这道闸只挡得住**运行这条弧的那个副本**;多副本部署里别的副本的 tick 照跑,那部分仍然
|
|
22
|
-
* 只能靠 183 I1 的运维前提(以及弧内那些 rev-CAS:载荷改写撞上并发写会响亮抛、整体回滚)。
|
|
23
|
-
* 更强的形(跨副本静默)需要一枚库上的 quiesce 租约 + 每条腿的取租,那是一次独立设计,不在本条射程。
|
|
24
|
-
* ② **排空有界**。等不到就带着读数继续(见下),所以「本进程零腿在写被迁的表」是**通常**成立而不是
|
|
25
|
-
* 恒成立 —— 超时那一次由 `onDrainTimeout` 响亮说出来,不许静默当成已排空。
|
|
26
|
-
* 本闸因此是**纵深防御**,不是把 I1 变成机器保证。
|
|
27
|
-
*
|
|
28
|
-
* ── 形态 ────────────────────────────────────────────────────────────────────────────────────────
|
|
29
|
-
* 模块级计数器而不是布尔:并发同参 POST 是本车明确支持的形(UNIQUE 仲裁),两条弧可以叠在一起,
|
|
30
|
-
* 布尔会被先结束的那条**提前**放开闸。计数器 + `try/finally` ⇒ 抛错也一定归零。
|
|
31
|
-
*/
|
|
32
|
-
/** 当前正在本进程里跑的收编弧条数。**只**由 {@link withAdoptionQuiesce} 增减。 */
|
|
33
1
|
let inFlight = 0;
|
|
34
|
-
/** 本进程此刻是否有收编弧在跑(维护 tick 的让路判据)。 */
|
|
35
2
|
export function isAdoptionQuiescing() {
|
|
36
3
|
return inFlight > 0;
|
|
37
4
|
}
|
|
38
|
-
/**
|
|
39
|
-
* **已经起飞**的维护腿(codex 对抗复审 R1 [high],验真后修)。
|
|
40
|
-
*
|
|
41
|
-
* 🔴 病:第一版的闸只在 tick 的**入口**判一次。可维护 tick 的每条腿都是 `void <promise>` 异步起飞的
|
|
42
|
-
* —— tick 回调本身立刻返回。于是这个次序完全可能:`t0` tick 起飞了审批收敛器(它按旧 owner 读出一批
|
|
43
|
-
* 行,正等库回话)→ `t0+ε` 收编弧开始改 `approval_ask.owner` → 收敛器拿着旧读数回来 CAS,把一只本该
|
|
44
|
-
* PARKED 的 ask 结算成 **DENIED**。也就是说:**这正是本条声称要消灭的那一形**,闸只是让它更难撞上。
|
|
45
|
-
* 我原来的钉也测不出来 —— 它先开闸再推进时钟,探针又是立即完成的,反向次序根本没被覆盖。
|
|
46
|
-
*
|
|
47
|
-
* 🔴 修:闸不止「拦住下一轮」,还要**排空这一轮**。每条腿起飞时在这里登记,收编弧进库之前先等它们落地。
|
|
48
|
-
*
|
|
49
|
-
* ⏱ **有界等待**是硬的:维护腿里有会长时间挂住的(`worktreeReap` 的 `git worktree prune` 随 worktree
|
|
50
|
-
* 数增长,它自己的重入守卫就是为此而设)。无界 await 会把「一条腿卡住」升级成「收编永远起不来」——
|
|
51
|
-
* 比原病更坏。所以排空是**尽力而为 + 响亮**:超时就带着读数继续,让运维看得见「这次收编没能等到 N 条
|
|
52
|
-
* 腿落地」,而不是既没等到也没人知道。
|
|
53
|
-
*/
|
|
54
5
|
const airborneLegs = new Set();
|
|
55
|
-
/**
|
|
56
|
-
* 登记一条**刚起飞**的维护腿。调用方仍然 `void` 它自己的链(错误处置不变);本函数只借用它的落地时刻。
|
|
57
|
-
*
|
|
58
|
-
* `catch` 是必须的:登记的这一份引用绝不能变成第二个未处理拒绝源(腿自己的链已经有 guard 接住了)。
|
|
59
|
-
*/
|
|
60
6
|
export function trackMaintenanceLeg(leg) {
|
|
61
7
|
const tracked = leg.catch(() => undefined);
|
|
62
8
|
airborneLegs.add(tracked);
|
|
63
9
|
void tracked.finally(() => {
|
|
64
10
|
airborneLegs.delete(tracked);
|
|
65
|
-
// 集合空了才是「排空」——通知所有在等的收编弧。这是**唯一**挂在腿身上的 handler(每条腿恰一个,
|
|
66
|
-
// 在它起飞时挂),排空等待方一律等下面那只**自己的** waiter,不再往腿上加第二个 handler。
|
|
67
11
|
if (airborneLegs.size === 0) {
|
|
68
12
|
for (const wake of drainWaiters)
|
|
69
13
|
wake();
|
|
@@ -71,23 +15,10 @@ export function trackMaintenanceLeg(leg) {
|
|
|
71
15
|
}
|
|
72
16
|
});
|
|
73
17
|
}
|
|
74
|
-
/**
|
|
75
|
-
* 正在等排空的收编弧(codex 对抗复审 R2 [medium],验真后修)。
|
|
76
|
-
*
|
|
77
|
-
* 🔴 病:第一版每次排空都现铸 `Promise.allSettled([...airborneLegs])`。**超时那一路**里,那只 allSettled
|
|
78
|
-
* 永远不会兑现,而它已经把 handler 挂在了每一条挂死的腿上 —— 且 `clearTimeout` 清不掉这些 promise
|
|
79
|
-
* reaction。于是「有一条腿永久挂住」这个**本来就是有界逃逸要处理的**场景,会让每一次 POST / 每一次 boot
|
|
80
|
-
* 续跑再挂一批清不掉的闭包上去:逃逸机制自己变成了泄漏源。
|
|
81
|
-
*
|
|
82
|
-
* 🔴 修:等待方注册一只**自己的** waiter,超时时把它注销掉(`finally` 里 delete)。腿身上的 handler 恒为
|
|
83
|
-
* 「起飞时挂的那一个」,与等待次数无关 ⇒ 重复超时不再增长任何东西。
|
|
84
|
-
*/
|
|
85
18
|
const drainWaiters = new Set();
|
|
86
|
-
/** 取证口(测试用):此刻有几只等待方挂着。生产不读 —— 它存在是为了让「重复超时不泄漏」可被断言。 */
|
|
87
19
|
export function drainWaiterCountForTest() {
|
|
88
20
|
return drainWaiters.size;
|
|
89
21
|
}
|
|
90
|
-
/** 有界排空(见 {@link airborneLegs} 的注)。`timeoutMs <= 0` ⇒ 不等,直接如实回报在飞数。 */
|
|
91
22
|
export async function drainMaintenanceLegs(timeoutMs) {
|
|
92
23
|
if (airborneLegs.size === 0)
|
|
93
24
|
return { drained: true, pending: 0 };
|
|
@@ -101,7 +32,7 @@ export async function drainMaintenanceLegs(timeoutMs) {
|
|
|
101
32
|
let timer;
|
|
102
33
|
const deadline = new Promise((resolve) => {
|
|
103
34
|
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
104
|
-
timer.unref?.();
|
|
35
|
+
timer.unref?.();
|
|
105
36
|
});
|
|
106
37
|
try {
|
|
107
38
|
const outcome = await Promise.race([drained, deadline]);
|
|
@@ -110,36 +41,22 @@ export async function drainMaintenanceLegs(timeoutMs) {
|
|
|
110
41
|
finally {
|
|
111
42
|
if (timer !== undefined)
|
|
112
43
|
clearTimeout(timer);
|
|
113
|
-
// 🔴 注销是本修的**全部内容**:超时之后这只 waiter 不许继续挂着(见 drainWaiters 的注)。
|
|
114
44
|
drainWaiters.delete(wake);
|
|
115
45
|
}
|
|
116
46
|
}
|
|
117
|
-
/** 排空的默认上界。取值理由:维护腿的常态是毫秒级的几条 SQL;5s 足够让它们全部落地,又不会把一次
|
|
118
|
-
* 收编吊死在一条卡住的腿上(卡住的那一条由它自己的重入守卫与 throttled-warn 负责被看见)。 */
|
|
119
47
|
export const ADOPTION_DRAIN_TIMEOUT_MS = 5_000;
|
|
120
|
-
/**
|
|
121
|
-
* 在闸内跑一条收编弧:**先关闸,再排空,最后才动数据**。
|
|
122
|
-
*
|
|
123
|
-
* 三步的次序是承重的 —— 先关闸(此后不会再有新 tick 起飞新腿),再等已经在飞的那些落地,然后才把
|
|
124
|
-
* 控制权交给弧。反过来(先排空再关闸)之间那道窗里正好可以起飞新的一轮。
|
|
125
|
-
*
|
|
126
|
-
* `finally` 归零是硬的:抛出去的异常照常上抛(调用方的错误语义一个字不改),但闸绝不能因为一次失败
|
|
127
|
-
* 就永久关着 —— 那会把一次收编失败升级成「这台副本从此不再做任何后台维护」。
|
|
128
|
-
*/
|
|
129
48
|
export async function withAdoptionQuiesce(fn, opts) {
|
|
130
|
-
inFlight += 1;
|
|
49
|
+
inFlight += 1;
|
|
131
50
|
try {
|
|
132
|
-
// ② 排空这一轮已经起飞的腿(有界;超时如实响亮)。
|
|
133
51
|
const reading = await drainMaintenanceLegs(opts?.drainTimeoutMs ?? ADOPTION_DRAIN_TIMEOUT_MS);
|
|
134
52
|
if (!reading.drained)
|
|
135
53
|
opts?.onDrainTimeout?.(reading);
|
|
136
|
-
return await fn();
|
|
54
|
+
return await fn();
|
|
137
55
|
}
|
|
138
56
|
finally {
|
|
139
57
|
inFlight -= 1;
|
|
140
58
|
}
|
|
141
59
|
}
|
|
142
|
-
/** 测试用的复位口(用例之间零串味)。生产路径**不调** —— 它会把别的在飞弧的计数一起抹掉。 */
|
|
143
60
|
export function resetAdoptionQuiesceForTest() {
|
|
144
61
|
inFlight = 0;
|
|
145
62
|
airborneLegs.clear();
|
package/dist/adoption/runner.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/183 §3.2 根级状态机的 **form b 实施**(纯身份重绑;数据已在 SQL、承运腿为零)。
|
|
3
|
-
*
|
|
4
|
-
* 一条弧的全部判断都在这里,店(`adoption-log-sql.ts`)只负责语句,计划表(`plan.ts`)只负责「迁什么」。
|
|
5
|
-
*
|
|
6
|
-
* ── 弧 ──────────────────────────────────────────────────────────────────────────────────────────
|
|
7
|
-
* ① 取弧锁(advisory;取不到 ⇒ **不等死也不假装成功**,回 `stalled` 回执)
|
|
8
|
-
* ② 意图落库(insert-or-fetch 原子形;`from_principal` UNIQUE 仲裁并发) ⇒ phase 2
|
|
9
|
-
* ③ 目的地冲突 preflight(**任何 UPDATE 之前**)+ 全部重绑腿(同一事务) ⇒ phase 3
|
|
10
|
-
* ④ 配置账物化(server 够不到的部署配置逐项点名,`migrated:false`) ⇒ phase 4
|
|
11
|
-
* ⑤ 承运腿:form b **恒零**(阶段位显式在场,给 form a 上车留挂点) ⇒ phase 5
|
|
12
|
-
* ⑥ 永久终态:`immutableReport` 一次落库,此后逐字节恒同 ⇒ phase 6
|
|
13
|
-
*
|
|
14
|
-
* ── 崩溃续跑 ────────────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
* 每一步都是「读当前 phase → 做这一步 → CAS 推进」。任一步崩溃,下一次(重发同参 POST、或副本 boot 的
|
|
16
|
-
* 在飞扫描)从库里的 phase 处接着跑;腿本身幂等(重跑改到同值 = 零行变化),所以「续跑」与「重做」在
|
|
17
|
-
* 数据面等价。**绝不**因为「上次跑到一半」就拒绝续跑 —— 那会把一次网络抖动变成永久半迁移。
|
|
18
|
-
*
|
|
19
|
-
* ── 拒绝 ────────────────────────────────────────────────────────────────────────────────────────
|
|
20
|
-
* 目的地冲突 ⇒ `rejected` 终态 + typed 409,**源/目的地两侧字节零变更**(preflight 在任何 UPDATE 之前;
|
|
21
|
-
* 跨轴组合形的漏网由事务里的唯一键违例整体回滚兜住)。不做逐表 merge —— 183 未裁,拒比合安全且可重来。
|
|
22
|
-
*/
|
|
23
1
|
import { randomUUID } from "node:crypto";
|
|
24
2
|
import { ADOPTION_PHASE, adoptionLockName, } from "../plugins/adoption-log-sql.js";
|
|
25
3
|
import { policyKeyFor } from "../plugins/session-policy-store-sql.js";
|
|
@@ -30,23 +8,13 @@ import { buildBlobScanSql, buildBlobWriteSql, buildConflictProbeSql, buildMemory
|
|
|
30
8
|
import { AdoptionConfigsSchema, AdoptionLegsSchema, AdoptionReceiptSchema, AdoptionRejectDetailSchema, AdoptionReportSchema, } from "./wire.js";
|
|
31
9
|
import { formatUserScope, encodeScopeSegment } from "@sema-agent/core";
|
|
32
10
|
import { isDupKeyError } from "../plugins/sql-errors.js";
|
|
33
|
-
/** MySQL `ER_DUP_ENTRY` / PG `unique_violation` —— 跨轴组合冲突的最后一道 fail-closed 门。
|
|
34
|
-
* 判据属主 = `plugins/sql-errors.ts`(A-032 P1-①:本站点旧形只认 errno,只带 `code` 的驱动错误漏判)。 */
|
|
35
11
|
const isDupKey = isDupKeyError;
|
|
36
12
|
function msgOf(e) {
|
|
37
13
|
return e instanceof Error ? e.message : String(e);
|
|
38
14
|
}
|
|
39
|
-
/**
|
|
40
|
-
* 一根身份轴上的「旧值 → 新值」对。`verbatim` 恒一对;`memory-scope` 由库里 DISTINCT 扫出来
|
|
41
|
-
* (user 盘 1 个 + 每个 project 盘各 1 个)——**扫而不猜**,理由见 `sql.ts` 的 LIKE 段。
|
|
42
|
-
*/
|
|
43
15
|
async function resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal) {
|
|
44
16
|
if (leg.encoding === "verbatim")
|
|
45
17
|
return [buildVerbatimPair(fromPrincipal, toPrincipal)];
|
|
46
|
-
// 规则桶键:恒一对,**算**出来而不是扫出来(原像里只有 principal,没有逐行变量)。
|
|
47
|
-
// 🔴 这一支同时服务 preflight 的冲突探针与 `countResidual` 的残留计数 —— 少了它,那两处会拿**明文**
|
|
48
|
-
// principal 去比 `owner_key`,而那个谓词在这张表上永远匹配不到任何一行:preflight 恒说「无冲突」、
|
|
49
|
-
// 残留恒报 0(A-010.16 / A-010.22 点名的正是后者)。
|
|
50
18
|
if (leg.encoding === "rule-owner-key") {
|
|
51
19
|
return [
|
|
52
20
|
{
|
|
@@ -76,13 +44,6 @@ async function resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal)
|
|
|
76
44
|
}
|
|
77
45
|
export function createAdoptionRunner(deps) {
|
|
78
46
|
const { store, dialect, now } = deps;
|
|
79
|
-
/**
|
|
80
|
-
* phase 0 的**列宽实证**(codex R2-F2,亲核属实):路由那道 `checkAdoptionWidths` 是纯函数,只看得见
|
|
81
|
-
* `user:<enc(to)>`;而 memory 迁移还会重写 `proj:`/`userproj:` 键并**保留原有的项目段后缀**。源侧
|
|
82
|
-
* principal 短 + 项目段长的键可以正好装进 VARCHAR(190),换上更长的目的地段之后就越界 —— 那时行已经
|
|
83
|
-
* 铸好、失败发生在 phase 3 的腿里,弧永远停在 INTENT,每次重发和每次 boot 续跑都确定性地再撞一次。
|
|
84
|
-
* ⇒ 在**任何 UPDATE 之前**拿库里真实存在的 scope 值算一遍,越界就 typed 拒(可重来,不留半迁移)。
|
|
85
|
-
*/
|
|
86
47
|
async function unrepresentableScopes(exec, fromPrincipal, toPrincipal) {
|
|
87
48
|
const bad = [];
|
|
88
49
|
const seen = new Set();
|
|
@@ -98,12 +59,9 @@ export function createAdoptionRunner(deps) {
|
|
|
98
59
|
}
|
|
99
60
|
return bad;
|
|
100
61
|
}
|
|
101
|
-
/** phase 0 探针:逐迁移表族查目的地侧是否已有与源侧重叠的逻辑键。**零 UPDATE**。 */
|
|
102
62
|
async function preflight(exec, fromPrincipal, toPrincipal) {
|
|
103
63
|
const hits = [];
|
|
104
64
|
for (const leg of REBIND_LEGS) {
|
|
105
|
-
// residualKey === undefined = 没有任何唯一键含这根轴 ⇒ 重写**结构上**不可能撞键。
|
|
106
|
-
// 这不是「跳过没查」:它是判定表里逐条写死的判据(plan.ts 每条腿的 residualKey 行内注记了它的唯一键)。
|
|
107
65
|
if (leg.residualKey === undefined)
|
|
108
66
|
continue;
|
|
109
67
|
const pairs = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
|
|
@@ -116,20 +74,11 @@ export function createAdoptionRunner(deps) {
|
|
|
116
74
|
}
|
|
117
75
|
return hits;
|
|
118
76
|
}
|
|
119
|
-
/**
|
|
120
|
-
* `blob-rewrite` 腿:逐行改写 JSON 载荷里的身份字段。
|
|
121
|
-
*
|
|
122
|
-
* 🔴 **只改值等于旧身份的字段**(不盲改):载荷里可能有同名但语义不同的字段,而且盲改会让「重跑」
|
|
123
|
-
* 变成一次真写。计数按**内容真变了**算 —— 重跑时新载荷与库里逐字节相同,行数报 0(诚实的幂等读数)。
|
|
124
|
-
* 载荷解析失败 ⇒ **原样上抛**:一条读不动的真源行,静默跳过就等于宣布它已迁移(fail-closed)。
|
|
125
|
-
*/
|
|
126
77
|
async function scanBlobRows(exec, leg, fromValue, toValue) {
|
|
127
78
|
const blob = leg.blob;
|
|
128
79
|
const cells = 1 + (blob.alsoMatch?.length ?? 0);
|
|
129
|
-
// 两个桶:旧值(还没迁的行)+ 新值(投影迁了而载荷停在旧身份的「劈开」行)。见 buildBlobScanSql 的注。
|
|
130
80
|
return (await exec.query(buildBlobScanSql(dialect, leg), [...Array.from({ length: cells }, () => fromValue), ...Array.from({ length: cells }, () => toValue)])).rows;
|
|
131
81
|
}
|
|
132
|
-
/** 载荷 JSON 的解析(两方言:node-pg 对 jsonb 自己 parse,mysql2 对 JSON 列同样可能已 parse)。 */
|
|
133
82
|
function parseBlob(leg, row) {
|
|
134
83
|
const raw = row[leg.blob.column];
|
|
135
84
|
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
@@ -150,23 +99,15 @@ export function createAdoptionRunner(deps) {
|
|
|
150
99
|
for (const field of blob.fields)
|
|
151
100
|
if (next[field] === fromValue)
|
|
152
101
|
next[field] = toValue;
|
|
153
|
-
// 载荷内嵌的版本号跟着列上的 `rev + 1` 走(见 BlobRewriteSpec.revInPayload 的注:两处是同一个
|
|
154
|
-
// CAS 的读写两端,让它们各说一个版本 = 下一次正常写的守卫读到的基准是假的)。
|
|
155
102
|
const bumped = { ...next };
|
|
156
103
|
const scannedRev = Number(row[blob.revColumn]);
|
|
157
|
-
// 只有真读到数才同步载荷里的版本号。读不到时**不编一个**(写进 NaN/null 比不写更坏):
|
|
158
|
-
// 列上的 `rev = rev + 1` 仍然是权威的那一次推进,而 CAS 的 `AND rev = ?` 会用同一个值,
|
|
159
|
-
// 所以读不到数的行根本写不进去(响亮抛),不存在「悄悄写了个坏版本号」这条路。
|
|
160
104
|
if (blob.revInPayload !== undefined && Number.isFinite(scannedRev))
|
|
161
105
|
bumped[blob.revInPayload] = scannedRev + 1;
|
|
162
106
|
const after = JSON.stringify(bumped);
|
|
163
|
-
// 判「有没有变」只看**身份字段**那一半:rev 是我们自己推的,拿它去判会让每次重跑都变成一次真写。
|
|
164
107
|
if (JSON.stringify(next) === before)
|
|
165
|
-
continue;
|
|
108
|
+
continue;
|
|
166
109
|
const res = await exec.query(buildBlobWriteSql(dialect, leg), [after, ...blob.pk.map((c) => row[c]), row[blob.revColumn]]);
|
|
167
110
|
if (res.affected !== 1) {
|
|
168
|
-
// rev-CAS 落空 = 读到写之间有一次正常业务写提交(codex R2-F1)。**响亮抛** ⇒ 整个事务回滚
|
|
169
|
-
// ⇒ 两侧字节零变更。绝不「重读再盖」:那正是把别人的写吞掉的那条路。
|
|
170
111
|
throw new Error(`adoption: concurrent write on ${leg.table} (leg ${leg.leg}, pk=${blob.pk.map((c) => String(row[c])).join("/")}) — ` +
|
|
171
112
|
"its rev moved between read and write, so the payload rewrite was refused and the whole rebind rolled back. " +
|
|
172
113
|
"Adoption requires the source identity to be quiesced (design/183 I1: stop the engine first), then retry.");
|
|
@@ -175,7 +116,6 @@ export function createAdoptionRunner(deps) {
|
|
|
175
116
|
}
|
|
176
117
|
return rows;
|
|
177
118
|
}
|
|
178
|
-
/** phase 3 的腿:全部在**一个事务**里(见 store.rebindTransaction 的顶注)。 */
|
|
179
119
|
async function runLegs(exec, fromPrincipal, toPrincipal) {
|
|
180
120
|
const out = [];
|
|
181
121
|
for (const leg of REBIND_LEGS) {
|
|
@@ -197,7 +137,6 @@ export function createAdoptionRunner(deps) {
|
|
|
197
137
|
continue;
|
|
198
138
|
}
|
|
199
139
|
if (leg.kind === "rule-owner-rekey") {
|
|
200
|
-
// 一对键算一次(见 buildRuleOwnerRekeySql 的注),一条等值 UPDATE 走完;`principal` 明文列同改。
|
|
201
140
|
const [pair] = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
|
|
202
141
|
if (pair === undefined)
|
|
203
142
|
throw new Error(`runLegs: leg ${leg.leg} resolved no identity pair`);
|
|
@@ -220,31 +159,18 @@ export function createAdoptionRunner(deps) {
|
|
|
220
159
|
deployment: t.deployment,
|
|
221
160
|
key: t.key,
|
|
222
161
|
requiredValue: t.requiredValue.replace("%TO%", toPrincipal),
|
|
223
|
-
// 🔴 恒 false:这些配置住在别的部署单元,收编动作够不到 —— 183 §3.3「改不到的配置,不假装改了」。
|
|
224
162
|
migrated: false,
|
|
225
163
|
ack: false,
|
|
226
164
|
witnessedAtMs: null,
|
|
227
165
|
}));
|
|
228
166
|
}
|
|
229
|
-
// 🔴 库行 → 内域一律过 schema(禁裸 `as`,见 wire.ts 的 AdoptionLegsSchema 段)。
|
|
230
167
|
function parseLegs(row) {
|
|
231
168
|
return AdoptionLegsSchema.parse(JSON.parse(row.legsJson));
|
|
232
169
|
}
|
|
233
170
|
function parseConfigs(row) {
|
|
234
171
|
return AdoptionConfigsSchema.parse(JSON.parse(row.configsJson));
|
|
235
172
|
}
|
|
236
|
-
/**
|
|
237
|
-
* 回执的铸造。终态行 ⇒ `immutableReport` 是**落库的那份字节**原样解回(逐字节恒同的来源);
|
|
238
|
-
* 非终态行 ⇒ 同形投影(`status:"stalled"` 就是「这还不是冻结快照」的机读位)。
|
|
239
|
-
* `current` 一律**现算**(183 §6:史实与现势各住各的字段)。
|
|
240
|
-
*/
|
|
241
|
-
/**
|
|
242
|
-
* 仍挂在旧身份名下的行数(跨全部腿求和,只读)。
|
|
243
|
-
* 每次回执都现算 —— 它是**现势**,不是史实(183 §6 的两半分家)。
|
|
244
|
-
*/
|
|
245
173
|
async function countResidual(exec, fromPrincipal, toPrincipal) {
|
|
246
|
-
// 🔴 按**表**聚合,不按腿(codex R3-F4):字段名承诺的是「行数」,而一条 background_agent 行可以同时
|
|
247
|
-
// 在 owner 轴、scope 轴、载荷三处带旧身份 —— 逐腿累加会把它报成 3。一张表一次判定,行只算一次。
|
|
248
174
|
const byTable = new Map();
|
|
249
175
|
for (const leg of REBIND_LEGS) {
|
|
250
176
|
const slot = byTable.get(leg.table) ?? { match: new Set() };
|
|
@@ -252,19 +178,11 @@ export function createAdoptionRunner(deps) {
|
|
|
252
178
|
slot.blob = leg;
|
|
253
179
|
else {
|
|
254
180
|
slot.match.add(leg.matchColumn);
|
|
255
|
-
// 🔴 A-010.16(验真后修):这里此前只认 `memory-scope` 一种**非明文**编码,别的编码一律
|
|
256
|
-
// 落到下面那条 `[{ from: fromPrincipal, … }]` 兜底 —— 也就是拿**明文 principal** 去比一根存
|
|
257
|
-
// 派生键的列。对 `permission_rule.owner_key`(sha256 原像里带身份)那个谓词**永远匹配不到
|
|
258
|
-
// 任何一行**,于是残留计数在这张表上结构性恒 0:回执一边说「adopted」、一边把一整桶还挂在
|
|
259
|
-
// 旧身份名下的活规则报成「零残留」。判据改成**编码非 verbatim 即需解析**,新编码天然进对臂。
|
|
260
181
|
if (leg.encoding !== "verbatim")
|
|
261
182
|
slot.codedLeg = leg;
|
|
262
183
|
}
|
|
263
184
|
byTable.set(leg.table, slot);
|
|
264
185
|
}
|
|
265
|
-
// 🔴 一张表内**混编码**会让下面那条「一条 COUNT + 一个值对」的形悄悄算错(OR 起来的每一列都会被
|
|
266
|
-
// 绑上同一个值,而它们的编码不同 ⇒ 至少一列的谓词恒假)。今天没有这样的表;真出现时必须响亮拦下,
|
|
267
|
-
// 而不是报一个偏小的残留数(偏小 = 说「没落下」,正是本条要消灭的谎)。
|
|
268
186
|
for (const [table, slot] of byTable) {
|
|
269
187
|
const encodings = new Set(REBIND_LEGS.filter((l) => l.table === table && l.kind !== "blob-rewrite").map((l) => l.encoding));
|
|
270
188
|
if (encodings.size > 1) {
|
|
@@ -275,7 +193,6 @@ export function createAdoptionRunner(deps) {
|
|
|
275
193
|
let n = 0;
|
|
276
194
|
for (const [table, slot] of byTable) {
|
|
277
195
|
if (slot.blob !== undefined) {
|
|
278
|
-
// 载荷表:一次并集扫描(旧值桶 + 新值桶),行只要**投影或载荷**任一处还带旧身份就计一次。
|
|
279
196
|
const blob = slot.blob;
|
|
280
197
|
const projection = [...slot.match, blob.matchColumn, ...(blob.blob?.alsoMatch ?? [])];
|
|
281
198
|
for (const row of await scanBlobRows(exec, blob, fromPrincipal, toPrincipal)) {
|
|
@@ -286,8 +203,6 @@ export function createAdoptionRunner(deps) {
|
|
|
286
203
|
}
|
|
287
204
|
continue;
|
|
288
205
|
}
|
|
289
|
-
// 普通表:一条 COUNT,谓词是该表全部身份轴的并。memory 族的值对逐对各来一次(不同 scope 键 =
|
|
290
|
-
// 不同的行集合,不会重复计数同一行)。
|
|
291
206
|
const cols = [...slot.match];
|
|
292
207
|
const pairs = slot.codedLeg !== undefined
|
|
293
208
|
? await resolveValuePairs(exec, dialect, slot.codedLeg, fromPrincipal, toPrincipal)
|
|
@@ -318,19 +233,15 @@ export function createAdoptionRunner(deps) {
|
|
|
318
233
|
immutableReport: report,
|
|
319
234
|
current: {
|
|
320
235
|
outstandingConfigs: configs.filter((c) => c.witnessedAtMs === null).map((c) => `${c.deployment}:${c.key}`),
|
|
321
|
-
// form b 零承运 ⇒ 无拒收行(结构位在场的理由见 wire.ts 顶注差异②)。
|
|
322
236
|
quarantined: 0,
|
|
323
237
|
residualSourceRows,
|
|
324
238
|
},
|
|
325
239
|
});
|
|
326
240
|
}
|
|
327
|
-
/** 拒绝详情的落库形 = `{detail, conflicts}` 的 JSON(一列存两件,免得再加两个只有拒才用的列)。 */
|
|
328
241
|
function rejectedOutcome(row) {
|
|
329
242
|
let detail = "destination conflict";
|
|
330
243
|
let conflicts = [];
|
|
331
244
|
if (row.rejectDetail !== null) {
|
|
332
|
-
// safeParse:一条**坏掉的**拒绝详情不该把「这条弧被拒了」这件更重要的事实吞掉 —— 拒仍然是拒,
|
|
333
|
-
// 只是失去了逐表族的明细(fail-closed 的方向:降级成粗理由,绝不降级成「成功」)。
|
|
334
245
|
const parsed = AdoptionRejectDetailSchema.safeParse(JSON.parse(row.rejectDetail));
|
|
335
246
|
if (parsed.success) {
|
|
336
247
|
detail = parsed.data.detail;
|
|
@@ -340,7 +251,6 @@ export function createAdoptionRunner(deps) {
|
|
|
340
251
|
const code = row.rejectCode === "adoption.destination_unrepresentable" ? "adoption.destination_unrepresentable" : "adoption.destination_conflict";
|
|
341
252
|
return { kind: "rejected", code, adoptionId: row.adoptionId, detail, conflicts };
|
|
342
253
|
}
|
|
343
|
-
/** 从任意 phase 把弧推到终态(幂等、可重入)。**必须在弧锁下调用**。 */
|
|
344
254
|
async function drive(start, sweep, conn) {
|
|
345
255
|
let row = start;
|
|
346
256
|
const reread = async () => {
|
|
@@ -352,7 +262,6 @@ export function createAdoptionRunner(deps) {
|
|
|
352
262
|
if (row.state === "rejected")
|
|
353
263
|
return rejectedOutcome(row);
|
|
354
264
|
if (row.state === "in_flight" && row.phase === ADOPTION_PHASE.INTENT) {
|
|
355
|
-
// ③a preflight —— 任何 UPDATE 之前,只读。先证「装得下」,再查「撞不撞」。
|
|
356
265
|
const unrepresentable = await unrepresentableScopes(conn, row.fromPrincipal, row.toPrincipal);
|
|
357
266
|
if (unrepresentable.length > 0) {
|
|
358
267
|
const detail = JSON.stringify({
|
|
@@ -368,15 +277,12 @@ export function createAdoptionRunner(deps) {
|
|
|
368
277
|
await store.finalizeRejected(row.adoptionId, "adoption.destination_conflict", detail, now(), conn);
|
|
369
278
|
return rejectedOutcome(await reread());
|
|
370
279
|
}
|
|
371
|
-
// ③b 全部腿 + phase CAS,同一个事务(任何异常 ⇒ 整体回滚 ⇒ 两侧字节零变更)。
|
|
372
280
|
try {
|
|
373
281
|
await store.rebindOn(conn, row.adoptionId, ADOPTION_PHASE.INTENT, ADOPTION_PHASE.REBOUND, now(), (exec) => runLegs(exec, row.fromPrincipal, row.toPrincipal), (legs) => JSON.stringify(legs));
|
|
374
282
|
}
|
|
375
283
|
catch (err) {
|
|
376
284
|
if (!isDupKey(dialect, err))
|
|
377
285
|
throw err;
|
|
378
|
-
// 跨轴组合形:preflight 按迁移**前**的库态逐轴算,两根轴的组合可能在中途撞唯一键。
|
|
379
|
-
// 事务已回滚 ⇒ 字节零变更;这里把它落成与 preflight 同码的拒(fail-closed,不 fail-open)。
|
|
380
286
|
const detail = JSON.stringify({
|
|
381
287
|
detail: `unique-key violation while rebinding (cross-axis destination conflict): ${msgOf(err)}`,
|
|
382
288
|
conflicts: [],
|
|
@@ -391,7 +297,6 @@ export function createAdoptionRunner(deps) {
|
|
|
391
297
|
row = await reread();
|
|
392
298
|
}
|
|
393
299
|
if (row.state === "in_flight" && row.phase === ADOPTION_PHASE.CONFIGS) {
|
|
394
|
-
// ⑤ 承运腿:form b 恒零(数据本来就在 SQL)。阶段位显式推进 —— 见本文件顶注的弧图。
|
|
395
300
|
await store.advancePhase(row.adoptionId, ADOPTION_PHASE.CONFIGS, ADOPTION_PHASE.CARRIED, now(), conn);
|
|
396
301
|
row = await reread();
|
|
397
302
|
}
|
|
@@ -411,23 +316,10 @@ export function createAdoptionRunner(deps) {
|
|
|
411
316
|
}
|
|
412
317
|
if (row.state === "rejected")
|
|
413
318
|
return rejectedOutcome(row);
|
|
414
|
-
// ⑦ 终态**之后**的迟到行清扫(codex R1-F1 的可检测/可收敛半)。见 sweepLate 的注。
|
|
415
319
|
if (sweep && row.state === "adopted")
|
|
416
320
|
await sweepLate(row, conn);
|
|
417
321
|
return { kind: "receipt", adoptionId: row.adoptionId, receipt: receiptOf(row, await residualOf(row, conn)) };
|
|
418
322
|
}
|
|
419
|
-
/**
|
|
420
|
-
* 把收编**结束之后**又落到旧身份下的行搬过去。
|
|
421
|
-
*
|
|
422
|
-
* 🔴 为什么需要它:收编不给普通写者上栅栏 —— 183 I1 的口径是「引擎必须先停」,那是**运维前提**不是
|
|
423
|
-
* 机器保证(§3.2 明写「单靠锁文件是运维假设」)。真实部署里配置随迁是**别的部署单元**的活
|
|
424
|
-
* (`affectedDeploymentConfigs` 全部 `migrated:false`),所以在配置改完之前,BFF/cli 会继续按旧身份
|
|
425
|
-
* 注入、继续往旧身份下写行。终态 + 幂等短路会让回执一直说「adopted」,而那些行永远没人搬。
|
|
426
|
-
*
|
|
427
|
-
* 处置:**每次 POST**(写动词)在终态之后再跑一遍腿(幂等,干净时零行变化)。这不是「二次收编」——
|
|
428
|
-
* `immutableReport` 一个字节不动,变的只有 `current.residualSourceRows` 这个现势读数。
|
|
429
|
-
* 清扫本身不改 phase(终态是永久的),失败也不把弧打回在飞:留痕 + 让残留读数继续报真话。
|
|
430
|
-
*/
|
|
431
323
|
async function sweepLate(row, conn) {
|
|
432
324
|
try {
|
|
433
325
|
const legs = await store.sweepOn(conn, (exec) => runLegs(exec, row.fromPrincipal, row.toPrincipal));
|
|
@@ -442,8 +334,6 @@ export function createAdoptionRunner(deps) {
|
|
|
442
334
|
}
|
|
443
335
|
}
|
|
444
336
|
catch (err) {
|
|
445
|
-
// 清扫是收敛动作,不是终态的一部分:失败不该把一个已完成的收编变成错误应答。
|
|
446
|
-
// 但也**绝不静默** —— 留痕 + 残留读数会继续报非零。
|
|
447
337
|
deps.logger?.warn?.("adoption_late_sweep_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
|
|
448
338
|
}
|
|
449
339
|
}
|
|
@@ -453,21 +343,11 @@ export function createAdoptionRunner(deps) {
|
|
|
453
343
|
}
|
|
454
344
|
catch (err) {
|
|
455
345
|
deps.logger?.warn?.("adoption_residual_count_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
|
|
456
|
-
// 数不出来 ⇒ **不报 0**(那是「一切干净」的字面断言)。报 -1 不合 schema,故原样上抛给调用方
|
|
457
|
-
// 之外的唯一诚实选择是把它当成「至少有一行没数到」。fail-closed:宁可报 1 让人去查。
|
|
458
346
|
return 1;
|
|
459
347
|
}
|
|
460
348
|
}
|
|
461
|
-
/**
|
|
462
|
-
* 🔴 A-010.18:**唯一**的静默闸挂点。三条入口(`start` / `get` 的在飞续跑 / boot 的 `resumeInFlight`)
|
|
463
|
-
* 全部经由本函数动数据,所以闸挂在这里一处就覆盖全部会写被迁表的路径 —— 挂在 `start` 上会漏掉后两条,
|
|
464
|
-
* 而那两条恰恰是「没人按重试、副本自己续跑」的场景,与维护 tick 撞车的概率并不更低。
|
|
465
|
-
* 闸的语义、为什么让开**整轮** tick、以及它只挡本副本这条射程边界,逐字见 `adoption/quiesce.ts`。
|
|
466
|
-
*/
|
|
467
349
|
async function driveUnderLock(row, sweep) {
|
|
468
350
|
return withAdoptionQuiesce(() => driveUnderLockInner(row, sweep), {
|
|
469
|
-
// 排空没等到 ⇒ **响亮**(codex R1 [high] 的第二半):等不到不等于可以装作等到了。这条 warn 是
|
|
470
|
-
// 运维判断「那次收编期间有没有腿在跟它抢同一批行」的唯一读数。
|
|
471
351
|
onDrainTimeout: (reading) => deps.logger?.warn?.("adoption_maintenance_drain_timeout", {
|
|
472
352
|
adoptionId: row.adoptionId,
|
|
473
353
|
pendingLegs: reading.pending,
|
|
@@ -479,15 +359,9 @@ export function createAdoptionRunner(deps) {
|
|
|
479
359
|
});
|
|
480
360
|
}
|
|
481
361
|
async function driveUnderLockInner(row, sweep) {
|
|
482
|
-
const out = await store.withLock(adoptionLockName(row.fromPrincipal),
|
|
483
|
-
// 🔴 **拿到锁之后重读**(codex R2-F3):`row` 是排队**之前**读到的快照,而排队期间别的副本可能已经
|
|
484
|
-
// 把这条弧推完或拒掉了。拿旧快照进状态机 = 按一个已经不成立的世界做决定(实测可达的形:一条已被
|
|
485
|
-
// 拒的弧被排队者推去真迁数据 —— 拒**故意**停在 phase INTENT,只判 phase 的 CAS 对它是满足的)。
|
|
486
|
-
// 库级守卫(rebind CAS 里的 `state='in_flight'`)是第二道;这一道让常路根本走不到那儿。
|
|
487
|
-
async (conn) => drive((await store.getById(row.adoptionId, conn)) ?? row, sweep, conn), deps.lockAttempts, deps.lockSleepMs);
|
|
362
|
+
const out = await store.withLock(adoptionLockName(row.fromPrincipal), async (conn) => drive((await store.getById(row.adoptionId, conn)) ?? row, sweep, conn), deps.lockAttempts, deps.lockSleepMs);
|
|
488
363
|
if (out !== undefined)
|
|
489
364
|
return out;
|
|
490
|
-
// 锁被别的副本持着 ⇒ 那条弧正在被推进。回**现势**回执(`stalled`),不等死也不假装成功。
|
|
491
365
|
const cur = (await store.getById(row.adoptionId)) ?? row;
|
|
492
366
|
deps.logger?.info?.("adoption_arc_locked_elsewhere", { adoptionId: cur.adoptionId, phase: cur.phase });
|
|
493
367
|
if (cur.state === "rejected")
|
|
@@ -496,10 +370,8 @@ export function createAdoptionRunner(deps) {
|
|
|
496
370
|
}
|
|
497
371
|
return {
|
|
498
372
|
async start(fromPrincipal, toPrincipal) {
|
|
499
|
-
// 幂等身份 = DB 约束:并发同参 POST 由 `from_principal` UNIQUE 仲裁,禁 SELECT-then-INSERT。
|
|
500
373
|
const { row } = await store.ensureAdoption({ adoptionId: randomUUID(), fromPrincipal, toPrincipal, nowMs: now() });
|
|
501
374
|
if (row.toPrincipal !== toPrincipal) {
|
|
502
|
-
// 同 from 异 to = 183 D7「二次收编/多租转让」,非本协议射程 ⇒ typed 409。
|
|
503
375
|
return { kind: "source_already_bound", adoptionId: row.adoptionId, fromPrincipal, boundTo: row.toPrincipal };
|
|
504
376
|
}
|
|
505
377
|
return driveUnderLock(row, true);
|
|
@@ -510,11 +382,8 @@ export function createAdoptionRunner(deps) {
|
|
|
510
382
|
return null;
|
|
511
383
|
if (row.state === "rejected")
|
|
512
384
|
return rejectedOutcome(row);
|
|
513
|
-
// GET 是读动词:只报残留读数,**不**顺带清扫(清扫是 POST 的活)。
|
|
514
385
|
if (row.state === "adopted")
|
|
515
386
|
return { kind: "receipt", adoptionId, receipt: receiptOf(row, await residualOf(row)) };
|
|
516
|
-
// 在飞行读面**顺手续跑**:一次崩溃之后没人再发 POST 的部署,读一次状态就把弧推完
|
|
517
|
-
// (与 boot 扫描同一条 drive,不是第二套状态机)。
|
|
518
387
|
return driveUnderLock(row, false);
|
|
519
388
|
},
|
|
520
389
|
async resumeInFlight() {
|
|
@@ -524,26 +393,20 @@ export function createAdoptionRunner(deps) {
|
|
|
524
393
|
const rejected = [];
|
|
525
394
|
const failed = [];
|
|
526
395
|
for (const row of rows) {
|
|
527
|
-
// 🔴 **逐弧兜住异常**(本车自测逮到):一条推不动的弧(库上有一行坏数据、一次并发写撞了 CAS…)
|
|
528
|
-
// 若把整趟扫描一起掀翻,**别的**弧就再也没人续跑了 —— 一条坏行绑架了全部恢复。
|
|
529
|
-
// 逐行 fail-loud(进 `failed` + 留痕),扫描继续。
|
|
530
396
|
let out;
|
|
531
397
|
try {
|
|
532
|
-
out = await driveUnderLock(row, true);
|
|
398
|
+
out = await driveUnderLock(row, true);
|
|
533
399
|
}
|
|
534
400
|
catch (err) {
|
|
535
401
|
failed.push(row.adoptionId);
|
|
536
402
|
deps.logger?.warn?.("adoption_resume_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
|
|
537
403
|
continue;
|
|
538
404
|
}
|
|
539
|
-
// 闭集穷举:结局多一种 ⇒ 这里编译红,不会静默落进某个默认臂。
|
|
540
405
|
switch (out.kind) {
|
|
541
406
|
case "rejected":
|
|
542
407
|
rejected.push(out.adoptionId);
|
|
543
408
|
break;
|
|
544
409
|
case "source_already_bound":
|
|
545
|
-
// 到不了:`drive` 只从库里既有行推进,不铸新行 —— 这一支是 `start()` 的入口判决。
|
|
546
|
-
// 留一条**响亮**的记账而不是 `default: break`(静默吞会把一次真的状态机漂移变成看不见的丢弧)。
|
|
547
410
|
stalled.push(out.adoptionId);
|
|
548
411
|
deps.logger?.warn?.("adoption_resume_unexpected_outcome", { adoptionId: out.adoptionId, kind: out.kind });
|
|
549
412
|
break;
|