@sema-agent/server 7.138.0 → 7.140.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 (332) hide show
  1. package/MIGRATION.md +457 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +91 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/runner.js +1 -1
  8. package/dist/adoption/sql.js +1 -1
  9. package/dist/adoption/wire.d.ts +3 -3
  10. package/dist/approval-card.d.ts +26 -26
  11. package/dist/bench/s1/arms.d.ts +1 -1
  12. package/dist/bench/s1/arms.js +1 -3
  13. package/dist/bench/s1/live-deps.js +3 -2
  14. package/dist/bench/s1/row.d.ts +5 -2
  15. package/dist/bench/s1/row.js +2 -22
  16. package/dist/bench/s1/run-firm.js +2 -2
  17. package/dist/boot/adoption.js +1 -1
  18. package/dist/boot/device-lane.d.ts +8 -3
  19. package/dist/boot/device-lane.js +7 -2
  20. package/dist/boot/execution-env.d.ts +4 -2
  21. package/dist/boot/execution-env.js +28 -34
  22. package/dist/boot/execution-lane.d.ts +9 -1
  23. package/dist/boot/execution-lane.js +6 -1
  24. package/dist/boot/governance-seams.d.ts +3 -2
  25. package/dist/boot/governance-seams.js +9 -11
  26. package/dist/boot/memory-boundary.d.ts +14 -51
  27. package/dist/boot/memory-boundary.js +10 -20
  28. package/dist/boot/memory-consolidation.d.ts +15 -11
  29. package/dist/boot/memory-consolidation.js +25 -14
  30. package/dist/boot/resolve-spec.d.ts +4 -6
  31. package/dist/boot/resolve-spec.js +10 -13
  32. package/dist/boot/retention-lane.d.ts +5 -5
  33. package/dist/boot/retention-lane.js +0 -6
  34. package/dist/boot/runner-deps.d.ts +6 -4
  35. package/dist/boot/runner-deps.js +3 -1
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  40. package/dist/boot/stage-03-store-backend.js +2 -3
  41. package/dist/boot/stage-04-stores.d.ts +2 -6
  42. package/dist/boot/stage-04-stores.js +5 -5
  43. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  44. package/dist/boot/stage-05-execution-env.js +2 -2
  45. package/dist/boot/stage-06-runners.d.ts +2 -6
  46. package/dist/boot/stage-06-runners.js +2 -3
  47. package/dist/boot/stage-07-capability-layer.d.ts +2 -6
  48. package/dist/boot/stage-08-reapers.d.ts +2 -6
  49. package/dist/boot/stage-09-leader.d.ts +2 -6
  50. package/dist/boot/stage-09-leader.js +1 -2
  51. package/dist/boot/stage-10-http-server.d.ts +2 -6
  52. package/dist/boot/stage-10-http-server.js +7 -6
  53. package/dist/boot/stores.d.ts +3 -6
  54. package/dist/boot/stores.js +27 -40
  55. package/dist/boot/task-list-lane.d.ts +1 -1
  56. package/dist/boot/task-list-lane.js +1 -1
  57. package/dist/boot/web-admin.js +37 -11
  58. package/dist/capabilities/execution-lane.d.ts +2 -2
  59. package/dist/capabilities/mcp-probe.js +1 -1
  60. package/dist/capabilities/scenarios.d.ts +1 -1
  61. package/dist/capability-facts.d.ts +15 -7
  62. package/dist/capability-facts.js +8 -3
  63. package/dist/config-catalog.js +9 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +33 -37
  67. package/dist/config-provider.js +51 -30
  68. package/dist/config-types.d.ts +21 -7
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +46 -16
  71. package/dist/delegated-session-owner.d.ts +4 -3
  72. package/dist/delegated-session-owner.js +1 -1
  73. package/dist/deployment-shape.d.ts +10 -9
  74. package/dist/deployment-shape.js +4 -4
  75. package/dist/device-enrollment.d.ts +41 -2
  76. package/dist/device-enrollment.js +27 -12
  77. package/dist/device-store.d.ts +58 -85
  78. package/dist/device-store.js +19 -49
  79. package/dist/device-ws-hub.js +2 -1
  80. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  81. package/dist/exec-cut.d.ts +60 -0
  82. package/dist/exec-cut.js +36 -0
  83. package/dist/execution-lane-caps.d.ts +115 -105
  84. package/dist/execution-lane-caps.js +76 -82
  85. package/dist/execution-placement.d.ts +259 -0
  86. package/dist/execution-placement.js +199 -0
  87. package/dist/field-conflict.d.ts +3 -2
  88. package/dist/field-conflict.js +1 -0
  89. package/dist/hosted-posture.d.ts +2 -2
  90. package/dist/http/admission.js +8 -19
  91. package/dist/http/checkpoint-ctx.d.ts +21 -12
  92. package/dist/http/checkpoint-ctx.js +4 -2
  93. package/dist/http/claimed-run.d.ts +1 -1
  94. package/dist/http/durable-run-leg.js +9 -5
  95. package/dist/http/execution-context.d.ts +218 -0
  96. package/dist/http/execution-context.js +330 -0
  97. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  98. package/dist/http/memory-compliance-failure-reply.js +5 -0
  99. package/dist/http/memory-layer-reply.d.ts +30 -0
  100. package/dist/http/memory-layer-reply.js +24 -0
  101. package/dist/http/resume-legs.js +63 -36
  102. package/dist/http/retired-service-deps.d.ts +1 -1
  103. package/dist/http/retired-service-deps.js +1 -1
  104. package/dist/http/route-ctx.d.ts +14 -8
  105. package/dist/http/routes/adoption.js +1 -1
  106. package/dist/http/routes/background.d.ts +29 -1
  107. package/dist/http/routes/background.js +29 -2
  108. package/dist/http/routes/devices.d.ts +13 -19
  109. package/dist/http/routes/devices.js +21 -26
  110. package/dist/http/routes/memory-bundle.js +3 -0
  111. package/dist/http/routes/memory-compliance.js +8 -1
  112. package/dist/http/routes/memory-consolidation.js +12 -1
  113. package/dist/http/routes/memory-origin.js +14 -2
  114. package/dist/http/routes/memory-policy.js +5 -0
  115. package/dist/http/routes/session-sync.js +110 -15
  116. package/dist/http/routes/sessions.d.ts +6 -0
  117. package/dist/http/routes/sessions.js +143 -22
  118. package/dist/http/routes/side-query.d.ts +12 -4
  119. package/dist/http/routes/side-query.js +4 -5
  120. package/dist/http/routes/tasks.js +41 -33
  121. package/dist/http/server.d.ts +40 -44
  122. package/dist/http/server.js +13 -2
  123. package/dist/http/session-inherit-env.d.ts +3 -4
  124. package/dist/http/session-inherit-env.js +1 -1
  125. package/dist/http/wire-types.d.ts +8 -12
  126. package/dist/identity-source.d.ts +1 -1
  127. package/dist/identity-source.js +1 -1
  128. package/dist/index.d.ts +1 -0
  129. package/dist/lane-home-probe.d.ts +68 -42
  130. package/dist/lane-home-probe.js +53 -30
  131. package/dist/leader/wire.js +5 -4
  132. package/dist/memory-bundle-engine.d.ts +16 -9
  133. package/dist/memory-bundle-engine.js +29 -6
  134. package/dist/memory-operator-faces.d.ts +85 -30
  135. package/dist/memory-operator-faces.js +136 -30
  136. package/dist/memory-project-layer.d.ts +3 -21
  137. package/dist/memory-project-layer.js +11 -8
  138. package/dist/memory-scope-stores.d.ts +73 -0
  139. package/dist/memory-scope-stores.js +86 -0
  140. package/dist/memory-scope.d.ts +29 -5
  141. package/dist/memory-scope.js +16 -4
  142. package/dist/memory-sync-client.d.ts +70 -18
  143. package/dist/memory-sync-client.js +215 -117
  144. package/dist/memory-sync.d.ts +1 -1
  145. package/dist/memory-sync.js +3 -2
  146. package/dist/observability/cost-taxonomy.d.ts +2 -2
  147. package/dist/observability/fail-open.d.ts +12 -0
  148. package/dist/observability/fail-open.js +12 -0
  149. package/dist/observability/run-terminal.js +0 -1
  150. package/dist/orchestration/workflow-notify-journal.js +4 -4
  151. package/dist/org-memory-admission.d.ts +4 -3
  152. package/dist/org-memory-admission.js +6 -6
  153. package/dist/parked-decide.d.ts +34 -9
  154. package/dist/parked-decide.js +12 -19
  155. package/dist/parked-revive-surface.d.ts +12 -15
  156. package/dist/parked-revive-surface.js +3 -6
  157. package/dist/peer-seat.d.ts +13 -7
  158. package/dist/peer-seat.js +37 -3
  159. package/dist/plugins/adoption-log-sql.js +4 -4
  160. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  161. package/dist/plugins/approval-ask-store-file.js +11 -0
  162. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  163. package/dist/plugins/approval-ask-store-memory.js +12 -0
  164. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  165. package/dist/plugins/approval-ask-store-sql.js +44 -26
  166. package/dist/plugins/approval-nonce-store.js +4 -4
  167. package/dist/plugins/blob-backend.d.ts +4 -4
  168. package/dist/plugins/blob-backend.js +7 -7
  169. package/dist/plugins/breaker-state-sql.js +2 -2
  170. package/dist/plugins/caching-session-store.d.ts +6 -2
  171. package/dist/plugins/caching-session-store.js +9 -3
  172. package/dist/plugins/checkpoint-index-columns.js +2 -2
  173. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  174. package/dist/plugins/checkpoint-store-sql.js +21 -23
  175. package/dist/plugins/device-store-sql.d.ts +25 -51
  176. package/dist/plugins/device-store-sql.js +68 -122
  177. package/dist/plugins/ensure-index.d.ts +1 -2
  178. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  179. package/dist/plugins/execution-placement-sql.js +223 -0
  180. package/dist/plugins/file-history-store-sql.d.ts +9 -8
  181. package/dist/plugins/file-history-store-sql.js +9 -9
  182. package/dist/plugins/file-run-store.d.ts +6 -1
  183. package/dist/plugins/file-run-store.js +4 -0
  184. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  185. package/dist/plugins/fork-routing-session-store.js +16 -0
  186. package/dist/plugins/image-bake-store-sql.js +9 -9
  187. package/dist/plugins/image-index-sql.js +11 -11
  188. package/dist/plugins/leader-run-store-sql.js +4 -4
  189. package/dist/plugins/local-session-store.d.ts +149 -50
  190. package/dist/plugins/local-session-store.js +382 -107
  191. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  192. package/dist/plugins/mailbox-store-sql.js +4 -4
  193. package/dist/plugins/memory-engine-tidb.js +5 -5
  194. package/dist/plugins/memory-key-guards.d.ts +4 -0
  195. package/dist/plugins/memory-key-guards.js +1 -0
  196. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  197. package/dist/plugins/memory-run-store.d.ts +1 -1
  198. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  199. package/dist/plugins/one-time-migrations.d.ts +117 -8
  200. package/dist/plugins/one-time-migrations.js +408 -38
  201. package/dist/plugins/org-delivery-quota-store.js +2 -2
  202. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  203. package/dist/plugins/outcome-ledger-sql.js +2 -2
  204. package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
  205. package/dist/plugins/permission-rule-store-sql.js +29 -32
  206. package/dist/plugins/pg-pool.d.ts +7 -3
  207. package/dist/plugins/pg-session-storage.d.ts +31 -20
  208. package/dist/plugins/pg-session-storage.js +120 -52
  209. package/dist/plugins/remote-env-adb.d.ts +18 -19
  210. package/dist/plugins/remote-env-adb.js +91 -66
  211. package/dist/plugins/remote-env-device.d.ts +23 -31
  212. package/dist/plugins/remote-env-device.js +19 -17
  213. package/dist/plugins/remote-env-e2b.d.ts +27 -21
  214. package/dist/plugins/remote-env-e2b.js +91 -45
  215. package/dist/plugins/remote-env-file-error.d.ts +3 -1
  216. package/dist/plugins/remote-env-file-error.js +9 -3
  217. package/dist/plugins/remote-env-host.d.ts +31 -34
  218. package/dist/plugins/remote-env-host.js +91 -72
  219. package/dist/plugins/remote-env-k8s.d.ts +13 -29
  220. package/dist/plugins/remote-env-k8s.js +63 -55
  221. package/dist/plugins/remote-env-local-docker.d.ts +24 -12
  222. package/dist/plugins/remote-env-local-docker.js +49 -61
  223. package/dist/plugins/remote-env-ssh.d.ts +11 -19
  224. package/dist/plugins/remote-env-ssh.js +74 -57
  225. package/dist/plugins/remote-shell.d.ts +100 -3
  226. package/dist/plugins/remote-shell.js +166 -2
  227. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  228. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  229. package/dist/plugins/retention-lane-store-sql.js +2 -2
  230. package/dist/plugins/retention-store-sql.d.ts +10 -7
  231. package/dist/plugins/retention-store-sql.js +13 -7
  232. package/dist/plugins/run-store-sql.d.ts +13 -3
  233. package/dist/plugins/run-store-sql.js +20 -15
  234. package/dist/plugins/schema-consistency.d.ts +21 -11
  235. package/dist/plugins/schema-consistency.js +15 -9
  236. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  237. package/dist/plugins/schema-drift-sentence.js +38 -0
  238. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  239. package/dist/plugins/schema-ensure-legs.js +2 -2
  240. package/dist/plugins/schema-live-columns.d.ts +1 -1
  241. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  242. package/dist/plugins/session-fork.d.ts +54 -0
  243. package/dist/plugins/session-fork.js +38 -1
  244. package/dist/plugins/session-row-sql.d.ts +22 -0
  245. package/dist/plugins/session-row-sql.js +81 -0
  246. package/dist/plugins/session-rules-column.d.ts +1 -1
  247. package/dist/plugins/session-rules-column.js +1 -1
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  250. package/dist/plugins/session-workspace-cell.js +1 -1
  251. package/dist/plugins/shared-memory-store-sql.js +2 -2
  252. package/dist/plugins/sql-driver.d.ts +10 -5
  253. package/dist/plugins/sql-driver.js +6 -6
  254. package/dist/plugins/sql-errors.js +1 -1
  255. package/dist/plugins/store-backend.d.ts +5 -7
  256. package/dist/plugins/store-backend.js +23 -11
  257. package/dist/plugins/store-contracts.d.ts +25 -5
  258. package/dist/plugins/task-list-store-sql.js +11 -11
  259. package/dist/plugins/tidb-pool.d.ts +12 -7
  260. package/dist/plugins/tidb-pool.js +13 -7
  261. package/dist/plugins/tidb-session-storage.js +4 -4
  262. package/dist/plugins/tidb-session-store.d.ts +57 -26
  263. package/dist/plugins/tidb-session-store.js +127 -59
  264. package/dist/plugins/tool-result-store-sql.d.ts +5 -4
  265. package/dist/plugins/tool-result-store-sql.js +21 -13
  266. package/dist/plugins/user-input-ledger-sql.js +2 -2
  267. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  268. package/dist/plugins/workflow-run-store-sql.js +19 -19
  269. package/dist/plugins/write-behind-counter.d.ts +2 -3
  270. package/dist/plugins/write-behind-counter.js +5 -5
  271. package/dist/project-memory.d.ts +9 -15
  272. package/dist/project-memory.js +5 -4
  273. package/dist/request-key-closure.js +1 -1
  274. package/dist/request-value-closure.d.ts +6 -7
  275. package/dist/request-value-closure.js +1 -3
  276. package/dist/resource-suspend.d.ts +14 -12
  277. package/dist/resource-suspend.js +1 -1
  278. package/dist/resource-window.d.ts +4 -3
  279. package/dist/run-local.d.ts +6 -0
  280. package/dist/run-local.js +34 -35
  281. package/dist/runs.d.ts +10 -5
  282. package/dist/runs.js +52 -66
  283. package/dist/security.d.ts +38 -12
  284. package/dist/security.js +8 -2
  285. package/dist/server-secret-env.d.ts +9 -5
  286. package/dist/session-children-retire.d.ts +27 -9
  287. package/dist/session-children-retire.js +9 -5
  288. package/dist/session-incarnation.d.ts +20 -10
  289. package/dist/session-incarnation.js +5 -3
  290. package/dist/session-policy-wire.d.ts +1 -1
  291. package/dist/session-row.d.ts +114 -5
  292. package/dist/session-row.js +55 -5
  293. package/dist/session-titler.d.ts +2 -2
  294. package/dist/sql-engine-posture.d.ts +1 -1
  295. package/dist/sql-engine-posture.js +1 -1
  296. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  297. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  298. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  299. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  300. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  301. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  302. package/dist/task-a2a.d.ts +1 -1
  303. package/dist/task-a2a.js +5 -6
  304. package/dist/task-cwd.d.ts +6 -1
  305. package/dist/task-mcp.d.ts +22 -9
  306. package/dist/task-mcp.js +17 -9
  307. package/dist/terminal.d.ts +27 -0
  308. package/dist/terminal.js +6 -0
  309. package/dist/tool-approval.d.ts +6 -0
  310. package/dist/tool-approval.js +8 -1
  311. package/dist/trace/project.d.ts +1 -0
  312. package/dist/trace/project.js +1 -0
  313. package/dist/trace/redact.d.ts +3 -1
  314. package/dist/trace/redact.js +80 -28
  315. package/dist/trace/self-minted-wire.d.ts +169 -2
  316. package/dist/trace/self-minted-wire.js +54 -1
  317. package/dist/url-refusal.d.ts +1 -1
  318. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  319. package/dist/web-admin-client/apply-effective.js +20 -10
  320. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  321. package/dist/web-admin-client/candidate-gate.js +1 -1
  322. package/dist/web-admin-client/http-client.d.ts +5 -4
  323. package/dist/web-admin-client/http-client.js +2 -2
  324. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  325. package/dist/web-admin-client/read-warnings.js +5 -9
  326. package/dist/web-admin-client/skills-mcp.js +3 -2
  327. package/dist/web-admin-client/types.d.ts +6 -0
  328. package/package.json +3 -3
  329. package/wire-changes/server@7.139.0.json +348 -0
  330. package/wire-changes/server@7.140.0.json +184 -0
  331. package/dist/http/device-placement.d.ts +0 -175
  332. package/dist/http/device-placement.js +0 -145
