@sema-agent/server 7.131.0 → 7.133.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 (509) hide show
  1. package/MIGRATION.md +575 -0
  2. package/README.md +12 -9
  3. package/README.zh-CN.md +9 -6
  4. package/USAGE.md +326 -98
  5. package/deploy/sema-up/README.md +19 -1
  6. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -2
  7. package/deploy/sema-up/chart/templates/server.yaml +5 -2
  8. package/deploy/sema-up/stack/compose.yaml +3 -0
  9. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
  10. package/dist/adoption/plan.d.ts +3 -2
  11. package/dist/adoption/plan.js +7 -5
  12. package/dist/adoption/runner.js +3 -10
  13. package/dist/adoption/sql.d.ts +3 -2
  14. package/dist/adoption/sql.js +3 -5
  15. package/dist/adoption/wire.d.ts +4 -1
  16. package/dist/adoption/wire.js +1 -0
  17. package/dist/approval-ask-audit-store.d.ts +31 -13
  18. package/dist/approval-ask-audit-store.js +272 -74
  19. package/dist/approval-ask-machine.d.ts +15 -0
  20. package/dist/approval-ask-machine.js +6 -0
  21. package/dist/approval-ask-retention.d.ts +136 -0
  22. package/dist/approval-ask-retention.js +102 -0
  23. package/dist/approval-card.d.ts +82 -20
  24. package/dist/approval-card.js +46 -16
  25. package/dist/approval-deny-reasons.d.ts +1 -1
  26. package/dist/approval.d.ts +9 -0
  27. package/dist/approval.js +24 -3
  28. package/dist/audit.d.ts +33 -19
  29. package/dist/audit.js +12 -16
  30. package/dist/auth-bridge.d.ts +4 -0
  31. package/dist/auth-bridge.js +10 -11
  32. package/dist/bake-runner/main.js +3 -2
  33. package/dist/boot/deferred-sandbox-path-env.js +1 -1
  34. package/dist/boot/deployment-mods.d.ts +31 -0
  35. package/dist/boot/deployment-mods.js +26 -0
  36. package/dist/boot/execution-env.d.ts +1 -1
  37. package/dist/boot/execution-env.js +27 -12
  38. package/dist/boot/execution-lane.js +11 -0
  39. package/dist/boot/leader.js +2 -0
  40. package/dist/boot/memory-boundary.d.ts +2 -2
  41. package/dist/boot/memory-boundary.js +2 -2
  42. package/dist/boot/memory-consolidation.d.ts +5 -4
  43. package/dist/boot/memory-consolidation.js +7 -6
  44. package/dist/boot/org-delivery.d.ts +28 -0
  45. package/dist/boot/org-delivery.js +20 -0
  46. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  47. package/dist/boot/read-deny-anchor-audit.js +36 -0
  48. package/dist/boot/reapers.d.ts +3 -0
  49. package/dist/boot/reapers.js +21 -1
  50. package/dist/boot/resolve-spec.d.ts +4 -5
  51. package/dist/boot/resolve-spec.js +74 -123
  52. package/dist/boot/runner-deps.d.ts +19 -8
  53. package/dist/boot/runner-deps.js +15 -2
  54. package/dist/boot/session-faces.d.ts +2 -7
  55. package/dist/boot/session-faces.js +4 -2
  56. package/dist/boot/stage-02-web-admin-pull.js +4 -1
  57. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  58. package/dist/boot/stage-03-store-backend.js +2 -2
  59. package/dist/boot/stage-04-stores.d.ts +1 -1
  60. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  61. package/dist/boot/stage-05-execution-env.js +7 -10
  62. package/dist/boot/stage-06-runners.d.ts +4 -2
  63. package/dist/boot/stage-06-runners.js +11 -7
  64. package/dist/boot/stage-07-capability-layer.d.ts +4 -2
  65. package/dist/boot/stage-07-capability-layer.js +27 -6
  66. package/dist/boot/stage-08-reapers.d.ts +4 -2
  67. package/dist/boot/stage-08-reapers.js +2 -2
  68. package/dist/boot/stage-09-leader.d.ts +6 -9
  69. package/dist/boot/stage-10-http-server.d.ts +6 -9
  70. package/dist/boot/stage-10-http-server.js +7 -9
  71. package/dist/boot/stores.d.ts +1 -1
  72. package/dist/boot/stores.js +7 -8
  73. package/dist/boot/web-admin.d.ts +4 -3
  74. package/dist/boot/web-admin.js +186 -98
  75. package/dist/boot/workflow-orchestration.d.ts +2 -0
  76. package/dist/boot/workflow-orchestration.js +2 -1
  77. package/dist/brain.js +25 -25
  78. package/dist/breaker-key.d.ts +6 -0
  79. package/dist/breaker-key.js +29 -0
  80. package/dist/budget.js +5 -4
  81. package/dist/caller-text-echo.d.ts +23 -0
  82. package/dist/caller-text-echo.js +21 -0
  83. package/dist/capabilities/brand-identity.d.ts +26 -0
  84. package/dist/capabilities/brand-identity.js +27 -0
  85. package/dist/capabilities/center-plugins.d.ts +7 -0
  86. package/dist/capabilities/center-plugins.js +1 -1
  87. package/dist/capabilities/center-prompts.d.ts +32 -59
  88. package/dist/capabilities/center-prompts.js +18 -32
  89. package/dist/capabilities/composite-members.d.ts +1 -1
  90. package/dist/capabilities/composite-members.js +1 -1
  91. package/dist/capabilities/engine-capabilities.js +2 -1
  92. package/dist/capabilities/execution-lane.js +2 -2
  93. package/dist/capabilities/hands-lane.d.ts +30 -6
  94. package/dist/capabilities/hands-lane.js +11 -1
  95. package/dist/capabilities/mcp-probe.d.ts +48 -3
  96. package/dist/capabilities/mcp-probe.js +47 -135
  97. package/dist/capabilities/scenarios.d.ts +95 -45
  98. package/dist/capabilities/scenarios.js +53 -38
  99. package/dist/capabilities/skills.d.ts +19 -4
  100. package/dist/capabilities/skills.js +30 -10
  101. package/dist/capability-facts.d.ts +34 -4
  102. package/dist/capability-facts.js +28 -2
  103. package/dist/center-config-tier.d.ts +69 -0
  104. package/dist/center-config-tier.js +42 -0
  105. package/dist/checkpoint-identity.d.ts +16 -0
  106. package/dist/checkpoint-identity.js +19 -0
  107. package/dist/config-catalog.d.ts +17 -10
  108. package/dist/config-catalog.js +47 -32
  109. package/dist/config-d-migration.d.ts +3 -1
  110. package/dist/config-d-migration.js +2 -2
  111. package/dist/config-dir-trust.d.ts +114 -0
  112. package/dist/config-dir-trust.js +388 -0
  113. package/dist/config-invariants.d.ts +18 -5
  114. package/dist/config-invariants.js +35 -3
  115. package/dist/config-lkg.d.ts +9 -9
  116. package/dist/config-lkg.js +20 -4
  117. package/dist/config-provider.d.ts +9 -2
  118. package/dist/config-provider.js +18 -6
  119. package/dist/config-types.d.ts +75 -20
  120. package/dist/config-types.js +8 -0
  121. package/dist/config.d.ts +13 -0
  122. package/dist/config.js +137 -62
  123. package/dist/delegated-session-owner.d.ts +185 -0
  124. package/dist/delegated-session-owner.js +119 -0
  125. package/dist/device-protocol.d.ts +5 -3
  126. package/dist/device-protocol.js +1 -1
  127. package/dist/device-ws-hub.js +21 -2
  128. package/dist/device-ws-protocol.d.ts +70 -6
  129. package/dist/device-ws-protocol.js +50 -22
  130. package/dist/elicitation.js +3 -2
  131. package/dist/entry-problems.d.ts +34 -0
  132. package/dist/entry-problems.js +14 -0
  133. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  134. package/dist/env-name-allowlist-knobs.js +5 -4
  135. package/dist/exec-bounds.d.ts +15 -3
  136. package/dist/exec-bounds.js +4 -0
  137. package/dist/execution-lane-caps.d.ts +56 -10
  138. package/dist/execution-lane-caps.js +50 -0
  139. package/dist/field-conflict.d.ts +43 -0
  140. package/dist/field-conflict.js +20 -0
  141. package/dist/fleet/fleet-bus.d.ts +80 -6
  142. package/dist/fleet/fleet-bus.js +41 -9
  143. package/dist/hooks/classic-stations.d.ts +91 -0
  144. package/dist/hooks/classic-stations.js +27 -0
  145. package/dist/hooks/command-spawn.d.ts +57 -0
  146. package/dist/hooks/command-spawn.js +139 -0
  147. package/dist/hooks/hook-llm.d.ts +8 -5
  148. package/dist/hooks/hook-llm.js +79 -36
  149. package/dist/hooks/hook-runner.d.ts +349 -95
  150. package/dist/hooks/hook-runner.js +855 -568
  151. package/dist/hooks/hook-source.d.ts +44 -0
  152. package/dist/hooks/hook-source.js +40 -0
  153. package/dist/http/active-run-conflict.d.ts +5 -13
  154. package/dist/http/active-run-conflict.js +17 -27
  155. package/dist/http/admission.d.ts +8 -2
  156. package/dist/http/admission.js +188 -101
  157. package/dist/http/approval-sweeps.js +99 -21
  158. package/dist/http/caller-trust.d.ts +77 -0
  159. package/dist/http/caller-trust.js +143 -0
  160. package/dist/http/checkpoint-ctx.d.ts +41 -24
  161. package/dist/http/checkpoint-ctx.js +23 -16
  162. package/dist/http/claimed-run.d.ts +14 -0
  163. package/dist/http/claimed-run.js +15 -0
  164. package/dist/http/device-placement.d.ts +2 -4
  165. package/dist/http/device-placement.js +7 -11
  166. package/dist/http/dispatch.d.ts +0 -4
  167. package/dist/http/dispatch.js +15 -13
  168. package/dist/http/durable-run-leg.d.ts +6 -33
  169. package/dist/http/durable-run-leg.js +36 -10
  170. package/dist/http/health-self-report.d.ts +7 -5
  171. package/dist/http/health-self-report.js +4 -18
  172. package/dist/http/owner-access.d.ts +56 -0
  173. package/dist/http/owner-access.js +42 -0
  174. package/dist/http/principal-gate.d.ts +70 -28
  175. package/dist/http/principal-gate.js +61 -15
  176. package/dist/http/readiness.d.ts +11 -5
  177. package/dist/http/readiness.js +10 -3
  178. package/dist/http/refusal-body.d.ts +21 -0
  179. package/dist/http/refusal-body.js +4 -0
  180. package/dist/http/request-guards.d.ts +43 -23
  181. package/dist/http/request-guards.js +53 -17
  182. package/dist/http/resume-ctx-read.d.ts +2 -5
  183. package/dist/http/resume-ctx-read.js +7 -4
  184. package/dist/http/resume-legs.d.ts +28 -4
  185. package/dist/http/resume-legs.js +169 -91
  186. package/dist/http/retired-service-deps.d.ts +43 -0
  187. package/dist/http/retired-service-deps.js +18 -0
  188. package/dist/http/route-ctx.d.ts +44 -28
  189. package/dist/http/route-ctx.js +4 -0
  190. package/dist/http/route-table.js +2 -0
  191. package/dist/http/routes/a2a-serve.js +3 -4
  192. package/dist/http/routes/admin-config-refresh.js +1 -1
  193. package/dist/http/routes/admin-drain.js +1 -1
  194. package/dist/http/routes/admin-memory-optout.js +5 -1
  195. package/dist/http/routes/adoption.js +2 -9
  196. package/dist/http/routes/approvals-assistant.js +49 -48
  197. package/dist/http/routes/background.js +3 -2
  198. package/dist/http/routes/capabilities.js +21 -5
  199. package/dist/http/routes/config-catalog.d.ts +3 -3
  200. package/dist/http/routes/config-catalog.js +1 -1
  201. package/dist/http/routes/devices.js +3 -17
  202. package/dist/http/routes/diagnostics.js +6 -2
  203. package/dist/http/routes/images.js +6 -7
  204. package/dist/http/routes/leader.js +1 -8
  205. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  206. package/dist/http/routes/mcp-reconnect.js +3 -2
  207. package/dist/http/routes/memory-bundle.js +1 -1
  208. package/dist/http/routes/memory-compliance.js +1 -1
  209. package/dist/http/routes/memory-consolidation.js +1 -1
  210. package/dist/http/routes/memory-origin.js +1 -1
  211. package/dist/http/routes/memory-policy.d.ts +1 -1
  212. package/dist/http/routes/memory-policy.js +3 -2
  213. package/dist/http/routes/notify-wake.js +8 -21
  214. package/dist/http/routes/observability.js +2 -2
  215. package/dist/http/routes/principal-inbox.d.ts +39 -0
  216. package/dist/http/routes/principal-inbox.js +76 -0
  217. package/dist/http/routes/retention-ops.js +2 -9
  218. package/dist/http/routes/rules.js +5 -18
  219. package/dist/http/routes/runs.d.ts +5 -0
  220. package/dist/http/routes/runs.js +34 -107
  221. package/dist/http/routes/session-sync.js +15 -26
  222. package/dist/http/routes/sessions.js +30 -40
  223. package/dist/http/routes/shared-memory.js +2 -13
  224. package/dist/http/routes/side-query.js +8 -7
  225. package/dist/http/routes/tasks.js +67 -21
  226. package/dist/http/routes/workflows.d.ts +351 -0
  227. package/dist/http/routes/workflows.js +153 -119
  228. package/dist/http/send.d.ts +33 -10
  229. package/dist/http/send.js +21 -4
  230. package/dist/http/server.d.ts +27 -18
  231. package/dist/http/server.js +34 -6
  232. package/dist/http/stored-body-replay.js +17 -4
  233. package/dist/http/submit-ctx.d.ts +25 -0
  234. package/dist/http/submit-ctx.js +40 -0
  235. package/dist/http/submit-replay.d.ts +4 -3
  236. package/dist/http/submit-replay.js +5 -4
  237. package/dist/http/wire-types.d.ts +11 -8
  238. package/dist/index.d.ts +2 -2
  239. package/dist/index.js +1 -2
  240. package/dist/lane-home-probe.d.ts +112 -0
  241. package/dist/lane-home-probe.js +101 -0
  242. package/dist/leader/endpoint.d.ts +4 -1
  243. package/dist/leader/endpoint.js +11 -10
  244. package/dist/limit-refusal.d.ts +59 -0
  245. package/dist/limit-refusal.js +15 -0
  246. package/dist/main.js +1 -1
  247. package/dist/mcp-assertion-header.d.ts +32 -19
  248. package/dist/mcp-assertion-header.js +10 -9
  249. package/dist/mcp-tool-faces.d.ts +5 -0
  250. package/dist/mcp-tool-faces.js +6 -4
  251. package/dist/memory-project-layer.d.ts +37 -0
  252. package/dist/memory-project-layer.js +46 -0
  253. package/dist/memory-scope.d.ts +24 -17
  254. package/dist/memory-scope.js +24 -8
  255. package/dist/memory-sync-client.d.ts +27 -13
  256. package/dist/memory-sync-client.js +107 -18
  257. package/dist/model-entry-refusal.d.ts +9 -1
  258. package/dist/model-entry-refusal.js +6 -1
  259. package/dist/model-plane-face.d.ts +51 -0
  260. package/dist/model-plane-face.js +24 -0
  261. package/dist/model-provider.d.ts +44 -12
  262. package/dist/model-provider.js +21 -2
  263. package/dist/model-route-endpoint.d.ts +9 -12
  264. package/dist/model-route-endpoint.js +8 -11
  265. package/dist/model-route-env.d.ts +22 -0
  266. package/dist/model-route-env.js +2 -0
  267. package/dist/model-route-gap.d.ts +60 -30
  268. package/dist/model-route-gap.js +21 -15
  269. package/dist/observability/corrupt-read-seat.d.ts +7 -0
  270. package/dist/observability/corrupt-read-seat.js +9 -0
  271. package/dist/observability/cost-taxonomy.d.ts +7 -7
  272. package/dist/observability/err-text.d.ts +6 -0
  273. package/dist/observability/err-text.js +18 -4
  274. package/dist/observability/fail-open.d.ts +76 -14
  275. package/dist/observability/fail-open.js +76 -14
  276. package/dist/observability/metrics.js +4 -1
  277. package/dist/observability/prompt-manifest.d.ts +33 -82
  278. package/dist/observability/prompt-manifest.js +122 -17
  279. package/dist/observability/run-terminal.js +14 -6
  280. package/dist/observability/secret-env-scrub.d.ts +13 -1
  281. package/dist/observability/secret-env-scrub.js +13 -1
  282. package/dist/observability/tool-trace.d.ts +16 -21
  283. package/dist/observability/tool-trace.js +58 -49
  284. package/dist/operator-ask.d.ts +10 -0
  285. package/dist/operator-ask.js +3 -0
  286. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
  287. package/dist/orchestration/workflow-agent-session-index.js +9 -13
  288. package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
  289. package/dist/orchestration/workflow-completion-inbox.js +13 -0
  290. package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
  291. package/dist/orchestration/workflow-notify-journal.js +27 -34
  292. package/dist/orchestration/workflow-park-decision.d.ts +89 -0
  293. package/dist/orchestration/workflow-park-decision.js +118 -0
  294. package/dist/org-delivery-gate.d.ts +81 -0
  295. package/dist/org-delivery-gate.js +59 -0
  296. package/dist/panel-tool-names.d.ts +23 -0
  297. package/dist/panel-tool-names.js +8 -0
  298. package/dist/parked-decide.d.ts +39 -6
  299. package/dist/parked-decide.js +98 -51
  300. package/dist/parked-revive-surface.d.ts +9 -2
  301. package/dist/parked-revive-surface.js +11 -8
  302. package/dist/per-task-image.d.ts +7 -1
  303. package/dist/per-task-image.js +3 -3
  304. package/dist/permission-rule-list.d.ts +1 -1
  305. package/dist/permission-rule-list.js +2 -2
  306. package/dist/plain-string-record.d.ts +25 -0
  307. package/dist/plain-string-record.js +32 -0
  308. package/dist/plugins/approval-ask-store-file.d.ts +16 -1
  309. package/dist/plugins/approval-ask-store-file.js +21 -0
  310. package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
  311. package/dist/plugins/approval-ask-store-memory.js +34 -2
  312. package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
  313. package/dist/plugins/approval-ask-store-sql.js +37 -1
  314. package/dist/plugins/blob-backend.d.ts +83 -15
  315. package/dist/plugins/blob-backend.js +68 -15
  316. package/dist/plugins/breaker-state-sql.d.ts +2 -2
  317. package/dist/plugins/breaker-state-sql.js +4 -4
  318. package/dist/plugins/caching-session-store.d.ts +3 -0
  319. package/dist/plugins/caching-session-store.js +3 -0
  320. package/dist/plugins/checkpoint-store-sql.d.ts +28 -4
  321. package/dist/plugins/checkpoint-store-sql.js +59 -28
  322. package/dist/plugins/file-history-store-sql.d.ts +5 -2
  323. package/dist/plugins/file-history-store-sql.js +3 -1
  324. package/dist/plugins/file-run-store.d.ts +5 -13
  325. package/dist/plugins/file-run-store.js +9 -0
  326. package/dist/plugins/fork-routing-session-store.d.ts +9 -1
  327. package/dist/plugins/fork-routing-session-store.js +12 -1
  328. package/dist/plugins/host-platform.d.ts +2 -0
  329. package/dist/plugins/host-platform.js +2 -1
  330. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  331. package/dist/plugins/k8s-bg-scripts.js +22 -0
  332. package/dist/plugins/local-checkpoint-store.d.ts +2 -0
  333. package/dist/plugins/local-checkpoint-store.js +11 -3
  334. package/dist/plugins/local-session-store.d.ts +1 -1
  335. package/dist/plugins/local-session-store.js +1 -1
  336. package/dist/plugins/local-session-window.d.ts +39 -0
  337. package/dist/plugins/local-session-window.js +54 -0
  338. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +30 -48
  340. package/dist/plugins/mailbox-store-sql.js +77 -52
  341. package/dist/plugins/memory-embedder-fingerprint.js +2 -1
  342. package/dist/plugins/memory-run-store.d.ts +5 -13
  343. package/dist/plugins/memory-run-store.js +8 -0
  344. package/dist/plugins/one-time-migrations.d.ts +245 -93
  345. package/dist/plugins/one-time-migrations.js +668 -207
  346. package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
  347. package/dist/plugins/org-delivery-quota-store.js +228 -0
  348. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  349. package/dist/plugins/permission-rule-store-sql.js +4 -18
  350. package/dist/plugins/pg-pool.js +39 -7
  351. package/dist/plugins/pg-session-storage.d.ts +20 -8
  352. package/dist/plugins/pg-session-storage.js +34 -21
  353. package/dist/plugins/remote-env-adb.d.ts +12 -12
  354. package/dist/plugins/remote-env-adb.js +21 -6
  355. package/dist/plugins/remote-env-device.js +3 -0
  356. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  357. package/dist/plugins/remote-env-e2b.js +21 -9
  358. package/dist/plugins/remote-env-host.d.ts +9 -9
  359. package/dist/plugins/remote-env-host.js +10 -32
  360. package/dist/plugins/remote-env-k8s.d.ts +103 -38
  361. package/dist/plugins/remote-env-k8s.js +288 -95
  362. package/dist/plugins/remote-env-local-docker.d.ts +27 -13
  363. package/dist/plugins/remote-env-local-docker.js +82 -11
  364. package/dist/plugins/remote-env-ssh.d.ts +21 -12
  365. package/dist/plugins/remote-env-ssh.js +117 -5
  366. package/dist/plugins/remote-shell.d.ts +0 -1
  367. package/dist/plugins/remote-shell.js +1 -2
  368. package/dist/plugins/retention-store-sql.js +6 -2
  369. package/dist/plugins/run-store-sql.d.ts +16 -14
  370. package/dist/plugins/run-store-sql.js +22 -2
  371. package/dist/plugins/schema-consistency.d.ts +54 -0
  372. package/dist/plugins/schema-consistency.js +52 -0
  373. package/dist/plugins/schema-indexes.js +2 -0
  374. package/dist/plugins/send-user-file.js +2 -2
  375. package/dist/plugins/session-epoch-anchor.d.ts +39 -0
  376. package/dist/plugins/session-epoch-anchor.js +22 -0
  377. package/dist/plugins/session-store.d.ts +18 -8
  378. package/dist/plugins/session-store.js +15 -4
  379. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  380. package/dist/plugins/shared-memory-store-sql.js +5 -16
  381. package/dist/plugins/sql-driver.d.ts +205 -1
  382. package/dist/plugins/sql-driver.js +325 -13
  383. package/dist/plugins/sql-errors.d.ts +64 -0
  384. package/dist/plugins/sql-errors.js +57 -0
  385. package/dist/plugins/store-backend.d.ts +36 -1
  386. package/dist/plugins/store-backend.js +42 -15
  387. package/dist/plugins/store-contracts.d.ts +42 -0
  388. package/dist/plugins/store-contracts.js +1 -0
  389. package/dist/plugins/tidb-pool.d.ts +6 -1
  390. package/dist/plugins/tidb-pool.js +44 -10
  391. package/dist/plugins/tidb-session-storage.d.ts +8 -11
  392. package/dist/plugins/tidb-session-storage.js +6 -16
  393. package/dist/plugins/tidb-session-store.d.ts +24 -4
  394. package/dist/plugins/tidb-session-store.js +29 -5
  395. package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
  396. package/dist/plugins/workflow-journal-store-sql.js +19 -6
  397. package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
  398. package/dist/plugins/workflow-run-store-sql.js +50 -9
  399. package/dist/posture-source.d.ts +18 -10
  400. package/dist/posture-source.js +4 -1
  401. package/dist/project-identity.d.ts +45 -6
  402. package/dist/project-identity.js +162 -8
  403. package/dist/project-layer-key.d.ts +28 -0
  404. package/dist/project-layer-key.js +29 -0
  405. package/dist/prompts-domain-validate.d.ts +17 -61
  406. package/dist/prompts-domain-validate.js +9 -119
  407. package/dist/question.js +3 -2
  408. package/dist/read-deny-entry.d.ts +25 -0
  409. package/dist/read-deny-entry.js +12 -0
  410. package/dist/read-face-posture.d.ts +37 -11
  411. package/dist/read-face-posture.js +32 -3
  412. package/dist/request-key-closure.d.ts +9 -2
  413. package/dist/request-key-closure.js +11 -5
  414. package/dist/request-value-closure.d.ts +5 -0
  415. package/dist/request-value-closure.js +11 -3
  416. package/dist/rule-entry-list.d.ts +38 -11
  417. package/dist/rule-entry-list.js +106 -30
  418. package/dist/rules-consent.js +3 -4
  419. package/dist/run-acceptance.d.ts +19 -3
  420. package/dist/run-acceptance.js +13 -1
  421. package/dist/run-local.js +54 -24
  422. package/dist/runs.d.ts +15 -8
  423. package/dist/runs.js +35 -1
  424. package/dist/runtime-caps-resolver.d.ts +16 -6
  425. package/dist/runtime-caps-resolver.js +18 -5
  426. package/dist/sandbox-pkg-source.d.ts +19 -1
  427. package/dist/sandbox-pkg-source.js +29 -7
  428. package/dist/security.d.ts +87 -9
  429. package/dist/security.js +52 -9
  430. package/dist/server-secret-env.d.ts +44 -18
  431. package/dist/server-secret-env.js +49 -8
  432. package/dist/session-error-wire.d.ts +19 -2
  433. package/dist/session-error-wire.js +5 -1
  434. package/dist/session-incarnation.d.ts +15 -5
  435. package/dist/session-incarnation.js +16 -9
  436. package/dist/session-titled-frame.d.ts +33 -0
  437. package/dist/session-titled-frame.js +4 -0
  438. package/dist/session-titler.d.ts +9 -1
  439. package/dist/session-titler.js +27 -6
  440. package/dist/spec-fields.d.ts +28 -16
  441. package/dist/spec-fields.js +66 -54
  442. package/dist/sql-engine-posture.d.ts +31 -4
  443. package/dist/sql-engine-posture.js +36 -1
  444. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  445. package/dist/stored-request-tool-axes-m19.js +30 -2
  446. package/dist/task-a2a.d.ts +9 -1
  447. package/dist/task-a2a.js +73 -47
  448. package/dist/task-cwd.d.ts +1 -1
  449. package/dist/task-mcp.d.ts +46 -58
  450. package/dist/task-mcp.js +82 -107
  451. package/dist/task-settings.d.ts +95 -108
  452. package/dist/task-settings.js +77 -98
  453. package/dist/text-bidi.d.ts +1 -48
  454. package/dist/text-bidi.js +3 -5
  455. package/dist/tighten-only-seats.d.ts +150 -0
  456. package/dist/tighten-only-seats.js +196 -0
  457. package/dist/tool-approval.d.ts +304 -88
  458. package/dist/tool-approval.js +623 -239
  459. package/dist/trace/core-keyset-guard.d.ts +122 -20
  460. package/dist/trace/engine-notice-wire.d.ts +11 -3
  461. package/dist/trace/engine-notice-wire.js +8 -1
  462. package/dist/trace/exit-seat.d.ts +28 -0
  463. package/dist/trace/exit-seat.js +76 -0
  464. package/dist/trace/ledger-events.d.ts +6 -3
  465. package/dist/trace/ledger-sink.js +4 -1
  466. package/dist/trace/model-segment.js +1 -1
  467. package/dist/trace/project.d.ts +98 -17
  468. package/dist/trace/project.js +98 -50
  469. package/dist/trace/projection-table.d.ts +100 -0
  470. package/dist/trace/projection-table.js +130 -0
  471. package/dist/trace/redact.d.ts +54 -0
  472. package/dist/trace/redact.js +778 -152
  473. package/dist/trace/self-minted-wire.d.ts +345 -1
  474. package/dist/trace/self-minted-wire.js +117 -1
  475. package/dist/trace/wire-projection-faces.d.ts +377 -3
  476. package/dist/trace/wire-projection-faces.js +164 -1
  477. package/dist/url-refusal.js +2 -0
  478. package/dist/web-admin-client/apply-effective.d.ts +12 -0
  479. package/dist/web-admin-client/apply-effective.js +56 -5
  480. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  481. package/dist/web-admin-client/candidate-gate.js +3 -3
  482. package/dist/web-admin-client/facade.d.ts +1 -1
  483. package/dist/web-admin-client/facade.js +1 -1
  484. package/dist/web-admin-client/http-client.js +0 -6
  485. package/dist/web-admin-client/read-face.js +5 -9
  486. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  487. package/dist/web-admin-client/read-warnings.js +10 -0
  488. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  489. package/dist/web-admin-client/skills-mcp.js +11 -9
  490. package/dist/web-admin-client/types.d.ts +2 -3
  491. package/dist/workspace-cache.d.ts +35 -0
  492. package/dist/workspace-cache.js +36 -0
  493. package/dist/workspace-space.d.ts +3 -2
  494. package/dist/workspace-space.js +2 -0
  495. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +65 -10
  496. package/package.json +8 -4
  497. package/scripts/drain-check-local.mjs +34 -1
  498. package/scripts/drain-check.mysql.sql +30 -2
  499. package/scripts/drain-check.pg.sql +29 -2
  500. package/dist/capabilities/memory-notice.d.ts +0 -138
  501. package/dist/capabilities/memory-notice.js +0 -64
  502. package/dist/capabilities/prompt.d.ts +0 -17
  503. package/dist/capabilities/prompt.js +0 -6
  504. package/dist/mcp-content-origin.d.ts +0 -34
  505. package/dist/mcp-content-origin.js +0 -10
  506. package/dist/tool-axis-words.d.ts +0 -27
  507. package/dist/tool-axis-words.js +0 -9
  508. package/dist/wire-name-bound.d.ts +0 -14
  509. package/dist/wire-name-bound.js +0 -13
