@sema-agent/server 7.131.0 → 7.132.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
@@ -1,39 +1,102 @@
1
- import { SESSION_RULES_GRAMMAR, compileSessionRuleBucket, readSessionRulesShape } from "@sema-agent/core";
1
+ import { PAUSE_REGISTRY, SESSION_RULES_GRAMMAR, compileSessionRuleBucket, isGateKind, readSessionRulesShape, unreadableDenyEntries } from "@sema-agent/core";
2
+ import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
3
+ import { dialectProtocolJsonEncoder } from "./sql-driver.js";
2
4
  import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
3
5
  import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
4
6
  import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
5
7
  import { ensureIndexes } from "./ensure-index.js";
6
- import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn } from "./sql-json-column.js";
8
+ import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
7
9
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
8
- import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
10
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
9
11
  import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
10
- import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
11
12
  import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
12
13
  import { readJsonObjectColumn } from "./sql-json-column.js";
13
14
  import { retireStoredCardWords } from "../retired-card-words.js";
14
15
  import { redactSecrets } from "../trace/redact.js";
15
16
  const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
16
- export function buildSessionRuleReceipts() {
17
- return { rows: [], renamed: false };
17
+ export function buildMigrationReceipts() {
18
+ return {
19
+ auditDetailJsonNullRows: 0,
20
+ sessionIncarnationsBackfilled: 0,
21
+ holderDeathCodeRowsBackfilled: 0,
22
+ allowRuleShadowRowsMigrated: 0,
23
+ allowRuleShadowRowsUnreadable: 0,
24
+ workflowParkIndexRowsCarried: 0,
25
+ undurableScopeRuleRows: [],
26
+ undurableScopeRuleBucketsDeferred: 0,
27
+ undurableScopeRuleBucketsUnreadable: [],
28
+ sessionRuleRowsWithRetiredBuckets: [],
29
+ sessionRuleRowsWithOldGrammarEntries: [],
30
+ sessionRuleRowsWithUnreadableDeny: [],
31
+ retiredCardWordAskRowsRewritten: 0,
32
+ retiredCardWordRowsUnreadable: 0,
33
+ parkedQuestionCardsWritten: 0,
34
+ parkedQuestionRowsLeftAsWritten: 0,
35
+ parkedRowGateKindsBackfilled: 0,
36
+ parkedRowsUnresumableExpired: 0,
37
+ };
38
+ }
39
+ function completedReport(r) {
40
+ const { sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn } = r;
41
+ if (sessionIncarnationColumn === undefined || workflowParkIndexKey === undefined || sessionRuleColumn === undefined) {
42
+ throw new Error(`one-time migrations finished without settling a shape reading (M5 ${String(sessionIncarnationColumn)} / M9 ${String(workflowParkIndexKey)} / M17 ${String(sessionRuleColumn)}) — a step did not record it`);
43
+ }
44
+ return { ...r, sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn };
45
+ }
46
+ function changedAnything(r) {
47
+ return (r.auditDetailJsonNullRows > 0 ||
48
+ r.sessionIncarnationColumn === "added" ||
49
+ r.sessionIncarnationsBackfilled > 0 ||
50
+ r.holderDeathCodeRowsBackfilled > 0 ||
51
+ r.allowRuleShadowRowsMigrated > 0 ||
52
+ r.allowRuleShadowRowsUnreadable > 0 ||
53
+ r.workflowParkIndexKey === "reshaped" ||
54
+ r.workflowParkIndexRowsCarried > 0 ||
55
+ r.undurableScopeRuleRows.length > 0 ||
56
+ r.undurableScopeRuleBucketsDeferred > 0 ||
57
+ r.undurableScopeRuleBucketsUnreadable.length > 0 ||
58
+ r.sessionRuleColumn === "renamed" ||
59
+ r.sessionRuleRowsWithRetiredBuckets.length > 0 ||
60
+ r.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
61
+ r.sessionRuleRowsWithUnreadableDeny.length > 0 ||
62
+ r.retiredCardWordAskRowsRewritten > 0 ||
63
+ r.retiredCardWordRowsUnreadable > 0 ||
64
+ r.parkedQuestionCardsWritten > 0 ||
65
+ r.parkedQuestionRowsLeftAsWritten > 0 ||
66
+ r.parkedRowGateKindsBackfilled > 0 ||
67
+ r.parkedRowsUnresumableExpired > 0);
68
+ }
69
+ function readingsSnapshot(r) {
70
+ return {
71
+ ...r,
72
+ undurableScopeRuleRows: [...r.undurableScopeRuleRows],
73
+ undurableScopeRuleBucketsUnreadable: [...r.undurableScopeRuleBucketsUnreadable],
74
+ sessionRuleRowsWithRetiredBuckets: [...r.sessionRuleRowsWithRetiredBuckets],
75
+ sessionRuleRowsWithOldGrammarEntries: [...r.sessionRuleRowsWithOldGrammarEntries],
76
+ sessionRuleRowsWithUnreadableDeny: [...r.sessionRuleRowsWithUnreadableDeny],
77
+ };
78
+ }
79
+ function receiptSentence(r) {
80
+ const counts = Object.entries(readingFields(r))
81
+ .filter(([, v]) => (typeof v === "number" ? v > 0 : v !== undefined))
82
+ .map(([k, v]) => `${k}=${String(v)}`);
83
+ const rows = [
84
+ ...r.undurableScopeRuleRows.map((x) => `removed ${x.kind} in permission-rule bucket ${x.ownerKey} (${x.behavior} ${x.tool}, scope root ${JSON.stringify(x.scopeRoot)})`),
85
+ ...r.sessionRuleRowsWithOldGrammarEntries.map((x) => `${x.policyKey} (session ${x.sessionId}): toolAllowDropped=${JSON.stringify(x.toolAllowDropped)}`),
86
+ ...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)}` : ""}`),
87
+ ];
88
+ return [counts.join(", "), ...rows].filter((part) => part !== "").join("; ");
18
89
  }
19
90
  export class OneTimeMigrationInterruptedError extends Error {
20
91
  receipts;
21
92
  constructor(cause, receipts) {
22
- super(`${cause instanceof Error ? cause.message : String(cause)} — one-time migration M17 had already committed changes whose receipts the next boot cannot re-derive ` +
23
- "(the rows below are rewritten, so a retry finds nothing left to remove from them) — record them before retrying: " +
24
- receipts.sessionRuleRowsWithOldGrammarEntries.map((r) => `${r.policyKey} (session ${r.sessionId}): toolAllowDropped=${JSON.stringify(r.toolAllowDropped)}`).join("; "), { cause });
93
+ super(`${cause instanceof Error ? cause.message : String(cause)} — the one-time migrations had already committed changes before this failure; a retried boot finds that work done ` +
94
+ "and cannot report it again — record these readings before retrying: " +
95
+ receiptSentence(receipts), { cause });
25
96
  this.name = "OneTimeMigrationInterruptedError";
26
97
  this.receipts = receipts;
27
98
  }
28
99
  }
