@sema-agent/server 7.130.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 (294) hide show
  1. package/MIGRATION.md +396 -0
  2. package/README.md +13 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +187 -50
  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/audit.js +4 -1
  9. package/dist/auth-bridge.d.ts +4 -0
  10. package/dist/auth-bridge.js +10 -11
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/deployment-mods.d.ts +31 -0
  13. package/dist/boot/deployment-mods.js +26 -0
  14. package/dist/boot/execution-env.d.ts +1 -1
  15. package/dist/boot/execution-env.js +19 -6
  16. package/dist/boot/memory-boundary.d.ts +2 -2
  17. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  18. package/dist/boot/read-deny-anchor-audit.js +36 -0
  19. package/dist/boot/resolve-spec.d.ts +1 -5
  20. package/dist/boot/resolve-spec.js +56 -61
  21. package/dist/boot/runner-deps.d.ts +19 -8
  22. package/dist/boot/runner-deps.js +14 -2
  23. package/dist/boot/session-faces.d.ts +1 -7
  24. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  25. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  26. package/dist/boot/stage-03-store-backend.js +2 -2
  27. package/dist/boot/stage-04-stores.d.ts +1 -1
  28. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  29. package/dist/boot/stage-05-execution-env.js +7 -10
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +5 -4
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  33. package/dist/boot/stage-07-capability-layer.js +3 -3
  34. package/dist/boot/stage-08-reapers.d.ts +2 -2
  35. package/dist/boot/stage-09-leader.d.ts +3 -9
  36. package/dist/boot/stage-10-http-server.d.ts +3 -9
  37. package/dist/boot/stage-10-http-server.js +12 -10
  38. package/dist/boot/stores.d.ts +1 -1
  39. package/dist/boot/stores.js +5 -6
  40. package/dist/boot/web-admin.d.ts +0 -3
  41. package/dist/boot/web-admin.js +35 -35
  42. package/dist/brain.js +17 -22
  43. package/dist/budget.js +5 -4
  44. package/dist/caller-text-echo.d.ts +20 -0
  45. package/dist/caller-text-echo.js +24 -0
  46. package/dist/capabilities/brand-identity.d.ts +26 -0
  47. package/dist/capabilities/brand-identity.js +27 -0
  48. package/dist/capabilities/builtin-tools.js +2 -1
  49. package/dist/capabilities/center-plugins.js +1 -1
  50. package/dist/capabilities/center-prompts.d.ts +32 -59
  51. package/dist/capabilities/center-prompts.js +18 -32
  52. package/dist/capabilities/composite-members.d.ts +1 -0
  53. package/dist/capabilities/composite-members.js +1 -0
  54. package/dist/capabilities/engine-capabilities.js +2 -1
  55. package/dist/capabilities/hands-lane.d.ts +9 -6
  56. package/dist/capabilities/hands-lane.js +3 -3
  57. package/dist/capabilities/host-tool-names.d.ts +17 -0
  58. package/dist/capabilities/host-tool-names.js +15 -0
  59. package/dist/capabilities/mcp-probe.d.ts +14 -1
  60. package/dist/capabilities/mcp-probe.js +22 -133
  61. package/dist/capabilities/repo-tools.js +4 -3
  62. package/dist/capabilities/scenarios.d.ts +42 -47
  63. package/dist/capabilities/scenarios.js +19 -35
  64. package/dist/capabilities/select-environment-tool.js +2 -1
  65. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  66. package/dist/capabilities/send-user-file-tool.js +2 -2
  67. package/dist/capabilities/skills.d.ts +19 -4
  68. package/dist/capabilities/skills.js +30 -10
  69. package/dist/capabilities/tool-defer.js +2 -1
  70. package/dist/config-catalog.js +22 -11
  71. package/dist/config-invariants.js +2 -2
  72. package/dist/config-lkg.d.ts +9 -9
  73. package/dist/config-lkg.js +20 -4
  74. package/dist/config-provider.d.ts +3 -1
  75. package/dist/config-provider.js +14 -6
  76. package/dist/config-types.d.ts +13 -9
  77. package/dist/config.d.ts +2 -2
  78. package/dist/config.js +34 -20
  79. package/dist/device-ws-protocol.d.ts +1 -1
  80. package/dist/device-ws-protocol.js +1 -0
  81. package/dist/elicitation.d.ts +7 -0
  82. package/dist/elicitation.js +20 -5
  83. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  84. package/dist/env-name-allowlist-knobs.js +5 -4
  85. package/dist/exec-bounds.d.ts +15 -3
  86. package/dist/exec-bounds.js +4 -0
  87. package/dist/execution-lane-caps.d.ts +9 -2
  88. package/dist/execution-lane-caps.js +11 -0
  89. package/dist/hooks/classic-stations.d.ts +86 -0
  90. package/dist/hooks/classic-stations.js +27 -0
  91. package/dist/hooks/hook-llm.d.ts +8 -5
  92. package/dist/hooks/hook-llm.js +80 -37
  93. package/dist/hooks/hook-runner.d.ts +273 -75
  94. package/dist/hooks/hook-runner.js +293 -317
  95. package/dist/http/admission.d.ts +8 -2
  96. package/dist/http/admission.js +103 -59
  97. package/dist/http/approval-sweeps.js +59 -18
  98. package/dist/http/checkpoint-ctx.d.ts +36 -18
  99. package/dist/http/checkpoint-ctx.js +14 -10
  100. package/dist/http/claimed-run.d.ts +13 -0
  101. package/dist/http/claimed-run.js +15 -0
  102. package/dist/http/device-placement.js +5 -10
  103. package/dist/http/dispatch.d.ts +0 -4
  104. package/dist/http/dispatch.js +6 -10
  105. package/dist/http/durable-run-leg.d.ts +2 -1
  106. package/dist/http/durable-run-leg.js +5 -3
  107. package/dist/http/health-self-report.d.ts +7 -5
  108. package/dist/http/health-self-report.js +4 -18
  109. package/dist/http/principal-gate.d.ts +23 -6
  110. package/dist/http/principal-gate.js +24 -5
  111. package/dist/http/refusal-body.d.ts +21 -0
  112. package/dist/http/refusal-body.js +4 -0
  113. package/dist/http/request-guards.d.ts +27 -0
  114. package/dist/http/request-guards.js +24 -3
  115. package/dist/http/resume-legs.d.ts +25 -0
  116. package/dist/http/resume-legs.js +48 -26
  117. package/dist/http/routes/approvals-assistant.js +1 -1
  118. package/dist/http/routes/capabilities.js +7 -17
  119. package/dist/http/routes/diagnostics.js +1 -1
  120. package/dist/http/routes/rules.js +2 -2
  121. package/dist/http/routes/runs.js +13 -17
  122. package/dist/http/routes/session-sync.js +4 -2
  123. package/dist/http/routes/sessions.js +23 -11
  124. package/dist/http/routes/shared-memory.js +2 -13
  125. package/dist/http/routes/side-query.js +2 -3
  126. package/dist/http/routes/tasks.js +34 -19
  127. package/dist/http/scenario-read-face.d.ts +14 -0
  128. package/dist/http/scenario-read-face.js +5 -0
  129. package/dist/http/send.d.ts +13 -1
  130. package/dist/http/send.js +12 -1
  131. package/dist/http/server.d.ts +37 -23
  132. package/dist/http/server.js +30 -4
  133. package/dist/http/stored-body-replay.d.ts +10 -3
  134. package/dist/http/stored-body-replay.js +27 -6
  135. package/dist/http/submit-ctx.d.ts +25 -0
  136. package/dist/http/submit-ctx.js +39 -0
  137. package/dist/http/wire-types.d.ts +18 -5
  138. package/dist/index.d.ts +3 -2
  139. package/dist/index.js +3 -2
  140. package/dist/lane-home-probe.d.ts +43 -0
  141. package/dist/lane-home-probe.js +35 -0
  142. package/dist/limit-refusal.d.ts +47 -0
  143. package/dist/limit-refusal.js +12 -0
  144. package/dist/main.js +1 -1
  145. package/dist/mcp-assertion-header.d.ts +3 -11
  146. package/dist/mcp-assertion-header.js +3 -9
  147. package/dist/mcp-tool-faces.d.ts +5 -0
  148. package/dist/mcp-tool-faces.js +5 -1
  149. package/dist/memory-scope.d.ts +1 -1
  150. package/dist/model-entry-refusal.d.ts +1 -7
  151. package/dist/model-provider.d.ts +39 -12
  152. package/dist/model-provider.js +19 -2
  153. package/dist/model-route-endpoint.d.ts +52 -12
  154. package/dist/model-route-endpoint.js +48 -26
  155. package/dist/model-route-gap.d.ts +5 -4
  156. package/dist/model-route-gap.js +1 -1
  157. package/dist/model-select.d.ts +58 -0
  158. package/dist/model-select.js +31 -0
  159. package/dist/namespaced-rule-name.d.ts +7 -4
  160. package/dist/namespaced-rule-name.js +4 -3
  161. package/dist/observability/cost-taxonomy.d.ts +7 -7
  162. package/dist/observability/fail-open.d.ts +53 -12
  163. package/dist/observability/fail-open.js +47 -7
  164. package/dist/observability/metrics.js +3 -1
  165. package/dist/observability/prompt-manifest.d.ts +47 -82
  166. package/dist/observability/prompt-manifest.js +119 -17
  167. package/dist/observability/run-terminal.js +14 -6
  168. package/dist/observability/secret-env-scrub.d.ts +13 -1
  169. package/dist/observability/secret-env-scrub.js +13 -1
  170. package/dist/observability/tool-trace.d.ts +16 -21
  171. package/dist/observability/tool-trace.js +51 -42
  172. package/dist/per-task-image.d.ts +7 -1
  173. package/dist/per-task-image.js +3 -3
  174. package/dist/plain-string-record.d.ts +25 -0
  175. package/dist/plain-string-record.js +32 -0
  176. package/dist/plugins/blob-backend.d.ts +68 -13
  177. package/dist/plugins/blob-backend.js +53 -10
  178. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  179. package/dist/plugins/checkpoint-store-sql.js +39 -26
  180. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  181. package/dist/plugins/k8s-bg-scripts.js +22 -0
  182. package/dist/plugins/local-checkpoint-store.js +5 -2
  183. package/dist/plugins/local-session-store.js +3 -3
  184. package/dist/plugins/one-time-migrations.d.ts +147 -91
  185. package/dist/plugins/one-time-migrations.js +372 -208
  186. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  187. package/dist/plugins/permission-rule-store-sql.js +4 -18
  188. package/dist/plugins/pg-session-storage.d.ts +4 -5
  189. package/dist/plugins/pg-session-storage.js +7 -17
  190. package/dist/plugins/remote-env-adb.d.ts +6 -0
  191. package/dist/plugins/remote-env-adb.js +9 -4
  192. package/dist/plugins/remote-env-device.js +3 -0
  193. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  194. package/dist/plugins/remote-env-e2b.js +21 -9
  195. package/dist/plugins/remote-env-host.d.ts +9 -9
  196. package/dist/plugins/remote-env-host.js +10 -32
  197. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  198. package/dist/plugins/remote-env-k8s.js +265 -90
  199. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  200. package/dist/plugins/remote-env-local-docker.js +77 -9
  201. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  202. package/dist/plugins/remote-env-ssh.js +109 -3
  203. package/dist/plugins/remote-shell.d.ts +0 -1
  204. package/dist/plugins/remote-shell.js +1 -2
  205. package/dist/plugins/retention-store-sql.js +6 -2
  206. package/dist/plugins/schema-consistency.d.ts +54 -0
  207. package/dist/plugins/schema-consistency.js +52 -0
  208. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  209. package/dist/plugins/session-epoch-anchor.js +23 -0
  210. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  211. package/dist/plugins/shared-memory-store-sql.js +5 -16
  212. package/dist/plugins/sql-driver.d.ts +141 -0
  213. package/dist/plugins/sql-driver.js +115 -1
  214. package/dist/plugins/sql-errors.d.ts +41 -0
  215. package/dist/plugins/sql-errors.js +36 -0
  216. package/dist/plugins/store-backend.js +2 -1
  217. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  218. package/dist/plugins/tidb-session-storage.js +6 -16
  219. package/dist/plugins/tidb-session-store.js +1 -1
  220. package/dist/prompts-domain-validate.d.ts +17 -61
  221. package/dist/prompts-domain-validate.js +9 -119
  222. package/dist/question.d.ts +18 -9
  223. package/dist/question.js +21 -13
  224. package/dist/request-key-closure.d.ts +4 -4
  225. package/dist/request-key-closure.js +18 -13
  226. package/dist/rule-entry-list.d.ts +134 -24
  227. package/dist/rule-entry-list.js +162 -30
  228. package/dist/run-local.d.ts +3 -3
  229. package/dist/run-local.js +26 -25
  230. package/dist/runs.d.ts +48 -2
  231. package/dist/runs.js +61 -2
  232. package/dist/server-secret-env.d.ts +44 -18
  233. package/dist/server-secret-env.js +49 -8
  234. package/dist/session-incarnation.d.ts +15 -5
  235. package/dist/session-incarnation.js +16 -9
  236. package/dist/session-titler.js +12 -2
  237. package/dist/spec-fields.d.ts +34 -18
  238. package/dist/spec-fields.js +46 -53
  239. package/dist/sql-engine-posture.d.ts +29 -4
  240. package/dist/sql-engine-posture.js +36 -1
  241. package/dist/stored-request-hooks-m20.d.ts +23 -0
  242. package/dist/stored-request-hooks-m20.js +124 -0
  243. package/dist/stored-request-tool-axes-m19.d.ts +20 -5
  244. package/dist/stored-request-tool-axes-m19.js +30 -2
  245. package/dist/task-a2a.d.ts +9 -1
  246. package/dist/task-a2a.js +73 -47
  247. package/dist/task-mcp.d.ts +54 -102
  248. package/dist/task-mcp.js +194 -92
  249. package/dist/task-settings.d.ts +47 -25
  250. package/dist/task-settings.js +46 -61
  251. package/dist/text-bidi.d.ts +3 -46
  252. package/dist/text-bidi.js +3 -3
  253. package/dist/tool-approval.d.ts +1 -1
  254. package/dist/trace/core-keyset-guard.d.ts +24 -4
  255. package/dist/trace/engine-notice-wire.d.ts +11 -3
  256. package/dist/trace/engine-notice-wire.js +8 -1
  257. package/dist/trace/ledger-events.d.ts +3 -1
  258. package/dist/trace/ledger-sink.d.ts +29 -0
  259. package/dist/trace/ledger-sink.js +15 -1
  260. package/dist/trace/model-segment.js +1 -1
  261. package/dist/trace/project.d.ts +25 -6
  262. package/dist/trace/project.js +68 -22
  263. package/dist/trace/wire-projection-faces.d.ts +80 -2
  264. package/dist/trace/wire-projection-faces.js +23 -1
  265. package/dist/user-authored-text.d.ts +7 -0
  266. package/dist/user-authored-text.js +3 -0
  267. package/dist/web-admin-client/apply-effective.js +10 -5
  268. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  269. package/dist/web-admin-client/candidate-gate.js +3 -3
  270. package/dist/web-admin-client/http-client.js +0 -6
  271. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  272. package/dist/web-admin-client/read-warnings.js +10 -0
  273. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  274. package/dist/web-admin-client/skills-mcp.js +9 -6
  275. package/dist/web-admin-client/types.d.ts +3 -4
  276. package/dist/workspace-cache.d.ts +35 -0
  277. package/dist/workspace-cache.js +36 -0
  278. package/dist/workspace-space.d.ts +3 -2
  279. package/dist/workspace-space.js +2 -0
  280. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  281. package/package.json +4 -4
  282. package/scripts/drain-check-local.mjs +37 -3
  283. package/scripts/drain-check.mysql.sql +30 -2
  284. package/scripts/drain-check.pg.sql +29 -2
  285. package/dist/capabilities/memory-notice.d.ts +0 -138
  286. package/dist/capabilities/memory-notice.js +0 -64
  287. package/dist/capabilities/prompt.d.ts +0 -17
  288. package/dist/capabilities/prompt.js +0 -6
  289. package/dist/mcp-content-origin.d.ts +0 -34
  290. package/dist/mcp-content-origin.js +0 -10
  291. package/dist/tool-axis-words.d.ts +0 -27
  292. package/dist/tool-axis-words.js +0 -9
  293. package/dist/wire-name-bound.d.ts +0 -14
  294. 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`",
@@ -149,7 +212,7 @@ export const ONE_TIME_MIGRATIONS = [
149
212
  removal: "删模式词读法记号的判据与拒臂:`http/stored-body-replay.ts` 的 `permissionModeReadingUnstated`;`http/checkpoint-ctx.ts` 的 `permissionModeReadingStated` / `permissionModeReadingRefusal` / `PERMISSION_MODE_READINGS`、" +
150
213
  "`StoredBodyReplayRefusal` 的 `permission_mode_reading` 臂及其日志字段与答复臂、`buildCheckpointCtx` 与 `withPermissionModeRewritten` 两处记号写点" +
151
214
  "(后者删后 `resume-legs.ts` `drivePlanReview` 里对它的那一处调用回到只换体的 `{ ...ctx, body }`)、`CheckpointCtx.permissionModeReading`、" +
152
- "`http/stored-body-replay.ts` `storedBodyReplayRefusal` 的 `autonomy` 形参(只服务这一问)与四个问点(`resume-legs.ts` 两处、`approval-sweeps.ts`、`routes/runs.ts`)传 `deps.config.autonomy` 的那一句",
215
+ "`http/stored-body-replay.ts` 里 `StoredReplayDeployment` 的 `autonomy` 位与 `storedBodyReplayRefusal` 读 `deployment.autonomy` 的那一句(这一位只服务这一问;四个问点传 `deps.config` 整份,home 位还服务 `permissions` 一问,不动)",
153
216
  afterRemoval: "没有响亮臂:删码之后仍停驻、在 7.121.x 及以前写下的行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)在设了 " +
154
217
  "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
155
218
  reportKeys: [],
@@ -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,60 @@ 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` —— 出路同上:拒绝那张卡,改好声明重新提交",
323
+ reportKeys: [],
324
+ },
325
+ {
326
+ id: "M20",
327
+ introducedIn: "7.131.0",
328
+ releasedOn: "2026-10-16",
329
+ removableAfter: "2026-11-15",
330
+ removal: "删 `src/stored-request-hooks-m20.ts` 整只文件(`storedHooksRereadM20` / `rereadStoredHooksM20` / `StoredHooksRereadM20` / `ENTRY_FIELD_NAMES` / `MATCHER_FIELD_NAMES` / `nodeAt` / `cloneDoc` / `removeKeyAt`);" +
331
+ "`boot/resolve-spec.ts` 里递给 `parseTaskSettings` 的重读座实参(`opts?.leg === \"fresh\" || !taskHooksHonored(config) ? {} : { storedHooksReread: storedHooksRereadM20(…) }`)与它的 import、" +
332
+ "`hooks_read_as_before_upgrade` 那一段 warn;`task-settings.ts` 的 `ParseTaskSettingsSeats` / `ParseTaskSettingsResult.hooksReread` / `parseTaskSettings` 第二形参与 hooks 臂里的重读那一支;" +
333
+ "`hooks/hook-runner.ts` 的 `ParseHooksConfigResult.pieces` / `HooksRefusalPiece`(若再无别的读者)与 `hooksCoordinate` 的 export;" +
334
+ "`test/hooks-stored-reread-m20.test.ts`(窗关那一格 —— 整份不采纳 + `server.hooks.resume-reader-refused` 计一笔 —— 改钉到 `test/hooks-entry-admission.test.ts` 的续跑腿组)",
335
+ afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里带这两形的 `settings.hooks`,续跑重放时整份不采纳(其中真在拦的 `PreToolUse` 钩子也不再拦)," +
336
+ "运维 warn `task_settings_deferred`(字段 `hooks`)+ fail-open 登记 `server.hooks.resume-reader-refused`(计数 + 探针 + 一次性 warn,census 第 140 行),任务照常续跑 —— " +
337
+ "出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
338
+ reportKeys: [],
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 同一种处置",
259
365
  reportKeys: [],
260
366
  },