@@ -1,39 +1,119 @@
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";
7
- 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";
8
+ import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
9
+ import { SESSION_META_INCARNATION_COLUMN, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.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";
16
+ import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, isOrphanSessionOwner, judgeDelegatedOwner } from "../delegated-session-owner.js";
17
+ import { MAILBOX_KEYED_TABLES, mailboxKeyDeleteSql, storedMailboxKeyless } from "./mailbox-store-sql.js";
15
18
  const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
16
- export function buildSessionRuleReceipts() {
17
- return { rows: [], renamed: false };
19
+ export function buildMigrationReceipts() {
20
+ return {
21
+ auditDetailJsonNullRows: 0,
22
+ sessionIncarnationsBackfilled: 0,
23
+ holderDeathCodeRowsBackfilled: 0,
24
+ allowRuleShadowRowsMigrated: 0,
25
+ allowRuleShadowRowsUnreadable: 0,
26
+ workflowParkIndexRowsCarried: 0,
27
+ undurableScopeRuleRows: [],
28
+ undurableScopeRuleBucketsDeferred: 0,
29
+ undurableScopeRuleBucketsUnreadable: [],
30
+ sessionRuleRowsWithRetiredBuckets: [],
31
+ sessionRuleRowsWithOldGrammarEntries: [],
32
+ sessionRuleRowsWithUnreadableDeny: [],
33
+ retiredCardWordAskRowsRewritten: 0,
34
+ retiredCardWordRowsUnreadable: 0,
35
+ parkedQuestionCardsWritten: 0,
36
+ parkedQuestionRowsLeftAsWritten: 0,
37
+ parkedRowGateKindsBackfilled: 0,
38
+ parkedRowsUnresumableExpired: 0,
39
+ placedSessionOwnersInherited: 0,
40
+ placedSessionOwnersOrphaned: 0,
41
+ placedSessionOwnersRemaining: 0,
42
+ workflowJournalOwnersBackfilled: 0,
43
+ workflowJournalForeignScopeRowsRemoved: 0,
44
+ retiredBreakerTablesDropped: 0,
45
+ mailboxRejectedKeyRowsRemoved: 0,
46
+ };
47
+ }
48
+ function completedReport(r) {
49
+ const { sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn } = r;
50
+ if (sessionIncarnationColumn === undefined || workflowParkIndexKey === undefined || sessionRuleColumn === undefined || workflowJournalScopeColumn === undefined) {
51
+ throw new Error(`one-time migrations finished without settling a shape reading (M5 ${String(sessionIncarnationColumn)} / M9 ${String(workflowParkIndexKey)} / M17 ${String(sessionRuleColumn)} / M24 ${String(workflowJournalScopeColumn)}) — a step did not record it`);
52
+ }
53
+ return { ...r, sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn };
54
+ }
55
+ function changedAnything(r) {
56
+ return (r.auditDetailJsonNullRows > 0 ||
57
+ r.sessionIncarnationColumn === "added" ||
58
+ r.sessionIncarnationsBackfilled > 0 ||
59
+ r.holderDeathCodeRowsBackfilled > 0 ||
60
+ r.allowRuleShadowRowsMigrated > 0 ||
61
+ r.allowRuleShadowRowsUnreadable > 0 ||
62
+ r.workflowParkIndexKey === "reshaped" ||
63
+ r.workflowParkIndexRowsCarried > 0 ||
64
+ r.undurableScopeRuleRows.length > 0 ||
65
+ r.undurableScopeRuleBucketsDeferred > 0 ||
66
+ r.undurableScopeRuleBucketsUnreadable.length > 0 ||
67
+ r.sessionRuleColumn === "renamed" ||
68
+ r.sessionRuleRowsWithRetiredBuckets.length > 0 ||
69
+ r.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
70
+ r.sessionRuleRowsWithUnreadableDeny.length > 0 ||
71
+ r.retiredCardWordAskRowsRewritten > 0 ||
72
+ r.retiredCardWordRowsUnreadable > 0 ||
73
+ r.parkedQuestionCardsWritten > 0 ||
74
+ r.parkedQuestionRowsLeftAsWritten > 0 ||
75
+ r.parkedRowGateKindsBackfilled > 0 ||
76
+ r.parkedRowsUnresumableExpired > 0 ||
77
+ r.placedSessionOwnersInherited > 0 ||
78
+ r.placedSessionOwnersOrphaned > 0 ||
79
+ r.placedSessionOwnersRemaining > 0 ||
80
+ r.workflowJournalScopeColumn === "moved" ||
81
+ r.workflowJournalOwnersBackfilled > 0 ||
82
+ r.workflowJournalForeignScopeRowsRemoved > 0 ||
83
+ r.retiredBreakerTablesDropped > 0 ||
84
+ r.mailboxRejectedKeyRowsRemoved > 0);
85
+ }
86
+ function readingsSnapshot(r) {
87
+ return {
88
+ ...r,
89
+ undurableScopeRuleRows: [...r.undurableScopeRuleRows],
90
+ undurableScopeRuleBucketsUnreadable: [...r.undurableScopeRuleBucketsUnreadable],
91
+ sessionRuleRowsWithRetiredBuckets: [...r.sessionRuleRowsWithRetiredBuckets],
92
+ sessionRuleRowsWithOldGrammarEntries: [...r.sessionRuleRowsWithOldGrammarEntries],
93
+ sessionRuleRowsWithUnreadableDeny: [...r.sessionRuleRowsWithUnreadableDeny],
94
+ };
95
+ }
96
+ function receiptSentence(r) {
97
+ const counts = Object.entries(readingFields(r))
98
+ .filter(([, v]) => (typeof v === "number" ? v > 0 : v !== undefined))
99
+ .map(([k, v]) => `${k}=${String(v)}`);
100
+ const rows = [
101
+ ...r.undurableScopeRuleRows.map((x) => `removed ${x.kind} in permission-rule bucket ${x.ownerKey} (${x.behavior} ${x.tool}, scope root ${JSON.stringify(x.scopeRoot)})`),
102
+ ...r.sessionRuleRowsWithOldGrammarEntries.map((x) => `${x.policyKey} (session ${x.sessionId}): toolAllowDropped=${JSON.stringify(x.toolAllowDropped)}`),
103
+ ...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)}` : ""}`),
104
+ ];
105
+ return [counts.join(", "), ...rows].filter((part) => part !== "").join("; ");
18
106
  }