29
- function interruptedWith(cause, receipts) {
30
- if (receipts.rows.length === 0)
31
- return cause;
32
- return new OneTimeMigrationInterruptedError(cause, {
33
- ...(receipts.renamed ? { sessionRuleColumn: "renamed" } : {}),
34
- sessionRuleRowsWithOldGrammarEntries: [...receipts.rows],
35
- });
36
- }
37
100
  export const ONE_TIME_MIGRATIONS = [
38
101
  {
39
102
  id: "M4",
@@ -132,7 +195,7 @@ export const ONE_TIME_MIGRATIONS = [
132
195
  releasedOn: "2026-10-08",
133
196
  removableAfter: "2026-11-07",
134
197
  removal: "删 migrateOn 的 M11 步(`permission_rule` 在场探针 + `purgeUndurableScopeRows` 调用)与 `permission-rule-store-sql.ts` 的 `purgeUndurableScopeRows` / " +
135
- "`purgeAllBuckets` / `purgeOneBucket` / `contentFreeRefusal` / `PurgeAccumulator` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurge` / " +
198
+ "`purgeOneBucket` / `contentFreeRefusal` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurgeReadings` / " +
136
199
  "`UndurableScopePurgeDb` / `UNDURABLE_PURGE_PAGE` / `UNDURABLE_PURGE_OCC_ATTEMPTS`、本文件的 `removedRuleRowNote`,以及 `BucketRefusedError` 的 `verdict` 位(若届时再无别的读者;" +
137
200
  "判官 `decodeBucketRow` 与写法 `writeBucket` 是读腿 / 写臂本身,留);报告、日志行(含逐行的 `permission_rule_row_removed` 与逐桶的 " +
138
201
  "`permission_rule_bucket_unreadable`)与 `NO_SQL_MIGRATIONS` 的 `undurableScopeRuleRows` / `undurableScopeRuleBucketsDeferred` / `undurableScopeRuleBucketsUnreadable`",
@@ -213,16 +276,15 @@ export const ONE_TIME_MIGRATIONS = [
213
276
  removal: "删 migrateOn 的 M17 步(`migrateSessionRuleGrammar`:`session_policy` 在场探针、旧名 → 进行中的名 → 判读 → 终名三段、退役命令桶扫描与点名)与 " +
214
277
  "`retiredSessionRuleBucketsOf` / `RetiredSessionRuleBucket` / `RETIRED_SESSION_RULE_BUCKETS` / `RETIRED_SESSION_RULES_COLUMN` / `OLD_GRAMMAR_SESSION_RULES_COLUMN` / `SESSION_RULES_SCAN_PAGE` / `SessionRuleRowRetiredBuckets` / " +
215
278
  "`oldGrammarEntriesOf` / `OldGrammarEntries` / `judgeOldGrammarRows` / `renameSessionRulesColumnSql` / `scanSessionRuleRows` / `SessionRuleScanRow` / " +
216
- "`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` / 收据账 `SessionRuleReceipts` / `buildSessionRuleReceipts` / `OneTimeMigrationReceipts` / " +
217
- "`OneTimeMigrationInterruptedError` / `interruptedWith`(`migrateOn` 外层的那一道 try / catch)/ `logOneTimeMigrationsInterrupted` / `runAndLogOneTimeMigrations` 里的失败日志臂 / `logSessionRuleOldGrammarRows`;" +
218
- "报告、日志行(含逐行的 `session_rule_row_retired_bucket` 与 `session_rule_row_old_grammar_entries`)与 `NO_SQL_MIGRATIONS` 的 `sessionRuleColumn` / " +
219
- "`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries`;" +
279
+ "`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` / `SessionRuleRowUnreadableDeny` / `UnreadableDenyEntry`;`logReceiptRows` 里 M17 的三段与 `receiptSentence` 里 M17 的两段;" +
280
+ "报告、日志行(含逐行的 `session_rule_row_retired_bucket` · `session_rule_row_old_grammar_entries` · `session_rule_row_unreadable_deny`)与 `NO_SQL_MIGRATIONS` 的 `sessionRuleColumn` / " +
281
+ "`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries` / `sessionRuleRowsWithUnreadableDeny`;" +
220
282
  "**列名本身留在中央 DDL 里**(`session-rules-column.ts`,它不是迁移的一部分)",
221
283
  afterRemoval: "① 列还叫 `rules`、或停在进行中的名 `rule_entries_old_grammar` 的存量库(跳过了带本条的版本直接升 / 判读没走完就跳版):起服列册探针 `schema-consistency.ts`(S-547)拒启 " +
222
284
  "`config.schema_mismatch`(缺 `rule_entries`、多那一列)—— " +
223
285
  "出路是先在一只带本条的版本上起服一次;② 行里仍带退役命令桶:SQL 店读回照旧答 `corrupt`(引擎的形读法点名那只桶,与本条在场时同一个结局),只是起服日志不再逐行点名 —— " +
224
286
  "运维在库里把那一行改写成引擎文法或删掉它(删会话即删规则);③ 跳过本条直接升的库同样没有改名那一拍的条目判读 —— 由 ① 的拒启接住(出路同 ①)",
225
- reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries"],
287
+ reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries", "sessionRuleRowsWithUnreadableDeny"],
226
288
  },
227
289
  {
228
290
  id: "M18",
@@ -246,16 +308,18 @@ export const ONE_TIME_MIGRATIONS = [
246
308
  introducedIn: "7.130.0",
247
309
  releasedOn: "2026-10-16",
248
310
  removableAfter: "2026-11-15",
249
- removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `StoredToolAxesRereadM19`);" +
250
- "`boot/resolve-spec.ts` 里递给 `resolveRequestMcp` 的那一实参(`opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)与它的 import;" +
311
+ removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `readA2aToolAxesAsServer7131M19` / `StoredToolAxesRereadM19`);" +
312
+ "`boot/resolve-spec.ts` 里两条请求腿共用的那一行(`const storedToolAxes = opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)、递给 `resolveRequestMcp` / `resolveRequestA2a` 的两处实参与它的 import;" +
251
313
  "`task-mcp.ts` 里 `resolveRequestMcp` / `classifyRequestMcp` / `classifyHonored` / `validateRequestMcpEntries` / `normalizeServer` 的 `storedToolAxes` 形参、" +
252
- "`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import;`tool-axis-words.ts` 头注与两只谓词 JSDoc 里点名 M19 的那几处;" +
314
+ "`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import(升级前读法的词表与两只谓词住在本模块里,随它一起删);" +
315
+ "`task-a2a.ts` 里 `resolveRequestA2a` / `validateRequestA2aEntries` / `normalizeA2aPeer` 的 `storedToolAxes` 形参、`toolAxesReread` 事实与运维行 `a2a_tool_axes_read_as_before_upgrade`、对本模块的型 import;" +
253
316
  "`test/mcp-tool-axes-m19.test.ts`(窗关那一格 —— 存量坏形整条不进、warn 与通告点名 `field: toolAxes` —— 改钉到 `test/task-mcp.test.ts`);" +
254
317
  "本文件的 `migrationWindowOpen`(若再无别的读者)",
255
318
  afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里引擎判官判不过的 `toolAxes`,续跑重放时这一条 server 整条不进," +
256
319
  "运维 warn `mcp_injection_dropped`(`reason: malformed_entry`、`field: toolAxes`)+ 用户面通告 `mcp.injection_dropped`,兄弟照挂;停驻在这台 server 工具审批上的任务" +
257
320
  "批准后撞 `resume.tool_unavailable`(拒绝照常可出)—— 出路是拒绝那张卡,用改好的 `toolAxes` 重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、" +
258
- "调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)",
321
+ "调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)。A2A 一面同:存量体里 `a2aPeers[].toolAxes` 判不过的对端续跑整条不挂," +
322
+ "运维 warn `a2a_injection_dropped` 点名,停驻在它 skill 上的审批批准后撞 `resume.tool_unavailable` —— 出路同上:拒绝那张卡,改好声明重新提交",
259
323
  reportKeys: [],
260
324
  },
261
325
  {
@@ -273,6 +337,33 @@ export const ONE_TIME_MIGRATIONS = [
273
337
  "出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
274
338
  reportKeys: [],
275
339
  },
340
+ {
341
+ id: "M21",
342
+ introducedIn: "7.132.0",
343
+ releasedOn: "2026-10-18",
344
+ removableAfter: "2026-11-17",
345
+ removal: "删 migrateOn 的 M21 步(`repairParkedRows`:pending 候选分页扫描、门型回填、续不了的置 expired、问答卡词的整只文档读 + CAS 回写)与 `parkedRowRepairOf` / `ParkedRowRepair` / " +
346
+ "`M21_GATE` / `M21_PENDING_ACTION` / `PARKED_ROW_REPAIR_PAGE` / `QUESTION_CARD` / `PARKED_APPROVAL_GATE_KINDS` / `readM21Subtree` / `readM21Document` / `m21Subtree`;" +
347
+ "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `parkedQuestionCardsWritten` / `parkedQuestionRowsLeftAsWritten` / `parkedRowGateKindsBackfilled` / `parkedRowsUnresumableExpired`;" +
348
+ "`test/one-time-migrations.test.ts` 的 M21 组与 `test/one-time-migrations-db-integration.test.ts` 的 M21 格",
349
+ afterRemoval: "① 门上没有卡词的存量问答行(跳过了带本条的版本直接升的库、把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、local 车道 —— 那一车道本条从来够不着):没有新的臂,回到本条之前的读法 —— " +
350
+ "队列行不带卡词(壳显示通用审批卡),到期走 SLA 拒扫腿(模型读到「拒」),决断口仍按工具名认问答(`approve` 必须带 `answer`,否则 400);出路 = 升级前答完或拒掉;" +
351
+ "② `gate_kind` 为 NULL 的停驻行:既有响亮臂 —— 运维待批队列(`listPending`)不列它、计 `server.approvals.queue-row-gate-kind-missing`(决断口按门型绑定取行,本来就不认它);" +
352
+ "出路是运维把那一行置 expired 或删掉会话(终局收割照旧在 `terminal_at_ms` 收它)",
353
+ reportKeys: ["parkedQuestionCardsWritten", "parkedQuestionRowsLeftAsWritten", "parkedRowGateKindsBackfilled", "parkedRowsUnresumableExpired"],
354
+ },
355
+ {
356
+ id: "M22",
357
+ introducedIn: "7.132.0",
358
+ releasedOn: "2026-10-18",
359
+ removableAfter: "2026-11-17",
360
+ removal: "删 `config-lkg.ts` 的 formatVersion 4 读回迁移:`migrateV4Lkg` 与其上的窗注;`loadLkg` 里调它的那一支(`formatVersion !== 5` 那一段)与版本判据里的 `4`" +
361
+ "(4 回到「unknown formatVersion」拒;若 M13 / M15 仍在窗内,先删到期的那几条再删本条,版本判据收成只认 5)、`LkgReadMigration.fromFormat` 的 `4`;" +
362
+ "文件头注 formatVersion 5 一段里「4 的载荷 …」那两句;`test/center-artifact-v3-lane.test.ts` 的两格 4 读回(5 原样读回那一格留)",
363
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 4 答「unknown formatVersion 4」):4 读回 ⇒ 不可用,中心可达照拉(整拉,新 LKG 按 5 覆盖)、断着则 env 兜底 —— " +
364
+ "与回滚到 7.131.x 读 5 同一种处置",
365
+ reportKeys: [],
366
+ },
276
367
  ];
277
368
  export function migrationWindowOpen(window, today) {
278
369
  return window.removableAfter === null || today <= window.removableAfter;
@@ -291,29 +382,16 @@ export const NO_SQL_MIGRATIONS = {
291
382
  sessionRuleColumn: "file-store",
292
383
  sessionRuleRowsWithRetiredBuckets: [],
293
384
  sessionRuleRowsWithOldGrammarEntries: [],
385
+ sessionRuleRowsWithUnreadableDeny: [],
294
386
  retiredCardWordAskRowsRewritten: 0,
295
387
  retiredCardWordRowsUnreadable: 0,
388
+ parkedQuestionCardsWritten: 0,
389
+ parkedQuestionRowsLeftAsWritten: 0,
390
+ parkedRowGateKindsBackfilled: 0,
391
+ parkedRowsUnresumableExpired: 0,
296
392
  };
297
393
  const AUDIT_TABLE = "device_audit";
298
394
  const AUDIT_DETAIL_COLUMN = "detail";
299
- const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
300
- const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
301
- export class RetiredSchemaPresentError extends StoreBootRefusalError {
302
- found;
303
- code = SCHEMA_MISMATCH_ERROR_CODE;
304
- constructor(found) {
305
- super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database still carries schema that the one-time migrations of server 7.74.0–7.125.x retired — refusing to start. ` +
306
- `Found: ${found.join("; ")}. ` +
307
- `Both come from builds that predate those migrations, so this database never booted a build that still carried them — which also means the third one never ran: ` +
308
- `ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
309
- `This build carries none of the three and will not guess. ` +
310
- `Way out: start a server 7.125.x build once against this database and let its one-time migrations finish (it drops the table and the column and rewrites the spelling; ` +
311
- `while the column still holds a steering instruction that can be delivered it refuses and names the rows and its own ways out), then upgrade to this build. ` +
312
- `The check behind this refusal is read-only and runs before this build creates any table or rewrites anything: it changed nothing in the database.`);
313
- this.found = found;
314
- this.name = "RetiredSchemaPresentError";
315
- }
316
- }
317
395
  function mysqlDb(conn) {
318
396
  return {
319
397
  async rows(sql, params) {
@@ -358,34 +436,6 @@ async function columnExists(db, dialect, table, column) {
358
436
  const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
359
437
  return (await db.rows(sql)).length > 0;
360
438
  }
361
- async function assertRetiredSchemaAbsent(db, dialect) {
362
- const found = [];
363
- if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
364
- found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
365
- if (await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN))
366
- found.push(`column checkpoint.${RETIRED_STEER_SEAT_COLUMN} (the retired single-seat steering column)`);
367
- if (found.length > 0)
368
- throw new RetiredSchemaPresentError(found);
369
- }
370
- export async function assertRetiredSchemaAbsentOnPool(target) {
371
- if (target.dialect === "tidb") {
372
- const conn = await target.pool.getConnection();
373
- try {
374
- await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
375
- }
376
- finally {
377
- conn.release();
378
- }
379
- return;
380
- }
381
- const client = await target.pool.connect();
382
- try {
383
- await assertRetiredSchemaAbsent(pgDb(client), "pg");
384
- }
385
- finally {
386
- client.release();
387
- }
388
- }
389
439
  export async function runSqlOneTimeMigrations(dialect, pool) {
390
440
  return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
391
441
  }
@@ -418,44 +468,33 @@ async function withPgLock(pool, run) {
418
468
  }
419
469
  }
420
470
  async function migrateOn(db, dialect) {
421
- const receipts = buildSessionRuleReceipts();
471
+ const receipts = buildMigrationReceipts();
422
472
  try {
423
- return await migrateStepsOn(db, dialect, receipts);
473
+ await migrateStepsOn(db, dialect, receipts);
424
474
  }
425
475
  catch (e) {
426
- throw interruptedWith(e, receipts);
476
+ throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
427
477
  }
478
+ return completedReport(receipts);
428
479
  }
429
480
  async function migrateStepsOn(db, dialect, receipts) {
430
- const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
431
- const auditDetailJsonNullRows = !auditTablePresent
432
- ? 0
433
- : await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
434
- `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
435
- const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
436
- const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
437
- ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
438
- : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR])) +
439
- (await db.exec(dialect === "tidb"
440
- ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
441
- : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
442
- const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
443
- const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
444
- const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
445
- const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
446
- let undurableScopeRuleRows = [];
447
- let undurableScopeRuleBucketsDeferred = 0;
448
- let undurableScopeRuleBucketsUnreadable = [];
449
- if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
450
- ({ removed: undurableScopeRuleRows, bucketsDeferred: undurableScopeRuleBucketsDeferred, bucketsUnreadable: undurableScopeRuleBucketsUnreadable } = await purgeUndurableScopeRows(db, dialect));
481
+ if (await tableExists(db, dialect, AUDIT_TABLE)) {
482
+ receipts.auditDetailJsonNullRows += await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` + `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
451
483
  }
452
- return {
453
- auditDetailJsonNullRows,
454
- sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
455
- workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
456
- sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries,
457
- retiredCardWordAskRowsRewritten, retiredCardWordRowsUnreadable,
458
- };
484
+ await migrateSessionIncarnations(db, dialect, receipts);
485
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
486
+ ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
487
+ : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR]);
488
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
489
+ ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
490
+ : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]);
491
+ await migrateShadowedRuleKey(db, dialect, receipts);
492
+ await migrateRetiredCardWords(db, dialect, receipts);
493
+ await reshapeWorkflowParkIndex(db, dialect, receipts);
494
+ await migrateSessionRuleGrammar(db, dialect, receipts);
495
+ await repairParkedRows(db, dialect, receipts);
496
+ if (await tableExists(db, dialect, PERMISSION_RULE_TABLE))
497
+ await purgeUndurableScopeRows(db, dialect, receipts);
459
498
  }
460
499
  const SESSION_POLICY_TABLE = "session_policy";
461
500
  const RETIRED_SESSION_RULES_COLUMN = "rules";
@@ -474,9 +513,10 @@ export function oldGrammarEntriesOf(body, rev) {
474
513
  const shaped = readSessionRulesShape({ ...buckets, rev });
475
514
  if ("fault" in shaped)
476
515
  return undefined;
516
+ const toolDenyUnreadable = unreadableDenyEntries(shaped.rules).map(({ entry, reason }) => ({ entry, reason }));
517
+ if (toolDenyUnreadable.length > 0)
518
+ return { toolAllowDropped: [], toolDenyUnreadable };
477
519
  const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
478
- if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
479
- return { toolAllowDropped: [], wildcardMasked: true };
480
520
  const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
481
521
  const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
482
522
  const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
@@ -519,8 +559,14 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
519
559
  : `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
520
560
  await scanSessionRuleRows(db, dialect, column, [], async (row) => {
521
561
  const read = oldGrammarEntriesOf(row.body, row.rev);
522
- if (read?.wildcardMasked) {
523
- receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: [], wildcardMasked: true });
562
+ if (read?.toolDenyUnreadable !== undefined) {
563
+ const retiredBuckets = retiredSessionRuleBucketsOf(row.body);
564
+ receipts.sessionRuleRowsWithUnreadableDeny.push({
565
+ policyKey: row.policyKey,
566
+ sessionId: row.sessionId,
567
+ toolDenyUnreadable: read.toolDenyUnreadable.map(({ entry, reason }) => ({ entry: redactSecrets(entry), reason })),
568
+ ...(retiredBuckets.length > 0 ? { retiredBuckets } : {}),
569
+ });
524
570
  return;
525
571
  }
526
572
  if (read?.rewritten === undefined)
@@ -529,32 +575,34 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
529
575
  throw new Error(`one-time migration M17: session_policy row ${row.policyKey} changed while its rule entries were being rewritten under the in-progress column ${column} — no writer can reach the ` +
530
576
  "table at that moment (builds up to 7.128.x lost the column, this build's replicas wait on the boot lock), so the step refuses rather than guess");
531
577
  }
532
- if (read.toolAllowDropped.length > 0)
533
- receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
578
+ if (read.toolAllowDropped.length > 0) {
579
+ receipts.sessionRuleRowsWithOldGrammarEntries.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
580
+ }
534
581
  });
535
582
  }
