@sema-agent/server 7.142.0 → 7.143.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.
Files changed (258) hide show
  1. package/MIGRATION.md +442 -0
  2. package/USAGE.md +41 -24
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +124 -25
  9. package/dist/approval-card.js +103 -23
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/governance-seams.d.ts +26 -41
  17. package/dist/boot/governance-seams.js +61 -28
  18. package/dist/boot/late-binds.d.ts +7 -4
  19. package/dist/boot/late-binds.js +3 -1
  20. package/dist/boot/leader.js +2 -4
  21. package/dist/boot/memory-consolidation.d.ts +0 -8
  22. package/dist/boot/memory-consolidation.js +3 -3
  23. package/dist/boot/reapers.js +5 -4
  24. package/dist/boot/resolve-spec.d.ts +1 -1
  25. package/dist/boot/resolve-spec.js +8 -8
  26. package/dist/boot/runner-deps.d.ts +10 -7
  27. package/dist/boot/runner-deps.js +5 -17
  28. package/dist/boot/shutdown.d.ts +9 -0
  29. package/dist/boot/shutdown.js +16 -2
  30. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  31. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  32. package/dist/boot/stage-03-store-backend.js +1 -2
  33. package/dist/boot/stage-04-stores.d.ts +5 -2
  34. package/dist/boot/stage-04-stores.js +3 -5
  35. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  36. package/dist/boot/stage-06-runners.d.ts +5 -2
  37. package/dist/boot/stage-06-runners.js +1 -3
  38. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-08-reapers.d.ts +5 -2
  41. package/dist/boot/stage-09-leader.d.ts +5 -2
  42. package/dist/boot/stage-10-http-server.d.ts +5 -2
  43. package/dist/boot/stage-10-http-server.js +9 -1
  44. package/dist/boot/stores.d.ts +5 -2
  45. package/dist/boot/stores.js +7 -7
  46. package/dist/boot/web-admin.d.ts +5 -1
  47. package/dist/boot/web-admin.js +31 -19
  48. package/dist/capabilities/hands-lane.d.ts +9 -39
  49. package/dist/capabilities/hands-lane.js +8 -13
  50. package/dist/capabilities/mcp-probe.d.ts +3 -2
  51. package/dist/capabilities/scenarios.d.ts +11 -1
  52. package/dist/capabilities/scenarios.js +1 -0
  53. package/dist/capabilities/skills.d.ts +1 -17
  54. package/dist/capabilities/skills.js +0 -8
  55. package/dist/capability-facts.d.ts +8 -8
  56. package/dist/capability-facts.js +4 -4
  57. package/dist/card-void.d.ts +6 -9
  58. package/dist/config-catalog.js +1 -1
  59. package/dist/config-dir-trust.js +58 -29
  60. package/dist/config-invariants.d.ts +7 -1
  61. package/dist/config-invariants.js +7 -0
  62. package/dist/config-provider.d.ts +10 -0
  63. package/dist/config-provider.js +15 -4
  64. package/dist/config-root-migration.d.ts +13 -0
  65. package/dist/config-root-migration.js +99 -0
  66. package/dist/config-types.d.ts +5 -5
  67. package/dist/config.d.ts +4 -3
  68. package/dist/config.js +1 -1
  69. package/dist/delegated-session-owner.d.ts +5 -3
  70. package/dist/delegated-session-owner.js +1 -1
  71. package/dist/device-ws-protocol.d.ts +1 -0
  72. package/dist/device-ws-protocol.js +2 -1
  73. package/dist/env-model-table-gate.d.ts +53 -0
  74. package/dist/env-model-table-gate.js +53 -0
  75. package/dist/fleet/fleet-bus.d.ts +28 -52
  76. package/dist/fleet/fleet-bus.js +35 -100
  77. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  78. package/dist/fleet/fleet-reconciler.js +1 -1
  79. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  80. package/dist/fleet/subagent-tail-bus.js +15 -28
  81. package/dist/hooks/hook-runner.js +28 -7
  82. package/dist/host-decision.d.ts +54 -20
  83. package/dist/host-decision.js +42 -14
  84. package/dist/http/admission.d.ts +1 -1
  85. package/dist/http/admission.js +7 -3
  86. package/dist/http/approval-sweeps.js +11 -8
  87. package/dist/http/caller-trust.d.ts +1 -1
  88. package/dist/http/durable-run-leg.js +2 -2
  89. package/dist/http/engine-refusal.d.ts +16 -0
  90. package/dist/http/engine-refusal.js +17 -0
  91. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  92. package/dist/http/memory-compliance-failure-reply.js +4 -6
  93. package/dist/http/owner-access.d.ts +11 -0
  94. package/dist/http/owner-access.js +13 -0
  95. package/dist/http/resume-legs.d.ts +21 -7
  96. package/dist/http/resume-legs.js +71 -49
  97. package/dist/http/resume-restatement.d.ts +39 -0
  98. package/dist/http/resume-restatement.js +25 -0
  99. package/dist/http/route-ctx.d.ts +35 -10
  100. package/dist/http/route-ctx.js +1 -1
  101. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  102. package/dist/http/routes/approvals-assistant.js +142 -31
  103. package/dist/http/routes/capabilities.js +11 -2
  104. package/dist/http/routes/memory-policy.d.ts +2 -2
  105. package/dist/http/routes/memory-policy.js +1 -1
  106. package/dist/http/routes/notify-wake.js +23 -25
  107. package/dist/http/routes/rules.js +48 -13
  108. package/dist/http/routes/runs.js +9 -8
  109. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  110. package/dist/http/routes/session-memory-sync.js +27 -10
  111. package/dist/http/routes/session-sync.js +13 -20
  112. package/dist/http/routes/sessions.d.ts +2 -2
  113. package/dist/http/routes/sessions.js +22 -23
  114. package/dist/http/routes/tasks.js +27 -49
  115. package/dist/http/server.d.ts +13 -7
  116. package/dist/http/server.js +2 -2
  117. package/dist/http/stored-body-replay.js +15 -4
  118. package/dist/http/wire-types.d.ts +17 -7
  119. package/dist/leader/leader.js +5 -2
  120. package/dist/leader/wire.d.ts +7 -1
  121. package/dist/leader/wire.js +6 -5
  122. package/dist/leg-facts.d.ts +37 -0
  123. package/dist/leg-facts.js +15 -0
  124. package/dist/memory-bundle-engine.d.ts +1 -4
  125. package/dist/memory-bundle-engine.js +2 -2
  126. package/dist/memory-operator-faces.d.ts +30 -66
  127. package/dist/memory-operator-faces.js +17 -88
  128. package/dist/memory-scope-stores.d.ts +38 -40
  129. package/dist/memory-scope-stores.js +51 -36
  130. package/dist/memory-scope.d.ts +0 -9
  131. package/dist/memory-scope.js +0 -9
  132. package/dist/memory-sync-client.js +14 -3
  133. package/dist/memory-sync.d.ts +20 -4
  134. package/dist/memory-sync.js +23 -4
  135. package/dist/model-plane-face.d.ts +13 -7
  136. package/dist/model-plane-face.js +2 -1
  137. package/dist/observability/fail-open.d.ts +20 -4
  138. package/dist/observability/fail-open.js +17 -4
  139. package/dist/observability/run-terminal.js +4 -0
  140. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  141. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  142. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  143. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  144. package/dist/orchestration/workflow-park-decision.js +5 -3
  145. package/dist/org-delivery-gate.d.ts +11 -0
  146. package/dist/org-delivery-gate.js +19 -1
  147. package/dist/parked-decide.d.ts +13 -9
  148. package/dist/parked-decide.js +7 -7
  149. package/dist/parked-revive-surface.js +1 -1
  150. package/dist/peer-inbound-face.d.ts +127 -0
  151. package/dist/peer-inbound-face.js +158 -0
  152. package/dist/permission-rule-list.d.ts +13 -9
  153. package/dist/permission-rule-list.js +7 -3
  154. package/dist/plugins/approval-ask-store-file.js +18 -6
  155. package/dist/plugins/approval-ask-store-memory.js +8 -6
  156. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  157. package/dist/plugins/approval-ask-store-sql.js +30 -11
  158. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  159. package/dist/plugins/checkpoint-store-sql.js +9 -7
  160. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  161. package/dist/plugins/file-history-store-sql.js +24 -19
  162. package/dist/plugins/file-run-store.d.ts +29 -0
  163. package/dist/plugins/file-run-store.js +97 -8
  164. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  165. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  166. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  167. package/dist/plugins/local-checkpoint-store.js +2 -2
  168. package/dist/plugins/local-session-store.d.ts +6 -2
  169. package/dist/plugins/local-session-store.js +8 -2
  170. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  171. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  172. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  173. package/dist/plugins/mailbox-store-sql.js +76 -7
  174. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  175. package/dist/plugins/memory-sync-store-pg.js +21 -0
  176. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  177. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  178. package/dist/plugins/one-time-migrations.d.ts +32 -2
  179. package/dist/plugins/one-time-migrations.js +106 -3
  180. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  181. package/dist/plugins/org-delivery-quota-store.js +2 -12
  182. package/dist/plugins/park-commit-watch.d.ts +1 -1
  183. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  184. package/dist/plugins/permission-rule-store-file.js +15 -2
  185. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  186. package/dist/plugins/permission-rule-store-sql.js +35 -2
  187. package/dist/plugins/remote-env-device.d.ts +8 -11
  188. package/dist/plugins/remote-env-device.js +6 -3
  189. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  190. package/dist/plugins/remote-env-k8s.js +50 -17
  191. package/dist/plugins/retention-store-sql.d.ts +2 -2
  192. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  193. package/dist/plugins/sandbox-read-floor.js +2 -7
  194. package/dist/plugins/sql-errors.d.ts +10 -1
  195. package/dist/plugins/sql-errors.js +13 -0
  196. package/dist/plugins/store-backend.d.ts +23 -28
  197. package/dist/plugins/store-backend.js +9 -1
  198. package/dist/plugins/store-contracts.d.ts +29 -5
  199. package/dist/plugins/store-contracts.js +4 -1
  200. package/dist/plugins/task-attachment-store.d.ts +2 -2
  201. package/dist/plugins/task-attachment-store.js +37 -52
  202. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  203. package/dist/plugins/tool-result-store-sql.js +23 -2
  204. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  205. package/dist/plugins/wiring-section-codec.js +55 -0
  206. package/dist/plugins/workspace-fill.d.ts +1 -1
  207. package/dist/rule-entry-list.d.ts +3 -2
  208. package/dist/rule-entry-list.js +5 -1
  209. package/dist/rules-consent.d.ts +28 -5
  210. package/dist/rules-consent.js +40 -28
  211. package/dist/run-local.js +9 -5
  212. package/dist/runs.d.ts +12 -8
  213. package/dist/runs.js +6 -7
  214. package/dist/runtime-governance.d.ts +1 -1
  215. package/dist/server-secret-env.d.ts +4 -0
  216. package/dist/session-error-wire.d.ts +12 -0
  217. package/dist/session-error-wire.js +6 -0
  218. package/dist/session-incarnation.d.ts +1 -1
  219. package/dist/session-notify-door.d.ts +106 -0
  220. package/dist/session-notify-door.js +93 -0
  221. package/dist/session-sync.d.ts +9 -14
  222. package/dist/session-sync.js +5 -5
  223. package/dist/session-workspace.d.ts +3 -13
  224. package/dist/session-workspace.js +0 -9
  225. package/dist/standing-answer-grant.d.ts +5 -0
  226. package/dist/standing-answer-grant.js +10 -0
  227. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  228. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  229. package/dist/task-mcp.d.ts +1 -1
  230. package/dist/task-settings.d.ts +11 -8
  231. package/dist/task-settings.js +31 -11
  232. package/dist/tool-approval.d.ts +52 -34
  233. package/dist/tool-approval.js +66 -57
  234. package/dist/trace/core-keyset-guard.d.ts +11 -8
  235. package/dist/trace/engine-notice-wire.d.ts +1 -1
  236. package/dist/trace/engine-notice-wire.js +3 -1
  237. package/dist/trace/exit-seat.d.ts +7 -1
  238. package/dist/trace/exit-seat.js +1 -0
  239. package/dist/trace/ledger-sink.d.ts +2 -2
  240. package/dist/trace/model-segment.d.ts +55 -25
  241. package/dist/trace/model-segment.js +101 -33
  242. package/dist/trace/project.d.ts +7 -4
  243. package/dist/trace/project.js +6 -0
  244. package/dist/trace/redact.d.ts +61 -9
  245. package/dist/trace/redact.js +1490 -468
  246. package/dist/trace/self-minted-wire.d.ts +510 -38
  247. package/dist/trace/self-minted-wire.js +165 -17
  248. package/dist/trace/wire-projection-faces.d.ts +1 -1
  249. package/dist/trace/wire-projection-faces.js +1 -1
  250. package/dist/verifier-pause-card.d.ts +33 -5
  251. package/dist/verifier-pause-card.js +15 -10
  252. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  253. package/dist/web-admin-client/apply-effective.js +90 -10
  254. package/dist/workspace-space.d.ts +19 -3
  255. package/dist/workspace-space.js +8 -4
  256. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  257. package/package.json +3 -2
  258. package/wire-changes/server@7.143.0.json +1167 -0