19
107
  export class OneTimeMigrationInterruptedError extends Error {
20
108
  receipts;
21
109
  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 });
110
+ 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 ` +
111
+ "and cannot report it again — record these readings before retrying: " +
112
+ receiptSentence(receipts), { cause });
25
113
  this.name = "OneTimeMigrationInterruptedError";
26
114
  this.receipts = receipts;
27
115
  }
28
116
  }
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
117
  export const ONE_TIME_MIGRATIONS = [
38
118
  {
39
119
  id: "M4",
@@ -132,7 +212,7 @@ export const ONE_TIME_MIGRATIONS = [
132
212
  releasedOn: "2026-10-08",
133
213
  removableAfter: "2026-11-07",
134
214
  removal: "删 migrateOn 的 M11 步(`permission_rule` 在场探针 + `purgeUndurableScopeRows` 调用)与 `permission-rule-store-sql.ts` 的 `purgeUndurableScopeRows` / " +
135
- "`purgeAllBuckets` / `purgeOneBucket` / `contentFreeRefusal` / `PurgeAccumulator` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurge` / " +
215
+ "`purgeOneBucket` / `contentFreeRefusal` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurgeReadings` / " +
136
216
  "`UndurableScopePurgeDb` / `UNDURABLE_PURGE_PAGE` / `UNDURABLE_PURGE_OCC_ATTEMPTS`、本文件的 `removedRuleRowNote`,以及 `BucketRefusedError` 的 `verdict` 位(若届时再无别的读者;" +
137
217
  "判官 `decodeBucketRow` 与写法 `writeBucket` 是读腿 / 写臂本身,留);报告、日志行(含逐行的 `permission_rule_row_removed` 与逐桶的 " +
138
218
  "`permission_rule_bucket_unreadable`)与 `NO_SQL_MIGRATIONS` 的 `undurableScopeRuleRows` / `undurableScopeRuleBucketsDeferred` / `undurableScopeRuleBucketsUnreadable`",
@@ -213,16 +293,15 @@ export const ONE_TIME_MIGRATIONS = [
213
293
  removal: "删 migrateOn 的 M17 步(`migrateSessionRuleGrammar`:`session_policy` 在场探针、旧名 → 进行中的名 → 判读 → 终名三段、退役命令桶扫描与点名)与 " +
214
294
  "`retiredSessionRuleBucketsOf` / `RetiredSessionRuleBucket` / `RETIRED_SESSION_RULE_BUCKETS` / `RETIRED_SESSION_RULES_COLUMN` / `OLD_GRAMMAR_SESSION_RULES_COLUMN` / `SESSION_RULES_SCAN_PAGE` / `SessionRuleRowRetiredBuckets` / " +
215
295
  "`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`;" +
296
+ "`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` / `SessionRuleRowUnreadableDeny` / `UnreadableDenyEntry`;`logReceiptRows` 里 M17 的三段与 `receiptSentence` 里 M17 的两段;" +
297
+ "报告、日志行(含逐行的 `session_rule_row_retired_bucket` · `session_rule_row_old_grammar_entries` · `session_rule_row_unreadable_deny`)与 `NO_SQL_MIGRATIONS` 的 `sessionRuleColumn` / " +
298
+ "`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries` / `sessionRuleRowsWithUnreadableDeny`;" +
220
299
  "**列名本身留在中央 DDL 里**(`session-rules-column.ts`,它不是迁移的一部分)",
221
300
  afterRemoval: "① 列还叫 `rules`、或停在进行中的名 `rule_entries_old_grammar` 的存量库(跳过了带本条的版本直接升 / 判读没走完就跳版):起服列册探针 `schema-consistency.ts`(S-547)拒启 " +
222
301
  "`config.schema_mismatch`(缺 `rule_entries`、多那一列)—— " +
223
302
  "出路是先在一只带本条的版本上起服一次;② 行里仍带退役命令桶:SQL 店读回照旧答 `corrupt`(引擎的形读法点名那只桶,与本条在场时同一个结局),只是起服日志不再逐行点名 —— " +
224
303
  "运维在库里把那一行改写成引擎文法或删掉它(删会话即删规则);③ 跳过本条直接升的库同样没有改名那一拍的条目判读 —— 由 ① 的拒启接住(出路同 ①)",
225
- reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries"],
304
+ reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries", "sessionRuleRowsWithUnreadableDeny"],
226
305
  },