@@ -8,7 +8,7 @@ import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
8
8
  import { ensureIndexes } from "./ensure-index.js";
9
9
  import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
10
10
  import { incarnationFromCell, sessionFaceFromRow } from "../session-incarnation.js";
11
- import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
11
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
12
12
  import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
13
13
  import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
14
14
  import { readJsonObjectColumn } from "./sql-json-column.js";
@@ -16,6 +16,9 @@ import { retireStoredCardWords } from "../retired-card-words.js";
16
16
  import { redactSecrets } from "../trace/redact.js";
17
17
  import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, isOrphanSessionOwner, judgeDelegatedOwner } from "../delegated-session-owner.js";
18
18
  import { MAILBOX_KEYED_TABLES, mailboxKeyDeleteSql, storedMailboxKeyless } from "./mailbox-store-sql.js";
19
+ import { SESSION_META_PLACEMENT_COLUMNS, backfillWriterStatement, countOrphanBindingsStatement, migrateDeviceBindingsStatement } from "./execution-placement-sql.js";
20
+ import { APPROVAL_ASK_EXECUTION_LANE_COLUMN, APPROVAL_ASK_TABLE } from "./approval-ask-store-sql.js";
21
+ import { isUnusableArchive } from "../execution-placement.js";
19
22
  const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