261
367
  ];
@@ -276,29 +382,16 @@ export const NO_SQL_MIGRATIONS = {
276
382
  sessionRuleColumn: "file-store",
277
383
  sessionRuleRowsWithRetiredBuckets: [],
278
384
  sessionRuleRowsWithOldGrammarEntries: [],
385
+ sessionRuleRowsWithUnreadableDeny: [],
279
386
  retiredCardWordAskRowsRewritten: 0,
280
387
  retiredCardWordRowsUnreadable: 0,
388
+ parkedQuestionCardsWritten: 0,
389
+ parkedQuestionRowsLeftAsWritten: 0,
390
+ parkedRowGateKindsBackfilled: 0,
391
+ parkedRowsUnresumableExpired: 0,
281
392
  };
282
393
  const AUDIT_TABLE = "device_audit";
283
394
  const AUDIT_DETAIL_COLUMN = "detail";
284
- const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
285
- const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
286
- export class RetiredSchemaPresentError extends StoreBootRefusalError {
287
- found;
288
- code = SCHEMA_MISMATCH_ERROR_CODE;
289
- constructor(found) {
290
- 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. ` +
291
- `Found: ${found.join("; ")}. ` +
292
- `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: ` +
293
- `ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
294
- `This build carries none of the three and will not guess. ` +
295
- `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; ` +
296
- `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. ` +
297
- `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.`);
298
- this.found = found;
299
- this.name = "RetiredSchemaPresentError";
300
- }
301
- }
302
395
  function mysqlDb(conn) {
303
396
  return {
304
397
  async rows(sql, params) {
@@ -343,34 +436,6 @@ async function columnExists(db, dialect, table, column) {
343
436
  const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
344
437
  return (await db.rows(sql)).length > 0;
345
438
  }
346
- async function assertRetiredSchemaAbsent(db, dialect) {
347
- const found = [];
348
- if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
349
- found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
350
- if (await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN))
351
- found.push(`column checkpoint.${RETIRED_STEER_SEAT_COLUMN} (the retired single-seat steering column)`);
352
- if (found.length > 0)
353
- throw new RetiredSchemaPresentError(found);
354
- }
355
- export async function assertRetiredSchemaAbsentOnPool(target) {
356
- if (target.dialect === "tidb") {
357
- const conn = await target.pool.getConnection();
358
- try {
359
- await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
360
- }
361
- finally {
362
- conn.release();
363
- }
364
- return;
365
- }
366
- const client = await target.pool.connect();
367
- try {
368
- await assertRetiredSchemaAbsent(pgDb(client), "pg");
369
- }
370
- finally {
371
- client.release();
372
- }
373
- }
374
439
  export async function runSqlOneTimeMigrations(dialect, pool) {
375
440
  return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
376
441
  }
@@ -403,44 +468,33 @@ async function withPgLock(pool, run) {
403
468
  }
404
469
  }
405
470
  async function migrateOn(db, dialect) {
406
- const receipts = buildSessionRuleReceipts();
471
+ const receipts = buildMigrationReceipts();
407
472
  try {
408
- return await migrateStepsOn(db, dialect, receipts);
473
+ await migrateStepsOn(db, dialect, receipts);
409
474
  }
410
475
  catch (e) {
411
- throw interruptedWith(e, receipts);
476
+ throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
412
477
  }
478
+ return completedReport(receipts);
413
479
  }
414
480
  async function migrateStepsOn(db, dialect, receipts) {
415
- const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
416
- const auditDetailJsonNullRows = !auditTablePresent
417
- ? 0
418
- : await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
419
- `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
420
- const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
421
- const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
422
- ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
423
- : "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])) +
424
- (await db.exec(dialect === "tidb"
425
- ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
426
- : "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} (%`]));
427
- const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
428
- const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
429
- const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
430
- const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
431
- let undurableScopeRuleRows = [];
432
- let undurableScopeRuleBucketsDeferred = 0;
433
- let undurableScopeRuleBucketsUnreadable = [];
434
- if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
435
- ({ 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'`);
436
483
  }
437
- return {
438
- auditDetailJsonNullRows,
439
- sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
440
- workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
441
- sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries,
442
- retiredCardWordAskRowsRewritten, retiredCardWordRowsUnreadable,
443
- };
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);
444
498
  }