227
306
  {
228
307
  id: "M18",
@@ -246,16 +325,18 @@ export const ONE_TIME_MIGRATIONS = [
246
325
  introducedIn: "7.130.0",
247
326
  releasedOn: "2026-10-16",
248
327
  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;" +
328
+ removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `readA2aToolAxesAsServer7131M19` / `StoredToolAxesRereadM19`);" +
329
+ "`boot/resolve-spec.ts` 里两条请求腿共用的那一行(`const storedToolAxes = opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)、递给 `resolveRequestMcp` / `resolveRequestA2a` 的两处实参与它的 import;" +
251
330
  "`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 的那几处;" +
331
+ "`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import(升级前读法的词表与两只谓词住在本模块里,随它一起删);" +
332
+ "`task-a2a.ts` 里 `resolveRequestA2a` / `validateRequestA2aEntries` / `normalizeA2aPeer` 的 `storedToolAxes` 形参、`toolAxesReread` 事实与运维行 `a2a_tool_axes_read_as_before_upgrade`、对本模块的型 import;" +
253
333
  "`test/mcp-tool-axes-m19.test.ts`(窗关那一格 —— 存量坏形整条不进、warn 与通告点名 `field: toolAxes` —— 改钉到 `test/task-mcp.test.ts`);" +
254
334
  "本文件的 `migrationWindowOpen`(若再无别的读者)",
255
335
  afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里引擎判官判不过的 `toolAxes`,续跑重放时这一条 server 整条不进," +
256
336
  "运维 warn `mcp_injection_dropped`(`reason: malformed_entry`、`field: toolAxes`)+ 用户面通告 `mcp.injection_dropped`,兄弟照挂;停驻在这台 server 工具审批上的任务" +
257
337
  "批准后撞 `resume.tool_unavailable`(拒绝照常可出)—— 出路是拒绝那张卡,用改好的 `toolAxes` 重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、" +
258
- "调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)",
338
+ "调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)。A2A 一面同:存量体里 `a2aPeers[].toolAxes` 判不过的对端续跑整条不挂," +
339
+ "运维 warn `a2a_injection_dropped` 点名,停驻在它 skill 上的审批批准后撞 `resume.tool_unavailable` —— 出路同上:拒绝那张卡,改好声明重新提交",
259
340
  reportKeys: [],
260
341
  },
261
342
  {
@@ -273,6 +354,102 @@ export const ONE_TIME_MIGRATIONS = [
273
354
  "出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
274
355
  reportKeys: [],
275
356
  },
357
+ {
358
+ id: "M21",
359
+ introducedIn: "7.132.0",
360
+ releasedOn: "2026-10-18",
361
+ removableAfter: "2026-11-17",
362
+ removal: "删 migrateOn 的 M21 步(`repairParkedRows`:pending 候选分页扫描、门型回填、续不了的置 expired、问答卡词的整只文档读 + CAS 回写)与 `parkedRowRepairOf` / `ParkedRowRepair` / " +
363
+ "`M21_GATE` / `M21_PENDING_ACTION` / `PARKED_ROW_REPAIR_PAGE` / `QUESTION_CARD` / `PARKED_APPROVAL_GATE_KINDS` / `readM21Subtree` / `readM21Document` / `m21Subtree`;" +
364
+ "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `parkedQuestionCardsWritten` / `parkedQuestionRowsLeftAsWritten` / `parkedRowGateKindsBackfilled` / `parkedRowsUnresumableExpired`;" +
365
+ "`test/one-time-migrations.test.ts` 的 M21 组与 `test/one-time-migrations-db-integration.test.ts` 的 M21 格",
366
+ afterRemoval: "① 门上没有卡词的存量问答行(跳过了带本条的版本直接升的库、把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、local 车道 —— 那一车道本条从来够不着):没有新的臂,回到本条之前的读法 —— " +
367
+ "队列行不带卡词(壳显示通用审批卡),到期走 SLA 拒扫腿(模型读到「拒」),决断口仍按工具名认问答(`approve` 必须带 `answer`,否则 400);出路 = 升级前答完或拒掉;" +
368
+ "② `gate_kind` 为 NULL 的停驻行:既有响亮臂 —— 运维待批队列(`listPending`)不列它、计 `server.approvals.queue-row-gate-kind-missing`(决断口按门型绑定取行,本来就不认它);" +
369
+ "出路是运维把那一行置 expired 或删掉会话(终局收割照旧在 `terminal_at_ms` 收它)",
370
+ reportKeys: ["parkedQuestionCardsWritten", "parkedQuestionRowsLeftAsWritten", "parkedRowGateKindsBackfilled", "parkedRowsUnresumableExpired"],
371
+ },
372
+ {
373
+ id: "M22",
374
+ introducedIn: "7.132.0",
375
+ releasedOn: "2026-10-18",
376
+ removableAfter: "2026-11-17",
377
+ removal: "删 `config-lkg.ts` 的 formatVersion 4 读回迁移:`migrateV4Lkg` 与其上的窗注;`loadLkg` 里调它的那一支(`formatVersion !== 5` 那一段)与版本判据里的 `4`" +
378
+ "(4 回到「unknown formatVersion」拒;若 M13 / M15 仍在窗内,先删到期的那几条再删本条,版本判据收成只认 5)、`LkgReadMigration.fromFormat` 的 `4`;" +
379
+ "文件头注 formatVersion 5 一段里「4 的载荷 …」那两句;`test/center-artifact-v3-lane.test.ts` 的两格 4 读回(5 原样读回那一格留)",
380
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 4 答「unknown formatVersion 4」):4 读回 ⇒ 不可用,中心可达照拉(整拉,新 LKG 按 5 覆盖)、断着则 env 兜底 —— " +
381
+ "与回滚到 7.131.x 读 5 同一种处置",
382
+ reportKeys: [],
383
+ },
384
+ {
385
+ id: "M23",
386
+ introducedIn: "7.133.0",
387
+ releasedOn: "2026-10-21",
388
+ removableAfter: "2026-11-20",
389
+ removal: "删 migrateOn 的 M23 步(`inheritPlacedSessionOwners`:placed ∧ 属主 NULL 的行按 session_id 键集分页单趟扫描 + 同一条语句连读根 / 父两行 + 按认领句回写属主)与 `PLACED_OWNER_SCAN_PAGE` / `placedOwnerScanSql` / `scannedPlacement` / `scannedFaces` / `scannedPlacedSources`(`isOrphanSessionOwner` 若再无别的读者一并删);" +
390
+ "**同窗同删的另一半**(认领:按律给升级前写下的无主子代行写属主 —— 停驻挂点对宿主无主行做的就是本条对那一行的单行步):`delegated-session-owner.ts` 的 `CLAIM_OWNERLESS_PLACED_SQL` / `claimOwnerlessParams` / `OwnerlessClaim` / " +
391
+ "`settleDelegatedOwner` 的认领一步(删后它只剩树比对与属主比对)/ `DelegationTreeStore.claimOwnerless`," +
392
+ "两只 SQL 店(`tidb-session-store.ts` / `pg-session-storage.ts`)的 `claimOwnerless`、`CachingSessionStore` 的转发、`OwnerAwareSessionStore.claimOwnerless`、`delegatedOwnerHostOf` 对它的「与 `placementOf` 同在或同缺」;" +
393
+ "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `placedSessionOwnersInherited` / `placedSessionOwnersOrphaned` / `placedSessionOwnersRemaining` 与 `placed_session_owners_orphaned` 那一行 warn;" +
394
+ "`test/one-time-migrations.test.ts` 的 M23 组与 `test/one-time-migrations-db-integration.test.ts` 的 M23 格。" +
395
+ "**不随本条删**:属主为 NULL 的 placed 行只对 fleet 调用方放行(`security.ts` 的 `ownerlessRowOpen` / `sessionOwnerCellOf` 与各门读 `placementOf` 的那一次)—— 那是常设的读面律",
396
+ afterRemoval: "会话面的读面律(常设,不随本条删)接住:跳过了带本条的版本直接升的库里,属主为 NULL 的存量 placed 子代行只对 fleet 调用方放行(出示 service 凭据、不报主体;dev-open)," +
397
+ "其余调用方答 404(与别人的行同体)—— 出路是升级时先过一次带本条的版本,或运维按 `placement_root_session_id`(缺则 `placement_parent_session_id`)那一行的属主手工回填、" +
398
+ "推不出的写一个任何主体都不匹配的值;停驻挂点读到无主的宿主子代行 ⇒ 回到比对的既有臂:无主 ≠ 律给的属主 ⇒ 停驻否决(fail-closed,响亮;出路同上);" +
399
+ "建行那一笔的继承律不随本条删(那是常设的写法,不是迁移)",
400
+ reportKeys: ["placedSessionOwnersInherited", "placedSessionOwnersOrphaned", "placedSessionOwnersRemaining"],
401
+ },
402
+ {
403
+ id: "M24",
404
+ introducedIn: "7.133.0",
405
+ releasedOn: "2026-10-21",
406
+ removableAfter: "2026-11-20",
407
+ removal: "删 migrateOn 的 M24 步(`moveWorkflowJournalScope`:scope 列在场探针 + 删残留暂存表 / 待删旧表 + 回填属主记录 + 数跨 scope 行 + 照中央 DDL 建暂存表 + 整表复制 + 原子换名 + 删旧表 + PG 主键约束名)与 " +
408
+ "`copyJournalInBatches` / `journalKeyBound` / `WORKFLOW_JOURNAL_TABLE` / `WORKFLOW_JOURNAL_OWNER_TABLE` / `WORKFLOW_JOURNAL_STAGING` / `WORKFLOW_JOURNAL_RETIRED` / `RETIRED_JOURNAL_SCOPE_COLUMN` / " +
409
+ "`M24_OWNER_INCARNATION` / `M24_COPY_BATCH_BYTES` / `M24_COPY_SCAN_PAGE`;" +
410
+ "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `workflowJournalScopeColumn` / `workflowJournalOwnersBackfilled` / `workflowJournalForeignScopeRowsRemoved` 与 `workflow_journal_foreign_scope_rows_removed` 那一行 warn;" +
411
+ "`test/one-time-migrations.test.ts` 的 M24 组与 `test/one-time-migrations-db-integration.test.ts` 的 M24 格。**不随本条删**:属主表、行主键里的令牌与店的写面 / 读面墙(那是常设的数据形)",
412
+ afterRemoval: "起服对账一族的列集探针接住(`schema-consistency.ts` 的 `assertSchemaMatchesCode`,常设):跳过了带本条的版本直接升的库,`workflow_journal` 上仍带 scope 列、没有 incarnation 列 = 库与代码列集不符 ⇒ " +
413
+ "响亮拒启(`config.schema_mismatch`),拒句点名两列;出路是先在带本条的版本(7.133.x)起服一次让它走完,再升",
414
+ reportKeys: ["workflowJournalScopeColumn", "workflowJournalOwnersBackfilled", "workflowJournalForeignScopeRowsRemoved"],
415
+ },
416
+ {
417
+ id: "M25",
418
+ introducedIn: "7.133.0",
419
+ releasedOn: "2026-10-21",
420
+ removableAfter: "2026-11-20",
421
+ removal: "删 `project-identity.ts` 的 `mintRepoScopeKey`(与它的窗注;`RETIRED_PROJECT_KEY_PREFIX` 留给 `isServerMintedProjectKey` 的起服拒)与 `projectLayerForTaskRoot` 的 `foldFrom` 里它那一员" +
422
+ "(`p` 档那一员留 —— 常设律);`memory-sync-client.ts` 的 `legacyMemorySyncCursorPath` / `adoptLegacyCursor` 与 runner 里调它的那一段(`legacyCursorChecked`);" +
423
+ "`test/memory-project-layer-fold.test.ts` 的 ① 格与 `test/memory-project-layer-fold-db-integration.test.ts` 整份、`test/memory-sync-client.test.ts` 的旧游标收编格、" +
424
+ "`test/project-identity.test.ts` 的 `repo:` 金样格(公式金样改钉 `p` 档摘要)",
425
+ afterRemoval: "没有新的臂(如实):窗内一次都没跑过任务的项目,它的 `repo:` 层留在库里、不再被任何读列表包含(不读、不删;起服拒只管运维点名这一族);出路 = 升级时先过一次带本条的版本并在每个项目跑一次任务。" +
426
+ "同步腿的旧游标文件不再收编:同步层从空基线首轮重收敛(两边都改过的条目按冲突上报,不静默丢)",
427
+ reportKeys: [],
428
+ },
429
+ {
430
+ id: "M26",
431
+ introducedIn: "7.133.0",
432
+ releasedOn: "2026-10-21",
433
+ removableAfter: "2026-11-20",
434
+ removal: "删 migrateOn 的 M26 步(`dropRetiredBreakerTable`)与 `RETIRED_BREAKER_TABLE`;报告、日志行与 `NO_SQL_MIGRATIONS` 的 `retiredBreakerTablesDropped`;" +
435
+ "`test/one-time-migrations.test.ts` 的 S-491 组与 `test/one-time-migrations-db-integration.test.ts` 的 M26 格。" +
436
+ "**不随本条删**:键的铸点 `breakerKeyOf` / `routeDigestOf`(常设的键形)与新表 `breaker_state`",
437
+ afterRemoval: "没有新的臂(如实):跳过了带本条的版本直接升的库,退役表 `circuit_breaker` 留在库里、没有读者也没有写者(断路器只读写 `breaker_state`)—— " +
438
+ "出路 = 升级时先过一次带本条的版本,或手工 `DROP TABLE circuit_breaker`(建议性状态,删表安全)",
439
+ reportKeys: ["retiredBreakerTablesDropped"],
440
+ },
441
+ {
442
+ id: "M27",
443
+ introducedIn: "7.133.0",
444
+ releasedOn: "2026-10-21",
445
+ removableAfter: "2026-11-20",
446
+ removal: "删 migrateOn 的 M27 步(`dropRejectedMailboxKeys`)与 `MAILBOX_KEY_SCAN_PAGE`;报告、日志行与 `NO_SQL_MIGRATIONS` 的 `mailboxRejectedKeyRowsRemoved`;" +
447
+ "`test/one-time-migrations.test.ts` 的 M27 组。**不随本条删**:判官 `mailboxKeyProblem`、存储键读回判 `storedMailboxKeyless`(常设 `reap` 读候选句柄也经它)与「判官拒的键没有箱」那条入口律(含 `reap` 跳过候选扫描交回的被拒句柄)、" +
448
+ "`MAILBOX_KEYED_TABLES` / `mailboxKeyDeleteSql`(`drop` 的常设语句)",
449
+ afterRemoval: "没有新的臂(如实):跳过了带本条的版本直接升的库,判官拒的键的行留在库里,任何入口对它都按「箱不在」答(不读、不投、不清)—— 出路 = 升级时先过一次带本条的版本," +
450
+ "或照 `docs/DEPLOY-PREREQS.md` 7.133.0 SQL 节的查法按键手工删",
451
+ reportKeys: ["mailboxRejectedKeyRowsRemoved"],
452
+ },
276
453
  ];