20
23
  export function buildMigrationReceipts() {
21
24
  return {
@@ -45,6 +48,13 @@ export function buildMigrationReceipts() {
45
48
  parkedApprovalCardsBackfilled: 0,
46
49
  parkedApprovalCardRowsUnreadable: [],
47
50
  parkedApprovalCardRowsTooLong: [],
51
+ executionPlacementColumnsAdded: 0,
52
+ deviceBindingsMoved: 0,
53
+ deviceBindingsOrphaned: 0,
54
+ deviceBindingTablesDropped: 0,
55
+ executionPlacementsBackfilled: 0,
56
+ askExecutionLanesBackfilled: 0,
57
+ executionPlacementSessionsUnreadable: [],
48
58
  };
49
59
  }
50
60
  function completedReport(r) {
@@ -83,7 +93,14 @@ function changedAnything(r) {
83
93
  r.mailboxRejectedKeyRowsRemoved > 0 ||
84
94
  r.parkedApprovalCardsBackfilled > 0 ||
85
95
  r.parkedApprovalCardRowsUnreadable.length > 0 ||
86
- r.parkedApprovalCardRowsTooLong.length > 0);
96
+ r.parkedApprovalCardRowsTooLong.length > 0 ||
97
+ r.executionPlacementColumnsAdded > 0 ||
98
+ r.deviceBindingsMoved > 0 ||
99
+ r.deviceBindingsOrphaned > 0 ||
100
+ r.deviceBindingTablesDropped > 0 ||
101
+ r.executionPlacementsBackfilled > 0 ||
102
+ r.askExecutionLanesBackfilled > 0 ||
103
+ r.executionPlacementSessionsUnreadable.length > 0);
87
104
  }