536
583
  export async function migrateSessionRuleGrammar(db, dialect, receipts) {
537
- if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
538
- return { sessionRuleColumn: "no-table", sessionRuleRowsWithRetiredBuckets: [], sessionRuleRowsWithOldGrammarEntries: [] };
584
+ if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE))) {
585
+ receipts.sessionRuleColumn = "no-table";
586
+ return;
587
+ }
539
588
  const col = SESSION_RULES_COLUMN;
540
589
  const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
541
590
  if (await has(RETIRED_SESSION_RULES_COLUMN)) {
542
591
  await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
543
592
  }
544
- let sessionRuleColumn = "current";
545
593
  if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
546
594
  await judgeOldGrammarRows(db, dialect, receipts);
547
595
  await execUntil(db, renameSessionRulesColumnSql(dialect, OLD_GRAMMAR_SESSION_RULES_COLUMN, col.name), async () => !(await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) && (await has(col.name)));
548
- receipts.renamed = true;
549
- sessionRuleColumn = "renamed";
596
+ receipts.sessionRuleColumn = "renamed";
597
+ }
598
+ else {
599
+ receipts.sessionRuleColumn = "current";
550
600
  }
551
- const named = [];
552
601
  await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
553
602
  const buckets = retiredSessionRuleBucketsOf(row.body);