445
499
  const SESSION_POLICY_TABLE = "session_policy";
446
500
  const RETIRED_SESSION_RULES_COLUMN = "rules";
@@ -459,9 +513,10 @@ export function oldGrammarEntriesOf(body, rev) {
459
513
  const shaped = readSessionRulesShape({ ...buckets, rev });
460
514
  if ("fault" in shaped)
461
515
  return undefined;
516
+ const toolDenyUnreadable = unreadableDenyEntries(shaped.rules).map(({ entry, reason }) => ({ entry, reason }));
517
+ if (toolDenyUnreadable.length > 0)
518
+ return { toolAllowDropped: [], toolDenyUnreadable };
462
519
  const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
463
- if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
464
- return { toolAllowDropped: [], wildcardMasked: true };
465
520
  const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
466
521
  const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
467
522
  const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
@@ -504,8 +559,14 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
504
559
  : `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
505
560
  await scanSessionRuleRows(db, dialect, column, [], async (row) => {
506
561
  const read = oldGrammarEntriesOf(row.body, row.rev);
507
- if (read?.wildcardMasked) {
508
- 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
+ });
509
570
  return;
510
571
  }
511
572
  if (read?.rewritten === undefined)
@@ -514,32 +575,34 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
514
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 ` +
515
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");
516
577
  }