277
454
  export function migrationWindowOpen(window, today) {
278
455
  return window.removableAfter === null || today <= window.removableAfter;
@@ -291,29 +468,24 @@ export const NO_SQL_MIGRATIONS = {
291
468
  sessionRuleColumn: "file-store",
292
469
  sessionRuleRowsWithRetiredBuckets: [],
293
470
  sessionRuleRowsWithOldGrammarEntries: [],
471
+ sessionRuleRowsWithUnreadableDeny: [],
294
472
  retiredCardWordAskRowsRewritten: 0,
295
473
  retiredCardWordRowsUnreadable: 0,
474
+ parkedQuestionCardsWritten: 0,
475
+ parkedQuestionRowsLeftAsWritten: 0,
476
+ parkedRowGateKindsBackfilled: 0,
477
+ parkedRowsUnresumableExpired: 0,
478
+ placedSessionOwnersInherited: 0,
479
+ placedSessionOwnersOrphaned: 0,
480
+ placedSessionOwnersRemaining: 0,
481
+ workflowJournalScopeColumn: "file-store",
482
+ workflowJournalOwnersBackfilled: 0,
483
+ workflowJournalForeignScopeRowsRemoved: 0,
484
+ retiredBreakerTablesDropped: 0,
485
+ mailboxRejectedKeyRowsRemoved: 0,
296
486
  };
297
487
  const AUDIT_TABLE = "device_audit";
298
488
  const AUDIT_DETAIL_COLUMN = "detail";
299
- const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
300
- const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
301
- export class RetiredSchemaPresentError extends StoreBootRefusalError {
302
- found;
303
- code = SCHEMA_MISMATCH_ERROR_CODE;
304
- constructor(found) {
305
- super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database still carries schema that the one-time migrations of server 7.74.0–7.125.x retired — refusing to start. ` +
306
- `Found: ${found.join("; ")}. ` +
307
- `Both come from builds that predate those migrations, so this database never booted a build that still carried them — which also means the third one never ran: ` +
308
- `ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
309
- `This build carries none of the three and will not guess. ` +
310
- `Way out: start a server 7.125.x build once against this database and let its one-time migrations finish (it drops the table and the column and rewrites the spelling; ` +
311
- `while the column still holds a steering instruction that can be delivered it refuses and names the rows and its own ways out), then upgrade to this build. ` +
312
- `The check behind this refusal is read-only and runs before this build creates any table or rewrites anything: it changed nothing in the database.`);
313
- this.found = found;
314
- this.name = "RetiredSchemaPresentError";
315
- }
316
- }
317
489
  function mysqlDb(conn) {
318
490
  return {
319
491
  async rows(sql, params) {
@@ -358,34 +530,6 @@ async function columnExists(db, dialect, table, column) {
358
530
  const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
359
531
  return (await db.rows(sql)).length > 0;
360
532
  }
361
- async function assertRetiredSchemaAbsent(db, dialect) {
362
- const found = [];
363
- if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
364
- found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
365
- if (await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN))
366
- found.push(`column checkpoint.${RETIRED_STEER_SEAT_COLUMN} (the retired single-seat steering column)`);
367
- if (found.length > 0)
368
- throw new RetiredSchemaPresentError(found);
369
- }
370
- export async function assertRetiredSchemaAbsentOnPool(target) {
371
- if (target.dialect === "tidb") {
372
- const conn = await target.pool.getConnection();
373
- try {
374
- await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
375
- }
376
- finally {
377
- conn.release();
378
- }
379
- return;
380
- }
381
- const client = await target.pool.connect();
382
- try {
383
- await assertRetiredSchemaAbsent(pgDb(client), "pg");
384
- }
385
- finally {
386
- client.release();
387
- }
388
- }
389
533
  export async function runSqlOneTimeMigrations(dialect, pool) {
390
534
  return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
391
535
  }
@@ -418,44 +562,150 @@ async function withPgLock(pool, run) {
418
562
  }
419
563
  }
420
564
  async function migrateOn(db, dialect) {
421
- const receipts = buildSessionRuleReceipts();
565
+ const receipts = buildMigrationReceipts();
422
566
  try {
423
- return await migrateStepsOn(db, dialect, receipts);
567
+ await migrateStepsOn(db, dialect, receipts);
424
568
  }
425
569
  catch (e) {
426
- throw interruptedWith(e, receipts);
570
+ throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
427
571
  }
572
+ return completedReport(receipts);
428
573
  }
429
574
  async function migrateStepsOn(db, dialect, receipts) {
430
- const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
431
- const auditDetailJsonNullRows = !auditTablePresent
432
- ? 0
433
- : await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
434
- `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
435
- const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
436
- const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
575
+ if (await tableExists(db, dialect, AUDIT_TABLE)) {
576
+ 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'`);
577
+ }
578
+ await migrateSessionIncarnations(db, dialect, receipts);
579
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
437
580
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
438
- : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR])) +
439
- (await db.exec(dialect === "tidb"
440
- ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
441
- : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
442
- const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
443
- const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
444
- const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
445
- const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
446
- let undurableScopeRuleRows = [];
447
- let undurableScopeRuleBucketsDeferred = 0;
448
- let undurableScopeRuleBucketsUnreadable = [];
449
- if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
450
- ({ removed: undurableScopeRuleRows, bucketsDeferred: undurableScopeRuleBucketsDeferred, bucketsUnreadable: undurableScopeRuleBucketsUnreadable } = await purgeUndurableScopeRows(db, dialect));
581
+ : "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]);
582
+ receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
583
+ ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
584
+ : "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} (%`]);
585
+ await migrateShadowedRuleKey(db, dialect, receipts);
586
+ await migrateRetiredCardWords(db, dialect, receipts);
587
+ await reshapeWorkflowParkIndex(db, dialect, receipts);
588
+ await migrateSessionRuleGrammar(db, dialect, receipts);
589
+ await repairParkedRows(db, dialect, receipts);
590
+ if (await tableExists(db, dialect, "session_meta"))
591
+ await inheritPlacedSessionOwners(db, dialect, receipts);
592
+ if (await tableExists(db, dialect, PERMISSION_RULE_TABLE))
593
+ await purgeUndurableScopeRows(db, dialect, receipts);
594
+ await moveWorkflowJournalScope(db, dialect, receipts);
595
+ if (await tableExists(db, dialect, RETIRED_BREAKER_TABLE))
596
+ await dropRetiredBreakerTable(db, dialect, receipts);
597
+ await dropRejectedMailboxKeys(db, dialect, receipts);
598
+ }
599
+ const MAILBOX_KEY_SCAN_PAGE = 1000;
600
+ async function dropRejectedMailboxKeys(db, dialect, receipts) {
601
+ const present = [];
602
+ for (const table of MAILBOX_KEYED_TABLES)
603
+ if (await tableExists(db, dialect, table))
604
+ present.push(table);
605
+ const p = (i) => (dialect === "tidb" ? "?" : `$${i}`);
606
+ for (const table of present) {
607
+ let after;
608
+ for (;;) {
609
+ const page = await db.rows(`SELECT DISTINCT scope_key, handle FROM ${table}` +
610
+ (after === undefined ? "" : ` WHERE scope_key > ${p(1)} OR (scope_key = ${p(2)} AND handle > ${p(3)})`) +
611
+ ` ORDER BY scope_key, handle LIMIT ${MAILBOX_KEY_SCAN_PAGE}`, after === undefined ? undefined : [after[0], after[0], after[1]]);
612
+ for (const row of page) {
613
+ if (!storedMailboxKeyless(row.scope_key, row.handle))
614
+ continue;
615
+ for (const t of present)
616
+ receipts.mailboxRejectedKeyRowsRemoved += await db.exec(mailboxKeyDeleteSql(dialect, t), [row.scope_key, row.handle]);
617
+ }
618
+ if (page.length < MAILBOX_KEY_SCAN_PAGE)
619
+ break;
620
+ const last = page[page.length - 1];
621
+ after = [last.scope_key, last.handle];
622
+ }
623
+ }
624
+ }
625
+ const RETIRED_BREAKER_TABLE = "circuit_breaker";
626
+ async function dropRetiredBreakerTable(db, dialect, receipts) {
627
+ await execUntil(db, `DROP TABLE IF EXISTS ${RETIRED_BREAKER_TABLE}`, async () => !(await tableExists(db, dialect, RETIRED_BREAKER_TABLE)));
628
+ receipts.retiredBreakerTablesDropped += 1;
629
+ }
630
+ const WORKFLOW_JOURNAL_TABLE = "workflow_journal";
631
+ const WORKFLOW_JOURNAL_OWNER_TABLE = "workflow_journal_owner";
632
+ const WORKFLOW_JOURNAL_STAGING = `${WORKFLOW_JOURNAL_TABLE}_m24`;
633
+ const WORKFLOW_JOURNAL_RETIRED = `${WORKFLOW_JOURNAL_TABLE}_m24_old`;
634
+ const RETIRED_JOURNAL_SCOPE_COLUMN = "scope";
635
+ const M24_OWNER_INCARNATION = "m24-backfill";
636
+ const M24_COPY_BATCH_BYTES = 32 * 1024 * 1024;
637
+ const M24_COPY_SCAN_PAGE = 1000;
638
+ function journalKeyBound(dialect, op, first) {
639
+ const p = (i) => (dialect === "tidb" ? "?" : `$${first + i}`);
640
+ return op === "after"
641
+ ? `(${WORKFLOW_JOURNAL_TABLE}.run_id > ${p(0)} OR (${WORKFLOW_JOURNAL_TABLE}.run_id = ${p(1)} AND ordinal > ${p(2)}))`
642
+ : `(${WORKFLOW_JOURNAL_TABLE}.run_id < ${p(0)} OR (${WORKFLOW_JOURNAL_TABLE}.run_id = ${p(1)} AND ordinal <= ${p(2)}))`;
643
+ }
644
+ async function copyJournalInBatches(db, dialect) {
645
+ const T = WORKFLOW_JOURNAL_TABLE;
646
+ const bytes = dialect === "tidb" ? "LENGTH(result)" : "OCTET_LENGTH(result)";
647
+ const copyHead = `INSERT INTO ${WORKFLOW_JOURNAL_STAGING} (run_id, incarnation, ordinal, call_key, result, created_at_ms) ` +
648
+ `SELECT ${T}.run_id, o.incarnation, ordinal, call_key, ${jsonColumnText(dialect, "result")}, created_at_ms FROM ${T} ` +
649
+ `JOIN ${WORKFLOW_JOURNAL_OWNER_TABLE} o ON o.run_id = ${T}.run_id AND o.scope = ${T}.${RETIRED_JOURNAL_SCOPE_COLUMN} WHERE `;
650
+ let after;
651
+ for (;;) {
652
+ const page = await db.rows(`SELECT ${T}.run_id, ordinal, ${bytes} AS n FROM ${T}` + (after === undefined ? "" : ` WHERE ${journalKeyBound(dialect, "after", 1)}`) + ` ORDER BY run_id, ordinal LIMIT ${M24_COPY_SCAN_PAGE}`, after === undefined ? [] : [after[0], after[0], after[1]]);
653
+ if (page.length === 0)
654
+ return;
655
+ let end = 0;
656
+ let total = 0;
657
+ while (end < page.length) {
658
+ const n = Number(page[end].n);
659
+ if (end > 0 && total + n > M24_COPY_BATCH_BYTES)
660
+ break;
661
+ total += n;
662
+ end += 1;
663
+ }
664
+ const last = page[end - 1];
665
+ const upTo = [String(last.run_id), Number(last.ordinal)];
666
+ if (after === undefined)
667
+ await db.exec(copyHead + journalKeyBound(dialect, "upTo", 1), [upTo[0], upTo[0], upTo[1]]);
668
+ else
669
+ await db.exec(`${copyHead}${journalKeyBound(dialect, "after", 1)} AND ${journalKeyBound(dialect, "upTo", 4)}`, [after[0], after[0], after[1], upTo[0], upTo[0], upTo[1]]);
670
+ after = upTo;
451
671
  }
452
- return {
453
- auditDetailJsonNullRows,
454
- sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
455
- workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
456
- sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries,
457
- retiredCardWordAskRowsRewritten, retiredCardWordRowsUnreadable,
458
- };
672
+ }
673
+ export async function moveWorkflowJournalScope(db, dialect, receipts) {
674
+ const T = WORKFLOW_JOURNAL_TABLE;
675
+ const S = WORKFLOW_JOURNAL_STAGING;
676
+ const O = WORKFLOW_JOURNAL_RETIRED;
677
+ const exists = (t) => tableExists(db, dialect, t);
678
+ const legacy = () => columnExists(db, dialect, T, RETIRED_JOURNAL_SCOPE_COLUMN);
679
+ if (!(await legacy())) {
680
+ if (await exists(O))
681
+ await execUntil(db, `DROP TABLE ${O}`, async () => !(await exists(O)));
682
+ receipts.workflowJournalScopeColumn = "current";
683
+ return;
684
+ }
685
+ const ddl = (dialect === "tidb" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
686
+ if (ddl === undefined)
687
+ throw new Error(`one-time migration M24: the central schema carries no definition of ${T} — refusing to rebuild a table it cannot define`);
688
+ if (await exists(S))
689
+ await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
690
+ receipts.workflowJournalOwnersBackfilled += await db.exec(`INSERT INTO ${WORKFLOW_JOURNAL_OWNER_TABLE} (run_id, scope, incarnation, appended_at_ms) ` +
691
+ `SELECT j.run_id, j.${RETIRED_JOURNAL_SCOPE_COLUMN}, '${M24_OWNER_INCARNATION}', m.last_ms FROM ${T} j ` +
692
+ `JOIN (SELECT run_id, MIN(ordinal) AS first_ordinal, MAX(created_at_ms) AS last_ms FROM ${T} GROUP BY run_id) m ` +
693
+ `ON m.run_id = j.run_id AND m.first_ordinal = j.ordinal ` +
694
+ `WHERE NOT EXISTS (SELECT 1 FROM ${WORKFLOW_JOURNAL_OWNER_TABLE} o WHERE o.run_id = j.run_id)`);
695
+ const foreign = Number((await db.rows(`SELECT COUNT(*) AS n FROM ${T} j JOIN ${WORKFLOW_JOURNAL_OWNER_TABLE} o ON o.run_id = j.run_id WHERE j.${RETIRED_JOURNAL_SCOPE_COLUMN} <> o.scope`))[0]?.n ?? 0);
696
+ await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
697
+ await copyJournalInBatches(db, dialect);
698
+ const swapped = async () => !(await exists(S)) && (await exists(T)) && !(await legacy());
699
+ if (dialect === "tidb") {
700
+ await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
701
+ receipts.workflowJournalForeignScopeRowsRemoved += foreign;
702
+ await execUntil(db, `DROP TABLE IF EXISTS ${O}`, async () => !(await exists(O)));
703
+ }
704
+ else {
705
+ await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`, `ALTER TABLE ${T} RENAME CONSTRAINT ${S}_pkey TO ${T}_pkey`], async () => (await swapped()) && !(await exists(O)));
706
+ receipts.workflowJournalForeignScopeRowsRemoved += foreign;
707
+ }
708
+ receipts.workflowJournalScopeColumn = "moved";
459
709
  }
460
710
  const SESSION_POLICY_TABLE = "session_policy";
461
711
  const RETIRED_SESSION_RULES_COLUMN = "rules";
@@ -474,9 +724,10 @@ export function oldGrammarEntriesOf(body, rev) {
474
724
  const shaped = readSessionRulesShape({ ...buckets, rev });
475
725
  if ("fault" in shaped)
476
726
  return undefined;
727
+ const toolDenyUnreadable = unreadableDenyEntries(shaped.rules).map(({ entry, reason }) => ({ entry, reason }));
728
+ if (toolDenyUnreadable.length > 0)
729
+ return { toolAllowDropped: [], toolDenyUnreadable };
477
730
  const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
478
- if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
479
- return { toolAllowDropped: [], wildcardMasked: true };
480
731
  const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
481
732
  const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
482
733
  const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
@@ -519,8 +770,14 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
519
770
  : `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
520
771
  await scanSessionRuleRows(db, dialect, column, [], async (row) => {
521
772
  const read = oldGrammarEntriesOf(row.body, row.rev);
522
- if (read?.wildcardMasked) {
523
- receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: [], wildcardMasked: true });
773
+ if (read?.toolDenyUnreadable !== undefined) {
774
+ const retiredBuckets = retiredSessionRuleBucketsOf(row.body);
775
+ receipts.sessionRuleRowsWithUnreadableDeny.push({
776
+ policyKey: row.policyKey,
777
+ sessionId: row.sessionId,
778
+ toolDenyUnreadable: read.toolDenyUnreadable.map(({ entry, reason }) => ({ entry: redactSecrets(entry), reason })),
779
+ ...(retiredBuckets.length > 0 ? { retiredBuckets } : {}),
780
+ });
524
781
  return;
525
782
  }
526
783
  if (read?.rewritten === undefined)
@@ -529,32 +786,34 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
529
786
  throw new Error(`one-time migration M17: session_policy row ${row.policyKey} changed while its rule entries were being rewritten under the in-progress column ${column} — no writer can reach the ` +
530
787
  "table at that moment (builds up to 7.128.x lost the column, this build's replicas wait on the boot lock), so the step refuses rather than guess");
531
788
  }
532
- if (read.toolAllowDropped.length > 0)
533
- receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
789
+ if (read.toolAllowDropped.length > 0) {
790
+ receipts.sessionRuleRowsWithOldGrammarEntries.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
791
+ }
534
792
  });
535
793
  }
536
794
  export async function migrateSessionRuleGrammar(db, dialect, receipts) {
537
- if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
538
- return { sessionRuleColumn: "no-table", sessionRuleRowsWithRetiredBuckets: [], sessionRuleRowsWithOldGrammarEntries: [] };
795
+ if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE))) {
796
+ receipts.sessionRuleColumn = "no-table";
797
+ return;
798
+ }
539
799
  const col = SESSION_RULES_COLUMN;