@@ -1,13 +1,14 @@
1
1
  import { isUtf8 } from "node:buffer";
2
- import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_RECORD_ID_MAX_CHARS, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MailboxStoreError, assertMailboxDecisionMove, judgeMailboxAppendById, readMailboxAppend, readMailboxPeerMeta, readStoredMailboxDecision } from "@sema-agent/core";
2
+ import { MAILBOX_CONSUMED_ID_WINDOW, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_RECORD_ID_MAX_CHARS, MAILBOX_REPLAY_FACETS, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MailboxStoreError, assertMailboxDecisionMove, judgeMailboxAppendById, mailboxReplayDigests, readMailboxAppend, readMailboxPeerMeta, readStoredMailboxDecision } from "@sema-agent/core";
3
3
  import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
4
4
  import { isWellFormedUnicode } from "../principal-jwt.js";
5
5
  import { ensureIndexes } from "./ensure-index.js";
6
6
  import { escapeLike } from "./sql-escape.js";
7
- import { decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readStringArrayColumn } from "./sql-json-column.js";
7
+ import { SqlJsonColumnError, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readStringArrayColumn } from "./sql-json-column.js";
8
8
  export const MAILBOX_TABLE = "mailbox";
9
9
  export const MAILBOX_MSG_TABLE = "mailbox_message";
10
10
  export const MAILBOX_TOMBSTONE_TABLE = "mailbox_tombstone";
11
+ export const MAILBOX_CONSUMED_TABLE = "mailbox_consumed";
11
12
  const TOMBSTONE_LABEL_MAX_CHARS = 120;
12
13
  function boundedLabel(label) {
13
14
  if (label === undefined || label === "")
@@ -49,8 +50,54 @@ function appendSnapshot(msg) {
49
50
  }
50
51
  return rec;
51
52
  }