517
- if (read.toolAllowDropped.length > 0)
518
- 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
+ }
519
581
  });
520
582
  }
521
583
  export async function migrateSessionRuleGrammar(db, dialect, receipts) {
522
- if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
523
- return { sessionRuleColumn: "no-table", sessionRuleRowsWithRetiredBuckets: [], sessionRuleRowsWithOldGrammarEntries: [] };
584
+ if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE))) {
585
+ receipts.sessionRuleColumn = "no-table";
586
+ return;
587
+ }
524
588
  const col = SESSION_RULES_COLUMN;
525
589
  const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
526
590
  if (await has(RETIRED_SESSION_RULES_COLUMN)) {
527
591
  await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
528
592
  }
529
- let sessionRuleColumn = "current";
530
593
  if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
531
594
  await judgeOldGrammarRows(db, dialect, receipts);
532
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)));
533
- receipts.renamed = true;
534
- sessionRuleColumn = "renamed";
596
+ receipts.sessionRuleColumn = "renamed";
597
+ }
598
+ else {
599
+ receipts.sessionRuleColumn = "current";
535
600
  }
536
- const named = [];
537
601
  await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
538
602
  const buckets = retiredSessionRuleBucketsOf(row.body);
539
603
  if (buckets.length > 0)
540
- named.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
604
+ receipts.sessionRuleRowsWithRetiredBuckets.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
541
605
  });