540
800
  const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
541
801
  if (await has(RETIRED_SESSION_RULES_COLUMN)) {
542
802
  await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
543
803
  }
544
- let sessionRuleColumn = "current";
545
804
  if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
546
805
  await judgeOldGrammarRows(db, dialect, receipts);
547
806
  await execUntil(db, renameSessionRulesColumnSql(dialect, OLD_GRAMMAR_SESSION_RULES_COLUMN, col.name), async () => !(await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) && (await has(col.name)));
548
- receipts.renamed = true;
549
- sessionRuleColumn = "renamed";
807
+ receipts.sessionRuleColumn = "renamed";
808
+ }
809
+ else {
810
+ receipts.sessionRuleColumn = "current";
550
811
  }
551
- const named = [];
552
812
  await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
553
813
  const buckets = retiredSessionRuleBucketsOf(row.body);
554
814
  if (buckets.length > 0)
555
- named.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
815
+ receipts.sessionRuleRowsWithRetiredBuckets.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
556
816
  });
557
- return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
558
817
  }
559
818
  function renameSessionRulesColumnSql(dialect, from, to) {
560
819
  const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
@@ -571,7 +830,7 @@ async function primaryKeyWidth(db, dialect, table) {
571
830
  }
572
831
  const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
573
832
  const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
574
- async function reshapeWorkflowParkIndex(db, dialect) {
833
+ async function reshapeWorkflowParkIndex(db, dialect, receipts) {
575
834
  const T = WORKFLOW_PARK_INDEX_TABLE;
576
835
  const S = WORKFLOW_PARK_INDEX_STAGING;
577
836
  const O = WORKFLOW_PARK_INDEX_RETIRED;
@@ -582,11 +841,10 @@ async function reshapeWorkflowParkIndex(db, dialect) {
582
841
  const copyPointers = (to, from) => db.exec(dialect === "tidb"
583
842
  ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
584
843
  : `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
585
- let carried = 0;
586
844
  const resumed = await exists(S);
587
845
  if (resumed) {
588
846
  await db.exec(ddl);
589
- carried += await copyPointers(T, S);
847
+ receipts.workflowParkIndexRowsCarried += await copyPointers(T, S);
590
848
  await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
591
849
  }
592
850
  if (await exists(O))
@@ -594,10 +852,11 @@ async function reshapeWorkflowParkIndex(db, dialect) {
594
852
  if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
595
853
  if (await exists(T))
596
854
  await finishParkIndexShape(db, dialect);
597
- return resumed ? { key: "reshaped", carried } : { key: "current", carried: 0 };
855
+ receipts.workflowParkIndexKey = resumed ? "reshaped" : "current";
856
+ return;
598
857
  }
599
858
  await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
600
- carried += await copyPointers(S, T);
859
+ receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
601
860
  const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
602
861
  if (dialect === "tidb") {
603
862
  await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
@@ -607,7 +866,7 @@ async function reshapeWorkflowParkIndex(db, dialect) {
607
866
  await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`], async () => (await swapped()) && !(await exists(O)));
608
867
  }
609
868
  await finishParkIndexShape(db, dialect);
610
- return { key: "reshaped", carried };
869
+ receipts.workflowParkIndexKey = "reshaped";
611
870
  }
612
871
  async function execAtomically(db, statements, postcondition) {
613
872
  try {
@@ -658,10 +917,8 @@ export function migrateShadowedRuleDescriptor(text) {
658
917
  return { kind: "rewrite", text: JSON.stringify(next) };
659
918
  }
660
919
  const SHADOWED_RULE_BATCH = 500;
661
- async function migrateShadowedRuleKey(db, dialect) {
920
+ async function migrateShadowedRuleKey(db, dialect, receipts) {
662
921
  const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
663
- let migrated = 0;
664
- let unreadable = 0;
665
922
  let after = "";
666
923
  for (;;) {
667
924
  const rows = await db.rows(dialect === "tidb"
@@ -675,17 +932,17 @@ async function migrateShadowedRuleKey(db, dialect) {
675
932
  continue;
676
933
  const next = migrateShadowedRuleDescriptor(text);
677
934
  if (next.kind === "unreadable") {
678
- unreadable += 1;
935
+ receipts.allowRuleShadowRowsUnreadable += 1;
679
936
  continue;
680
937
  }
681
938
  if (next.kind === "untouched")
682
939
  continue;
683
- migrated += await db.exec(dialect === "tidb"
940
+ receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
684
941
  ? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
685
942
  : `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
686
943
  }
687
944
  if (rows.length < SHADOWED_RULE_BATCH)
688
- return { migrated, unreadable };
945
+ return;
689
946
  }
690
947
  }
691
948
  const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
@@ -707,11 +964,9 @@ export function migrateRetiredCardWordsInCardJson(text) {
707
964
  const next = retireStoredCardWords(decoded.value);
708
965
  return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
709
966
  }
710
- async function migrateRetiredCardWords(db, dialect) {
711
- let askRows = 0;
712
- let unreadable = 0;
967
+ async function migrateRetiredCardWords(db, dialect, receipts) {
713
968
  if (!(await tableExists(db, dialect, "approval_ask")))
714
- return { askRows, unreadable };
969
+ return;
715
970
  const cardText = jsonColumnTextExpr(dialect, "card_json");
716
971
  let after = "";
717
972
  for (;;) {
@@ -726,42 +981,189 @@ async function migrateRetiredCardWords(db, dialect) {
726
981
  continue;
727
982
  const next = migrateRetiredCardWordsInCardJson(text);
728
983
  if (next.kind === "unreadable") {
729
- unreadable += 1;
984
+ receipts.retiredCardWordRowsUnreadable += 1;
730
985
  continue;
731
986
  }
732
987
  if (next.kind === "untouched")
733
988
  continue;
734
- askRows += await db.exec(dialect === "tidb"
989
+ receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
735
990
  ? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
736
991
  : `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
737
992
  }
738
993
  if (rows.length < RETIRED_CARD_WORD_BATCH)
739
- return { askRows, unreadable };
994
+ return;
995
+ }
996
+ }
997
+ const M21_GATE = { key: "gate", as: "m21_gate" };
998
+ const M21_PENDING_ACTION = { key: "pendingAction", as: "m21_pending_action" };
999
+ const PARKED_ROW_REPAIR_PAGE = 200;
1000
+ const QUESTION_CARD = "question";
1001
+ const PARKED_APPROVAL_GATE_KINDS = Object.keys(PAUSE_REGISTRY).filter((k) => PAUSE_REGISTRY[k].pending === "tool_approval");
1002
+ const readM21Subtree = createObjectColumnReader();
1003
+ const readM21Document = createObjectColumnReader();
1004
+ export function parkedRowRepairOf(gateKindCell, gate, pendingAction) {
1005
+ const gateObj = gate !== null && typeof gate === "object" && !Array.isArray(gate) ? gate : undefined;
1006
+ const columnKind = typeof gateKindCell === "string" && isGateKind(gateKindCell) ? gateKindCell : undefined;
1007
+ const documentKind = gateObj !== undefined && isGateKind(gateObj.kind) ? gateObj.kind : undefined;
1008
+ const kind = columnKind ?? documentKind;
1009
+ const pa = pendingAction !== null && typeof pendingAction === "object" && !Array.isArray(pendingAction) ? pendingAction : undefined;
1010
+ const questionCard = kind !== undefined &&
1011
+ PARKED_APPROVAL_GATE_KINDS.includes(kind) &&
1012
+ gateObj !== undefined &&
1013
+ !Object.hasOwn(gateObj, "approvalCard") &&
1014
+ pa?.kind === "tool_approval" &&
1015
+ pa.toolName === ASK_USER_QUESTION_TOOL_NAME;
1016
+ const columnNull = gateKindCell === null || gateKindCell === undefined;
1017
+ return { ...(columnNull && documentKind !== undefined ? { gateKind: documentKind } : {}), questionCard, unresumable: columnNull && documentKind === undefined };
1018
+ }
1019
+ function m21Subtree(cell, where) {
1020
+ try {
1021
+ return decodeNullableJsonColumn(cell, readM21Subtree, where);
1022
+ }
1023
+ catch (e) {
1024
+ if (!isJsonColumnBodyProblem(e))
1025
+ throw e;
1026
+ return undefined;
1027
+ }
1028
+ }
1029
+ export async function repairParkedRows(db, dialect, receipts) {
1030
+ const ph = (n) => (dialect === "tidb" ? "?" : `$${n}`);
1031
+ const kinds = PARKED_APPROVAL_GATE_KINDS;
1032
+ const kindSlots = kinds.map((_, i) => ph(i + 1)).join(", ");
1033
+ const scan = `SELECT token, session_id, gate_kind, ${jsonColumnText(dialect, "checkpoint", M21_GATE)}, ${jsonColumnText(dialect, "checkpoint", M21_PENDING_ACTION)} FROM checkpoint ` +
1034
+ `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)} ` +
1035
+ `ORDER BY token LIMIT ${PARKED_ROW_REPAIR_PAGE}`;
1036
+ const docText = jsonColumnTextExpr(dialect, "checkpoint");
1037
+ const encode = dialectProtocolJsonEncoder(dialect);
1038
+ let after = "";
1039
+ for (;;) {
1040
+ const page = await db.rows(scan, [...kinds, ASK_USER_QUESTION_TOOL_NAME, after]);
1041
+ for (const r of page) {
1042
+ const token = String(r.token);
1043
+ after = token;
1044
+ 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)"));
1045
+ if (plan.unresumable) {
1046
+ 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]);
1047
+ continue;
1048
+ }
1049
+ if (plan.gateKind !== undefined) {
1050
+ 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]);
1051
+ }
1052
+ if (!plan.questionCard)
1053
+ continue;
1054
+ const [full] = await db.rows(`SELECT ${jsonColumnText(dialect, "checkpoint")} FROM checkpoint WHERE token = ${ph(1)} AND status = 'pending'`, [token]);
1055
+ if (full === undefined)
1056
+ continue;
1057
+ let doc;
1058
+ try {
1059
+ doc = decodeJsonColumn(full.checkpoint, readM21Document, "checkpoint.checkpoint (M21 question-card repair)");
1060
+ }
1061
+ catch (e) {
1062
+ if (!isJsonColumnBodyProblem(e))
1063
+ throw e;
1064
+ receipts.parkedQuestionRowsLeftAsWritten += 1;
1065
+ continue;
1066
+ }
1067
+ const gate = doc.gate;
1068
+ if (Object.hasOwn(doc, "requirementsDigest") || gate === null || typeof gate !== "object" || Array.isArray(gate)) {
1069
+ receipts.parkedQuestionRowsLeftAsWritten += 1;
1070
+ continue;
1071
+ }
1072
+ const next = encode({ ...doc, gate: { ...gate, approvalCard: QUESTION_CARD } }, "checkpoint (M21 question-card repair)");
1073
+ receipts.parkedQuestionCardsWritten += await db.exec(dialect === "tidb"
1074
+ ? `UPDATE checkpoint SET checkpoint = ? WHERE token = ? AND status = 'pending' AND CAST(${docText} AS BINARY) = CAST(? AS BINARY)`
1075
+ : `UPDATE checkpoint SET checkpoint = $1::jsonb WHERE token = $2 AND status = 'pending' AND ${docText} = $3`, [next, token, String(full.checkpoint)]);
1076
+ }
1077
+ if (page.length < PARKED_ROW_REPAIR_PAGE)
1078
+ return;
1079
+ }
1080
+ }
1081
+ const PLACED_OWNER_SCAN_PAGE = 500;
1082
+ function placedOwnerScanSql(dialect) {
1083
+ return ("SELECT c.session_id, c.incarnation, r.session_id AS root_row, r.owner AS root_owner, r.incarnation AS root_incarnation, r.placement_kind AS root_kind, " +
1084
+ "p.session_id AS parent_row, p.owner AS parent_owner, p.incarnation AS parent_incarnation, p.placement_kind AS parent_kind, " +
1085
+ "c.placement_root_session_id AS root_id, c.placement_parent_session_id AS parent_id " +
1086
+ "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, '') " +
1087
+ `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}`);
1088
+ }
1089
+ function scannedPlacement(r) {
1090
+ return {
1091
+ ...(typeof r.root_id === "string" ? { rootSessionId: r.root_id } : {}),
1092
+ ...(typeof r.parent_id === "string" ? { parentSessionId: r.parent_id } : {}),
1093
+ };
1094
+ }
1095
+ function scannedFaces(r) {
1096
+ const faces = new Map();
1097
+ const put = (id, row, owner, incarnation) => {
1098
+ if (typeof id !== "string" || row === null || row === undefined)
1099
+ return;
1100
+ faces.set(id, sessionFaceFromRow(id, typeof owner === "string" ? owner : null, incarnation));
1101
+ };
1102
+ put(r.root_id, r.root_row, r.root_owner, r.root_incarnation);
1103
+ put(r.parent_id, r.parent_row, r.parent_owner, r.parent_incarnation);
1104
+ return faces;
1105
+ }
1106
+ function scannedPlacedSources(r) {
1107
+ const placed = new Set();
1108
+ if (typeof r.root_id === "string" && r.root_row !== null && r.root_row !== undefined && r.root_kind !== null && r.root_kind !== undefined)
1109
+ placed.add(r.root_id);
1110
+ if (typeof r.parent_id === "string" && r.parent_row !== null && r.parent_row !== undefined && r.parent_kind !== null && r.parent_kind !== undefined)
1111
+ placed.add(r.parent_id);
1112
+ return placed;
1113
+ }
1114
+ export async function inheritPlacedSessionOwners(db, dialect, receipts) {
1115
+ const scan = placedOwnerScanSql(dialect);
1116
+ const cas = CLAIM_OWNERLESS_PLACED_SQL[dialect];
1117
+ let after = "";
1118
+ for (;;) {
1119
+ const page = await db.rows(scan, [after]);
1120
+ for (const r of page) {
1121
+ const sessionId = String(r.session_id);
1122
+ after = sessionId;
1123
+ const incarnation = incarnationFromCell(sessionId, r.incarnation);
1124
+ const judged = judgeDelegatedOwner(sessionId, scannedPlacement(r), scannedFaces(r));
1125
+ const owner = judged.inherited.owner;
1126
+ if (owner === null) {
1127
+ if (judged.inherited.kind === "inherited" && scannedPlacedSources(r).has(judged.inherited.from))
1128
+ receipts.placedSessionOwnersRemaining += 1;
1129
+ continue;
1130
+ }
1131
+ const landed = await db.exec(cas, [...claimOwnerlessParams(sessionId, incarnation, owner)]);
1132
+ if (isOrphanSessionOwner(owner))
1133
+ receipts.placedSessionOwnersOrphaned += landed;
1134
+ else
1135
+ receipts.placedSessionOwnersInherited += landed;
1136
+ }
1137
+ if (page.length < PLACED_OWNER_SCAN_PAGE)
1138
+ return;
740
1139
  }
741
1140
  }
742
1141
  const INCARNATION_BACKFILL_BATCH = 500;
743
- async function migrateSessionIncarnations(db, dialect) {
744
- if (!(await tableExists(db, dialect, "session_meta")))
745
- return { sessionIncarnationColumn: "no-table", sessionIncarnationsBackfilled: 0 };
1142
+ async function migrateSessionIncarnations(db, dialect, receipts) {
1143
+ if (!(await tableExists(db, dialect, "session_meta"))) {
1144
+ receipts.sessionIncarnationColumn = "no-table";
1145
+ return;
1146
+ }
746
1147
  const col = SESSION_META_INCARNATION_COLUMN;
747
- let sessionIncarnationColumn = "present";
1148
+ let column = "present";
748
1149
  if (!(await columnExists(db, dialect, "session_meta", col.name))) {
749
1150
  try {
750
1151
  await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
751
- sessionIncarnationColumn = "added";
1152
+ column = "added";
752
1153
  }
753
1154
  catch (e) {
754
1155
  if (!(await columnExists(db, dialect, "session_meta", col.name)))
755
1156
  throw e;
756
1157
  }
757
1158
  }
758
- return { sessionIncarnationColumn, sessionIncarnationsBackfilled: await backfillSessionIncarnations(db, dialect) };
1159
+ receipts.sessionIncarnationColumn = column;
1160
+ await backfillSessionIncarnations(db, dialect, receipts);
759
1161
  }
760
- async function backfillSessionIncarnations(db, dialect) {
1162
+ async function backfillSessionIncarnations(db, dialect, receipts) {
761
1163
  const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
762
1164
  const initial = await remaining();
763
1165
  if (initial === 0)
764
- return 0;
1166
+ return;
765
1167
  const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
766
1168
  let written = 0;
767
1169
  let after;
@@ -771,7 +1173,7 @@ async function backfillSessionIncarnations(db, dialect) {
771
1173
  : await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' AND session_id > ${dialect === "pg" ? "$1" : "?"} ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`, [after])).map((r) => String(r.session_id));
772
1174
  if (ids.length === 0) {
773
1175
  if (after === undefined)
774
- return written;
1176
+ return;
775
1177
  after = undefined;
776
1178
  continue;
777
1179
  }
@@ -782,7 +1184,9 @@ async function backfillSessionIncarnations(db, dialect) {
782
1184
  };
783
1185
  const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
784
1186
  const inList = ids.map((id) => bind(id)).join(", ");
785
- written += await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
1187
+ const landed = await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
1188
+ written += landed;
1189
+ receipts.sessionIncarnationsBackfilled += landed;
786
1190
  after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
787
1191
  }
788
1192
  const left = await remaining();
@@ -791,33 +1195,44 @@ async function backfillSessionIncarnations(db, dialect) {
791
1195
  `(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
792
1196
  `if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
793
1197
  }
794
- return written;
1198
+ }
1199
+ function readingFields(r) {
1200
+ return {
1201
+ auditDetailJsonNullRows: r.auditDetailJsonNullRows,
1202
+ ...(r.sessionIncarnationColumn !== undefined ? { sessionIncarnationColumn: r.sessionIncarnationColumn } : {}),
1203
+ sessionIncarnationsBackfilled: r.sessionIncarnationsBackfilled,
1204
+ holderDeathCodeRowsBackfilled: r.holderDeathCodeRowsBackfilled,
1205
+ allowRuleShadowRowsMigrated: r.allowRuleShadowRowsMigrated,
1206
+ allowRuleShadowRowsUnreadable: r.allowRuleShadowRowsUnreadable,
1207
+ ...(r.workflowParkIndexKey !== undefined ? { workflowParkIndexKey: r.workflowParkIndexKey } : {}),
1208
+ workflowParkIndexRowsCarried: r.workflowParkIndexRowsCarried,
1209
+ undurableScopeRuleRowsRemoved: r.undurableScopeRuleRows.length,
1210
+ undurableScopeRuleBucketsDeferred: r.undurableScopeRuleBucketsDeferred,
1211
+ undurableScopeRuleBucketsUnreadable: r.undurableScopeRuleBucketsUnreadable.length,
1212
+ ...(r.sessionRuleColumn !== undefined ? { sessionRuleColumn: r.sessionRuleColumn } : {}),
1213
+ sessionRuleRowsWithRetiredBuckets: r.sessionRuleRowsWithRetiredBuckets.length,
1214
+ sessionRuleRowsWithOldGrammarEntries: r.sessionRuleRowsWithOldGrammarEntries.length,
1215
+ sessionRuleRowsWithUnreadableDeny: r.sessionRuleRowsWithUnreadableDeny.length,
1216
+ retiredCardWordAskRowsRewritten: r.retiredCardWordAskRowsRewritten,
1217
+ retiredCardWordRowsUnreadable: r.retiredCardWordRowsUnreadable,
1218
+ parkedQuestionCardsWritten: r.parkedQuestionCardsWritten,
1219
+ parkedQuestionRowsLeftAsWritten: r.parkedQuestionRowsLeftAsWritten,
1220
+ parkedRowGateKindsBackfilled: r.parkedRowGateKindsBackfilled,
1221
+ parkedRowsUnresumableExpired: r.parkedRowsUnresumableExpired,
1222
+ placedSessionOwnersInherited: r.placedSessionOwnersInherited,
1223
+ placedSessionOwnersOrphaned: r.placedSessionOwnersOrphaned,
1224
+ placedSessionOwnersRemaining: r.placedSessionOwnersRemaining,
1225
+ ...(r.workflowJournalScopeColumn !== undefined ? { workflowJournalScopeColumn: r.workflowJournalScopeColumn } : {}),
1226
+ workflowJournalOwnersBackfilled: r.workflowJournalOwnersBackfilled,
1227
+ workflowJournalForeignScopeRowsRemoved: r.workflowJournalForeignScopeRowsRemoved,
1228
+ retiredBreakerTablesDropped: r.retiredBreakerTablesDropped,
1229
+ mailboxRejectedKeyRowsRemoved: r.mailboxRejectedKeyRowsRemoved,
1230
+ };
795
1231
  }
796
1232
  export function logOneTimeMigrations(log, backend, report) {
797
- const changed = report.auditDetailJsonNullRows > 0 ||
798
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
799
- report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
800
- report.undurableScopeRuleBucketsUnreadable.length > 0 ||
801
- report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
802
- report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
803
- (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
1233
+ (changedAnything(report) ? log.warn : log.info).call(log, "one_time_migrations_applied", {
804
1234
  backend,
805
- auditDetailJsonNullRows: report.auditDetailJsonNullRows,
806
- sessionIncarnationColumn: report.sessionIncarnationColumn,
807
- sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
808
- holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
809
- allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
810
- allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
811
- workflowParkIndexKey: report.workflowParkIndexKey,
812
- workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
813
- undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
814
- undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
815
- undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
816
- sessionRuleColumn: report.sessionRuleColumn,
817
- sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
818
- sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
819
- retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
820
- retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
1235
+ ...readingFields(report),
821
1236
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
822
1237
  note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
823
1238
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
@@ -835,16 +1250,56 @@ export function logOneTimeMigrations(log, backend, report) {
835
1250
  "are read by the engine in the grammar the row carries (none = the grammar before the stamp) and rewritten as the same statement in the current grammar " +
836
1251
  "(the column carries the in-progress name rule_entries_old_grammar until every row is rewritten, so an interrupted boot resumes there) — toolAllow entries " +
837
1252
  "that the earlier grammar read as naming no tool are removed from the row, every row is stamped with the current grammar, one session_rule_row_old_grammar_entries " +
838
- "line per row that lost entries; " +
1253
+ "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 " +
1254
+ "call of such a session until an operator rewrites its rules), one session_rule_row_unreadable_deny line per row; " +
839
1255
  "rows still holding a retired command bucket keep that bucket and are named, one session_rule_row_retired_bucket line per row on every boot; " +
840
1256
  "M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
841
1257
  "governance_unavailable read as rule_unreadable; the engine's own parked-row documents are never rewritten (their row digest covers the ask class), they and " +
842
- "the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read). Each step " +
1258
+ "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 " +
1259
+ "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 " +
1260
+ "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 " +
1261
+ "registered kind cannot be resumed and is expired); M23: delegated (placed) session rows written ownerless by an earlier version get the owner of their " +
1262
+ "delegation tree as this boot reads it — the root session's owner, or the parent's when the root row is gone; a row under an ownerless root stays ownerless; a row whose root and parent " +
1263
+ "are both gone gets an owner no caller can match and the rows below it inherit that owner, all counted and named by a placed_session_owners_orphaned line; " +
1264
+ "placedSessionOwnersRemaining counts the rows this boot could not settle because the session they inherit from was itself still ownerless — until a later boot or " +
1265
+ "their next park settles them, only fleet callers can read them; M24: the scope that owns a workflow run's resume journal moved from a column on every journal row " +
1266
+ "into one row per run in workflow_journal_owner — each run got the scope of its lowest-ordinal row, and the journal table was rebuilt (copy + atomic rename) " +
1267
+ "keyed by (run, owner-record incarnation, ordinal) without the scope column; rows another scope had written into the run were not carried " +
1268
+ "(counted, one workflow_journal_foreign_scope_rows_removed line); M26: the retired shared circuit-breaker table circuit_breaker dropped — its keys " +
1269
+ "carried a route's base URL, which could carry a credential from the URL's query, and the whole table is dropped rather than judged row by row; " +
1270
+ "the breakers live in the new table breaker_state, which this step never touches; only the count is logged, never a key; M27: rows of the three " +
1271
+ "mailbox tables (mailbox / mailbox_message / mailbox_tombstone) whose key — scope or handle — the mailbox key judge refuses (control characters, " +
1272
+ "malformed Unicode, wider than 190 UTF-8 bytes, empty scope) removed, a whole box at a time: no entry point reaches such a key any more, so neither " +
1273
+ "delivery, reading nor session deletion would ever have touched them again; only the count is logged, never a key). Each step " +
843
1274
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
844
1275
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
845
1276
  "version that still carries a step at least once before upgrading past its deletion.",
846
1277
  });
847
- for (const row of report.undurableScopeRuleRows) {
1278
+ logReceiptRows(log, backend, report);
1279
+ }
1280
+ function logReceiptRows(log, backend, r) {
1281
+ if (r.workflowJournalForeignScopeRowsRemoved > 0) {
1282
+ log.warn("workflow_journal_foreign_scope_rows_removed", {
1283
+ backend,
1284
+ rows: r.workflowJournalForeignScopeRowsRemoved,
1285
+ note: "workflow journal rows whose scope differed from the scope that owns their run were removed while the run's owning scope moved into workflow_journal_owner. " +
1286
+ "Earlier versions accepted an append from another scope into a run (a new step was inserted, an existing step overwritten under the owner's scope); the current " +
1287
+ "schema keeps one scope per run, so such rows have nowhere to live. Only the scope that wrote them could read them, and only to replay those steps when resuming " +
1288
+ "that run — such a resume now runs those steps live. No action needed",
1289
+ });
1290
+ }
1291
+ if (r.placedSessionOwnersOrphaned > 0) {
1292
+ log.warn("placed_session_owners_orphaned", {
1293
+ backend,
1294
+ rows: r.placedSessionOwnersOrphaned,
1295
+ note: "delegated (placed) session rows that an earlier version wrote without an owner were given an owner no caller can match, in one of two forms: a row whose root and parent " +
1296
+ "sessions are both gone gets `_orphan:<its own session id>`; a row whose parent is such a row inherits that same value, `_orphan:<that ancestor's session id>` (rows below it " +
1297
+ "inherit it in turn). Their transcripts are no longer readable through the session faces by anyone but fleet-wide and operator callers. They are the rows whose owner " +
1298
+ "starts with `_orphan:` (an owner naming the row's own id had no surviving ancestor; one naming another session came down from that orphaned ancestor); set a real owner " +
1299
+ "in the session store by hand if someone should read one, or leave them to the transcript retention",
1300
+ });
1301
+ }
1302
+ for (const row of r.undurableScopeRuleRows) {
848
1303
  log.warn("permission_rule_row_removed", {
849
1304
  backend,
850
1305
  ownerKey: row.ownerKey,
@@ -855,7 +1310,7 @@ export function logOneTimeMigrations(log, backend, report) {
855
1310
  note: removedRuleRowNote(row.kind),
856
1311
  });
857
1312
  }
858
- for (const bucket of report.undurableScopeRuleBucketsUnreadable) {
1313
+ for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
859
1314
  log.warn("permission_rule_bucket_unreadable", {
860
1315
  backend,
861
1316
  ownerKey: bucket.ownerKey,
@@ -865,8 +1320,37 @@ export function logOneTimeMigrations(log, backend, report) {
865
1320
  "keeps being refused, as before this release, until someone repairs or deletes that row by hand",
866
1321
  });
867
1322
  }
868
- logSessionRuleOldGrammarRows(log, backend, report.sessionRuleRowsWithOldGrammarEntries);
869
- for (const row of report.sessionRuleRowsWithRetiredBuckets) {
1323
+ for (const row of r.sessionRuleRowsWithOldGrammarEntries) {
1324
+ log.warn("session_rule_row_old_grammar_entries", {
1325
+ backend,
1326
+ policyKey: row.policyKey,
1327
+ sessionId: row.sessionId,
1328
+ toolAllowDropped: row.toolAllowDropped,
1329
+ 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 " +
1330
+ "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
1331
+ "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 " +
1332
+ "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 " +
1333
+ "session_rule_row_unreadable_deny instead). Every entry named on this line is named exactly once, by the boot that " +
1334
+ "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",
1335
+ });
1336
+ }
1337
+ for (const row of r.sessionRuleRowsWithUnreadableDeny) {
1338
+ log.warn("session_rule_row_unreadable_deny", {
1339
+ backend,
1340
+ policyKey: row.policyKey,
1341
+ sessionId: row.sessionId,
1342
+ toolDenyUnreadable: row.toolDenyUnreadable,
1343
+ ...(row.retiredBuckets !== undefined ? { retiredBuckets: row.retiredBuckets } : {}),
1344
+ 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 " +
1345
+ "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 " +
1346
+ "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. " +
1347
+ "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 " +
1348
+ "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 " +
1349
+ "(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 " +
1350
+ "(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",
1351
+ });
1352
+ }
1353
+ for (const row of r.sessionRuleRowsWithRetiredBuckets) {
870
1354
  log.warn("session_rule_row_retired_bucket", {
871
1355
  backend,
872
1356
  policyKey: row.policyKey,
@@ -880,40 +1364,17 @@ export function logOneTimeMigrations(log, backend, report) {
880
1364
  });
881
1365
  }
882
1366
  }
883
- function logSessionRuleOldGrammarRows(log, backend, rows) {
884
- for (const row of rows) {
885
- log.warn("session_rule_row_old_grammar_entries", {
886
- backend,
887
- policyKey: row.policyKey,
888
- sessionId: row.sessionId,
889
- toolAllowDropped: row.toolAllowDropped,
890
- ...(row.wildcardMasked ? { wildcardMasked: true } : {}),
891
- note: row.wildcardMasked
892
- ? "a session's stored rules were written before the rule-grammar stamp and its toolAllow list holds `*`: the engine reads that list as void in every grammar (every call " +
893
- "refused), which hides what the other entries meant in the earlier grammar, so this row was NOT rewritten or stamped — it stays in its earlier grammar. To restore the other " +
894
- "entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
895
- :
896
- "a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
897
- "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
898
- "The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
899
- "nothing); toolDeny entries read the same in every grammar and were left as written. Every entry named on this line is named exactly once, by the boot that " +
900
- "removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
901
- });
902
- }
903
- }
904
1367
  function logOneTimeMigrationsInterrupted(log, backend, error) {
905
- const { receipts } = error;
906
1368
  log.warn("one_time_migrations_interrupted", {
907
1369
  backend,
908
- sessionRuleColumn: receipts.sessionRuleColumn ?? "in-progress",
909
- sessionRuleRowsWithOldGrammarEntries: receipts.sessionRuleRowsWithOldGrammarEntries.length,
910
- note: "the one-time migrations failed after committing changes whose receipts the next boot cannot re-derive: the session_rule_row_old_grammar_entries lines that follow name " +
911
- "rows this boot already rewrote — a retried boot finds them rewritten and removes nothing from them. These lines are the only place the rewritten rows are ever " +
912
- "reported — no later boot lists them — so save them. The failure itself follows as this boot's refusal. sessionRuleColumn \"renamed\": every row was rewritten and the final " +
913
- "column name was published before the failure; \"in-progress\": the column still carries its in-progress name — the next boot rewrites the remaining rows (rows already " +
914
- "rewritten are left alone) and publishes the final name",
1370
+ ...readingFields(error.receipts),
1371
+ 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 " +
1372
+ "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 " +
1373
+ "as this boot's refusal. A shape reading absent from this line (sessionIncarnationColumn / workflowParkIndexKey / sessionRuleColumn / workflowJournalScopeColumn) belongs to a step that did not " +
1374
+ "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 " +
1375
+ "remaining rows (rows already rewritten are left alone) and publishes the final name",
915
1376
  });
916
- logSessionRuleOldGrammarRows(log, backend, receipts.sessionRuleRowsWithOldGrammarEntries);
1377
+ logReceiptRows(log, backend, error.receipts);
917
1378
  }
918
1379
  export async function runAndLogOneTimeMigrations(log, backend) {
919
1380
  let report;