52
- function replayedSeq(held, scope, handle, logger, asStored) {
53
- return judgeMailboxAppendById(rowsToMessages(held, scope, handle, logger), asStored);
53
+ function replayedSeq(held, consumed, scope, handle, logger, asStored) {
54
+ return judgeMailboxAppendById({ held: rowsToMessages(held, scope, handle, logger), consumed: consumedRecordsOf(consumed, scope, handle, logger) }, asStored);
55
+ }
56
+ function consumedRecordsOf(rows, scope, handle, logger) {
57
+ return rows.map((r) => {
58
+ const seq = Number(r.seq);
59
+ let digests;
60
+ try {
61
+ digests = decodeJsonColumn(bufToStr(r.digests), readConsumedDigestsShape, `${MAILBOX_CONSUMED_TABLE}.digests for seq ${seq}`);
62
+ }
63
+ catch (e) {
64
+ if (!(e instanceof SqlJsonColumnError))
65
+ throw e;
66
+ logger?.warn("mailbox_message_column_corrupt", {
67
+ scope,
68
+ handle,
69
+ seq,
70
+ column: "consumed_digests",
71
+ note: "appends under this record id are refused for the box (fail-closed); repair or delete this row of the received window",
72
+ });
73
+ throw new Error(`MailboxStore: received-window row seq ${seq} has unreadable digests (refusing — the replay judgment cannot be made)`);
74
+ }
75
+ return { id: String(r.record_id), seq, digests };
76
+ });
77
+ }
78
+ const readConsumedDigestsShape = (body) => {
79
+ if (typeof body !== "object" || body === null || Array.isArray(body))
80
+ return { problem: "the digests are not an object" };
81
+ const facets = body;
82
+ if (Object.keys(facets).length !== MAILBOX_REPLAY_FACETS.length || !MAILBOX_REPLAY_FACETS.every((f) => typeof facets[f] === "string")) {
83
+ return { problem: `the digests are not exactly the ${MAILBOX_REPLAY_FACETS.length} replay facets as strings` };
84
+ }
85
+ return { value: Object.freeze(Object.fromEntries(MAILBOX_REPLAY_FACETS.map((f) => [f, facets[f]]))) };
86
+ };
87
+ async function enterConsumedWindow(run, removed, scope, handle) {
88
+ const entered = removed.filter((m) => m.id !== undefined);
89
+ if (entered.length === 0)
90
+ return;
91
+ for (const m of entered) {
92
+ await run({ mysql: `INSERT INTO ${MAILBOX_CONSUMED_TABLE} (scope_key, handle, seq, record_id, digests) VALUES (?, ?, ?, ?, ?)`, pg: `INSERT INTO ${MAILBOX_CONSUMED_TABLE} (scope_key, handle, seq, record_id, digests) VALUES ($1, $2, $3, $4, $5)` }, [scope, handle, m.seq, m.id, JSON.stringify(mailboxReplayDigests(m))]);
93
+ }
94
+ const edge = await run({
95
+ mysql: `SELECT seq FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq DESC LIMIT 1 OFFSET ?`,
96
+ pg: `SELECT seq FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq DESC LIMIT 1 OFFSET $3`,
97
+ }, [scope, handle, MAILBOX_CONSUMED_ID_WINDOW - 1]);
98
+ if (edge.length === 0)
99
+ return;
100
+ await run({ mysql: `DELETE FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = ? AND handle = ? AND seq < ?`, pg: `DELETE FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq < $3` }, [scope, handle, Number(edge[0].seq)]);
54
101
  }
55
102
  function asUtf8Stored(v) {
56
103
  return Buffer.from(v, "utf8").toString("utf8");
@@ -71,7 +118,7 @@ export function storedMailboxKeyless(scopeKey, handle) {
71
118
  const h = storedKeyText(handle);
72
119
  return scope === undefined || h === undefined || keyless(scope, h);
73
120
  }
74
- export const MAILBOX_KEYED_TABLES = [MAILBOX_TABLE, MAILBOX_MSG_TABLE, MAILBOX_TOMBSTONE_TABLE];
121
+ export const MAILBOX_KEYED_TABLES = [MAILBOX_TABLE, MAILBOX_MSG_TABLE, MAILBOX_CONSUMED_TABLE, MAILBOX_TOMBSTONE_TABLE];
75
122
  export function mailboxKeyDeleteSql(dialect, table) {
76
123
  return dialect === "mysql" ? `DELETE FROM ${table} WHERE scope_key = ? AND handle = ?` : `DELETE FROM ${table} WHERE scope_key = $1 AND handle = $2`;
77
124
  }
@@ -210,6 +257,14 @@ export async function ensureTiDBMailboxSchema(pool) {
210
257
  record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) NULL,
211
258
  PRIMARY KEY (scope_key, handle, seq),
212
259
  KEY idx_mbm_sent (scope_key, handle, sent_at_ms)
260
+ ) COLLATE utf8mb4_bin`);
261
+ await pool.query(`CREATE TABLE IF NOT EXISTS ${MAILBOX_CONSUMED_TABLE} (
262
+ scope_key VARBINARY(190) NOT NULL,
263
+ handle VARBINARY(190) NOT NULL,
264
+ seq BIGINT NOT NULL,
265
+ record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) NOT NULL,
266
+ digests TEXT NOT NULL,
267
+ PRIMARY KEY (scope_key, handle, seq)
213
268
  ) COLLATE utf8mb4_bin`);
214
269
  await pool.query(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TOMBSTONE_TABLE} (
215
270
  scope_key VARBINARY(190) NOT NULL,
@@ -247,6 +302,14 @@ export async function ensurePgMailboxSchema(q) {
247
302
  decision VARCHAR(16) COLLATE "C", -- core 7.57.0 #972b(TiDB twin 同注:留置决定,NULL = 从未 held;只经 moveDecision 移动,随记录亡)
248
303
  record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) COLLATE "C", -- core 7.59.0(TiDB twin 同注:生产者铸的记录身份,NULL = 没说身份;盒行锁之内经引擎判,随记录亡)
249
304
  PRIMARY KEY (scope_key, handle, seq)
305
+ )`);
306
+ await q(`CREATE TABLE IF NOT EXISTS ${MAILBOX_CONSUMED_TABLE} (
307
+ scope_key VARCHAR(190) COLLATE "C" NOT NULL,
308
+ handle VARCHAR(190) COLLATE "C" NOT NULL,
309
+ seq BIGINT NOT NULL,
310
+ record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) COLLATE "C" NOT NULL,
311
+ digests TEXT COLLATE "C" NOT NULL, -- TiDB twin 同注:已收窗(core 7.61.0),四面摘要 JSON
312
+ PRIMARY KEY (scope_key, handle, seq)
250
313
  )`);