88
105
  function readingsSnapshot(r) {
89
106
  return {
@@ -95,6 +112,7 @@ function readingsSnapshot(r) {
95
112
  sessionRuleRowsWithUnreadableDeny: [...r.sessionRuleRowsWithUnreadableDeny],
96
113
  parkedApprovalCardRowsUnreadable: [...r.parkedApprovalCardRowsUnreadable],
97
114
  parkedApprovalCardRowsTooLong: [...r.parkedApprovalCardRowsTooLong],
115
+ executionPlacementSessionsUnreadable: [...r.executionPlacementSessionsUnreadable],
98
116
  };
99
117
  }
100
118
  function receiptSentence(r) {
@@ -408,7 +426,7 @@ export const ONE_TIME_MIGRATIONS = [
408
426
  "`test/one-time-migrations.test.ts` 的 S-491 组与 `test/one-time-migrations-db-integration.test.ts` 的 M26 格。" +
409
427
  "**不随本条删**:键的铸点 `breakerKeyOf` / `routeDigestOf`(常设的键形)与新表 `breaker_state`",
410
428
  afterRemoval: "没有新的臂(如实):跳过了带本条的版本直接升的库,退役表 `circuit_breaker` 留在库里、没有读者也没有写者(断路器只读写 `breaker_state`)—— " +
411
- "出路 = 升级时先过一次带本条的版本,或手工 `DROP TABLE circuit_breaker`(建议性状态,删表安全)",
429
+ "出路 = 升级时先过一次带本条的版本(它起服删掉退役表);不先过也无害 —— 退役表没有读者也没有写者,留在库里只占空间(建议性状态,无数据价值)",
412
430
  reportKeys: ["retiredBreakerTablesDropped"],
413
431
  },
414
432
  {
@@ -466,7 +484,7 @@ export const ONE_TIME_MIGRATIONS = [
466
484
  "改后句:「this session's stored request still carries a request key this version retired and nothing rewrites it any more; it is not replayed — the task stays suspended until someone cancels it and resubmits it as a new task (promptLean in place of promptProfile)」);" +
467
485
  "同族注释(每个半句只归本条或 M28 条之一,不重复改;标〔另一条的〕的几处住在另一条迁移的文件 / 头注段里,那条已先删则宿主已不在,跳过):`src/http/checkpoint-ctx.ts`:「两条出路(运维在一次性迁移的窗内重启本服务 ⇒ 起服改写存体、同一请求可再发;否则作为新任务重提,仍停驻的先取消)」改成「出路(作为新任务重提,仍停驻的先取消)」、「**窗的日期不手写**」起到「删法见登记表 M30 的」止整段删、「存体里的旧键由一次性迁移在起服时原地改写」改成「存体里的旧键没有迁移改写」、「(M30,`stored-request-prompt-profile-m30.ts`,行改写经本模块的 {@link rewriteStoredBodyKeys})。改写不到的残留行」改成「仍带旧键的残留行」、「;存体由 M30 起服改写」删、「与 {@link rewriteStoredBodyKeys}」删、「(存量行非凭据顶层体键的改写,凭据位与读法记号原样随行)」删;`src/http/wire-types.ts`:「由一次性迁移 M30 在起服时按同一张映射原地改写」改成「没有迁移改写」、「(`stored-request-prompt-profile-m30.ts`;M30 · 窗口见 `plugins/one-time-migrations.ts` 登记表)」删;`src/spec-fields.ts`:「由一次性迁移 M30 在起服时改写成 `promptLean`(`stored-request-prompt-profile-m30.ts`),」改成「不改写,」;`src/boot/stage-06-runners.ts`:「M30(存体里的退役请求键 `promptProfile` 原地改写)同在这里、」删、「(同一只 ctx 行改写店腿;不要封签器),同样早于任何续跑入口。」删;`src/plugins/checkpoint-store-sql.ts`:「、M30(退役请求键改写)」删;`src/plugins/one-time-migrations.ts`:〔另一条的〕「M30 用同一只;」删;`src/checkpoint-ctx-seal-m28.ts`:〔另一条的〕「(M30 用同一只)」删;`test/task-agents-876.test.ts`:「体由一次性迁移 M30 在起服时原地改写」改成「体没有迁移改写」;`test/allow-list-reading-park-refusal.test.ts`:「一次性迁移 M30 没改写到的残留」改成「没有迁移改写的残留」;`test/api-error-text-freeze.test.ts`:「一次性迁移 M30 没改写到的残留」改成「没有迁移改写的残留」、「洞 = 窗的最后一天(绝对日期)」起到「不手写。」止整段删;" +
468
486
  "`http/stored-body-replay.ts` 里 `RETIRED_KEY_REWRITE_MIGRATION` 的 JSDoc 随表删;" +
469
- "`http/stored-body-replay.ts` 的 `RETIRED_KEY_REWRITE_MIGRATION`(`satisfies Record<RetiredRequestKey, string>`:M30 删后 `promptProfile` 仍是退役键,这张表只能整张删)、" +
487
+ "`http/stored-body-replay.ts` 的 `RETIRED_KEY_REWRITE_MIGRATION`(`satisfies Record<ReplayRefusedRetiredKey, string>`:M30 删后 `promptProfile` 仍是退役键,这张表只能整张删)、" +
470
488
  "判据里 `rewriteWindowEnd: migrationWindowEnd(…)` 那一项与 `migrationWindowEnd` 的引入行;`plugins/one-time-migrations.ts` 导出的 `migrationWindowEnd` 在无别的读者时一起删;" +
471
489
  "冻结表 `test/api-error-text-freeze.test.ts` 的 `FROZEN_RESULT_BODY_TEMPLATE` 里那一行搬回字面表(删完半句后拒句是整句字面);" +
472
490
  "`test/stored-request-prompt-profile-m30.test.ts` 的起服改写组、登记行组、「M28 登记行」组(M28 没先删时它还在:它读本文件顶部的引入与 COMMENTS 表,与登记行组同命)、「M30 起服接线」组(它钉的就是被删的那处调用与起服 info 行)与「M30 的混跑与回滚」组;「M30 的混跑与回滚」组随删:它读登记表头注 M30 段(随本行一起删)与 MIGRATION 7.137.0 节第 4 条(已发布的历史文字,留着不动);doc-claims ㊵ 的「嵌入方一段分清两只迁移的窗」格同样读本行的窗末日,随删(它与 M28 行共读,哪一行先删就先删这一格);「窗关后的残留」组保留,但它里面 (d) 读登记行的 `m30Row()` 用处(期望里的 `rewriteWindowEnd`)与 (d′) 整格随删,`m30Row` 助手随之删;" +
@@ -480,6 +498,39 @@ export const ONE_TIME_MIGRATIONS = [
480
498
  "(判据 `retired_request_key`,`http/stored-body-replay.ts`)、行字节不动 —— 出路 = 作为新任务重提(`promptLean` 代替 `promptProfile`;仍停驻的先取消),或升级时先过一次带本条的版本并在窗内起服",
481
499
  reportKeys: [],
482
500
  },
501
+ {
502
+ id: "M31",
503
+ introducedIn: "7.139.0",
504
+ releasedOn: "2026-10-29",
505
+ removableAfter: "2026-11-28",
506
+ removal: "删 `src/memory-sync-client.ts` 里的 `relocateSyncCursorsM31` / `SyncCursorRelocationM31Report` / `m31Window` / `scopeOfLegacyCursor` / `LEGACY_PEER_SEGMENT` / `LEGACY_ROOT_CURSOR_FILE`、" +
507
+ "`createMemorySyncRunner` 里的 `relocated` 旗标与 `round` 开头对 `relocateSyncCursorsM31` 的那一处调用(连同它上面那一行注释、info `memory_sync_cursor_relocated` 与 warn `memory_sync_cursor_not_relocated` 两行日志)、" +
508
+ "文件头注的 ⑤ 段与 `ONE_TIME_MIGRATIONS` / `migrationWindowOpen` / `OneTimeMigrationWindow` 的引入行;本文件头注的 M31 段;" +
509
+ "`test/memory-sync-client.test.ts` 里标题以 `🔴 M31:` 或 `M31 ` 起头的格、对 `relocateSyncCursorsM31` 的引入与常量 `LEGACY_PEER_DIR`,常量 `CURSOR_FILE` 的行注改成「没有读者」(用它的格随删后无人用它 ⇒ 常量一起删)",
510
+ afterRemoval: "没有新的臂(如实):跳过了带本条的版本直接升的数据根,升级前的游标还在 `sync-cursors/central/` 与 `sync-cursor-central.json`,没有读者 ⇒ 那几层以空基线首轮同步" +
511
+ "(两边都改过的条目按冲突上报,上一轮之后在一侧删掉的条目从另一侧回来)。出路 = 升级时先过一次带本条的版本并在窗内跑一轮同步,或升级前在旧版上跑完一轮同步、再删掉这些文件",
512
+ reportKeys: [],
513
+ },
514
+ {
515
+ id: "M32",
516
+ introducedIn: "7.139.0",
517
+ releasedOn: "2026-10-29",
518
+ removableAfter: "2026-11-28",
519
+ removal: "删 migrateStepsOn 的 M32 步(`switchExecutionPlacement`、步骤表 `EXECUTION_PLACEMENT_SWITCH_STEPS`、回填前提表 `EXECUTION_PLACEMENT_BACKFILL_NEEDS`、" +
520
+ "`readSwitchShape` / `switchStarted` / `switchComplete` / `refuseOnSwitchFence` / `refuseOnAmbiguousBackfill` / `sessionRowsAwaitingBackfill` / `bindingRootKey`、" +
521
+ "`RETIRED_DEVICE_BINDING_TABLE` / `PARKED_DEVICE_BINDING_TABLE` / `FENCE_ROOTS_SHOWN`、`ExecutionPlacementSwitchRefusedError` / `EXECUTION_PLACEMENT_SWITCH_REFUSED_CODE` / `ExecutionPlacementSwitchRefusalReason`)" +
522
+ "与 DDL 前探针(`assertExecutionPlacementSwitchAdmissible` / `assertExecutionPlacementSwitchAdmissibleOnPool`,`store-backend.ts` 的 `withRetiredSchemaProbe` 里调它的那一行);" +
523
+ "`runSqlOneTimeMigrations` 的第三参 `ExecutionPlacementSwitchInput`(若再无别的条目用它)与它在两只 SQL 后端 / 测试夹具上的实参;local 腿(`LocalBackend.runOneTimeMigrations` 里的 `backfillLocalExecutionPlacements` 与 `FileRunStore.sessionIdsWithRuns`);" +
524
+ "`execution-placement-sql.ts` 的 `backfillWriterStatement` / `migrateDeviceBindingsStatement` / `countOrphanBindingsStatement`;报告、日志行与 `NO_SQL_MIGRATIONS` 的 `executionPlacementColumnsAdded` / `deviceBindingsMoved` / " +
525
+ "`deviceBindingsOrphaned` / `deviceBindingTablesDropped` / `executionPlacementsBackfilled` / `executionPlacementSessionsUnreadable` 与 `device_bindings_orphaned` / `execution_placement_session_unreadable` 两行 warn;`test/execution-placement-switch-db-integration.test.ts` 整份与 `test/one-time-migrations.test.ts` 的 M32 组。" +
526
+ "**local 腿的回填席与它的点名词一并删**:`LocalSessionStore` 的 `executionPlacementBackfill`(属性与构造里的那一臂)、`execution-placement.ts` 的 `LocalPlacementBackfillSeat` / `UnusableArchive` / `UnusableArchiveWord`(含它的 `storage` 词 —— 读存档文件时抛了别的故障、`errno` 随带,该词只有回填点名用)/ `isUnusableArchive`、" +
527
+ "`ExecutionPlacementSessionUnreadable`;`local-session-store.ts` 里 `placementRead` 并回 `readExecutionPlacement`、`metaRead` 的 unusable 臂与 `forkingOf` 的 `storage` 臂改成就地上抛 `probe.fault`(`archiveFaultWord` / `unusableArchiveOf` 随词删,探针只留「不在」与已知在而用不了的读法);" +
528
+ "`test/execution-placement-switch-local.test.ts` 与 `test/execution-placement-local-boot-unreadable-e2e.test.ts` 整份。" +
529
+ "**不随本条删**:执行放置八列、会话店的执行席、设备店的首绑 / 换绑两只写(常设的数据形)",
530
+ afterRemoval: "起服对账一族的跳版库拒启臂接住:删码同版把 `device_session` / `device_session_retired` 两张表加进退役 schema 判据(`schema-consistency.ts` 的跳版库探针,先于建表)⇒ " +
531
+ "跳过了带本条的版本直接升的库响亮拒启,出路 = 先在带本条的版本起服一次让切换走完,再升;缺执行放置列的库由列集对账缺列拒启(`config.schema_mismatch`,拒句给 ALTER),同一出路",
532
+ reportKeys: ["executionPlacementColumnsAdded", "deviceBindingsMoved", "deviceBindingsOrphaned", "deviceBindingTablesDropped", "executionPlacementsBackfilled", "askExecutionLanesBackfilled", "executionPlacementSessionsUnreadable"],
533
+ },
483
534
  ];
484
535
  export function migrationWindowOpen(window, today) {
485
536
  return window.removableAfter === null || today <= window.removableAfter;
@@ -521,6 +572,10 @@ export const NO_SQL_MIGRATIONS = {
521
572
  parkedApprovalCardsBackfilled: 0,
522
573
  parkedApprovalCardRowsUnreadable: [],
523
574
  parkedApprovalCardRowsTooLong: [],
575
+ executionPlacementColumnsAdded: 0,
576
+ deviceBindingsMoved: 0,
577
+ deviceBindingsOrphaned: 0,
578
+ deviceBindingTablesDropped: 0,
524
579
  };
525
580
  function mysqlDb(conn) {
526
581
  return {
@@ -547,7 +602,7 @@ function pgDb(client) {
547
602
  };
548
603
  }
549
604
  async function tableExists(db, dialect, table) {
550
- const sql = dialect === "tidb"
605
+ const sql = dialect === "mysql"
551
606
  ? `SELECT 1 AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${table}'`
552
607
  : `SELECT 1 AS present FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = '${table}'`;
553
608
  return (await db.rows(sql)).length > 0;
@@ -562,12 +617,12 @@ async function execUntil(db, sql, postcondition) {
562
617
  }
563
618
  }
564
619
  async function columnExists(db, dialect, table, column) {
565
- const scope = dialect === "tidb" ? "DATABASE()" : "current_schema()";
620
+ const scope = dialect === "mysql" ? "DATABASE()" : "current_schema()";
566
621
  const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
567
622
  return (await db.rows(sql)).length > 0;
568
623
  }
569
- export async function runSqlOneTimeMigrations(dialect, pool) {
570
- return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
624
+ export async function runSqlOneTimeMigrations(dialect, pool, input) {
625
+ return dialect === "mysql" ? withMysqlLock(pool, (db) => migrateOn(db, "mysql", input)) : withPgLock(pool, (db) => migrateOn(db, "pg", input));
571
626
  }
572
627
  async function withMysqlLock(pool, run) {
573
628
  const conn = await pool.getConnection();
@@ -597,21 +652,21 @@ async function withPgLock(pool, run) {
597
652
  client.release();
598
653
  }
599
654
  }
600
- async function migrateOn(db, dialect) {
655
+ async function migrateOn(db, dialect, input) {
601
656
  const receipts = buildMigrationReceipts();
602
657
  try {
603
- await migrateStepsOn(db, dialect, receipts);
658
+ await migrateStepsOn(db, dialect, receipts, input);
604
659
  }
605
660
  catch (e) {
606
661
  throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
607
662
  }
608
663
  return completedReport(receipts);
609
664
  }
610
- async function migrateStepsOn(db, dialect, receipts) {
611
- receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
665
+ async function migrateStepsOn(db, dialect, receipts, input) {
666
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "mysql"
612
667
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
613
668
  : "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]);
614
- receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
669
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "mysql"
615
670
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
616
671
  : "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} (%`]);
617
672
  await migrateShadowedRuleKey(db, dialect, receipts);
@@ -628,6 +683,289 @@ async function migrateStepsOn(db, dialect, receipts) {
628
683
  if (await tableExists(db, dialect, RETIRED_BREAKER_TABLE))
629
684
  await dropRetiredBreakerTable(db, dialect, receipts);
630
685
  await dropRejectedMailboxKeys(db, dialect, receipts);
686
+ await switchExecutionPlacement(db, dialect, receipts, input);
687
+ }
688
+ const RETIRED_DEVICE_BINDING_TABLE = "device_session";
689
+ const PARKED_DEVICE_BINDING_TABLE = "device_session_retired";
690
+ const BINDING_COLUMNS = "root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev";
691
+ const FENCE_ROOTS_SHOWN = 20;
692
+ export const EXECUTION_PLACEMENT_SWITCH_REFUSED_CODE = "config.execution_placement_switch_refused";
693
+ export class ExecutionPlacementSwitchRefusedError extends StoreBootRefusalError {
694
+ reason;
695
+ code = EXECUTION_PLACEMENT_SWITCH_REFUSED_CODE;
696
+ constructor(reason, detail) {
697
+ super(`${EXECUTION_PLACEMENT_SWITCH_REFUSED_CODE} (reason: ${reason}): ${detail}`);
698
+ this.reason = reason;
699
+ this.name = "ExecutionPlacementSwitchRefusedError";
700
+ }
701
+ }
702
+ async function readSwitchShape(db, dialect) {
703
+ const sessionMeta = await tableExists(db, dialect, "session_meta");
704
+ const placementColumns = new Set();
705
+ if (sessionMeta)
706
+ for (const c of SESSION_META_PLACEMENT_COLUMNS)
707
+ if (await columnExists(db, dialect, "session_meta", c.name))
708
+ placementColumns.add(c.name);
709
+ return {
710
+ sessionMeta,
711
+ placementColumns,
712
+ bindingTable: await tableExists(db, dialect, RETIRED_DEVICE_BINDING_TABLE),
713
+ parkedTable: await tableExists(db, dialect, PARKED_DEVICE_BINDING_TABLE),
714
+ taskRun: sessionMeta && (await tableExists(db, dialect, "task_run")),
715
+ ...(await askTableShape(db, dialect)),
716
+ };
717
+ }
718
+ async function askTableShape(db, dialect) {
719
+ const askTable = await tableExists(db, dialect, APPROVAL_ASK_TABLE);
720
+ return { askTable, askLaneColumn: askTable && (await columnExists(db, dialect, APPROVAL_ASK_TABLE, "execution_lane")) };
721
+ }
722
+ function switchStarted(s) {
723
+ return s.placementColumns.has("execution_role") || s.parkedTable;
724
+ }
725
+ const SWITCH_COMPLETION_COLUMN = "fork_key";
726
+ function switchComplete(s) {
727
+ return s.placementColumns.size === SESSION_META_PLACEMENT_COLUMNS.length && (!s.askTable || s.askLaneColumn) && !s.bindingTable && !s.parkedTable;
728
+ }
729
+ function askLaneBackfillSql(dialect) {
730
+ return dialect === "mysql"
731
+ ? `UPDATE ${APPROVAL_ASK_TABLE} a LEFT JOIN session_meta sm ON sm.session_id = a.session_id AND sm.placement_kind IS NULL SET a.execution_lane = COALESCE(sm.execution_lane, ?) WHERE a.execution_lane = ''`
732
+ : `UPDATE ${APPROVAL_ASK_TABLE} a SET execution_lane = COALESCE((SELECT sm.execution_lane FROM session_meta sm WHERE sm.session_id = a.session_id AND sm.placement_kind IS NULL), $1) WHERE a.execution_lane = ''`;
733
+ }
734
+ async function parkedRelationNamesWithRetiredPrefix(db) {
735
+ const rows = await db.rows(`SELECT i.relname AS name FROM pg_index x JOIN pg_class i ON i.oid = x.indexrelid JOIN pg_class t ON t.oid = x.indrelid WHERE t.relname = '${PARKED_DEVICE_BINDING_TABLE}' AND t.relnamespace = current_schema()::regnamespace AND i.relname LIKE '${RETIRED_DEVICE_BINDING_TABLE}\\_%'`);
736
+ return rows.map((r) => String(r["name"])).filter((name) => name.startsWith(`${RETIRED_DEVICE_BINDING_TABLE}_`) && !name.startsWith(PARKED_DEVICE_BINDING_TABLE));
737
+ }
738
+ const bindingRootKey = (dialect, alias) => (dialect === "mysql" ? `CONVERT(${alias}.root_session_id USING utf8mb4) COLLATE utf8mb4_bin` : `${alias}.root_session_id`);
739
+ async function refuseOnSwitchFence(read, dialect, shape) {
740
+ if (!switchStarted(shape) || !shape.bindingTable)
741
+ return;
742
+ const rows = await read(`SELECT ${bindingRootKey(dialect, "ds")} AS root FROM ${RETIRED_DEVICE_BINDING_TABLE} ds LIMIT ${FENCE_ROOTS_SHOWN + 1}`);
743
+ if (rows.length === 0)
744
+ return;
745
+ const roots = rows.slice(0, FENCE_ROOTS_SHOWN).map((r) => (Buffer.isBuffer(r["root"]) ? r["root"].toString("utf8") : String(r["root"])));
746
+ const more = rows.length > FENCE_ROOTS_SHOWN ? ` (and more — the table holds over ${FENCE_ROOTS_SHOWN} rows)` : "";
747
+ const rename = dialect === "mysql" ? `RENAME TABLE ${RETIRED_DEVICE_BINDING_TABLE} TO ${PARKED_DEVICE_BINDING_TABLE};` : `ALTER TABLE ${RETIRED_DEVICE_BINDING_TABLE} RENAME TO ${PARKED_DEVICE_BINDING_TABLE};`;
748
+ const adopt = shape.parkedTable
749
+ ? `DELETE FROM ${PARKED_DEVICE_BINDING_TABLE} WHERE root_session_id IN (SELECT root_session_id FROM ${RETIRED_DEVICE_BINDING_TABLE}); INSERT INTO ${PARKED_DEVICE_BINDING_TABLE} (${BINDING_COLUMNS}) SELECT ${BINDING_COLUMNS} FROM ${RETIRED_DEVICE_BINDING_TABLE}; DELETE FROM ${RETIRED_DEVICE_BINDING_TABLE};`
750
+ : rename;
751
+ throw new ExecutionPlacementSwitchRefusedError("fence", `the switch of session↔device bindings onto the root session rows has started on this database, yet the retired binding table ${RETIRED_DEVICE_BINDING_TABLE} holds rows again — ` +
752
+ `an earlier server version still writing it after the switch began, or rows written by hand. This server cannot tell whether they are newer than the placements ` +
753
+ `already on the session rows and refuses to pick: nothing was changed (this check runs before any DDL). Root sessions in that table: ${roots.join(", ")}${more}. ` +
754
+ `Stop every earlier-version replica first, then choose one: adopt those rows (they overwrite the device placement of their root sessions on the next start): \`${adopt}\` — ` +
755
+ `or discard them (the session rows keep what they hold): \`DELETE FROM ${RETIRED_DEVICE_BINDING_TABLE};\` — then start this version again; its own migration removes the emptied table on that start`);
756
+ }
757
+ async function sessionRowsAwaitingBackfill(read, dialect, shape) {
758
+ if (!shape.sessionMeta || !shape.taskRun)
759
+ return false;
760
+ const bindingSource = shape.parkedTable ? PARKED_DEVICE_BINDING_TABLE : shape.bindingTable ? RETIRED_DEVICE_BINDING_TABLE : undefined;
761
+ const sql = "SELECT 1 AS hit FROM session_meta sm WHERE sm.placement_kind IS NULL" +
762
+ (shape.placementColumns.has("execution_role") ? " AND sm.execution_role IS NULL" : "") +
763
+ " AND EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = sm.session_id)" +
764
+ (bindingSource !== undefined ? ` AND NOT EXISTS (SELECT 1 FROM ${bindingSource} ds WHERE ${bindingRootKey(dialect, "ds")} = sm.session_id)` : "") +
765
+ " LIMIT 1";
766
+ return (await read(sql)).length > 0;
767
+ }
768
+ async function askRowsAwaitingLane(read, _dialect, shape) {
769
+ if (!shape.askTable)
770
+ return false;
771
+ return (await read(`SELECT 1 AS hit FROM ${APPROVAL_ASK_TABLE}${shape.askLaneColumn ? " WHERE execution_lane = ''" : ""} LIMIT 1`)).length > 0;
772
+ }
773
+ const EXECUTION_PLACEMENT_BACKFILL_NEEDS = [
774
+ { what: "session rows that have runs but no execution placement (they would be placed on the only declared lane)", holds: sessionRowsAwaitingBackfill },
775
+ { what: "approval ask rows that do not record the execution lane they were raised on (they would record their root session's lane, or the only declared lane)", holds: askRowsAwaitingLane },
776
+ ];
777
+ async function refuseOnAmbiguousBackfill(read, dialect, shape, lanes) {
778
+ if (!shape.sessionMeta || switchComplete(shape) || lanes.length === 1)
779
+ return;
780
+ const pending = [];
781
+ for (const need of EXECUTION_PLACEMENT_BACKFILL_NEEDS)
782
+ if (await need.holds(read, dialect, shape))
783
+ pending.push(need.what);
784
+ if (pending.length === 0)
785
+ return;
786
+ throw new ExecutionPlacementSwitchRefusedError("backfill_ambiguous", `this database still holds rows the switch must place on an execution lane (${pending.join("; ")}), and the deployment declares ${lanes.length === 0 ? "no lane" : `${lanes.length} lanes (${lanes.join(", ")})`} — ` +
787
+ "with more than one lane there is no single right answer, and this server will not guess one. Nothing was changed (this check runs before any DDL). " +
788
+ "Start this version once with a single declared lane (REMOTE_EXEC set to one lane) so the switch completes, then declare the additional lanes");
789
+ }
790
+ export async function assertExecutionPlacementSwitchAdmissible(read, dialect, input) {
791
+ const shape = await readSwitchShape({ rows: (sql) => read(sql) }, dialect);
792
+ if (!shape.sessionMeta)
793
+ return;
794
+ await refuseOnSwitchFence(read, dialect, shape);
795
+ await refuseOnAmbiguousBackfill(read, dialect, shape, input.lanes);
796
+ }
797
+ export async function assertExecutionPlacementSwitchAdmissibleOnPool(dialect, pool, input) {
798
+ if (dialect === "mysql") {
799
+ const conn = await pool.getConnection();
800
+ try {
801
+ await assertExecutionPlacementSwitchAdmissible(async (sql) => (await conn.query(sql))[0], "mysql", input);
802
+ }
803
+ finally {
804
+ conn.release();
805
+ }
806
+ return;
807
+ }
808
+ const client = await pool.connect();
809
+ try {
810
+ await assertExecutionPlacementSwitchAdmissible(async (sql) => (await client.query(sql)).rows, "pg", input);
811
+ }
812
+ finally {
813
+ client.release();
814
+ }
815
+ }
816
+ const EXECUTION_PLACEMENT_SWITCH_STEPS = [
817
+ {
818
+ id: "⓪ drop the empty retired binding table",
819
+ run: async ({ db, receipts, shape }) => {
820
+ const s = await shape();
821
+ if (!switchStarted(s) || !s.bindingTable)
822
+ return;
823
+ await execUntil(db, `DROP TABLE ${RETIRED_DEVICE_BINDING_TABLE}`, async () => !(await shape()).bindingTable);
824
+ receipts.deviceBindingTablesDropped += 1;
825
+ },
826
+ },
827
+ {
828
+ id: "① park the retired binding table",
829
+ run: async ({ db, dialect, shape }) => {
830
+ const s = await shape();
831
+ if (!s.bindingTable || s.placementColumns.has("execution_role"))
832
+ return;
833
+ const sql = dialect === "mysql" ? `RENAME TABLE ${RETIRED_DEVICE_BINDING_TABLE} TO ${PARKED_DEVICE_BINDING_TABLE}` : `ALTER TABLE ${RETIRED_DEVICE_BINDING_TABLE} RENAME TO ${PARKED_DEVICE_BINDING_TABLE}`;
834
+ await execUntil(db, sql, async () => {
835
+ const after = await shape();
836
+ return after.parkedTable && !after.bindingTable;
837
+ });
838
+ },
839
+ },
840
+ {
841
+ id: "①′ rename the parked table's constraints and indexes (PG)",
842
+ run: async ({ db, dialect, shape }) => {
843
+ if (dialect !== "pg" || !(await shape()).parkedTable)
844
+ return;
845
+ const stale = async () => parkedRelationNamesWithRetiredPrefix(db);
846
+ for (const name of await stale()) {
847
+ const renamed = `${PARKED_DEVICE_BINDING_TABLE}${name.slice(RETIRED_DEVICE_BINDING_TABLE.length)}`;
848
+ await execUntil(db, `ALTER INDEX "${name}" RENAME TO "${renamed}"`, async () => !(await stale()).includes(name));
849
+ }
850
+ },
851
+ },
852
+ {
853
+ id: "② add the execution placement columns",
854
+ run: async ({ db, dialect, receipts, shape }) => {
855
+ for (const column of SESSION_META_PLACEMENT_COLUMNS) {
856
+ if (column.name === SWITCH_COMPLETION_COLUMN)
857
+ continue;
858
+ if ((await shape()).placementColumns.has(column.name))
859
+ continue;
860
+ await execUntil(db, `ALTER TABLE session_meta ADD COLUMN ${column[dialect]}`, async () => (await shape()).placementColumns.has(column.name));
861
+ receipts.executionPlacementColumnsAdded += 1;
862
+ }
863
+ const ask = await shape();
864
+ if (ask.askTable && !ask.askLaneColumn) {
865
+ await execUntil(db, `ALTER TABLE ${APPROVAL_ASK_TABLE} ADD COLUMN ${APPROVAL_ASK_EXECUTION_LANE_COLUMN[dialect]}`, async () => (await shape()).askLaneColumn);
866
+ receipts.executionPlacementColumnsAdded += 1;
867
+ }
868
+ },
869
+ },
870
+ {
871
+ id: "③ move the bindings onto the root session rows",
872
+ run: async ({ db, dialect, receipts, shape }) => {
873
+ if (!(await shape()).parkedTable)
874
+ return;
875
+ const orphans = countOrphanBindingsStatement(dialect, PARKED_DEVICE_BINDING_TABLE);
876
+ receipts.deviceBindingsOrphaned += Number((await db.rows(orphans.text, orphans.values))[0]?.["orphans"] ?? 0);
877
+ const move = migrateDeviceBindingsStatement(dialect, PARKED_DEVICE_BINDING_TABLE);
878
+ receipts.deviceBindingsMoved += await db.exec(move.text, move.values);
879
+ },
880
+ },
881
+ {
882
+ id: "④ backfill placements of sessions with runs",
883
+ run: async ({ db, dialect, input, receipts, shape }) => {
884
+ const s = await shape();
885
+ if (input.lanes.length !== 1) {
886
+ await refuseOnAmbiguousBackfill((sql) => db.rows(sql), dialect, s, input.lanes);
887
+ return;
888
+ }
889
+ const lane = input.lanes[0];
890
+ const backfill = backfillWriterStatement(dialect, lane, new Date());
891
+ receipts.executionPlacementsBackfilled += await db.exec(backfill.text, backfill.values);
892
+ },
893
+ },
894
+ {
895
+ id: "④b backfill the execution lane of approval ask rows",
896
+ run: async ({ db, dialect, input, receipts, shape }) => {
897
+ const s = await shape();
898
+ if (!s.askTable || !s.askLaneColumn)
899
+ return;
900
+ if (input.lanes.length !== 1) {
901
+ await refuseOnAmbiguousBackfill((sql) => db.rows(sql), dialect, s, input.lanes);
902
+ return;
903
+ }
904
+ receipts.askExecutionLanesBackfilled += await db.exec(askLaneBackfillSql(dialect), [input.lanes[0]]);
905
+ },
906
+ },
907
+ {
908
+ id: "⑤ drop the parked binding table",
909
+ run: async ({ db, receipts, shape }) => {
910
+ if (!(await shape()).parkedTable)
911
+ return;
912
+ await execUntil(db, `DROP TABLE ${PARKED_DEVICE_BINDING_TABLE}`, async () => !(await shape()).parkedTable);
913
+ receipts.deviceBindingTablesDropped += 1;
914
+ },
915
+ },
916
+ {
917
+ id: "⑥ add the completion marker column",
918
+ run: async ({ db, dialect, receipts, shape }) => {
919
+ if ((await shape()).placementColumns.has(SWITCH_COMPLETION_COLUMN))
920
+ return;
921
+ const marker = SESSION_META_PLACEMENT_COLUMNS.find((c) => c.name === SWITCH_COMPLETION_COLUMN);
922
+ await execUntil(db, `ALTER TABLE session_meta ADD COLUMN ${marker[dialect]}`, async () => (await shape()).placementColumns.has(SWITCH_COMPLETION_COLUMN));
923
+ receipts.executionPlacementColumnsAdded += 1;
924
+ },
925
+ },
926
+ ];
927
+ export async function switchExecutionPlacement(db, dialect, receipts, input) {
928
+ const shape = () => readSwitchShape(db, dialect);
929
+ const start = await shape();
930
+ if (!start.sessionMeta || switchComplete(start))
931
+ return;
932
+ await refuseOnSwitchFence((sql) => db.rows(sql), dialect, start);
933
+ for (const step of EXECUTION_PLACEMENT_SWITCH_STEPS)
934
+ await step.run({ db, dialect, input, receipts, shape });
935
+ }
936
+ export async function backfillLocalExecutionPlacements(sessionIds, seat, input) {
937
+ const absent = [];
938
+ const unreadable = [];
939
+ for (const root of sessionIds) {
940
+ const reading = await seat.read(root);
941
+ if (reading === undefined)
942
+ continue;
943
+ if (isUnusableArchive(reading))
944
+ unreadable.push({ sessionId: root, fault: reading.word, ...(reading.errno !== undefined ? { errno: reading.errno } : {}) });
945
+ else if (reading.cell.role === "absent")
946
+ absent.push({ root, occupant: reading.occupant });
947
+ }
948
+ if (absent.length === 0)
949
+ return { backfilled: 0, unreadable };
950
+ if (input.lanes.length !== 1) {
951
+ throw new ExecutionPlacementSwitchRefusedError("backfill_ambiguous", `the local data root holds sessions that have runs but no execution placement (${absent.length}), and the deployment declares ${input.lanes.length === 0 ? "no lane" : `${input.lanes.length} lanes (${input.lanes.join(", ")})`} — ` +
952
+ "with more than one lane there is no single right answer, and this server will not guess one. Nothing was changed. " +
953
+ "Start this version once with a single declared lane (REMOTE_EXEC set to one lane), then declare the additional lanes");
954
+ }
955
+ const lane = input.lanes[0];
956
+ let placed = 0;
957
+ for (const { root, occupant } of absent) {
958
+ if ((await seat.place(root, occupant, { role: "writer", lane })).outcome === "placed")
959
+ placed += 1;
960
+ }
961
+ return { backfilled: placed, unreadable };
962
+ }
963
+ export function backfillLocalAskExecutionLanes(store, input) {
964
+ return store.backfillExecutionLanes(input.lanes, (pending) => {
965
+ throw new ExecutionPlacementSwitchRefusedError("backfill_ambiguous", `the local approval ask journal holds asks that do not record the execution lane they were raised on (${pending}), and the deployment declares ${input.lanes.length === 0 ? "no lane" : `${input.lanes.length} lanes (${input.lanes.join(", ")})`} — ` +
966
+ "with more than one lane there is no single right answer, and this server will not guess one. Nothing was changed. " +
967
+ "Start this version once with a single declared lane (REMOTE_EXEC set to one lane), then declare the additional lanes");
968
+ });
631
969
  }
632
970
  const MAILBOX_KEY_SCAN_PAGE = 1000;
633
971
  async function dropRejectedMailboxKeys(db, dialect, receipts) {
@@ -635,7 +973,7 @@ async function dropRejectedMailboxKeys(db, dialect, receipts) {
635
973
  for (const table of MAILBOX_KEYED_TABLES)
636
974
  if (await tableExists(db, dialect, table))
637
975
  present.push(table);
638
- const p = (i) => (dialect === "tidb" ? "?" : `$${i}`);
976
+ const p = (i) => (dialect === "mysql" ? "?" : `$${i}`);
639
977
  for (const table of present) {
640
978
  let after;
641
979
  for (;;) {
@@ -669,14 +1007,14 @@ const M24_OWNER_INCARNATION = "m24-backfill";
669
1007
  const M24_COPY_BATCH_BYTES = 32 * 1024 * 1024;
670
1008
  const M24_COPY_SCAN_PAGE = 1000;
671
1009
  function journalKeyBound(dialect, op, first) {
672
- const p = (i) => (dialect === "tidb" ? "?" : `$${first + i}`);
1010
+ const p = (i) => (dialect === "mysql" ? "?" : `$${first + i}`);
673
1011
  return op === "after"
674
1012
  ? `(${WORKFLOW_JOURNAL_TABLE}.run_id > ${p(0)} OR (${WORKFLOW_JOURNAL_TABLE}.run_id = ${p(1)} AND ordinal > ${p(2)}))`
675
1013
  : `(${WORKFLOW_JOURNAL_TABLE}.run_id < ${p(0)} OR (${WORKFLOW_JOURNAL_TABLE}.run_id = ${p(1)} AND ordinal <= ${p(2)}))`;
676
1014
  }
677
1015
  async function copyJournalInBatches(db, dialect) {
678
1016
  const T = WORKFLOW_JOURNAL_TABLE;
679
- const bytes = dialect === "tidb" ? "LENGTH(result)" : "OCTET_LENGTH(result)";
1017
+ const bytes = dialect === "mysql" ? "LENGTH(result)" : "OCTET_LENGTH(result)";
680
1018
  const copyHead = `INSERT INTO ${WORKFLOW_JOURNAL_STAGING} (run_id, incarnation, ordinal, call_key, result, created_at_ms) ` +
681
1019
  `SELECT ${T}.run_id, o.incarnation, ordinal, call_key, ${jsonColumnText(dialect, "result")}, created_at_ms FROM ${T} ` +
682
1020
  `JOIN ${WORKFLOW_JOURNAL_OWNER_TABLE} o ON o.run_id = ${T}.run_id AND o.scope = ${T}.${RETIRED_JOURNAL_SCOPE_COLUMN} WHERE `;
@@ -715,7 +1053,7 @@ export async function moveWorkflowJournalScope(db, dialect, receipts) {
715
1053
  receipts.workflowJournalScopeColumn = "current";
716
1054
  return;
717
1055
  }
718
- const ddl = (dialect === "tidb" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
1056
+ const ddl = (dialect === "mysql" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
719
1057
  if (ddl === undefined)
720
1058
  throw new Error(`one-time migration M24: the central schema carries no definition of ${T} — refusing to rebuild a table it cannot define`);
721
1059
  if (await exists(S))
@@ -729,7 +1067,7 @@ export async function moveWorkflowJournalScope(db, dialect, receipts) {
729
1067
  await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
730
1068
  await copyJournalInBatches(db, dialect);
731
1069
  const swapped = async () => !(await exists(S)) && (await exists(T)) && !(await legacy());
732
- if (dialect === "tidb") {
1070
+ if (dialect === "mysql") {
733
1071
  await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
734
1072
  receipts.workflowJournalForeignScopeRowsRemoved += foreign;
735
1073
  await execUntil(db, `DROP TABLE IF EXISTS ${O}`, async () => !(await exists(O)));
@@ -772,9 +1110,9 @@ const M17_PROVEN_GRAMMAR = 1;
772
1110
  const RETIRED_BUCKET_PATTERNS = ['%"commandDeny"%', '%"commandAllow"%'];
773
1111
  async function scanSessionRuleRows(db, dialect, column, patterns, visit) {
774
1112
  const text = jsonColumnTextExpr(dialect, column);
775
- const prefilter = patterns.length === 0 ? "" : ` AND (${patterns.map((_, i) => `${text} LIKE ${dialect === "tidb" ? "?" : `$${i + 2}`}`).join(" OR ")})`;
776
- const scan = dialect === "tidb"
777
- ? `SELECT policy_key, session_id, rev, ${jsonColumnText("tidb", column)} FROM ${SESSION_POLICY_TABLE} WHERE policy_key > ?${prefilter} ORDER BY policy_key LIMIT ${SESSION_RULES_SCAN_PAGE}`
1113
+ const prefilter = patterns.length === 0 ? "" : ` AND (${patterns.map((_, i) => `${text} LIKE ${dialect === "mysql" ? "?" : `$${i + 2}`}`).join(" OR ")})`;
1114
+ const scan = dialect === "mysql"
1115
+ ? `SELECT policy_key, session_id, rev, ${jsonColumnText("mysql", column)} FROM ${SESSION_POLICY_TABLE} WHERE policy_key > ?${prefilter} ORDER BY policy_key LIMIT ${SESSION_RULES_SCAN_PAGE}`
778
1116
  : `SELECT policy_key, session_id, rev, ${jsonColumnText("pg", column)} FROM ${SESSION_POLICY_TABLE} WHERE policy_key > $1${prefilter} ORDER BY policy_key LIMIT ${SESSION_RULES_SCAN_PAGE}`;
779
1117
  let cursor = "";
780
1118
  for (;;) {
@@ -799,7 +1137,7 @@ async function scanSessionRuleRows(db, dialect, column, patterns, visit) {
799
1137
  async function judgeOldGrammarRows(db, dialect, receipts) {
800
1138
  const column = OLD_GRAMMAR_SESSION_RULES_COLUMN;
801
1139
  const text = jsonColumnTextExpr(dialect, column);
802
- const cas = dialect === "tidb"
1140
+ const cas = dialect === "mysql"
803
1141
  ? `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = ? WHERE policy_key = ? AND CAST(${text} AS BINARY) = CAST(? AS BINARY)`
804
1142
  : `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
805
1143
  await scanSessionRuleRows(db, dialect, column, [], async (row) => {
@@ -850,12 +1188,12 @@ export async function migrateSessionRuleGrammar(db, dialect, receipts) {
850
1188
  });
851
1189
  }
852
1190
  function renameSessionRulesColumnSql(dialect, from, to) {
853
- const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
854
- return dialect === "tidb" ? `ALTER TABLE ${SESSION_POLICY_TABLE} CHANGE COLUMN ${from} ${to}${definition}` : `ALTER TABLE ${SESSION_POLICY_TABLE} RENAME COLUMN ${from} TO ${to}`;
1191
+ const definition = SESSION_RULES_COLUMN.mysql.slice(SESSION_RULES_COLUMN.name.length);
1192
+ return dialect === "mysql" ? `ALTER TABLE ${SESSION_POLICY_TABLE} CHANGE COLUMN ${from} ${to}${definition}` : `ALTER TABLE ${SESSION_POLICY_TABLE} RENAME COLUMN ${from} TO ${to}`;
855
1193
  }
856
1194
  const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
857
1195
  async function primaryKeyWidth(db, dialect, table) {
858
- const sql = dialect === "tidb"
1196
+ const sql = dialect === "mysql"
859
1197
  ? `SELECT COUNT(*) AS n FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = '${table}' AND constraint_name = 'PRIMARY'`
860
1198
  : `SELECT COUNT(*) AS n FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ` +
861
1199
  `ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name ` +
@@ -869,10 +1207,10 @@ async function reshapeWorkflowParkIndex(db, dialect, receipts) {
869
1207
  const S = WORKFLOW_PARK_INDEX_STAGING;
870
1208
  const O = WORKFLOW_PARK_INDEX_RETIRED;
871
1209
  const exists = (t) => tableExists(db, dialect, t);
872
- const ddl = (dialect === "tidb" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
1210
+ const ddl = (dialect === "mysql" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
873
1211
  if (ddl === undefined)
874
1212
  throw new Error(`one-time migration M9: the central schema carries no definition of ${T} — refusing to reshape a table it cannot rebuild`);
875
- const copyPointers = (to, from) => db.exec(dialect === "tidb"
1213
+ const copyPointers = (to, from) => db.exec(dialect === "mysql"
876
1214
  ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
877
1215
  : `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
878
1216
  const resumed = await exists(S);
@@ -892,7 +1230,7 @@ async function reshapeWorkflowParkIndex(db, dialect, receipts) {
892
1230
  await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
893
1231
  receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
894
1232
  const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
895
- if (dialect === "tidb") {
1233
+ if (dialect === "mysql") {
896
1234
  await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
897
1235
  await execUntil(db, `DROP TABLE IF EXISTS ${O}`, async () => !(await exists(O)));
898
1236
  }
@@ -922,7 +1260,7 @@ async function execAtomically(db, statements, postcondition) {
922
1260
  }
923
1261
  }
924
1262
  async function finishParkIndexShape(db, dialect) {
925
- await ensureIndexes(async (sql, params) => (dialect === "tidb" ? [await db.rows(sql, params)] : { rows: await db.rows(sql, params) }), dialect === "tidb" ? "mysql" : "pg", CENTRAL_SCHEMA_INDEXES.filter((spec) => spec.table === WORKFLOW_PARK_INDEX_TABLE));
1263
+ await ensureIndexes(async (sql, params) => (dialect === "mysql" ? [await db.rows(sql, params)] : { rows: await db.rows(sql, params) }), dialect, CENTRAL_SCHEMA_INDEXES.filter((spec) => spec.table === WORKFLOW_PARK_INDEX_TABLE));
926
1264
  if (dialect !== "pg")
927
1265
  return;
928
1266
  const conventional = `${WORKFLOW_PARK_INDEX_TABLE}_pkey`;
@@ -955,7 +1293,7 @@ async function migrateShadowedRuleKey(db, dialect, receipts) {
955
1293
  const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
956
1294
  let after = "";
957
1295
  for (;;) {
958
- const rows = await db.rows(dialect === "tidb"
1296
+ const rows = await db.rows(dialect === "mysql"
959
1297
  ? `SELECT token, ${jsonColumnText(dialect, "risk_descriptor")} FROM checkpoint WHERE ${jsonColumnTextExpr(dialect, "risk_descriptor")} LIKE ? AND token > ? ORDER BY token LIMIT ${SHADOWED_RULE_BATCH}`
960
1298
  : `SELECT token, ${jsonColumnText(dialect, "risk_descriptor")} FROM checkpoint WHERE ${jsonColumnTextExpr(dialect, "risk_descriptor")} LIKE $1 AND token > $2 ORDER BY token LIMIT ${SHADOWED_RULE_BATCH}`, [like, after]);
961
1299
  for (const r of rows) {
@@ -971,7 +1309,7 @@ async function migrateShadowedRuleKey(db, dialect, receipts) {
971
1309
  }
972
1310
  if (next.kind === "untouched")
973
1311
  continue;
974
- receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
1312
+ receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "mysql"
975
1313
  ? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
976
1314
  : `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
977
1315
  }
@@ -1004,7 +1342,7 @@ async function migrateRetiredCardWords(db, dialect, receipts) {
1004
1342
  const cardText = jsonColumnTextExpr(dialect, "card_json");
1005
1343
  let after = "";
1006
1344
  for (;;) {
1007
- const rows = await db.rows(dialect === "tidb"
1345
+ const rows = await db.rows(dialect === "mysql"
1008
1346
  ? `SELECT ask_id, ${jsonColumnText(dialect, "card_json")} FROM approval_ask WHERE (${cardText} LIKE ? OR ${cardText} LIKE ?) AND ask_id > ? ORDER BY ask_id LIMIT ${RETIRED_CARD_WORD_BATCH}`
1009
1347
  : `SELECT ask_id, ${jsonColumnText(dialect, "card_json")} FROM approval_ask WHERE (${cardText} LIKE $1 OR ${cardText} LIKE $2) AND ask_id > $3 ORDER BY ask_id LIMIT ${RETIRED_CARD_WORD_BATCH}`, [...RETIRED_CARD_WORD_PATTERNS, after]);
1010
1348
  for (const r of rows) {
@@ -1020,7 +1358,7 @@ async function migrateRetiredCardWords(db, dialect, receipts) {
1020
1358
  }
1021
1359
  if (next.kind === "untouched")
1022
1360
  continue;
1023
- receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
1361
+ receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "mysql"
1024
1362
  ? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
1025
1363
  : `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
1026
1364
  }
@@ -1060,7 +1398,7 @@ function m21Subtree(cell, where) {
1060
1398
  }
1061
1399
  }
1062
1400
  export async function repairParkedRows(db, dialect, receipts) {
1063
- const ph = (n) => (dialect === "tidb" ? "?" : `$${n}`);
1401
+ const ph = (n) => (dialect === "mysql" ? "?" : `$${n}`);
1064
1402
  const kinds = APPROVAL_GATE_KINDS;
1065
1403
  const kindSlots = kinds.map((_, i) => ph(i + 1)).join(", ");
1066
1404
  const scan = `SELECT token, session_id, gate_kind, ${jsonColumnText(dialect, "checkpoint", M21_GATE)}, ${jsonColumnText(dialect, "checkpoint", M21_PENDING_ACTION)} FROM checkpoint ` +
@@ -1103,7 +1441,7 @@ export async function repairParkedRows(db, dialect, receipts) {
1103
1441
  continue;
1104
1442
  }
1105
1443
  const next = encode({ ...doc, gate: { ...gate, approvalCard: QUESTION_CARD } }, "checkpoint (M21 question-card repair)");
1106
- receipts.parkedQuestionCardsWritten += await db.exec(dialect === "tidb"
1444
+ receipts.parkedQuestionCardsWritten += await db.exec(dialect === "mysql"
1107
1445
  ? `UPDATE checkpoint SET checkpoint = ? WHERE token = ? AND status = 'pending' AND CAST(${docText} AS BINARY) = CAST(? AS BINARY)`
1108
1446
  : `UPDATE checkpoint SET checkpoint = $1::jsonb WHERE token = $2 AND status = 'pending' AND ${docText} = $3`, [next, token, String(full.checkpoint)]);
1109
1447
  }
@@ -1116,7 +1454,7 @@ const readM29Document = createObjectColumnReader();
1116
1454
  export async function backfillParkedApprovalCards(db, dialect, receipts) {
1117
1455
  if (!(await columnExists(db, dialect, "checkpoint", "approval_card")))
1118
1456
  return;
1119
- const ph = (n) => (dialect === "tidb" ? "?" : `$${n}`);
1457
+ const ph = (n) => (dialect === "mysql" ? "?" : `$${n}`);
1120
1458
  const kinds = APPROVAL_GATE_KINDS;
1121
1459
  const scan = `SELECT token FROM checkpoint WHERE status = 'pending' AND approval_card IS NULL AND gate_kind IN (${kinds.map((_, i) => ph(i + 1)).join(", ")}) ` +
1122
1460
  `AND token > ${ph(kinds.length + 1)} ORDER BY token LIMIT ${PARKED_CARD_BACKFILL_PAGE}`;
@@ -1172,7 +1510,7 @@ function placedOwnerScanSql(dialect) {
1172
1510
  "p.session_id AS parent_row, p.owner AS parent_owner, p.incarnation AS parent_incarnation, p.placement_kind AS parent_kind, " +
1173
1511
  "c.placement_root_session_id AS root_id, c.placement_parent_session_id AS parent_id " +
1174
1512
  "FROM session_meta c LEFT JOIN session_meta r ON r.session_id = NULLIF(c.placement_root_session_id, '') LEFT JOIN session_meta p ON p.session_id = NULLIF(c.placement_parent_session_id, '') " +
1175
- `WHERE c.placement_kind IS NOT NULL AND c.owner IS NULL AND c.session_id > ${dialect === "tidb" ? "?" : "$1"} ORDER BY c.session_id LIMIT ${PLACED_OWNER_SCAN_PAGE}`);
1513
+ `WHERE c.placement_kind IS NOT NULL AND c.owner IS NULL AND c.session_id > ${dialect === "mysql" ? "?" : "$1"} ORDER BY c.session_id LIMIT ${PLACED_OWNER_SCAN_PAGE}`);
1176
1514
  }
1177
1515
  function scannedPlacement(r) {
1178
1516
  return {
@@ -1257,6 +1595,13 @@ function readingFields(r) {
1257
1595
  parkedApprovalCardsBackfilled: r.parkedApprovalCardsBackfilled,
1258
1596
  parkedApprovalCardRowsUnreadable: r.parkedApprovalCardRowsUnreadable.length,
1259
1597
  parkedApprovalCardRowsTooLong: r.parkedApprovalCardRowsTooLong.length,
1598
+ executionPlacementColumnsAdded: r.executionPlacementColumnsAdded,
1599
+ deviceBindingsMoved: r.deviceBindingsMoved,
1600
+ deviceBindingsOrphaned: r.deviceBindingsOrphaned,
1601
+ deviceBindingTablesDropped: r.deviceBindingTablesDropped,
1602
+ executionPlacementsBackfilled: r.executionPlacementsBackfilled,
1603
+ askExecutionLanesBackfilled: r.askExecutionLanesBackfilled,
1604
+ executionPlacementSessionsUnreadable: r.executionPlacementSessionsUnreadable.length,
1260
1605
  };
1261
1606
  }
1262
1607
  export function logOneTimeMigrations(log, backend, report) {
@@ -1301,7 +1646,11 @@ export function logOneTimeMigrations(log, backend, report) {
1301
1646
  "delivery, reading nor session deletion would ever have touched them again; only the count is logged, never a key; M29: suspended rows " +
1302
1647
  "parked before the checkpoint column approval_card existed and still pending get that column — the approval card word on their gate, " +
1303
1648
  "derived by the same function the write path uses (the engine's checkpointIndexFacts); a row whose document cannot be read as a checkpoint " +
1304
- "is left NULL and counted, named by a parked_approval_card_row_unreadable line, and so is a row whose card word is longer than the column can hold, named by a parked_approval_card_row_too_long line). Each step " +
1649
+ "is left NULL and counted, named by a parked_approval_card_row_unreadable line, and so is a row whose card word is longer than the column can hold, named by a parked_approval_card_row_too_long line; " +
1650
+ "M32: session↔device bindings moved from the retired table device_session onto the root session rows (the table is first renamed device_session_retired, " +
1651
+ "the eight execution placement columns are added to session_meta, every binding overwrites the device placement of its root session row — a binding whose " +
1652
+ "root session row is gone is dropped and counted, named by a device_bindings_orphaned line — and the renamed table is dropped), and session rows that have " +
1653
+ "runs but no execution placement placed on the only declared lane; once the switch is complete this step does nothing). Each step " +
1305
1654
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
1306
1655
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
1307
1656
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -1309,6 +1658,27 @@ export function logOneTimeMigrations(log, backend, report) {
1309
1658
  logReceiptRows(log, backend, report);
1310
1659
  }
1311
1660
  function logReceiptRows(log, backend, r) {
1661
+ if (r.deviceBindingsOrphaned > 0) {
1662
+ log.warn("device_bindings_orphaned", {
1663
+ backend,
1664
+ count: r.deviceBindingsOrphaned,
1665
+ note: "session↔device bindings whose root session row no longer exists (or is a delegated child row) were dropped while moving bindings onto the root session rows — no session can run on them; nothing else changed",
1666
+ });
1667
+ }
1668
+ for (const row of r.executionPlacementSessionsUnreadable) {
1669
+ log.warn("execution_placement_session_unreadable", {
1670
+ backend,
1671
+ sessionId: row.sessionId,
1672
+ fault: row.fault,
1673
+ ...(row.errno !== undefined ? { errno: row.errno } : {}),
1674
+ note: "a local session that has runs (and either no session-metadata cell — it was written before the cell existed and has not been read since — or a fork-created cell) has an archive the server cannot read, " +
1675
+ "so its execution placement could not be backfilled. The session was skipped and nothing was written or changed: boot is not blocked, and every request that needs the " +
1676
+ "session keeps answering the archive's own fault (fault names it: corrupt_session = the session header cannot be read; invalid_session = the stored log's structure is broken; " +
1677
+ "storage = reading the archive file itself failed — errno names it, e.g. EACCES = no permission, EIO = disk I/O error, ERR_STRING_TOO_LONG = the file is too large to read) until the " +
1678
+ "archive file under the data root's sessions directory is readable again — restored from a kept copy, or (storage) its permissions or disk repaired; the next boot then backfills its placement as for any other session. " +
1679
+ "(Removing the archive file ends the warning but places nothing: the session then no longer exists on this server — except a fork-created session, whose cell stays and reads as a fork that has not landed: retry the same fork, or delete the session.) The session is named on every boot until then",
1680
+ });
1681
+ }
1312
1682
  for (const row of r.parkedApprovalCardRowsUnreadable) {
1313
1683
  log.warn("parked_approval_card_row_unreadable", {
1314
1684
  backend,