554
603
  if (buckets.length > 0)
555
- named.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
604
+ receipts.sessionRuleRowsWithRetiredBuckets.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
556
605
  });
557
- return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
558
606
  }
559
607
  function renameSessionRulesColumnSql(dialect, from, to) {
560
608
  const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
@@ -571,7 +619,7 @@ async function primaryKeyWidth(db, dialect, table) {
571
619
  }
572
620
  const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
573
621
  const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
574
- async function reshapeWorkflowParkIndex(db, dialect) {
622
+ async function reshapeWorkflowParkIndex(db, dialect, receipts) {
575
623
  const T = WORKFLOW_PARK_INDEX_TABLE;
576
624
  const S = WORKFLOW_PARK_INDEX_STAGING;
577
625
  const O = WORKFLOW_PARK_INDEX_RETIRED;
@@ -582,11 +630,10 @@ async function reshapeWorkflowParkIndex(db, dialect) {
582
630
  const copyPointers = (to, from) => db.exec(dialect === "tidb"
583
631
  ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
584
632
  : `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
585
- let carried = 0;
586
633
  const resumed = await exists(S);
587
634
  if (resumed) {
588
635
  await db.exec(ddl);
589
- carried += await copyPointers(T, S);
636
+ receipts.workflowParkIndexRowsCarried += await copyPointers(T, S);
590
637
  await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
591
638
  }
592
639
  if (await exists(O))
@@ -594,10 +641,11 @@ async function reshapeWorkflowParkIndex(db, dialect) {
594
641
  if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
595
642
  if (await exists(T))
596
643
  await finishParkIndexShape(db, dialect);
597
- return resumed ? { key: "reshaped", carried } : { key: "current", carried: 0 };
644
+ receipts.workflowParkIndexKey = resumed ? "reshaped" : "current";
645
+ return;
598
646
  }
599
647
  await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
600
- carried += await copyPointers(S, T);
648
+ receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
601
649
  const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
602
650
  if (dialect === "tidb") {
603
651
  await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
@@ -607,7 +655,7 @@ async function reshapeWorkflowParkIndex(db, dialect) {
607
655
  await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`], async () => (await swapped()) && !(await exists(O)));
608
656
  }
609
657
  await finishParkIndexShape(db, dialect);
610
- return { key: "reshaped", carried };
658
+ receipts.workflowParkIndexKey = "reshaped";
611
659
  }
612
660
  async function execAtomically(db, statements, postcondition) {
613
661
  try {
@@ -658,10 +706,8 @@ export function migrateShadowedRuleDescriptor(text) {
658
706
  return { kind: "rewrite", text: JSON.stringify(next) };
659
707
  }
660
708
  const SHADOWED_RULE_BATCH = 500;
661
- async function migrateShadowedRuleKey(db, dialect) {
709
+ async function migrateShadowedRuleKey(db, dialect, receipts) {
662
710
  const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
663
- let migrated = 0;
664
- let unreadable = 0;
665
711
  let after = "";
666
712
  for (;;) {
667
713
  const rows = await db.rows(dialect === "tidb"
@@ -675,17 +721,17 @@ async function migrateShadowedRuleKey(db, dialect) {
675
721
  continue;
676
722
  const next = migrateShadowedRuleDescriptor(text);
677
723
  if (next.kind === "unreadable") {
678
- unreadable += 1;
724
+ receipts.allowRuleShadowRowsUnreadable += 1;
679
725
  continue;
680
726
  }
681
727
  if (next.kind === "untouched")
682
728
  continue;
683
- migrated += await db.exec(dialect === "tidb"
729
+ receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
684
730
  ? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
685
731
  : `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
686
732
  }
687
733
  if (rows.length < SHADOWED_RULE_BATCH)
688
- return { migrated, unreadable };
734
+ return;
689
735
  }
690
736
  }
691
737
  const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
@@ -707,11 +753,9 @@ export function migrateRetiredCardWordsInCardJson(text) {
707
753
  const next = retireStoredCardWords(decoded.value);
708
754
  return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
709
755
  }
710
- async function migrateRetiredCardWords(db, dialect) {
711
- let askRows = 0;
712
- let unreadable = 0;
756
+ async function migrateRetiredCardWords(db, dialect, receipts) {
713
757
  if (!(await tableExists(db, dialect, "approval_ask")))
714
- return { askRows, unreadable };
758
+ return;
715
759
  const cardText = jsonColumnTextExpr(dialect, "card_json");
716
760
  let after = "";
717
761
  for (;;) {
@@ -726,42 +770,129 @@ async function migrateRetiredCardWords(db, dialect) {
726
770
  continue;
727
771
  const next = migrateRetiredCardWordsInCardJson(text);
728
772
  if (next.kind === "unreadable") {
729
- unreadable += 1;
773
+ receipts.retiredCardWordRowsUnreadable += 1;
730
774
  continue;
731
775
  }
732
776
  if (next.kind === "untouched")
733
777
  continue;
734
- askRows += await db.exec(dialect === "tidb"
778
+ receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
735
779
  ? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
736
780
  : `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
737
781
  }
738
782
  if (rows.length < RETIRED_CARD_WORD_BATCH)
739
- return { askRows, unreadable };
783
+ return;
784
+ }
785
+ }
786
+ const M21_GATE = { key: "gate", as: "m21_gate" };
787
+ const M21_PENDING_ACTION = { key: "pendingAction", as: "m21_pending_action" };
788
+ const PARKED_ROW_REPAIR_PAGE = 200;
789
+ const QUESTION_CARD = "question";
790
+ const PARKED_APPROVAL_GATE_KINDS = Object.keys(PAUSE_REGISTRY).filter((k) => PAUSE_REGISTRY[k].pending === "tool_approval");
791
+ const readM21Subtree = createObjectColumnReader();
792
+ const readM21Document = createObjectColumnReader();
793
+ export function parkedRowRepairOf(gateKindCell, gate, pendingAction) {
794
+ const gateObj = gate !== null && typeof gate === "object" && !Array.isArray(gate) ? gate : undefined;
795
+ const columnKind = typeof gateKindCell === "string" && isGateKind(gateKindCell) ? gateKindCell : undefined;
796
+ const documentKind = gateObj !== undefined && isGateKind(gateObj.kind) ? gateObj.kind : undefined;
797
+ const kind = columnKind ?? documentKind;
798
+ const pa = pendingAction !== null && typeof pendingAction === "object" && !Array.isArray(pendingAction) ? pendingAction : undefined;
799
+ const questionCard = kind !== undefined &&
800
+ PARKED_APPROVAL_GATE_KINDS.includes(kind) &&
801
+ gateObj !== undefined &&
802
+ !Object.hasOwn(gateObj, "approvalCard") &&
803
+ pa?.kind === "tool_approval" &&
804
+ pa.toolName === ASK_USER_QUESTION_TOOL_NAME;
805
+ const columnNull = gateKindCell === null || gateKindCell === undefined;
806
+ return { ...(columnNull && documentKind !== undefined ? { gateKind: documentKind } : {}), questionCard, unresumable: columnNull && documentKind === undefined };
807
+ }
808
+ function m21Subtree(cell, where) {
809
+ try {
810
+ return decodeNullableJsonColumn(cell, readM21Subtree, where);
811
+ }
812
+ catch (e) {
813
+ if (!isJsonColumnBodyProblem(e))
814
+ throw e;
815
+ return undefined;
816
+ }
817
+ }
818
+ export async function repairParkedRows(db, dialect, receipts) {
819
+ const ph = (n) => (dialect === "tidb" ? "?" : `$${n}`);
820
+ const kinds = PARKED_APPROVAL_GATE_KINDS;
821
+ const kindSlots = kinds.map((_, i) => ph(i + 1)).join(", ");
822
+ const scan = `SELECT token, session_id, gate_kind, ${jsonColumnText(dialect, "checkpoint", M21_GATE)}, ${jsonColumnText(dialect, "checkpoint", M21_PENDING_ACTION)} FROM checkpoint ` +
823
+ `WHERE status = 'pending' AND (gate_kind IS NULL OR (gate_kind IN (${kindSlots}) AND tool_name = ${ph(kinds.length + 1)})) AND token > ${ph(kinds.length + 2)} ` +
824
+ `ORDER BY token LIMIT ${PARKED_ROW_REPAIR_PAGE}`;
825
+ const docText = jsonColumnTextExpr(dialect, "checkpoint");
826
+ const encode = dialectProtocolJsonEncoder(dialect);
827
+ let after = "";
828
+ for (;;) {
829
+ const page = await db.rows(scan, [...kinds, ASK_USER_QUESTION_TOOL_NAME, after]);
830
+ for (const r of page) {
831
+ const token = String(r.token);
832
+ after = token;
833
+ const plan = parkedRowRepairOf(r.gate_kind, m21Subtree(r[M21_GATE.as], "checkpoint.checkpoint $.gate (M21 parked-row repair)"), m21Subtree(r[M21_PENDING_ACTION.as], "checkpoint.checkpoint $.pendingAction (M21 parked-row repair)"));
834
+ if (plan.unresumable) {
835
+ receipts.parkedRowsUnresumableExpired += await db.exec(`UPDATE checkpoint SET status = 'expired', decided_at_ms = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND gate_kind IS NULL`, [Date.now(), token]);
836
+ continue;
837
+ }
838
+ if (plan.gateKind !== undefined) {
839
+ receipts.parkedRowGateKindsBackfilled += await db.exec(`UPDATE checkpoint SET gate_kind = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND gate_kind IS NULL`, [plan.gateKind, token]);
840
+ }
841
+ if (!plan.questionCard)
842
+ continue;
843
+ const [full] = await db.rows(`SELECT ${jsonColumnText(dialect, "checkpoint")} FROM checkpoint WHERE token = ${ph(1)} AND status = 'pending'`, [token]);
844
+ if (full === undefined)
845
+ continue;
846
+ let doc;
847
+ try {
848
+ doc = decodeJsonColumn(full.checkpoint, readM21Document, "checkpoint.checkpoint (M21 question-card repair)");
849
+ }
850
+ catch (e) {
851
+ if (!isJsonColumnBodyProblem(e))
852
+ throw e;
853
+ receipts.parkedQuestionRowsLeftAsWritten += 1;
854
+ continue;
855
+ }
856
+ const gate = doc.gate;
857
+ if (Object.hasOwn(doc, "requirementsDigest") || gate === null || typeof gate !== "object" || Array.isArray(gate)) {
858
+ receipts.parkedQuestionRowsLeftAsWritten += 1;
859
+ continue;
860
+ }
861
+ const next = encode({ ...doc, gate: { ...gate, approvalCard: QUESTION_CARD } }, "checkpoint (M21 question-card repair)");
862
+ receipts.parkedQuestionCardsWritten += await db.exec(dialect === "tidb"
863
+ ? `UPDATE checkpoint SET checkpoint = ? WHERE token = ? AND status = 'pending' AND CAST(${docText} AS BINARY) = CAST(? AS BINARY)`
864
+ : `UPDATE checkpoint SET checkpoint = $1::jsonb WHERE token = $2 AND status = 'pending' AND ${docText} = $3`, [next, token, String(full.checkpoint)]);
865
+ }
866
+ if (page.length < PARKED_ROW_REPAIR_PAGE)
867
+ return;
740
868
  }
741
869
  }
742
870
  const INCARNATION_BACKFILL_BATCH = 500;
743
- async function migrateSessionIncarnations(db, dialect) {
744
- if (!(await tableExists(db, dialect, "session_meta")))
745
- return { sessionIncarnationColumn: "no-table", sessionIncarnationsBackfilled: 0 };
871
+ async function migrateSessionIncarnations(db, dialect, receipts) {
872
+ if (!(await tableExists(db, dialect, "session_meta"))) {
873
+ receipts.sessionIncarnationColumn = "no-table";
874
+ return;
875
+ }
746
876
  const col = SESSION_META_INCARNATION_COLUMN;
747
- let sessionIncarnationColumn = "present";
877
+ let column = "present";
748
878
  if (!(await columnExists(db, dialect, "session_meta", col.name))) {
749
879
  try {
750
880
  await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
751
- sessionIncarnationColumn = "added";
881
+ column = "added";
752
882
  }
753
883
  catch (e) {
754
884
  if (!(await columnExists(db, dialect, "session_meta", col.name)))
755
885
  throw e;
756
886
  }
757
887
  }
758
- return { sessionIncarnationColumn, sessionIncarnationsBackfilled: await backfillSessionIncarnations(db, dialect) };
888
+ receipts.sessionIncarnationColumn = column;
889
+ await backfillSessionIncarnations(db, dialect, receipts);
759
890
  }
760
- async function backfillSessionIncarnations(db, dialect) {
891
+ async function backfillSessionIncarnations(db, dialect, receipts) {
761
892
  const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
762
893
  const initial = await remaining();
763
894
  if (initial === 0)
764
- return 0;
895
+ return;
765
896
  const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
766
897
  let written = 0;
767
898
  let after;
@@ -771,7 +902,7 @@ async function backfillSessionIncarnations(db, dialect) {
771
902
  : await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' AND session_id > ${dialect === "pg" ? "$1" : "?"} ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`, [after])).map((r) => String(r.session_id));
772
903
  if (ids.length === 0) {
773
904
  if (after === undefined)
774
- return written;
905
+ return;
775
906
  after = undefined;
776
907
  continue;
777
908
  }
@@ -782,7 +913,9 @@ async function backfillSessionIncarnations(db, dialect) {
782
913
  };
783
914
  const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
784
915
  const inList = ids.map((id) => bind(id)).join(", ");
785
- written += await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
916
+ const landed = await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
917
+ written += landed;
918
+ receipts.sessionIncarnationsBackfilled += landed;
786
919
  after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
787
920
  }
788
921
  const left = await remaining();
@@ -791,33 +924,36 @@ async function backfillSessionIncarnations(db, dialect) {
791
924
  `(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
792
925
  `if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
793
926
  }
794
- return written;
927
+ }
928
+ function readingFields(r) {
929
+ return {
930
+ auditDetailJsonNullRows: r.auditDetailJsonNullRows,
931
+ ...(r.sessionIncarnationColumn !== undefined ? { sessionIncarnationColumn: r.sessionIncarnationColumn } : {}),
932
+ sessionIncarnationsBackfilled: r.sessionIncarnationsBackfilled,
933
+ holderDeathCodeRowsBackfilled: r.holderDeathCodeRowsBackfilled,
934
+ allowRuleShadowRowsMigrated: r.allowRuleShadowRowsMigrated,
935
+ allowRuleShadowRowsUnreadable: r.allowRuleShadowRowsUnreadable,
936
+ ...(r.workflowParkIndexKey !== undefined ? { workflowParkIndexKey: r.workflowParkIndexKey } : {}),
937
+ workflowParkIndexRowsCarried: r.workflowParkIndexRowsCarried,
938
+ undurableScopeRuleRowsRemoved: r.undurableScopeRuleRows.length,
939
+ undurableScopeRuleBucketsDeferred: r.undurableScopeRuleBucketsDeferred,
940
+ undurableScopeRuleBucketsUnreadable: r.undurableScopeRuleBucketsUnreadable.length,
941
+ ...(r.sessionRuleColumn !== undefined ? { sessionRuleColumn: r.sessionRuleColumn } : {}),
942
+ sessionRuleRowsWithRetiredBuckets: r.sessionRuleRowsWithRetiredBuckets.length,
943
+ sessionRuleRowsWithOldGrammarEntries: r.sessionRuleRowsWithOldGrammarEntries.length,
944
+ sessionRuleRowsWithUnreadableDeny: r.sessionRuleRowsWithUnreadableDeny.length,
945
+ retiredCardWordAskRowsRewritten: r.retiredCardWordAskRowsRewritten,
946
+ retiredCardWordRowsUnreadable: r.retiredCardWordRowsUnreadable,
947
+ parkedQuestionCardsWritten: r.parkedQuestionCardsWritten,
948
+ parkedQuestionRowsLeftAsWritten: r.parkedQuestionRowsLeftAsWritten,
949
+ parkedRowGateKindsBackfilled: r.parkedRowGateKindsBackfilled,
950
+ parkedRowsUnresumableExpired: r.parkedRowsUnresumableExpired,
951
+ };
795
952
  }
796
953
  export function logOneTimeMigrations(log, backend, report) {
797
- const changed = report.auditDetailJsonNullRows > 0 ||
798
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
799
- report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
800
- report.undurableScopeRuleBucketsUnreadable.length > 0 ||
801
- report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
802
- report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
803
- (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
954
+ (changedAnything(report) ? log.warn : log.info).call(log, "one_time_migrations_applied", {
804
955
  backend,
805
- auditDetailJsonNullRows: report.auditDetailJsonNullRows,
806
- sessionIncarnationColumn: report.sessionIncarnationColumn,
807
- sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
808
- holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
809
- allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
810
- allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
811
- workflowParkIndexKey: report.workflowParkIndexKey,
812
- workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
813
- undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
814
- undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
815
- undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
816
- sessionRuleColumn: report.sessionRuleColumn,
817
- sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
818
- sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
819
- retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
820
- retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
956
+ ...readingFields(report),
821
957
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
822
958
  note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
823
959
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
@@ -835,16 +971,23 @@ export function logOneTimeMigrations(log, backend, report) {
835
971
  "are read by the engine in the grammar the row carries (none = the grammar before the stamp) and rewritten as the same statement in the current grammar " +
836
972
  "(the column carries the in-progress name rule_entries_old_grammar until every row is rewritten, so an interrupted boot resumes there) — toolAllow entries " +
837
973
  "that the earlier grammar read as naming no tool are removed from the row, every row is stamped with the current grammar, one session_rule_row_old_grammar_entries " +
838
- "line per row that lost entries; " +
974
+ "line per row that lost entries — except a row whose toolDeny holds an entry the engine reads as no rule: that row is left whole and unstamped (the engine refuses every " +
975
+ "call of such a session until an operator rewrites its rules), one session_rule_row_unreadable_deny line per row; " +
839
976
  "rows still holding a retired command bucket keep that bucket and are named, one session_rule_row_retired_bucket line per row on every boot; " +
840
977
  "M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
841
978
  "governance_unavailable read as rule_unreadable; the engine's own parked-row documents are never rewritten (their row digest covers the ask class), they and " +
842
- "the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read). Each step " +
979
+ "the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read; M21: suspended rows parked by an engine " +
980
+ "before the question card existed and still pending — a question approval whose gate carries no card — get the card word question (rows the engine sealed " +
981
+ "with its row digest are left as written and counted), and suspended rows with no gate kind on the row get it from the gate (a row whose gate names no " +
982
+ "registered kind cannot be resumed and is expired)). Each step " +
843
983
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
844
984
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
845
985
  "version that still carries a step at least once before upgrading past its deletion.",
846
986
  });
847
- for (const row of report.undurableScopeRuleRows) {
987
+ logReceiptRows(log, backend, report);
988
+ }
989
+ function logReceiptRows(log, backend, r) {
990
+ for (const row of r.undurableScopeRuleRows) {
848
991
  log.warn("permission_rule_row_removed", {
849
992
  backend,
850
993
  ownerKey: row.ownerKey,
@@ -855,7 +998,7 @@ export function logOneTimeMigrations(log, backend, report) {
855
998
  note: removedRuleRowNote(row.kind),
856
999
  });
857
1000
  }
858
- for (const bucket of report.undurableScopeRuleBucketsUnreadable) {
1001
+ for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
859
1002
  log.warn("permission_rule_bucket_unreadable", {
860
1003
  backend,
861
1004
  ownerKey: bucket.ownerKey,
@@ -865,8 +1008,37 @@ export function logOneTimeMigrations(log, backend, report) {
865
1008
  "keeps being refused, as before this release, until someone repairs or deletes that row by hand",
866
1009
  });
867
1010
  }
868
- logSessionRuleOldGrammarRows(log, backend, report.sessionRuleRowsWithOldGrammarEntries);
869
- for (const row of report.sessionRuleRowsWithRetiredBuckets) {
1011
+ for (const row of r.sessionRuleRowsWithOldGrammarEntries) {
1012
+ log.warn("session_rule_row_old_grammar_entries", {
1013
+ backend,
1014
+ policyKey: row.policyKey,
1015
+ sessionId: row.sessionId,
1016
+ toolAllowDropped: row.toolAllowDropped,
1017
+ note: "a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
1018
+ "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
1019
+ "The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
1020
+ "nothing); toolDeny entries were left as written (a row whose toolDeny holds an entry the engine reads as no rule is not rewritten at all — it is named by " +
1021
+ "session_rule_row_unreadable_deny instead). Every entry named on this line is named exactly once, by the boot that " +
1022
+ "removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
1023
+ });
1024
+ }
1025
+ for (const row of r.sessionRuleRowsWithUnreadableDeny) {
1026
+ log.warn("session_rule_row_unreadable_deny", {
1027
+ backend,
1028
+ policyKey: row.policyKey,
1029
+ sessionId: row.sessionId,
1030
+ toolDenyUnreadable: row.toolDenyUnreadable,
1031
+ ...(row.retiredBuckets !== undefined ? { retiredBuckets: row.retiredBuckets } : {}),
1032
+ note: "a session's stored rules hold toolDeny entries the engine's rule reader reads as no rule at all (the reason on each entry is the reader's own word). The engine never " +
1033
+ "reads rules it cannot read as fewer rules, so every tool call in this session is refused until the rules are rewritten. The row was not rewritten and not stamped " +
1034
+ "with the current rule grammar: the stamp would only silence the engine's notice about the earlier grammar (config.session_rules_earlier_grammar) and lift nothing. " +
1035
+ "Nothing was removed for you — a deny nobody can read is the operator's to rewrite. Write the session's rules afresh as an operator through the session policy " +
1036
+ "endpoint (PUT /v1/sessions/:id/policy, a new value without the stored row's rev, leaving these entries out or spelling each as a rule), or delete the session " +
1037
+ "(deleting the session deletes its rules). A line carrying retiredBuckets names a row that also holds a retired command bucket: the policy endpoint cannot read such a row " +
1038
+ "(not even for an operator), so rewrite the row in the database in the current grammar (keep rev and gen) or delete the session. Each row is named exactly once, by the boot that ran the judgement — no later boot lists it again — so keep the line",
1039
+ });
1040
+ }
1041
+ for (const row of r.sessionRuleRowsWithRetiredBuckets) {
870
1042
  log.warn("session_rule_row_retired_bucket", {
871
1043
  backend,
872
1044
  policyKey: row.policyKey,
@@ -880,40 +1052,17 @@ export function logOneTimeMigrations(log, backend, report) {
880
1052
  });
881
1053
  }
882
1054
  }
883
- function logSessionRuleOldGrammarRows(log, backend, rows) {
884
- for (const row of rows) {
885
- log.warn("session_rule_row_old_grammar_entries", {
886
- backend,
887
- policyKey: row.policyKey,
888
- sessionId: row.sessionId,
889
- toolAllowDropped: row.toolAllowDropped,
890
- ...(row.wildcardMasked ? { wildcardMasked: true } : {}),
891
- note: row.wildcardMasked
892
- ? "a session's stored rules were written before the rule-grammar stamp and its toolAllow list holds `*`: the engine reads that list as void in every grammar (every call " +
893
- "refused), which hides what the other entries meant in the earlier grammar, so this row was NOT rewritten or stamped — it stays in its earlier grammar. To restore the other " +
894
- "entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
895
- :
896
- "a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
897
- "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
898
- "The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
899
- "nothing); toolDeny entries read the same in every grammar and were left as written. Every entry named on this line is named exactly once, by the boot that " +
900
- "removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
901
- });
902
- }
903
- }
904
1055
  function logOneTimeMigrationsInterrupted(log, backend, error) {
905
- const { receipts } = error;
906
1056
  log.warn("one_time_migrations_interrupted", {
907
1057
  backend,
908
- sessionRuleColumn: receipts.sessionRuleColumn ?? "in-progress",
909
- sessionRuleRowsWithOldGrammarEntries: receipts.sessionRuleRowsWithOldGrammarEntries.length,
910
- note: "the one-time migrations failed after committing changes whose receipts the next boot cannot re-derive: the session_rule_row_old_grammar_entries lines that follow name " +
911
- "rows this boot already rewrote — a retried boot finds them rewritten and removes nothing from them. These lines are the only place the rewritten rows are ever " +
912
- "reported — no later boot lists them — so save them. The failure itself follows as this boot's refusal. sessionRuleColumn \"renamed\": every row was rewritten and the final " +
913
- "column name was published before the failure; \"in-progress\": the column still carries its in-progress name — the next boot rewrites the remaining rows (rows already " +
914
- "rewritten are left alone) and publishes the final name",
1058
+ ...readingFields(error.receipts),
1059
+ note: "the one-time migrations failed after committing changes: the readings on this line and the lines that follow are what this boot already committed — a retried " +
1060
+ "boot finds that work done and does not report it again, so these lines are the only record of them, written once by this boot — save them. The failure itself follows " +
1061
+ "as this boot's refusal. A shape reading absent from this line (sessionIncarnationColumn / workflowParkIndexKey / sessionRuleColumn) belongs to a step that did not " +
1062
+ "finish; sessionRuleColumn absent after session_rule_row_old_grammar_entries lines means the column still carries its in-progress name — the next boot rewrites the " +
1063
+ "remaining rows (rows already rewritten are left alone) and publishes the final name",
915
1064
  });
916
- logSessionRuleOldGrammarRows(log, backend, receipts.sessionRuleRowsWithOldGrammarEntries);
1065
+ logReceiptRows(log, backend, error.receipts);
917
1066
  }
918
1067
  export async function runAndLogOneTimeMigrations(log, backend) {
919
1068
  let report;