542
- return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
543
606
  }
544
607
  function renameSessionRulesColumnSql(dialect, from, to) {
545
608
  const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
@@ -556,7 +619,7 @@ async function primaryKeyWidth(db, dialect, table) {
556
619
  }
557
620
  const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
558
621
  const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
559
- async function reshapeWorkflowParkIndex(db, dialect) {
622
+ async function reshapeWorkflowParkIndex(db, dialect, receipts) {
560
623
  const T = WORKFLOW_PARK_INDEX_TABLE;
561
624
  const S = WORKFLOW_PARK_INDEX_STAGING;
562
625
  const O = WORKFLOW_PARK_INDEX_RETIRED;
@@ -567,11 +630,10 @@ async function reshapeWorkflowParkIndex(db, dialect) {
567
630
  const copyPointers = (to, from) => db.exec(dialect === "tidb"
568
631
  ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
569
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`);
570
- let carried = 0;
571
633
  const resumed = await exists(S);
572
634
  if (resumed) {
573
635
  await db.exec(ddl);
574
- carried += await copyPointers(T, S);
636
+ receipts.workflowParkIndexRowsCarried += await copyPointers(T, S);
575
637
  await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
576
638
  }
577
639
  if (await exists(O))
@@ -579,10 +641,11 @@ async function reshapeWorkflowParkIndex(db, dialect) {
579
641
  if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
580
642
  if (await exists(T))
581
643
  await finishParkIndexShape(db, dialect);
582
- return resumed ? { key: "reshaped", carried } : { key: "current", carried: 0 };
644
+ receipts.workflowParkIndexKey = resumed ? "reshaped" : "current";
645
+ return;
583
646
  }
584
647
  await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
585
- carried += await copyPointers(S, T);
648
+ receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
586
649
  const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
587
650
  if (dialect === "tidb") {
588
651
  await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
@@ -592,7 +655,7 @@ async function reshapeWorkflowParkIndex(db, dialect) {
592
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)));
593
656
  }
594
657
  await finishParkIndexShape(db, dialect);
595
- return { key: "reshaped", carried };
658
+ receipts.workflowParkIndexKey = "reshaped";
596
659
  }
597
660
  async function execAtomically(db, statements, postcondition) {
598
661
  try {
@@ -643,10 +706,8 @@ export function migrateShadowedRuleDescriptor(text) {
643
706
  return { kind: "rewrite", text: JSON.stringify(next) };
644
707
  }
645
708
  const SHADOWED_RULE_BATCH = 500;
646
- async function migrateShadowedRuleKey(db, dialect) {
709
+ async function migrateShadowedRuleKey(db, dialect, receipts) {
647
710
  const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
648
- let migrated = 0;
649
- let unreadable = 0;
650
711
  let after = "";
651
712
  for (;;) {
652
713
  const rows = await db.rows(dialect === "tidb"
@@ -660,17 +721,17 @@ async function migrateShadowedRuleKey(db, dialect) {
660
721
  continue;
661
722
  const next = migrateShadowedRuleDescriptor(text);
662
723
  if (next.kind === "unreadable") {
663
- unreadable += 1;
724
+ receipts.allowRuleShadowRowsUnreadable += 1;
664
725
  continue;
665
726
  }
666
727
  if (next.kind === "untouched")
667
728
  continue;
668
- migrated += await db.exec(dialect === "tidb"
729
+ receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
669
730
  ? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
670
731
  : `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
671
732
  }
672
733
  if (rows.length < SHADOWED_RULE_BATCH)
673
- return { migrated, unreadable };
734
+ return;
674
735
  }
675
736
  }
676
737
  const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
@@ -692,11 +753,9 @@ export function migrateRetiredCardWordsInCardJson(text) {
692
753
  const next = retireStoredCardWords(decoded.value);
693
754
  return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
694
755
  }
695
- async function migrateRetiredCardWords(db, dialect) {
696
- let askRows = 0;
697
- let unreadable = 0;
756
+ async function migrateRetiredCardWords(db, dialect, receipts) {
698
757
  if (!(await tableExists(db, dialect, "approval_ask")))
699
- return { askRows, unreadable };
758
+ return;
700
759
  const cardText = jsonColumnTextExpr(dialect, "card_json");
701
760
  let after = "";
702
761
  for (;;) {
@@ -711,42 +770,129 @@ async function migrateRetiredCardWords(db, dialect) {
711
770
  continue;
712
771
  const next = migrateRetiredCardWordsInCardJson(text);
713
772
  if (next.kind === "unreadable") {
714
- unreadable += 1;
773
+ receipts.retiredCardWordRowsUnreadable += 1;
715
774
  continue;
716
775
  }
717
776
  if (next.kind === "untouched")
718
777
  continue;
719
- askRows += await db.exec(dialect === "tidb"
778
+ receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
720
779
  ? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
721
780
  : `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
722
781
  }
723
782
  if (rows.length < RETIRED_CARD_WORD_BATCH)
724
- 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;
725
868
  }
726
869
  }
727
870
  const INCARNATION_BACKFILL_BATCH = 500;
728
- async function migrateSessionIncarnations(db, dialect) {
729
- if (!(await tableExists(db, dialect, "session_meta")))
730
- 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
+ }
731
876
  const col = SESSION_META_INCARNATION_COLUMN;
732
- let sessionIncarnationColumn = "present";
877
+ let column = "present";
733
878
  if (!(await columnExists(db, dialect, "session_meta", col.name))) {
734
879
  try {
735
880
  await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
736
- sessionIncarnationColumn = "added";
881
+ column = "added";
737
882
  }
738
883
  catch (e) {
739
884
  if (!(await columnExists(db, dialect, "session_meta", col.name)))
740
885
  throw e;
741
886
  }
742
887
  }
743
- return { sessionIncarnationColumn, sessionIncarnationsBackfilled: await backfillSessionIncarnations(db, dialect) };
888
+ receipts.sessionIncarnationColumn = column;
889
+ await backfillSessionIncarnations(db, dialect, receipts);
744
890
  }
745
- async function backfillSessionIncarnations(db, dialect) {
891
+ async function backfillSessionIncarnations(db, dialect, receipts) {
746
892
  const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
747
893
  const initial = await remaining();
748
894
  if (initial === 0)
749
- return 0;
895
+ return;
750
896
  const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
751
897
  let written = 0;
752
898
  let after;
@@ -756,7 +902,7 @@ async function backfillSessionIncarnations(db, dialect) {
756
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));
757
903
  if (ids.length === 0) {
758
904
  if (after === undefined)
759
- return written;
905
+ return;
760
906
  after = undefined;
761
907
  continue;
762
908
  }
@@ -767,7 +913,9 @@ async function backfillSessionIncarnations(db, dialect) {
767
913
  };
768
914
  const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
769
915
  const inList = ids.map((id) => bind(id)).join(", ");
770
- 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;
771
919
  after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
772
920
  }
773
921
  const left = await remaining();
@@ -776,33 +924,36 @@ async function backfillSessionIncarnations(db, dialect) {
776
924
  `(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
777
925
  `if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
778
926
  }
779
- 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
+ };
780
952
  }
781
953
  export function logOneTimeMigrations(log, backend, report) {
782
- const changed = report.auditDetailJsonNullRows > 0 ||
783
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
784
- report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
785
- report.undurableScopeRuleBucketsUnreadable.length > 0 ||
786
- report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
787
- report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
788
- (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
954
+ (changedAnything(report) ? log.warn : log.info).call(log, "one_time_migrations_applied", {
789
955
  backend,
790
- auditDetailJsonNullRows: report.auditDetailJsonNullRows,
791
- sessionIncarnationColumn: report.sessionIncarnationColumn,
792
- sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
793
- holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
794
- allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
795
- allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
796
- workflowParkIndexKey: report.workflowParkIndexKey,
797
- workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
798
- undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
799
- undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
800
- undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
801
- sessionRuleColumn: report.sessionRuleColumn,
802
- sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
803
- sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
804
- retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
805
- retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
956
+ ...readingFields(report),
806
957
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
807
958
  note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
808
959
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
@@ -820,16 +971,23 @@ export function logOneTimeMigrations(log, backend, report) {
820
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 " +
821
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 " +
822
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 " +
823
- "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; " +
824
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; " +
825
977
  "M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
826
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 " +
827
- "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 " +
828
983
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
829
984
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
830
985
  "version that still carries a step at least once before upgrading past its deletion.",
831
986
  });
832
- 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) {
833
991
  log.warn("permission_rule_row_removed", {
834
992
  backend,
835
993
  ownerKey: row.ownerKey,
@@ -840,7 +998,7 @@ export function logOneTimeMigrations(log, backend, report) {
840
998
  note: removedRuleRowNote(row.kind),
841
999
  });
842
1000
  }
843
- for (const bucket of report.undurableScopeRuleBucketsUnreadable) {
1001
+ for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
844
1002
  log.warn("permission_rule_bucket_unreadable", {
845
1003
  backend,
846
1004
  ownerKey: bucket.ownerKey,
@@ -850,8 +1008,37 @@ export function logOneTimeMigrations(log, backend, report) {
850
1008
  "keeps being refused, as before this release, until someone repairs or deletes that row by hand",
851
1009
  });
852
1010
  }
853
- logSessionRuleOldGrammarRows(log, backend, report.sessionRuleRowsWithOldGrammarEntries);
854
- 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) {
855
1042
  log.warn("session_rule_row_retired_bucket", {
856
1043
  backend,
857
1044
  policyKey: row.policyKey,
@@ -865,40 +1052,17 @@ export function logOneTimeMigrations(log, backend, report) {
865
1052
  });
866
1053
  }
867
1054
  }
868
- function logSessionRuleOldGrammarRows(log, backend, rows) {
869
- for (const row of rows) {
870
- log.warn("session_rule_row_old_grammar_entries", {
871
- backend,
872
- policyKey: row.policyKey,
873
- sessionId: row.sessionId,
874
- toolAllowDropped: row.toolAllowDropped,
875
- ...(row.wildcardMasked ? { wildcardMasked: true } : {}),
876
- note: row.wildcardMasked
877
- ? "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 " +
878
- "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 " +
879
- "entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
880
- :
881
- "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 " +
882
- "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
883
- "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 " +
884
- "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 " +
885
- "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",
886
- });
887
- }
888
- }
889
1055
  function logOneTimeMigrationsInterrupted(log, backend, error) {
890
- const { receipts } = error;
891
1056
  log.warn("one_time_migrations_interrupted", {
892
1057
  backend,
893
- sessionRuleColumn: receipts.sessionRuleColumn ?? "in-progress",
894
- sessionRuleRowsWithOldGrammarEntries: receipts.sessionRuleRowsWithOldGrammarEntries.length,
895
- 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 " +
896
- "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 " +
897
- "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 " +
898
- "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 " +
899
- "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",
900
1064
  });
901
- logSessionRuleOldGrammarRows(log, backend, receipts.sessionRuleRowsWithOldGrammarEntries);
1065
+ logReceiptRows(log, backend, error.receipts);
902
1066
  }
903
1067
  export async function runAndLogOneTimeMigrations(log, backend) {
904
1068
  let report;