251
314
  await q(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TOMBSTONE_TABLE} (
252
315
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -299,7 +362,8 @@ export class TiDBMailboxStore {
299
362
  throw tombstonedRecipientError(scope, handle);
300
363
  if (rec.id !== undefined) {
301
364
  const [held] = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("mysql", "hop_chain")}, ${jsonColumnText("mysql", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND record_id = ? FOR UPDATE`, [scope, handle, rec.id]);
302
- const replay = replayedSeq(held, scope, handle, this.logger, { ...rec, content: asUtf8Stored(rec.content), ...(rec.from !== undefined ? { from: asUtf8Stored(rec.from) } : {}) });
365
+ const [consumed] = await c.query(`SELECT seq, record_id, digests FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = ? AND handle = ? AND record_id = ? FOR UPDATE`, [scope, handle, rec.id]);
366
+ const replay = replayedSeq(held, consumed, scope, handle, this.logger, { ...rec, content: asUtf8Stored(rec.content), ...(rec.from !== undefined ? { from: asUtf8Stored(rec.from) } : {}) });
303
367
  if (replay !== undefined)
304
368
  return replay;
305
369
  }
@@ -338,7 +402,9 @@ export class TiDBMailboxStore {
338
402
  const [rows] = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
339
403
  if (rows.length === 0 || rows[0].lease_owner == null || bufToStr(rows[0].lease_owner) !== owner)
340
404
  return { acked: false };
405
+ const [removed] = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("mysql", "hop_chain")}, ${jsonColumnText("mysql", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq <= ? AND record_id IS NOT NULL ORDER BY seq FOR UPDATE`, [scope, handle, upToSeq]);
341
406
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq <= ?`, [scope, handle, upToSeq]);
407
+ await enterConsumedWindow(async (sql, params) => (await c.query(sql.mysql, params))[0], rowsToMessages(removed, scope, handle, this.logger), scope, handle);
342
408
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
343
409
  WHERE scope_key = ? AND handle = ? AND lease_max_seq IS NOT NULL AND lease_max_seq <= ?`, [scope, handle, upToSeq]);
344
410
  return { acked: true };
@@ -475,7 +541,8 @@ export class PgMailboxStore {
475
541
  throw tombstonedRecipientError(scope, handle);
476
542
  if (rec.id !== undefined) {
477
543
  const { rows: held } = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("pg", "hop_chain")}, ${jsonColumnText("pg", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND record_id = $3 FOR UPDATE`, [scope, handle, rec.id]);
478
- const replay = replayedSeq(held, scope, handle, this.logger, { ...rec, content });
544
+ const { rows: consumed } = await c.query(`SELECT seq, record_id, digests FROM ${MAILBOX_CONSUMED_TABLE} WHERE scope_key = $1 AND handle = $2 AND record_id = $3 FOR UPDATE`, [scope, handle, rec.id]);
545
+ const replay = replayedSeq(held, consumed, scope, handle, this.logger, { ...rec, content });
479
546
  if (replay !== undefined)
480
547
  return replay;
481
548
  }
@@ -511,7 +578,9 @@ export class PgMailboxStore {
511
578
  const r = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]));
512
579
  if (r.rows.length === 0 || r.rows[0].lease_owner == null || bufToStr(r.rows[0].lease_owner) !== owner)
513
580
  return { acked: false };
581
+ const { rows: removed } = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("pg", "hop_chain")}, ${jsonColumnText("pg", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq <= $3 AND record_id IS NOT NULL ORDER BY seq FOR UPDATE`, [scope, handle, upToSeq]);
514
582
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq <= $3`, [scope, handle, upToSeq]);
583
+ await enterConsumedWindow(async (sql, params) => (await c.query(sql.pg, params)).rows, rowsToMessages(removed, scope, handle, this.logger), scope, handle);
515
584
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
516
585
  WHERE scope_key = $1 AND handle = $2 AND lease_max_seq IS NOT NULL AND lease_max_seq <= $3`, [scope, handle, upToSeq]);
517
586
  return { acked: true };
@@ -3,9 +3,16 @@ import { type IndexSpec } from "./ensure-index.js";
3
3
  /** Table names (single source). SAME names as the TiDB twin (the two dialects never share one DB). */
4
4
  export declare const PG_MEMORY_SYNC_TABLES: {
5
5
  readonly syncCursor: "agent_memory_engine_sync_cursor";
6
+ readonly syncPartition: "agent_memory_engine_sync_partition";
6
7
  readonly pushQueue: "agent_memory_engine_push_queue";
7
8
  readonly history: "agent_memory_engine_history";
8
9
  };
10
+ /**
11
+ * 一个层分区的化身的**唯一铸点**(两只方言孪生同读)。化身说的是「中心此刻答话的是这个分区的哪一份状态」:分区出生时铸一次、存进
12
+ * `agent_memory_engine_sync_partition`,此后每轮同步答它;库被清空重建 ⇒ 那一行随之没了,下一轮再出生 ⇒ 新值。两条契约句(引擎的):
13
+ * (i) 没有别的分区、别的状态带同一个值 —— 随机 UUID(122 位熵);(ii) 不带凭据 —— 客户端的引擎拒带凭据的化身(钉:十六进制与连字符,凭据判官判否)。
14
+ */
15
+ export declare function mintSyncPartitionIncarnation(): string;
9
16
  /** What the caller hands `enqueue` — the transport-neutral unit (§3 传输中立包:payload = the full
10
17
  * entry, opaque to this store; this queue never interprets it). */
11
18
  export interface PushQueueItem {
@@ -45,6 +52,11 @@ export interface CentralSyncCursor {
45
52
  /** The dialect-neutral contract both twins implement (anti-drift: one shared integration suite,
46
53
  * test/memory-sync-store-db-integration.test.ts, runs the SAME scenarios against BOTH engines). */
47
54
  export interface MemorySyncStore {
55
+ /**
56
+ * 层分区 `scope` 的化身:有 ⇒ 存着的那一只;没有 ⇒ 分区此刻出生(铸点 {@link mintSyncPartitionIncarnation},插入即定;并发的第一次只有一只落定,
57
+ * 两边读回同一只)。中心同步每轮在任何读写之前读它一次、写完之后再读一次(栅栏两端,`memory-sync.ts`)。
58
+ */
59
+ partitionIncarnation(scope: string): Promise<string>;
48
60
  getCursor(scope: string, peer: string): Promise<CentralSyncCursor | undefined>;
49
61
  /** Whole-row replace (round semantics — baseRevs is swapped as one map, never merged). */
50
62
  putCursor(cursor: CentralSyncCursor): Promise<void>;
@@ -67,6 +79,8 @@ export declare function ensurePgMemorySyncSchema(query: PgQueryFn): Promise<void
67
79
  * ([ref] 车①② 裁定,沿用;不新铸码)。两个孪生共用本函数(MySQL 腿 import 它 —— 一条折码规则一个写者)。
68
80
  */
69
81
  export declare function refusingMemorySyncRow<T>(read: () => T): T;
82
+ /** 读回的分区化身(两孪生同读):插入即定之后必有一行、值是非空串;否则响亮抛(不铸替身值 —— 一个读不出的化身没有安全的答法)。 */
83
+ export declare function partitionIncarnationOf(value: unknown, scope: string): string;
70
84
  /** Options for {@link PgMemorySyncStore}. */
71
85
  export interface PgMemorySyncStoreOptions {
72
86
  /** How long a `claimDue` lease holds a row invisible before a crashed worker's claim self-heals.
@@ -77,6 +91,7 @@ export declare class PgMemorySyncStore implements MemorySyncStore {
77
91
  private readonly query;
78
92
  private readonly claimLeaseMs;
79
93
  constructor(query: PgQueryFn, opts?: PgMemorySyncStoreOptions);
94
+ partitionIncarnation(scope: string): Promise<string>;
80
95
  getCursor(scope: string, peer: string): Promise<CentralSyncCursor | undefined>;
81
96
  putCursor(cursor: CentralSyncCursor): Promise<void>;
82
97
  enqueue(item: PushQueueItem): Promise<void>;
@@ -1,12 +1,17 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { ensureIndexes } from "./ensure-index.js";
2
3
  import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
3
4
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
4
5
  import { decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, readStringRecordColumn, } from "./sql-json-column.js";
5
6
  export const PG_MEMORY_SYNC_TABLES = {
6
7
  syncCursor: "agent_memory_engine_sync_cursor",
8
+ syncPartition: "agent_memory_engine_sync_partition",
7
9
  pushQueue: "agent_memory_engine_push_queue",
8
10
  history: "agent_memory_engine_history",
9
11
  };
12
+ export function mintSyncPartitionIncarnation() {
13
+ return randomUUID();
14
+ }
10
15
  export const MEMORY_SYNC_INDEXES = [
11
16
  { table: `${PG_MEMORY_SYNC_TABLES.pushQueue}`, name: `idx_${PG_MEMORY_SYNC_TABLES.pushQueue}_next_attempt_ms`, columns: ["next_attempt_at_ms"] },
12
17
  ];
@@ -17,6 +22,11 @@ export async function ensurePgMemorySyncSchema(query) {
17
22
  base_revs jsonb NOT NULL,
18
23
  updated_at_ms bigint NOT NULL,
19
24
  PRIMARY KEY (scope, peer)
25
+ )`);
26
+ await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.syncPartition} (
27
+ scope varchar(190) COLLATE "C" NOT NULL,
28
+ incarnation varchar(64) COLLATE "C" NOT NULL,
29
+ PRIMARY KEY (scope)
20
30
  )`);
21
31
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.pushQueue} (
22
32
  id char(36) COLLATE "C" PRIMARY KEY,
@@ -37,6 +47,11 @@ export function refusingMemorySyncRow(read) {
37
47
  throw foldJsonColumn(e, (m) => `memory-sync store: ${m} — refusing rather than serve a sync row nobody can read`);
38
48
  }
39
49
  }
50
+ export function partitionIncarnationOf(value, scope) {
51
+ if (typeof value === "string" && value.length > 0)
52
+ return value;
53
+ throw new Error(`memory-sync store: ${PG_MEMORY_SYNC_TABLES.syncPartition}.incarnation for scope ${scope} is ${value === undefined ? "missing after insert" : "not a non-empty string"} — refusing rather than answer a sync under an unknown state`);
54
+ }
40
55
  function toMs(v) {
41
56
  return typeof v === "number" ? v : Number(v);
42
57
  }
@@ -51,6 +66,12 @@ export class PgMemorySyncStore {
51
66
  }
52
67
  this.claimLeaseMs = lease;
53
68
  }
69
+ async partitionIncarnation(scope) {
70
+ assertMemoryScopeWidth(scope);
71
+ await this.query(`INSERT INTO ${PG_MEMORY_SYNC_TABLES.syncPartition} (scope, incarnation) VALUES ($1, $2) ON CONFLICT (scope) DO NOTHING`, [scope, mintSyncPartitionIncarnation()]);
72
+ const res = await this.query(`SELECT incarnation FROM ${PG_MEMORY_SYNC_TABLES.syncPartition} WHERE scope = $1`, [scope]);
73
+ return partitionIncarnationOf(res.rows[0]?.incarnation, scope);
74
+ }
54
75
  async getCursor(scope, peer) {
55
76
  const res = await this.query(`SELECT ${jsonColumnText("pg", "base_revs")}, updated_at_ms FROM ${PG_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = $1 AND peer = $2`, [scope, peer]);
56
77
  const row = res.rows[0];
@@ -5,6 +5,7 @@ import type { Pool } from "mysql2/promise";
5
5
  * one database). */
6
6
  export declare const TIDB_MEMORY_SYNC_TABLES: {
7
7
  readonly syncCursor: "agent_memory_engine_sync_cursor";
8
+ readonly syncPartition: "agent_memory_engine_sync_partition";
8
9
  readonly pushQueue: "agent_memory_engine_push_queue";
9
10
  readonly history: "agent_memory_engine_history";
10
11
  };
@@ -21,6 +22,7 @@ export declare class TiDBMemorySyncStore implements MemorySyncStore {
21
22
  private readonly pool;
22
23
  private readonly claimLeaseMs;
23
24
  constructor(pool: Pool, opts?: TiDBMemorySyncStoreOptions);
25
+ partitionIncarnation(scope: string): Promise<string>;
24
26
  getCursor(scope: string, peer: string): Promise<CentralSyncCursor | undefined>;
25
27
  putCursor(cursor: CentralSyncCursor): Promise<void>;
26
28
  enqueue(item: PushQueueItem): Promise<void>;
@@ -1,10 +1,11 @@
1
- import { historyRowFrom, refusingMemorySyncRow } from "./memory-sync-store-pg.js";
1
+ import { historyRowFrom, mintSyncPartitionIncarnation, partitionIncarnationOf, refusingMemorySyncRow } from "./memory-sync-store-pg.js";
2
2
  import { decodeJsonColumn, jsonColumnText, readOpaqueJsonValueColumn, readStringRecordColumn } from "./sql-json-column.js";
3
3
  import { ensureIndexes } from "./ensure-index.js";
4
4
  import { MEMORY_SYNC_INDEXES, MEMORY_HISTORY_INDEXES, PG_MEMORY_SYNC_TABLES } from "./memory-sync-store-pg.js";
5
5
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
6
6
  export const TIDB_MEMORY_SYNC_TABLES = {
7
7
  syncCursor: "agent_memory_engine_sync_cursor",
8
+ syncPartition: "agent_memory_engine_sync_partition",
8
9
  pushQueue: "agent_memory_engine_push_queue",
9
10
  history: "agent_memory_engine_history",
10
11
  };
@@ -17,6 +18,11 @@ export async function ensureTiDBMemorySyncSchema(pool) {
17
18
  base_revs JSON NOT NULL,
18
19
  updated_at_ms BIGINT NOT NULL,
19
20
  PRIMARY KEY (scope, peer)
21
+ ) COLLATE utf8mb4_bin`);
22
+ await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.syncPartition} (
23
+ scope VARCHAR(190) NOT NULL,
24
+ incarnation VARCHAR(64) NOT NULL,
25
+ PRIMARY KEY (scope)
20
26
  ) COLLATE utf8mb4_bin`);
21
27
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.pushQueue} (
22
28
  id CHAR(36) NOT NULL,
@@ -45,6 +51,12 @@ export class TiDBMemorySyncStore {
45
51
  }
46
52
  this.claimLeaseMs = lease;
47
53
  }
54
+ async partitionIncarnation(scope) {
55
+ assertMemoryScopeWidth(scope);
56
+ await this.pool.query(`INSERT INTO ${TIDB_MEMORY_SYNC_TABLES.syncPartition} (scope, incarnation) VALUES (?, ?) ON DUPLICATE KEY UPDATE scope = scope`, [scope, mintSyncPartitionIncarnation()]);
57
+ const [rows] = await this.pool.query(`SELECT incarnation FROM ${TIDB_MEMORY_SYNC_TABLES.syncPartition} WHERE scope = ?`, [scope]);
58
+ return partitionIncarnationOf(rows[0]?.incarnation, scope);
59
+ }
48
60
  async getCursor(scope, peer) {
49
61
  const [rows] = await this.pool.query(`SELECT ${jsonColumnText("mysql", "base_revs")}, updated_at_ms FROM ${TIDB_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = ? AND peer = ?`, [scope, peer]);
50
62
  const row = rows[0];
@@ -338,6 +338,25 @@
338
338
  * 起服迁移的 local 腿(`FileApprovalAskStore.upgradeJournalEnvelope`)一次压实把整本重写成 v2。
339
339
  * **回滚**:7.141.x 对改名后的表每一条读写都响亮失败、对 v2 日志拒启 —— 回滚的唯一支持形 = 恢复升级前的备份,或手工把列改回(MIGRATION 写了语句)。
340
340
  * 读数:`askCauseColumnsChanged`(0–2 处表形改动)/ `askParkTriggersMoved`(SQL;local 恒 0)/ `askJournalRecordsUpgraded`(local;SQL 恒 0)。
341
+ * · **M35 配置根上 env 模型表的决定(settings-schema SS-M11)**(7.143.0)—— 不在店里(与 M16 同为本地配置根上的起服迁移):本版起本地配置腿上,一份管模型面的
342
+ * `config.d/models.json` 在 env 模型表已声明时,只有配置根上的决定文档 `config.env-models.json` 读作 `replaced` 才被采纳(否则模型面组拒
343
+ * `config.catalog_replaces_env_models`,`env-model-table-gate.ts`);升级前这种目录静默顶替 env 表。本条保住升级前的读法:起服、本地配置第一次读之前
344
+ * (`config-root-migration.ts`,两个入口同调),调契约的 `migrateEnvModelsDecision` —— 先读决定与目录,再独占创建运行记录 `config.migration.SS-M11.json`
345
+ * (建成的进程是本根唯一的铸造者),决定缺席且目录管模型面才铸 `replaced`;运行记录在场之后决定缺席 ≡ `kept`。没有读数键(它不写库)。
346
+ *
347
+ * · **M36 引擎 7.61.0 之前导入的、在每个项目根放行的全局 allow 退役**(7.143.0,引擎 7.61.0)—— 更早的导入门把用户层里从 `/` 写起的路径规则存成全局行:全局行在每个任务的
348
+ * 项目根读 `/x`,用户层在它自己的配置目录读,同一串字落店之后放行的是另一个地方。引擎不迁、不在读侧丢,只给一句判据(INTEGRATION-CORE §5.14):行是 allow ∧ add 的出处是
349
+ * `imported-cc` ∧ scope 是 global ∧ `ruleBasesNeeded` 点名 `root` ⇒ 这只 add 退役;按 add、不按行(每只选中的行一只 observed-remove 墓碑,`removedDots` 恰是选中的 add,
350
+ * 人亲手同意过的 `user` add 留着);deny / ask 不退役。判据、读哪些桶、写在哪里与失败方向的唯一属主是 `imported-layer-root-retirement.ts`(头注逐条)—— 两腿同调一只函数:
351
+ * SQL 在本段握锁的那一条连接上(`SqlDurableRulePartitionProvider` 收这条连接,墓碑经桶的写面 = 店内唯一写法 `writeBucket` 落),排在 M11 之后;local 在
352
+ * `LocalBackend.runOneTimeMigrations` 里经引擎 File 分区的写面落(规则目录不在 ⇒ 没有行、零读,不为它建目录)。读的桶:SQL = 规则表里的每一只;File = 导入记录点名过的属主(引擎的 File 分区后端没有列桶口;缺口见那份头注)。
353
+ * 每次起服都跑(窗内;更早的引擎回滚上来会重新铸出这种 add),第二跑起零选零写。读数两键:退役了的行(一行一条 warn,不带规则正文)与没能读 / 写的桶(一桶一条 warn,
354
+ * 每次起服都报)。删码另有一个前提:不再支持回滚到引擎 7.61.0 之前(登记表 M36 的 removal)。
355
+ *
356
+ * · **M37 存卡信封上委派出处的旧键(只有读侧)**(7.143.0,引擎 7.61.0)—— 引擎把审批请求的发起会话键 `sourceTaskId` 改名 `sourceSessionId`(值不变),本服务的卡随改、零别名;
357
+ * 本版之前铸的卡信封(`approval_ask.card_json` / local 问账)上那一格还是旧名,`.strict()` 的卡 schema 只认新名。存卡的唯一读者入口 `readStoredCardEnvelope`
358
+ * 在 M18 的读侧改写与 schema 之前把它改名(`approval-card.ts` 的 `renameRetiredSourceKey`,交浅拷贝、零写回,响亮计数)。不起服改写:存卡的寿命以分钟计、
359
+ * 升级前结清在途审批是部署前置(MIGRATION 本版第 17 条),读侧接住的是滚动升级窗里旧副本新写的与升级前已结仍被读的那几行。没有读数键。
341
360
  *
342
361
  * ## 收据账(一次起服一只,每一条迁移同一只)
343
362
  * 每一步把它**已提交**的改写 / 删除与点名读数当场记进同一只收据账({@link MigrationReceipts}):计数随每次提交累加、行随每次提交追加、形读数在那一步落定时写。走完 ⇒
@@ -357,9 +376,10 @@ import type { Pool as PgPool } from "pg";
357
376
  import { type AllowEntryInertReason, type GateKind } from "@sema-agent/core";
358
377
  import { type SqlDialect } from "./sql-driver.js";
359
378
  import { StoreBootRefusalError } from "./store-contracts.js";
360
- import { type UndurableScopeRuleRow, type UnreadableRuleBucket } from "./permission-rule-store-sql.js";
379
+ import { listRuleBucketOwners, type UndurableScopeRuleRow, type UnreadableRuleBucket } from "./permission-rule-store-sql.js";
361
380
  import type { SchemaProbeRead } from "./schema-live-columns.js";
362
381
  import type { ExecutionLane } from "../execution-lane-caps.js";
382
+ import { type ImportedLayerRootAllowRetired, type ImportedLayerRootBucketSkipped, type ImportedLayerRootRetirementSource } from "./imported-layer-root-retirement.js";
363
383
  import { type LocalPlacementBackfillSeat, type UnusableArchiveWord } from "../execution-placement.js";
364
384
  /** 迁移读数(启动日志一行写全;测试按它断言,不靠日志文本)。 */
365
385
  export interface OneTimeMigrationReport {
@@ -462,6 +482,11 @@ export interface OneTimeMigrationReport {
462
482
  readonly askParkTriggersMoved: number;
463
483
  /** M34 local 腿:问账日志里信封号 1 的记录数(窗内读侧已按意义译读;本次一次压实把整本重写成 v2)。非零 = 真重写了日志。local 独有;SQL 恒 0。 */
464
484
  readonly askJournalRecordsUpgraded: number;
485
+ /** M36:本次退役了引擎 7.61.0 之前导入的全局 `/x` allow add 的行(一行一条:桶键 / 工具名 / 退役的 add 数 / 这一行是否还活着;不带规则正文),启动日志逐行 warn。
486
+ * SQL 与 local 都有(同一只函数,见 `imported-layer-root-retirement.ts`)。非空 = 真写了墓碑;第二跑起恒空。 */
487
+ readonly importedLayerRootAllowsRetired: readonly ImportedLayerRootAllowRetired[];
488
+ /** M36:这次没能读 / 没能写的桶(闭集四词:读不出 / 写它的那一步里桶被并发删掉 / 没有写面或写面拒 / 并发争不过),一桶一条 warn;后两种那几行照旧放行,每次起服都点名直到处置。 */
489
+ readonly importedLayerRootBucketsSkipped: readonly ImportedLayerRootBucketSkipped[];
465
490
  }
466
491
  /** 退役的两只命令桶(引擎 7.51.0 起不是桶;只给 M17 点名认)。 */
467
492
  export type RetiredSessionRuleBucket = "commandAllow" | "commandDeny";
@@ -601,7 +626,7 @@ export declare function migrationWindowOpen(window: Pick<OneTimeMigrationWindow,
601
626
  */
602
627
  export declare function migrationWindowEnd(id: string): string;
603
628
  /** local 车道上 SQL 独有的几条(M8 / M9 / M11 / M17 / M18 / M21 / M23 / M24 / M26 / M27 / M29)的读数(M6 与 M32 的回填两车道都有、M33 只在 local,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
604
- export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "holderDeathCodeRowsBackfilled" | "executionPlacementsBackfilled" | "askExecutionLanesBackfilled" | "executionPlacementSessionsUnreadable" | "localArchiveRowFactsBackfilled" | "localArchivesAnonymized" | "localArchivesKeptStamped" | "localArchivesUnreadable" | "localArchiveSnapshotsRecovered" | "askJournalRecordsUpgraded">;
629
+ export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "holderDeathCodeRowsBackfilled" | "executionPlacementsBackfilled" | "askExecutionLanesBackfilled" | "executionPlacementSessionsUnreadable" | "localArchiveRowFactsBackfilled" | "localArchivesAnonymized" | "localArchivesKeptStamped" | "localArchivesUnreadable" | "localArchiveSnapshotsRecovered" | "askJournalRecordsUpgraded" | "importedLayerRootAllowsRetired" | "importedLayerRootBucketsSkipped">;
605
630
  /** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
606
631
  export interface MigrationLogSink {
607
632
  warn(msg: string, meta?: Record<string, unknown>): void;
@@ -634,6 +659,11 @@ export interface MigrationDb {
634
659
  * 本函数**不问**退役 schema:带着它的库在更早处就被 `ensureSchema()` 自身拒掉了(起服对账一族 `schema-consistency.ts` 的探针口,先于建表);走公开序列
635
660
  * `createStoreBackend()` → `ensureSchema()` → `runOneTimeMigrations()` 的嵌入方与本服务的起服序列一样,有这一问、零动作。
636
661
  */
662
+ /**
663
+ * M36 的 SQL 腿交给退役步的那一只源(读哪些桶 = 规则表里的每一只 · 桶的 durable 分区后端 · 店的判官):起服序列(握锁的那一条连接)与真库钉同一只构造,
664
+ * 判官不在两处各写一份。
665
+ */
666
+ export declare function createSqlImportedLayerRootSource(exec: Parameters<typeof listRuleBucketOwners>[0]): ImportedLayerRootRetirementSource;
637
667
  export declare function runSqlOneTimeMigrations(dialect: SqlDialect, pool: MysqlPool | PgPool, input: ExecutionPlacementSwitchInput): Promise<OneTimeMigrationReport>;
638
668
  /**
639
669
  * M34 —— 问账行成因两列(判据与形见头注 M34 段)。三步,每一步先读形再判,DDL 按后置条件收敛({@link execUntil}),中断 ⇒ 下一次起服从首个未满足的后置条件续:
@@ -9,7 +9,7 @@ import { ensureIndexes } from "./ensure-index.js";
9
9
  import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
10
10
  import { incarnationFromCell, sessionFaceFromRow } from "../session-incarnation.js";
11
11
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
12
- import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
12
+ import { PERMISSION_RULE_TABLE, SqlDurableRulePartitionProvider, isSqlBucketRefusal, isSqlBucketVanished, listRuleBucketOwners, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
13
13
  import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
14
14
  import { readJsonObjectColumn } from "./sql-json-column.js";
15
15
  import { retireStoredCardWords } from "../retired-card-words.js";
@@ -18,6 +18,7 @@ import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, isOrphanSessionOwner,
18
18
  import { MAILBOX_KEYED_TABLES, mailboxKeyDeleteSql, storedMailboxKeyless } from "./mailbox-store-sql.js";
19
19
  import { SESSION_META_PLACEMENT_COLUMNS, backfillWriterStatement, countOrphanBindingsStatement, migrateDeviceBindingsStatement } from "./execution-placement-sql.js";
20
20
  import { APPROVAL_ASK_ENDING_CAUSE_COLUMN, APPROVAL_ASK_EXECUTION_LANE_COLUMN, APPROVAL_ASK_PARK_TRIGGER_COLUMN, APPROVAL_ASK_TABLE } from "./approval-ask-store-sql.js";
21
+ import { retireImportedLayerRootAllows } from "./imported-layer-root-retirement.js";
21
22
  import { isUnusableArchive } from "../execution-placement.js";
22
23
  const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
23
24
  export function buildMigrationReceipts() {
@@ -63,6 +64,8 @@ export function buildMigrationReceipts() {
63
64
  askCauseColumnsChanged: 0,
64
65
  askParkTriggersMoved: 0,
65
66
  askJournalRecordsUpgraded: 0,
67
+ importedLayerRootAllowsRetired: [],
68
+ importedLayerRootBucketsSkipped: [],
66
69
  };
67
70
  }
68
71
  function completedReport(r) {
@@ -116,7 +119,9 @@ function changedAnything(r) {
116
119
  r.localArchiveSnapshotsRecovered > 0 ||
117
120
  r.askCauseColumnsChanged > 0 ||
118
121
  r.askParkTriggersMoved > 0 ||
119
- r.askJournalRecordsUpgraded > 0);
122
+ r.askJournalRecordsUpgraded > 0 ||
123
+ r.importedLayerRootAllowsRetired.length > 0 ||
124
+ r.importedLayerRootBucketsSkipped.length > 0);
120
125
  }
121
126
  function readingsSnapshot(r) {
122
127
  return {
@@ -131,6 +136,8 @@ function readingsSnapshot(r) {
131
136
  executionPlacementSessionsUnreadable: [...r.executionPlacementSessionsUnreadable],
132
137
  localArchivesKeptStamped: [...r.localArchivesKeptStamped],
133
138
  localArchivesUnreadable: [...r.localArchivesUnreadable],
139
+ importedLayerRootAllowsRetired: [...r.importedLayerRootAllowsRetired],
140
+ importedLayerRootBucketsSkipped: [...r.importedLayerRootBucketsSkipped],
134
141
  };
135
142
  }
136
143
  function receiptSentence(r) {
@@ -141,6 +148,7 @@ function receiptSentence(r) {
141
148
  ...r.undurableScopeRuleRows.map((x) => `removed ${x.kind} in permission-rule bucket ${x.ownerKey} (${x.behavior} ${x.tool}, scope root ${JSON.stringify(x.scopeRoot)})`),
142
149
  ...r.sessionRuleRowsWithOldGrammarEntries.map((x) => `${x.policyKey} (session ${x.sessionId}): toolAllowDropped=${JSON.stringify(x.toolAllowDropped)}`),
143
150
  ...r.sessionRuleRowsWithUnreadableDeny.map((x) => `${x.policyKey} (session ${x.sessionId}): left as stored, toolDenyUnreadable=${JSON.stringify(x.toolDenyUnreadable)}${x.retiredBuckets !== undefined ? `, retiredBuckets=${JSON.stringify(x.retiredBuckets)}` : ""}`),
151
+ ...r.importedLayerRootAllowsRetired.map((x) => `retired ${x.addsRetired} imported add(s) of an allow ${x.tool} rule in permission-rule bucket ${x.ownerKey} (${x.stillLive ? "the rule stays in effect through another approval" : "the rule no longer takes effect"})`),
144
152
  ];
145
153
  return [counts.join(", "), ...rows].filter((part) => part !== "").join("; ");
146
154
  }
@@ -583,6 +591,47 @@ export const ONE_TIME_MIGRATIONS = [
583
591
  "② local —— 信封号 1 的日志记录走 File 问账店「认不出的信封号」拒启(`unknown envelope version`,点名文件与出路:用写下它的那一版起服一次,或移开文件)",
584
592
  reportKeys: ["askCauseColumnsChanged", "askParkTriggersMoved", "askJournalRecordsUpgraded"],
585
593
  },
594
+ {
595
+ id: "M35",
596
+ introducedIn: "7.143.0",
597
+ releasedOn: "2026-10-31",
598
+ removableAfter: "2026-11-30",
599
+ removal: "删 `config-root-migration.ts` 整只文件与两个入口的调用(`boot/stage-02-web-admin-pull.ts` 目录信任判官之后、`run-local.ts` 同一位置各两行 + import);" +
600
+ "`test/env-model-table-gate.test.ts` 里标题以 `迁移` 起头的那一组;部署文档里「起服自动铸 config.env-models.json」那一句。" +
601
+ "**不随本条删**:模型面闸 `env-model-table-gate.ts`、`LocalConfigProvider` 读决定(etag 覆盖)、目录信任判官的根级文档名单(常设)",
602
+ afterRemoval: "常设的模型面闸接住:跳过了带本条的版本直接升的本地根(或运行记录在场而决定缺席的根),决定缺席读作 kept ⇒ env 模型表已声明时管模型面的本地目录被组拒 " +
603
+ "`config.catalog_replaces_env_models`,拒句点名被顶替的 env 模型与两条出路(在配置根写 `config.env-models.json` = `{\"envModels\":\"replaced\"}`,或撤掉 MODEL_ID / MODEL_CHEAP_ID)",
604
+ reportKeys: [],
605
+ },
606
+ {
607
+ id: "M36",
608
+ introducedIn: "7.143.0",
609
+ releasedOn: "2026-10-31",
610
+ removableAfter: "2026-11-30",
611
+ removal: "**删码前提**:本服务已不支持回滚到引擎 7.61.0 之前的版本(更早的引擎的导入门会重新铸出这种 add,本条要在每次起服把它们收掉)—— 窗关时仍支持这种回滚 ⇒ 本行两格顺延(改日期,并在本注里写明为什么),不删。" +
612
+ "删什么:`src/plugins/imported-layer-root-retirement.ts` 整个文件;migrateStepsOn 的 M36 步(规则表与 `grammar` 列的在场探针 + 对 `retireImportedLayerRootAllows` 的调用)与握锁连接上的 `query` 口" +
613
+ "(`LockedMigrationDb` 与 `mysqlDb` / `pgDb` 里的那一员,若届时再无别的步骤用它);`permission-rule-store-sql.ts` 的 `listRuleBucketOwners` / `isSqlBucketRefusal` / `isSqlBucketVanished` / `RuleBucketVanishedError`(取点那一句的具名类,届时若再无别的读者按类判它,收回成裸错)/ `IMPORT_RECORD_KIND`;" +
614
+ "`permission-rule-store-file.ts` 的 `FileRuleApprovalRecordStore.importRecordOwners`;`store-backend.ts` 的 `LocalBackend.runOneTimeMigrations` 里 M36 那一腿(规则目录在场判与那一处调用)与报告两键的那两行;" +
615
+ "本文件的 `createSqlImportedLayerRootSource` 与 `importedLayerRootAllowsRetired` / `importedLayerRootBucketsSkipped`(报告 / 收据 / `changedAnything` / `readingsSnapshot` / `readingFields` / `receiptSentence` / `NO_SQL_MIGRATIONS` 的排除表)、" +
616
+ "`imported_layer_root_allow_retired` / `imported_layer_root_bucket_skipped` 两行 warn 与 `importedLayerRootSkipNote`、读数行 note 里的 M36 句与头注 M36 段;" +
617
+ "`test/imported-layer-root-retirement.test.ts`、`test/imported-layer-root-retirement-db-integration.test.ts`(连同 `scripts/db-integration-files.json` 里它那一行)与 `test/helpers/imported-layer-root-fixture.ts` 整文件。" +
618
+ "**不随本条删**:`SqlDurableRulePartitionProvider` 收「方言 + 执行口」的那一形(`BucketSql`;常设,池照旧满足它)",
619
+ afterRemoval: "没有响亮臂(如实):删码之后,跳过了带本条的版本直接升的库(或回滚到引擎 7.61.0 之前、在其上又导入过的库),存量的这种全局 `/x` allow 照旧在每个项目根放行,本服务不再点名 —— " +
620
+ "出路 = 升级时先过一次带本条的版本并起服;或运维列出桶里出处为导入(`adds[].origin` = `imported-cc`)、scope 为全局、从 `/` 写起的 allow(`GET /v1/rules`)并撤掉(`DELETE /v1/rules`,撤的是这条规则的全部批准)",
621
+ reportKeys: ["importedLayerRootAllowsRetired", "importedLayerRootBucketsSkipped"],
622
+ },
623
+ {
624
+ id: "M37",
625
+ introducedIn: "7.143.0",
626
+ releasedOn: "2026-10-31",
627
+ removableAfter: "2026-11-30",
628
+ removal: "删 `approval-card.ts` 的 `renameRetiredSourceKey` 与 `readStoredCardEnvelope` 里调它的那一步(回到直接把传入的那一只交 M18 改写)、" +
629
+ "fail-open tag `server.approval-card.retired-card-key-read`(`observability/fail-open.ts` 与 `docs/FAIL-OPEN-CENSUS.md` 那一行)、" +
630
+ "`test/approval-card-source-key-m37.test.ts` 整只文件、本文件头注 M37 段",
631
+ afterRemoval: "既有坏行臂接住(响亮,不再改名):仍带旧键的存卡(跳过了带本条的版本直接升的库、窗关之后还在写的 7.142.x 副本)整卡读不出 —— 断连重放跳过 + 一次 warn、" +
632
+ "幂等重入走 park、回决口按要人",
633
+ reportKeys: [],
634
+ },
586
635
  ];
587
636
  export function migrationWindowOpen(window, today) {
588
637
  return window.removableAfter === null || today <= window.removableAfter;
@@ -633,6 +682,10 @@ export const NO_SQL_MIGRATIONS = {
633
682
  };
634
683
  function mysqlDb(conn) {
635
684
  return {
685
+ async query(sql, params) {
686
+ const [r] = await conn.query(sql, params === undefined ? [] : [...params]);
687
+ return Array.isArray(r) ? { rows: r, affected: 0 } : { rows: [], affected: Number(r.affectedRows ?? 0) };
688
+ },
636
689
  async rows(sql, params) {
637
690
  const [rows] = params === undefined ? await conn.query(sql) : await conn.query(sql, [...params]);
638
691
  return rows;
@@ -645,6 +698,10 @@ function mysqlDb(conn) {
645
698
  }
646
699
  function pgDb(client) {
647
700
  return {
701
+ async query(sql, params) {
702
+ const r = await client.query(sql, params === undefined ? [] : [...params]);
703
+ return { rows: r.rows, affected: r.rowCount ?? 0 };
704
+ },
648
705
  async rows(sql, params) {
649
706
  const r = params === undefined ? await client.query(sql) : await client.query(sql, [...params]);
650
707
  return r.rows;
@@ -675,6 +732,9 @@ async function columnExists(db, dialect, table, column) {
675
732
  const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
676
733
  return (await db.rows(sql)).length > 0;
677
734
  }
735
+ export function createSqlImportedLayerRootSource(exec) {
736
+ return { candidateOwners: () => listRuleBucketOwners(exec), durable: new SqlDurableRulePartitionProvider(exec), isWholeBucketRefusal: isSqlBucketRefusal, isVanishedBucket: isSqlBucketVanished };
737
+ }
678
738
  export async function runSqlOneTimeMigrations(dialect, pool, input) {
679
739
  return dialect === "mysql" ? withMysqlLock(pool, (db) => migrateOn(db, "mysql", input)) : withPgLock(pool, (db) => migrateOn(db, "pg", input));
680
740
  }
@@ -734,6 +794,9 @@ async function migrateStepsOn(db, dialect, receipts, input) {
734
794
  await inheritPlacedSessionOwners(db, dialect, receipts);
735
795
  if ((await tableExists(db, dialect, PERMISSION_RULE_TABLE)) && (await columnExists(db, dialect, PERMISSION_RULE_TABLE, "grammar")))
736
796
  await purgeUndurableScopeRows(db, dialect, receipts);
797
+ if ((await tableExists(db, dialect, PERMISSION_RULE_TABLE)) && (await columnExists(db, dialect, PERMISSION_RULE_TABLE, "grammar"))) {
798
+ await retireImportedLayerRootAllows(createSqlImportedLayerRootSource({ dialect, query: (sql, params) => db.query(sql, params) }), receipts);
799
+ }
737
800
  await moveWorkflowJournalScope(db, dialect, receipts);
738
801
  if (await tableExists(db, dialect, RETIRED_BREAKER_TABLE))
739
802
  await dropRetiredBreakerTable(db, dialect, receipts);
@@ -1687,6 +1750,8 @@ function readingFields(r) {
1687
1750
  askCauseColumnsChanged: r.askCauseColumnsChanged,
1688
1751
  askParkTriggersMoved: r.askParkTriggersMoved,
1689
1752
  askJournalRecordsUpgraded: r.askJournalRecordsUpgraded,
1753
+ importedLayerRootAllowsRetired: r.importedLayerRootAllowsRetired.length,
1754
+ importedLayerRootBucketsSkipped: r.importedLayerRootBucketsSkipped.length,
1690
1755
  };
1691
1756
  }
1692
1757
  export function logOneTimeMigrations(log, backend, report) {
@@ -1744,7 +1809,11 @@ export function logOneTimeMigrations(log, backend, report) {
1744
1809
  "M34: the approval ask table's single cause column denied_reason — the reason a row was moved to the delivery surface while it is PARKING or PARKED, " +
1745
1810
  "the reason it ended without a human answer while it is VOID or DENIED, overwritten by the second when a parked row was closed — renamed ending_cause, " +
1746
1811
  "a column park_trigger added, and the word on PARKING and PARKED rows moved into park_trigger; on the local backend the approval ask journal is rewritten " +
1747
- "once with record envelope 2, its envelope-1 records read with the same meaning). Each step " +
1812
+ "once with record envelope 2, its envelope-1 records read with the same meaning; " +
1813
+ "M36: allow approvals that an import before engine 7.61.0 stored as global rules from a settings file's user-layer path written from `/` — which the user layer reads at its " +
1814
+ "configuration directory and a global rule reads at every task's project root — retired by tombstones naming exactly those approvals (a rule that also holds an approval a person " +
1815
+ "gave directly stays in effect; deny and ask rules are never retired), one imported_layer_root_allow_retired line per rule; a bucket that could not be read or written, or that another replica deleted while it was being written, is named by an " +
1816
+ "imported_layer_root_bucket_skipped line on every boot). Each step " +
1748
1817
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
1749
1818
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
1750
1819
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -1847,6 +1916,23 @@ function logReceiptRows(log, backend, r) {
1847
1916
  note: removedRuleRowNote(row.kind),
1848
1917
  });
1849
1918
  }
1919
+ for (const row of r.importedLayerRootAllowsRetired) {
1920
+ log.warn("imported_layer_root_allow_retired", {
1921
+ backend,
1922
+ ownerKey: row.ownerKey,
1923
+ tool: row.tool,
1924
+ addsRetired: row.addsRetired,
1925
+ stillLive: row.stillLive,
1926
+ note: "an allow rule imported from a settings file's user layer by an import before engine 7.61.0 was written from `/` — the user layer reads such a path at its own configuration " +
1927
+ "directory — and was stored as a global rule, which reads it at every task's project root: it let that path be acted on in every project without a question. A one-time migration " +
1928
+ "retired the imported approval of it (a tombstone naming exactly that approval). stillLive false = the rule no longer takes effect and an action it covered asks again; stillLive true = " +
1929
+ "the rule stays in effect through another approval of it (one a person gave directly). To carry the rule again, spell its directory out (`//<directory>/…`, or `~/<path>` under the home) " +
1930
+ "and approve it again. This line is written once, by the boot that retired it — keep it; the rule text is not on it (list the bucket's rules to see what remains)",
1931
+ });
1932
+ }
1933
+ for (const bucket of r.importedLayerRootBucketsSkipped) {
1934
+ log.warn("imported_layer_root_bucket_skipped", { backend, ownerKey: bucket.ownerKey, reason: bucket.reason, why: bucket.why, note: importedLayerRootSkipNote(bucket.reason) });
1935
+ }
1850
1936
  for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
1851
1937
  log.warn("permission_rule_bucket_unreadable", {
1852
1938
  backend,
@@ -1925,6 +2011,23 @@ export async function runAndLogOneTimeMigrations(log, backend) {
1925
2011
  }
1926
2012
  logOneTimeMigrations(log, backend.kind, report);
1927
2013
  }
2014
+ function importedLayerRootSkipNote(reason) {
2015
+ const head = "the one-time retirement of allow approvals that an import before engine 7.61.0 stored as global rules from a user-layer path written from `/` (read at every project root) did not finish in this permission-rule bucket: ";
2016
+ switch (reason) {
2017
+ case "bucket_unreadable":
2018
+ return `${head}the bucket could not be read. The read path refuses such a bucket in whole as well, so none of its rules takes effect; once it is repaired, the next boot retires what it selects. Named on every boot until then`;
2019
+ case "vanished":
2020
+ return `${head}the bucket was deleted (by another replica) while this step was writing to it. Nothing is left to retire and none of its rules takes effect; not a boot refusal`;
2021
+ case "not_writable":
2022
+ return `${head}it holds approvals the retirement selects, but no tombstone could be written (the bucket is served read-only — a rule-store adoption in progress — or the store refused the tombstone), so those allow rules keep taking effect at every project root. Finish the adoption or repair the store and restart; named on every boot until then`;
2023
+ case "contended":
2024
+ return `${head}every write lost to a concurrent writer. Those allow rules keep taking effect until a later boot retires them; named on every boot until then`;
2025
+ default: {
2026
+ const unknown = reason;
2027
+ throw new Error(`M36: no log sentence for skip reason ${JSON.stringify(unknown)}`);
2028
+ }
2029
+ }
2030
+ }
1928
2031
  function removedRuleRowNote(kind) {
1929
2032
  switch (kind) {
1930
2033
  case "rule":
@@ -101,7 +101,7 @@ export declare class SqlOrgDeliveryQuotaStore implements OrgDeliveryQuotaStore {
101
101
  take(fromScope: string, toScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
102
102
  /**
103
103
  * 一个记账事务:库钟先读(池上一条值语句,不在事务里留普通 SELECT),判过 ⇒ 提交,判拒 ⇒ 回滚(零残留)。
104
- * 🔴 **引擎判了死锁 ⇒ 整事务重来**(有界,`isDeadlockError` 是全仓唯一的可重试锁故障判据):被挑中的一方已整事务回滚、锁全放,
104
+ * 🔴 **引擎判了死锁 ⇒ 整事务重来**(`retryOnDeadlock`:全仓唯一的重来写法、唯一的上界与判据):被挑中的一方已整事务回滚、锁全放,
105
105
  * 重来时读-判-写全部重做,语义等价。真 InnoDB(REPEATABLE READ)上这条路是真的:窗前进时按收件人前缀删旧对偶行要取范围 / 间隙锁,
106
106
  * 两位相邻收件人同时翻窗、各自往两人之间的间隙里插新对偶行 ⇒ 互等(TiDB 无间隙锁、PG 读已提交不取间隙锁)。谁当牺牲者是引擎的任意选择,
107
107
  * 抛给调用方等于把一次内部仲裁当成投递失败。锁等待超时**不**重来(延迟信号,照抛)。