@sema-agent/server 7.141.0 → 7.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/MIGRATION.md +657 -0
  2. package/USAGE.md +102 -30
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +61 -43
  5. package/dist/approval-ask-audit-store.js +69 -44
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +126 -27
  9. package/dist/approval-card.js +103 -23
  10. package/dist/approval-crash-convergence.d.ts +17 -7
  11. package/dist/approval-crash-convergence.js +94 -22
  12. package/dist/approval-deny-reasons.d.ts +34 -30
  13. package/dist/approval-deny-reasons.js +21 -6
  14. package/dist/approval-policy-settlement.d.ts +1 -1
  15. package/dist/approval-queue-kind.d.ts +27 -0
  16. package/dist/approval-queue-kind.js +6 -0
  17. package/dist/approval-reconciler.d.ts +41 -9
  18. package/dist/approval-reconciler.js +45 -42
  19. package/dist/audit.js +4 -2
  20. package/dist/auth-bridge.d.ts +9 -6
  21. package/dist/auth-bridge.js +9 -3
  22. package/dist/boot/adoption.js +2 -1
  23. package/dist/boot/budget-tracing.d.ts +17 -1
  24. package/dist/boot/budget-tracing.js +10 -6
  25. package/dist/boot/device-lane.d.ts +1 -1
  26. package/dist/boot/execution-env.d.ts +24 -4
  27. package/dist/boot/execution-env.js +26 -23
  28. package/dist/boot/execution-lane.js +2 -1
  29. package/dist/boot/exit-boundary.js +5 -4
  30. package/dist/boot/governance-seams.d.ts +26 -41
  31. package/dist/boot/governance-seams.js +61 -28
  32. package/dist/boot/late-binds.d.ts +7 -4
  33. package/dist/boot/late-binds.js +3 -1
  34. package/dist/boot/leader.js +2 -4
  35. package/dist/boot/memory-consolidation.d.ts +0 -8
  36. package/dist/boot/memory-consolidation.js +5 -4
  37. package/dist/boot/parked-revive-gate.js +2 -1
  38. package/dist/boot/permission-rules-audit.d.ts +0 -28
  39. package/dist/boot/permission-rules-audit.js +2 -1
  40. package/dist/boot/read-deny-anchor-audit.js +2 -1
  41. package/dist/boot/reapers.js +9 -7
  42. package/dist/boot/resolve-spec.d.ts +1 -1
  43. package/dist/boot/resolve-spec.js +19 -10
  44. package/dist/boot/retention-lane.js +5 -3
  45. package/dist/boot/runner-deps.d.ts +10 -7
  46. package/dist/boot/runner-deps.js +20 -29
  47. package/dist/boot/runtime-caps.js +3 -2
  48. package/dist/boot/session-faces.js +4 -3
  49. package/dist/boot/shutdown.d.ts +9 -0
  50. package/dist/boot/shutdown.js +19 -4
  51. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  52. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  53. package/dist/boot/stage-03-store-backend.js +1 -2
  54. package/dist/boot/stage-04-stores.d.ts +5 -2
  55. package/dist/boot/stage-04-stores.js +6 -7
  56. package/dist/boot/stage-05-execution-env.d.ts +6 -3
  57. package/dist/boot/stage-05-execution-env.js +8 -6
  58. package/dist/boot/stage-06-runners.d.ts +6 -3
  59. package/dist/boot/stage-06-runners.js +3 -4
  60. package/dist/boot/stage-07-capability-layer.d.ts +6 -3
  61. package/dist/boot/stage-07-capability-layer.js +7 -2
  62. package/dist/boot/stage-08-reapers.d.ts +6 -3
  63. package/dist/boot/stage-08-reapers.js +3 -2
  64. package/dist/boot/stage-09-leader.d.ts +6 -3
  65. package/dist/boot/stage-10-http-server.d.ts +6 -3
  66. package/dist/boot/stage-10-http-server.js +14 -5
  67. package/dist/boot/stores.d.ts +5 -2
  68. package/dist/boot/stores.js +10 -9
  69. package/dist/boot/web-admin.d.ts +5 -1
  70. package/dist/boot/web-admin.js +48 -36
  71. package/dist/boot/webfetch-summarize-lane.js +2 -1
  72. package/dist/boot/workflow-orchestration.js +3 -2
  73. package/dist/capabilities/builtin-tools.d.ts +2 -1
  74. package/dist/capabilities/builtin-tools.js +2 -4
  75. package/dist/capabilities/composite-members.d.ts +2 -1
  76. package/dist/capabilities/composite-members.js +2 -1
  77. package/dist/capabilities/hands-lane.d.ts +9 -39
  78. package/dist/capabilities/hands-lane.js +8 -13
  79. package/dist/capabilities/mcp-probe.d.ts +3 -2
  80. package/dist/capabilities/scenarios.d.ts +11 -1
  81. package/dist/capabilities/scenarios.js +1 -0
  82. package/dist/capabilities/skills.d.ts +1 -17
  83. package/dist/capabilities/skills.js +0 -8
  84. package/dist/capability-facts.d.ts +31 -7
  85. package/dist/capability-facts.js +27 -4
  86. package/dist/card-void.d.ts +6 -9
  87. package/dist/config-catalog.js +2 -1
  88. package/dist/config-dir-trust.js +58 -29
  89. package/dist/config-invariants.d.ts +7 -1
  90. package/dist/config-invariants.js +7 -0
  91. package/dist/config-provider.d.ts +10 -0
  92. package/dist/config-provider.js +15 -4
  93. package/dist/config-root-migration.d.ts +13 -0
  94. package/dist/config-root-migration.js +99 -0
  95. package/dist/config-types.d.ts +19 -8
  96. package/dist/config.d.ts +10 -3
  97. package/dist/config.js +18 -4
  98. package/dist/delegated-session-owner.d.ts +5 -3
  99. package/dist/delegated-session-owner.js +1 -1
  100. package/dist/device-ws-protocol.d.ts +1 -0
  101. package/dist/device-ws-protocol.js +2 -1
  102. package/dist/env-model-table-gate.d.ts +53 -0
  103. package/dist/env-model-table-gate.js +53 -0
  104. package/dist/execution-lane-caps.d.ts +2 -1
  105. package/dist/fleet/fleet-bus.d.ts +34 -55
  106. package/dist/fleet/fleet-bus.js +35 -100
  107. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  108. package/dist/fleet/fleet-reconciler.js +1 -1
  109. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  110. package/dist/fleet/subagent-tail-bus.js +15 -28
  111. package/dist/hooks/hook-runner.d.ts +6 -0
  112. package/dist/hooks/hook-runner.js +58 -8
  113. package/dist/host-decision.d.ts +54 -20
  114. package/dist/host-decision.js +42 -14
  115. package/dist/http/admission.d.ts +1 -1
  116. package/dist/http/admission.js +7 -3
  117. package/dist/http/approval-sweeps.js +11 -8
  118. package/dist/http/caller-trust.d.ts +1 -1
  119. package/dist/http/dispatch.js +4 -2
  120. package/dist/http/durable-run-leg.js +2 -2
  121. package/dist/http/engine-refusal.d.ts +16 -0
  122. package/dist/http/engine-refusal.js +17 -0
  123. package/dist/http/execution-context.d.ts +2 -2
  124. package/dist/http/idempotency.d.ts +1 -1
  125. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  126. package/dist/http/memory-compliance-failure-reply.js +4 -6
  127. package/dist/http/owner-access.d.ts +11 -0
  128. package/dist/http/owner-access.js +13 -0
  129. package/dist/http/principal-gate.d.ts +3 -1
  130. package/dist/http/principal-gate.js +17 -8
  131. package/dist/http/resume-legs.d.ts +21 -7
  132. package/dist/http/resume-legs.js +76 -58
  133. package/dist/http/resume-restatement.d.ts +39 -0
  134. package/dist/http/resume-restatement.js +25 -0
  135. package/dist/http/route-ctx.d.ts +35 -10
  136. package/dist/http/route-ctx.js +1 -1
  137. package/dist/http/route-table.js +2 -0
  138. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  139. package/dist/http/routes/approvals-assistant.js +142 -31
  140. package/dist/http/routes/attachments.js +18 -0
  141. package/dist/http/routes/capabilities.js +16 -4
  142. package/dist/http/routes/memory-policy.d.ts +2 -2
  143. package/dist/http/routes/memory-policy.js +27 -7
  144. package/dist/http/routes/notify-wake.js +23 -25
  145. package/dist/http/routes/rules.js +48 -13
  146. package/dist/http/routes/runs.js +12 -11
  147. package/dist/http/routes/session-memory-sync.d.ts +91 -0
  148. package/dist/http/routes/session-memory-sync.js +174 -0
  149. package/dist/http/routes/session-sync.js +13 -20
  150. package/dist/http/routes/sessions-list.d.ts +2 -1
  151. package/dist/http/routes/sessions.d.ts +2 -2
  152. package/dist/http/routes/sessions.js +22 -23
  153. package/dist/http/routes/tasks.js +34 -54
  154. package/dist/http/run-read-face.d.ts +7 -0
  155. package/dist/http/run-read-face.js +9 -3
  156. package/dist/http/send.d.ts +5 -0
  157. package/dist/http/send.js +9 -2
  158. package/dist/http/server.d.ts +27 -15
  159. package/dist/http/server.js +4 -6
  160. package/dist/http/stored-body-replay.js +15 -4
  161. package/dist/http/submit-replay.d.ts +4 -3
  162. package/dist/http/submit-replay.js +2 -2
  163. package/dist/http/sync-submit-body.d.ts +21 -4
  164. package/dist/http/sync-submit-body.js +13 -0
  165. package/dist/http/wire-types.d.ts +17 -7
  166. package/dist/identity-source.d.ts +21 -2
  167. package/dist/identity-source.js +6 -0
  168. package/dist/leader/leader.js +5 -2
  169. package/dist/leader/wire.d.ts +7 -1
  170. package/dist/leader/wire.js +6 -5
  171. package/dist/leg-facts.d.ts +37 -0
  172. package/dist/leg-facts.js +15 -0
  173. package/dist/memory-bundle-engine.d.ts +1 -4
  174. package/dist/memory-bundle-engine.js +2 -2
  175. package/dist/memory-incident-sink.d.ts +19 -6
  176. package/dist/memory-incident-sink.js +8 -4
  177. package/dist/memory-operator-faces.d.ts +30 -66
  178. package/dist/memory-operator-faces.js +17 -88
  179. package/dist/memory-scope-stores.d.ts +38 -40
  180. package/dist/memory-scope-stores.js +51 -36
  181. package/dist/memory-scope.d.ts +0 -9
  182. package/dist/memory-scope.js +0 -9
  183. package/dist/memory-sync-client.d.ts +21 -21
  184. package/dist/memory-sync-client.js +62 -43
  185. package/dist/memory-sync-identity.d.ts +24 -0
  186. package/dist/memory-sync-identity.js +57 -0
  187. package/dist/memory-sync.d.ts +27 -5
  188. package/dist/memory-sync.js +29 -7
  189. package/dist/model-plane-face.d.ts +13 -7
  190. package/dist/model-plane-face.js +2 -1
  191. package/dist/observability/fail-open.d.ts +47 -14
  192. package/dist/observability/fail-open.js +37 -11
  193. package/dist/observability/run-terminal.d.ts +92 -34
  194. package/dist/observability/run-terminal.js +171 -162
  195. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  196. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  197. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  198. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  199. package/dist/orchestration/workflow-park-decision.js +5 -3
  200. package/dist/org-delivery-gate.d.ts +11 -0
  201. package/dist/org-delivery-gate.js +19 -1
  202. package/dist/parked-decide.d.ts +13 -9
  203. package/dist/parked-decide.js +7 -7
  204. package/dist/parked-revive-surface.js +1 -1
  205. package/dist/peer-inbound-face.d.ts +127 -0
  206. package/dist/peer-inbound-face.js +158 -0
  207. package/dist/permission-rule-list.d.ts +13 -9
  208. package/dist/permission-rule-list.js +7 -3
  209. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  210. package/dist/plugins/approval-ask-store-file.js +64 -11
  211. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  212. package/dist/plugins/approval-ask-store-memory.js +17 -14
  213. package/dist/plugins/approval-ask-store-sql.d.ts +70 -23
  214. package/dist/plugins/approval-ask-store-sql.js +79 -30
  215. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  216. package/dist/plugins/checkpoint-store-sql.js +9 -7
  217. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  218. package/dist/plugins/file-history-store-sql.js +24 -19
  219. package/dist/plugins/file-run-store.d.ts +35 -1
  220. package/dist/plugins/file-run-store.js +108 -9
  221. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  222. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  223. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  224. package/dist/plugins/local-checkpoint-store.js +2 -2
  225. package/dist/plugins/local-session-store.d.ts +42 -9
  226. package/dist/plugins/local-session-store.js +83 -33
  227. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  228. package/dist/plugins/local-task-attachment-store.js +67 -43
  229. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  230. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  231. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  232. package/dist/plugins/mailbox-store-sql.js +76 -7
  233. package/dist/plugins/memory-key-guards.d.ts +3 -3
  234. package/dist/plugins/memory-run-store.d.ts +6 -1
  235. package/dist/plugins/memory-run-store.js +11 -1
  236. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  237. package/dist/plugins/memory-sync-store-pg.js +21 -0
  238. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  239. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  240. package/dist/plugins/one-time-migrations.d.ts +58 -2
  241. package/dist/plugins/one-time-migrations.js +161 -4
  242. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  243. package/dist/plugins/org-delivery-quota-store.js +2 -12
  244. package/dist/plugins/park-commit-watch.d.ts +1 -1
  245. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  246. package/dist/plugins/permission-rule-store-file.js +15 -2
  247. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  248. package/dist/plugins/permission-rule-store-sql.js +35 -2
  249. package/dist/plugins/remote-env-device.d.ts +8 -11
  250. package/dist/plugins/remote-env-device.js +6 -3
  251. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  252. package/dist/plugins/remote-env-e2b.js +111 -28
  253. package/dist/plugins/remote-env-k8s.d.ts +61 -22
  254. package/dist/plugins/remote-env-k8s.js +133 -33
  255. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  256. package/dist/plugins/remote-env-local-docker.js +40 -12
  257. package/dist/plugins/retention-store-sql.d.ts +2 -2
  258. package/dist/plugins/run-store-sql.d.ts +9 -1
  259. package/dist/plugins/run-store-sql.js +9 -1
  260. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  261. package/dist/plugins/sandbox-read-floor.js +2 -7
  262. package/dist/plugins/sql-errors.d.ts +10 -1
  263. package/dist/plugins/sql-errors.js +13 -0
  264. package/dist/plugins/store-backend.d.ts +33 -35
  265. package/dist/plugins/store-backend.js +10 -1
  266. package/dist/plugins/store-contracts.d.ts +50 -1
  267. package/dist/plugins/store-contracts.js +6 -0
  268. package/dist/plugins/task-attachment-store.d.ts +112 -14
  269. package/dist/plugins/task-attachment-store.js +207 -29
  270. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  271. package/dist/plugins/tool-result-store-sql.js +23 -2
  272. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  273. package/dist/plugins/wiring-section-codec.js +55 -0
  274. package/dist/plugins/workspace-fill.d.ts +60 -0
  275. package/dist/plugins/workspace-fill.js +106 -0
  276. package/dist/project-bundle.d.ts +38 -0
  277. package/dist/project-bundle.js +609 -0
  278. package/dist/rule-entry-list.d.ts +3 -2
  279. package/dist/rule-entry-list.js +5 -1
  280. package/dist/rules-consent.d.ts +28 -5
  281. package/dist/rules-consent.js +40 -28
  282. package/dist/run-local.js +11 -13
  283. package/dist/runs.d.ts +12 -8
  284. package/dist/runs.js +6 -8
  285. package/dist/runtime-governance.d.ts +1 -1
  286. package/dist/sandbox-egress-policy.d.ts +4 -1
  287. package/dist/sandbox-egress-policy.js +3 -1
  288. package/dist/sandbox-git-plane.d.ts +31 -12
  289. package/dist/sandbox-git-plane.js +24 -4
  290. package/dist/security.d.ts +9 -0
  291. package/dist/security.js +8 -0
  292. package/dist/server-secret-env.d.ts +4 -0
  293. package/dist/session-error-wire.d.ts +36 -0
  294. package/dist/session-error-wire.js +13 -0
  295. package/dist/session-incarnation.d.ts +1 -1
  296. package/dist/session-notify-door.d.ts +106 -0
  297. package/dist/session-notify-door.js +93 -0
  298. package/dist/session-sync.d.ts +9 -14
  299. package/dist/session-sync.js +5 -5
  300. package/dist/session-workspace.d.ts +3 -13
  301. package/dist/session-workspace.js +0 -9
  302. package/dist/standing-answer-grant.d.ts +5 -0
  303. package/dist/standing-answer-grant.js +10 -0
  304. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  305. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  306. package/dist/task-mcp.d.ts +1 -1
  307. package/dist/task-settings.d.ts +21 -10
  308. package/dist/task-settings.js +46 -13
  309. package/dist/terminal.d.ts +39 -19
  310. package/dist/terminal.js +65 -22
  311. package/dist/tool-approval.d.ts +56 -37
  312. package/dist/tool-approval.js +78 -71
  313. package/dist/trace/core-keyset-guard.d.ts +134 -14
  314. package/dist/trace/engine-notice-wire.d.ts +9 -3
  315. package/dist/trace/engine-notice-wire.js +7 -3
  316. package/dist/trace/exit-seat.d.ts +9 -2
  317. package/dist/trace/exit-seat.js +1 -0
  318. package/dist/trace/ledger-sink.d.ts +10 -6
  319. package/dist/trace/ledger-sink.js +2 -2
  320. package/dist/trace/model-segment.d.ts +55 -25
  321. package/dist/trace/model-segment.js +101 -33
  322. package/dist/trace/project.d.ts +25 -5
  323. package/dist/trace/project.js +22 -0
  324. package/dist/trace/redact.d.ts +61 -9
  325. package/dist/trace/redact.js +1490 -468
  326. package/dist/trace/self-minted-wire.d.ts +745 -24
  327. package/dist/trace/self-minted-wire.js +239 -9
  328. package/dist/trace/wire-projection-faces.d.ts +2 -2
  329. package/dist/trace/wire-projection-faces.js +2 -2
  330. package/dist/verifier-pause-card.d.ts +33 -5
  331. package/dist/verifier-pause-card.js +15 -10
  332. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  333. package/dist/web-admin-client/apply-effective.js +109 -9
  334. package/dist/web-admin-client/effective-keys.js +2 -0
  335. package/dist/web-admin-client/types.d.ts +4 -0
  336. package/dist/workspace-space.d.ts +19 -3
  337. package/dist/workspace-space.js +8 -4
  338. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  339. package/package.json +3 -2
  340. package/wire-changes/server@7.142.0.json +639 -0
  341. package/wire-changes/server@7.143.0.json +1167 -0
@@ -1,6 +1,7 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
3
- import { APPROVAL_ENDING_CAUSES, causeOfAskRow } from "../approval-deny-reasons.js";
3
+ import { DECIDED_VIA } from "@sema-agent/core";
4
+ import { APPROVAL_ENDING_CAUSES, askRowEndingCause, askRowParkTrigger } from "../approval-deny-reasons.js";
4
5
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
5
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
6
7
  import { decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, readTriStateJsonValueColumn } from "./sql-json-column.js";
@@ -25,7 +26,8 @@ export const MAX_SETTLED_BY_CHARS = 16;
25
26
  export function settledByFitsColumn(v) {
26
27
  return v.length > 0 && v.length <= MAX_SETTLED_BY_CHARS;
27
28
  }
28
- export function assertDecideSettledBy(decision) {
29
+ export function assertDecideSource(decision) {
30
+ assertDecidedViaBesidePolicy(decision);
29
31
  const v = decision.settledBy;
30
32
  if (v === undefined)
31
33
  return;
@@ -33,6 +35,15 @@ export function assertDecideSettledBy(decision) {
33
35
  throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
34
36
  }
35
37
  }
38
+ export const MAX_DECIDED_VIA_CHARS = 32;
39
+ function assertDecidedViaBesidePolicy(decision) {
40
+ const via = decision.decidedVia;
41
+ if (via === undefined)
42
+ return;
43
+ if (!DECIDED_VIA.some((w) => w === via) || via.length > MAX_DECIDED_VIA_CHARS || decision.settledBy !== "policy") {
44
+ throw new Error(`invalid decidedVia: the machine seat is one of ${DECIDED_VIA.map((w) => `"${w}"`).join(" / ")} and is stated only beside settledBy "policy" (got ${JSON.stringify(String(via).slice(0, 32))} beside ${JSON.stringify(decision.settledBy ?? null)})`);
45
+ }
46
+ }
36
47
  export class ApprovalAskReadAbortedError extends Error {
37
48
  constructor(where) {
38
49
  super(`approval-ask read ${where} aborted by caller`);
@@ -61,6 +72,18 @@ async function withReadSignal(op, signal, where) {
61
72
  }
62
73
  }
63
74
  export const POLICY_REFUSAL_DECISION = { decision: "deny", settledBy: "policy" };
75
+ export function claimCauseColumn(intent) {
76
+ switch (intent) {
77
+ case "cancel":
78
+ return "ending_cause";
79
+ case "expire":
80
+ return "park_trigger";
81
+ default: {
82
+ const unreachable = intent;
83
+ throw new Error(`approval-ask-store: no cause column for terminal claim intent ${String(unreachable)}`);
84
+ }
85
+ }
86
+ }
64
87
  export function claimTargetState(intent) {
65
88
  switch (intent) {
66
89
  case "expire":
@@ -78,18 +101,21 @@ export function claimTargetState(intent) {
78
101
  export function projectAskTerminalClaimTruth(row) {
79
102
  if (row === null || row.state === "STREAM_PENDING")
80
103
  return { state: "absent" };
81
- const cause = causeOfAskRow(row);
104
+ const endingCause = askRowEndingCause(row);
105
+ const parkTrigger = askRowParkTrigger(row);
82
106
  return {
83
107
  state: row.state,
84
108
  rev: row.rev,
85
109
  decision: row.decision,
86
110
  ...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
87
111
  settledBy: row.settledBy,
88
- ...(cause !== undefined ? { cause } : {}),
112
+ decidedVia: row.decidedVia,
113
+ ...(endingCause !== undefined ? { endingCause } : {}),
114
+ ...(parkTrigger !== undefined ? { parkTrigger } : {}),
89
115
  };
90
116
  }
91
117
  export function askRowOnCall(row, call) {
92
- return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceTaskId === call.sourceTaskId;
118
+ return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceSessionId === call.sourceSessionId;
93
119
  }
94
120
  export function askOwnerAdmits(owner, principal) {
95
121
  return owner === null || owner === principal;
@@ -105,6 +131,16 @@ export const APPROVAL_ASK_EXECUTION_LANE_COLUMN = {
105
131
  mysql: "execution_lane VARCHAR(32) NOT NULL DEFAULT ''",
106
132
  pg: `execution_lane VARCHAR(32) COLLATE "C" NOT NULL DEFAULT ''`,
107
133
  };
134
+ export const APPROVAL_ASK_ENDING_CAUSE_COLUMN = {
135
+ name: "ending_cause",
136
+ mysql: "ending_cause VARCHAR(64) NULL",
137
+ pg: `ending_cause VARCHAR(64) COLLATE "C"`,
138
+ };
139
+ export const APPROVAL_ASK_PARK_TRIGGER_COLUMN = {
140
+ name: "park_trigger",
141
+ mysql: "park_trigger VARCHAR(64) NULL",
142
+ pg: `park_trigger VARCHAR(64) COLLATE "C"`,
143
+ };
108
144
  function executionLaneColumnOf(raw, askId) {
109
145
  const lane = typeof raw === "string" ? raw : "";
110
146
  if (!isExecutionLane(lane)) {
@@ -155,15 +191,21 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
155
191
  -- settled_by(S-558 / #1016 持久半场):这条决议的结算来源词(NULL = 人答的)。语义、两态读法与
156
192
  -- 「为什么是自己一列而不是塞进 decision_actor」的全文见 AskRow.settledBy 顶注。
157
193
  -- VARCHAR(16):词表属主是 core(AskOutcome.settledBy,今天最长 7 字符 "timeout"),宽度与同表
158
- -- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSettledBy),
194
+ -- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSource),
159
195
  -- 不靠「列够宽就不会截」这种运气(MySQL 非严格模式静默截、PG 直接报错 = 两方言不同答)。
160
196
  settled_by VARCHAR(16) NULL,
197
+ -- decided_via(core 7.61.0 #3169):settled_by = 'policy' 旁的机器座位词(NULL = 没陈述 = 部署自己的策略;旧行同读)。
198
+ -- 与 settled_by 同一条 UPDATE 落列、同一条理由住在行上(语义全文见 AskRow.decidedVia)。闭集词列,宽度 = MAX_DECIDED_VIA_CHARS。
199
+ decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) NULL,
161
200
  -- updated_input(S-26 / #304,A-054.15 收口):人批准时带的 ctrl+g 编辑实参,JSON 文本,与决议同一条
162
201
  -- UPDATE 原子落列;NULL = 没带编辑(与 JSON "null" = 编辑成 null 分家)。LONGTEXT 照 card_json 先例:
163
202
  -- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
164
203
  updated_input LONGTEXT NULL,
165
204
  decided_at_ms BIGINT NULL,
166
- denied_reason VARCHAR(64) NULL,
205
+ -- 成因两列,一列一义(语义见 AskRow.endingCause / AskRow.parkTrigger):ending_cause = 终态成因(到 VOID / DENIED 的转移写);
206
+ -- park_trigger = 转 park 的触发成因(只由 expire claim 写一次,终态不覆盖)。
207
+ ${APPROVAL_ASK_ENDING_CAUSE_COLUMN.mysql},
208
+ ${APPROVAL_ASK_PARK_TRIGGER_COLUMN.mysql},
167
209
  gate_token VARCHAR(120) NULL,
168
210
  gate_bound_call_id VARCHAR(255) NULL,
169
211
  gate_bound_input_hash VARCHAR(255) NULL,
@@ -241,10 +283,14 @@ export async function ensurePgApprovalAskSchema(q) {
241
283
  decision_note TEXT COLLATE "C",
242
284
  -- settled_by(S-558)—— MySQL 孪生逐字同形(宽度/语义/形门的理由写在那侧的行内注)。
243
285
  settled_by VARCHAR(16) COLLATE "C",
286
+ -- decided_via(#3169)—— MySQL 孪生逐字同形。
287
+ decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) COLLATE "C",
244
288
  -- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
245
289
  updated_input TEXT COLLATE "C",
246
290
  decided_at_ms BIGINT,
247
- denied_reason VARCHAR(64) COLLATE "C",
291
+ -- 成因两列 —— MySQL 孪生逐字同形(语义见那侧行内注)。
292
+ ${APPROVAL_ASK_ENDING_CAUSE_COLUMN.pg},
293
+ ${APPROVAL_ASK_PARK_TRIGGER_COLUMN.pg},
248
294
  gate_token VARCHAR(120) COLLATE "C",
249
295
  gate_bound_call_id VARCHAR(255) COLLATE "C",
250
296
  gate_bound_input_hash VARCHAR(255) COLLATE "C",
@@ -288,9 +334,9 @@ function askCols(dialect) {
288
334
  "ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state",
289
335
  "provisional, rev, decision",
290
336
  jsonColumnText(dialect, "decision_actor"),
291
- "decision_note, settled_by",
337
+ "decision_note, settled_by, decided_via",
292
338
  jsonColumnText(dialect, "updated_input"),
293
- "decided_at_ms, denied_reason, gate_token",
339
+ "decided_at_ms, ending_cause, park_trigger, gate_token",
294
340
  "gate_bound_call_id, gate_bound_input_hash, idempotency_key",
295
341
  jsonColumnText(dialect, "card_json"),
296
342
  "rule_command, rule_scope_root, schema_version, expires_at_ms",
@@ -320,7 +366,7 @@ function mapAskRowBody(r, askId) {
320
366
  return {
321
367
  askId: String(r.ask_id),
322
368
  taskId: String(r.task_id),
323
- sourceTaskId: String(r.source_task_id),
369
+ sourceSessionId: String(r.source_task_id),
324
370
  sessionId: String(r.session_id),
325
371
  owner: strOrNull(r.owner),
326
372
  executionLane: executionLaneColumnOf(r.execution_lane, askId),
@@ -336,9 +382,11 @@ function mapAskRowBody(r, askId) {
336
382
  decisionActor: r.decision_actor == null ? null : decodeJsonColumn(r.decision_actor, readJsonObjectColumn, `${APPROVAL_ASK_TABLE}.decision_actor for ask ${askId}`),
337
383
  decisionNote: strOrNull(r.decision_note),
338
384
  settledBy: strOrNull(r.settled_by),
385
+ decidedVia: strOrNull(r.decided_via),
339
386
  ...(r.updated_input == null ? {} : { updatedInput: decodeJsonColumn(r.updated_input, readTriStateJsonValueColumn, `${APPROVAL_ASK_TABLE}.updated_input for ask ${askId}`) }),
340
387
  decidedAtMs: numOrNull(r.decided_at_ms),
341
- deniedReason: strOrNull(r.denied_reason),
388
+ endingCause: strOrNull(r.ending_cause),
389
+ parkTrigger: strOrNull(r.park_trigger),
342
390
  gateToken: strOrNull(r.gate_token),
343
391
  gateBoundCallId: strOrNull(r.gate_bound_call_id),
344
392
  gateBoundInputHash: strOrNull(r.gate_bound_input_hash),
@@ -373,8 +421,8 @@ function patchColumns(patch, json) {
373
421
  out.push(["decision_note", patch.decisionNote]);
374
422
  if (patch.decidedAtMs !== undefined)
375
423
  out.push(["decided_at_ms", patch.decidedAtMs]);
376
- if (patch.deniedReason !== undefined)
377
- out.push(["denied_reason", patch.deniedReason]);
424
+ if (patch.endingCause !== undefined)
425
+ out.push(["ending_cause", patch.endingCause]);
378
426
  if (patch.gateToken !== undefined)
379
427
  out.push(["gate_token", patch.gateToken]);
380
428
  if (patch.gateBoundCallId !== undefined)
@@ -427,12 +475,12 @@ export class SqlApprovalAskStore {
427
475
  try {
428
476
  await conn.begin();
429
477
  await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
430
- const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
431
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
432
- "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
478
+ const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, ending_cause, park_trigger, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
479
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, ending_cause, park_trigger, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
480
+ "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
433
481
  row.askId,
434
482
  row.taskId,
435
- row.sourceTaskId,
483
+ row.sourceSessionId,
436
484
  row.sessionId,
437
485
  row.owner ?? null,
438
486
  row.executionLane,
@@ -486,10 +534,10 @@ export class SqlApprovalAskStore {
486
534
  async decideAsk(askId, batchId, owner, decision) {
487
535
  if (decision.idempotencyKey != null)
488
536
  assertIdempotencyKeyShape(decision.idempotencyKey);
489
- assertDecideSettledBy(decision);
537
+ assertDecideSource(decision);
490
538
  const now = Date.now();
491
539
  const dialect = this.db.dialect;
492
- const ownerWhere = decideOwnerWhere(dialect, owner, 11);
540
+ const ownerWhere = decideOwnerWhere(dialect, owner, 12);
493
541
  const conn = await this.db.connect();
494
542
  try {
495
543
  await conn.begin();
@@ -500,11 +548,12 @@ export class SqlApprovalAskStore {
500
548
  }
501
549
  let askRes;
502
550
  try {
503
- askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, updated_input=${ph(dialect, 5)}, decided_at_ms=${ph(dialect, 6)}, idempotency_key=${ph(dialect, 7)}, rev=rev+1, updated_at_ms=${ph(dialect, 8)} WHERE ${pendingClaimWhere(dialect, 9)} AND ${ownerWhere.sql}`, [
551
+ askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, decided_via=${ph(dialect, 5)}, updated_input=${ph(dialect, 6)}, decided_at_ms=${ph(dialect, 7)}, idempotency_key=${ph(dialect, 8)}, rev=rev+1, updated_at_ms=${ph(dialect, 9)} WHERE ${pendingClaimWhere(dialect, 10)} AND ${ownerWhere.sql}`, [
504
552
  decision.decision,
505
553
  decision.decisionActor == null ? null : this.json(decision.decisionActor),
506
554
  decision.decisionNote ?? null,
507
555
  decision.settledBy ?? null,
556
+ decision.decidedVia ?? null,
508
557
  decision.updatedInput === undefined ? null : this.json(decision.updatedInput),
509
558
  now,
510
559
  decision.idempotencyKey ?? null,
@@ -549,7 +598,7 @@ export class SqlApprovalAskStore {
549
598
  throw new Error(`approval-ask-store.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
550
599
  }
551
600
  if (intent === "refuse")
552
- assertDecideSettledBy(POLICY_REFUSAL_DECISION);
601
+ assertDecideSource(POLICY_REFUSAL_DECISION);
553
602
  const now = Date.now();
554
603
  const dialect = this.db.dialect;
555
604
  const conn = await this.db.connect();
@@ -566,7 +615,7 @@ export class SqlApprovalAskStore {
566
615
  ? await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, decision = ${ph(dialect, 2)}, settled_by = ${ph(dialect, 3)}, decided_at_ms = ${ph(dialect, 4)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 5)} WHERE ${pendingClaimWhere(dialect, 6)}`, [to, POLICY_REFUSAL_DECISION.decision, POLICY_REFUSAL_DECISION.settledBy, now, now, askId, batchId])
567
616
  : (intent === "cancel" || intent === "expire") && opts?.cause !== undefined
568
617
  ?
569
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, denied_reason = ${ph(dialect, 2)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 3)} WHERE ${pendingClaimWhere(dialect, 4)}`, [to, opts.cause, now, askId, batchId])
618
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, ${claimCauseColumn(intent)} = ${ph(dialect, 2)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 3)} WHERE ${pendingClaimWhere(dialect, 4)}`, [to, opts.cause, now, askId, batchId])
570
619
  : await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [
571
620
  to,
572
621
  now,
@@ -585,7 +634,7 @@ export class SqlApprovalAskStore {
585
634
  await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
586
635
  let voidedSiblings;
587
636
  if (this.db.dialect === "pg") {
588
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN]);
637
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN]);
589
638
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
590
639
  }
591
640
  else {
@@ -593,7 +642,7 @@ export class SqlApprovalAskStore {
593
642
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
594
643
  if (voidedSiblings.length > 0) {
595
644
  const inList = voidedSiblings.map(() => "?").join(", ");
596
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
645
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
597
646
  APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN,
598
647
  now,
599
648
  ...voidedSiblings,
@@ -639,7 +688,7 @@ export class SqlApprovalAskStore {
639
688
  }
640
689
  let voidedSiblings;
641
690
  if (this.db.dialect === "pg") {
642
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE]);
691
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE]);
643
692
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
644
693
  }
645
694
  else {
@@ -647,7 +696,7 @@ export class SqlApprovalAskStore {
647
696
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
648
697
  if (voidedSiblings.length > 0) {
649
698
  const inList = voidedSiblings.map(() => "?").join(", ");
650
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE, now, ...voidedSiblings]);
699
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE, now, ...voidedSiblings]);
651
700
  }
652
701
  }
653
702
  await conn.commit();
@@ -677,7 +726,7 @@ export class SqlApprovalAskStore {
677
726
  }
678
727
  let voided;
679
728
  if (this.db.dialect === "pg") {
680
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$3, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId, APPROVAL_ENDING_CAUSES.BATCH_ABORTED]);
729
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$3, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId, APPROVAL_ENDING_CAUSES.BATCH_ABORTED]);
681
730
  voided = res.rows.map((r) => String(r.ask_id));
682
731
  }
683
732
  else {
@@ -685,7 +734,7 @@ export class SqlApprovalAskStore {
685
734
  voided = sel.rows.map((r) => String(r.ask_id));
686
735
  if (voided.length > 0) {
687
736
  const inList = voided.map(() => "?").join(", ");
688
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [APPROVAL_ENDING_CAUSES.BATCH_ABORTED, now, ...voided]);
737
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [APPROVAL_ENDING_CAUSES.BATCH_ABORTED, now, ...voided]);
689
738
  }
690
739
  }
691
740
  await conn.commit();
@@ -722,7 +771,7 @@ export class SqlApprovalAskStore {
722
771
  return rows.map(mapAskRow);
723
772
  }
724
773
  async listByCall(call) {
725
- const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
774
+ const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceSessionId]);
726
775
  return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
727
776
  }
728
777
  async listParkingBySession(sessionId) {
@@ -343,7 +343,7 @@ export interface CheckpointAskCandidate {
343
343
  status: string;
344
344
  createdAtMs: number;
345
345
  /**
346
- * 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `sourceTaskId`(core 在
346
+ * 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `sourceSessionId`(core 在
347
347
  * suspendAsk 铸行时恒填该腿的 `sessionId`)。**没有对应的列**,只能从 blob 窄读;读不出/缺席 = null。
348
348
  *
349
349
  * 为什么必须进候选集:读口按 `(scope, session_id, tool_call_id)` 查,而委派子代那条腿的 ask 落行时
@@ -351,7 +351,7 @@ export interface CheckpointAskCandidate {
351
351
  * (toolCallId, hash) 两维,一条根腿的 park 会被一只子代的 ask 认领(摘要相同的两次同 args 调用完全
352
352
  * 正常)。三元组全同才算同一件事。
353
353
  */
354
- sourceTaskId: string | null;
354
+ sourceSessionId: string | null;
355
355
  /** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
356
356
  boundCallId: string | null;
357
357
  /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_ask.bound_input_hash` 相等才算命中)。 */
@@ -444,7 +444,7 @@ export declare function pendingActionToolCallId(body: unknown): {
444
444
  toolCallId: string | null;
445
445
  boundInputHash: string | null;
446
446
  kind: string | null;
447
- sourceTaskId: string | null;
447
+ sourceSessionId: string | null;
448
448
  } | {
449
449
  readable: false;
450
450
  };
@@ -781,7 +781,7 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
781
781
  * 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
782
782
  * blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
783
783
  * 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
784
- * 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
784
+ * 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceSessionId` 是和解三元组的第一
785
785
  * 维,列命中行结构上也必须解 blob 才拿得到它。
786
786
  */
787
787
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
@@ -239,12 +239,14 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
239
239
  export function pendingActionToolCallId(body) {
240
240
  if (typeof body !== "object" || body === null)
241
241
  return { readable: false };
242
- const rawSource = "sourceTaskId" in body ? body.sourceTaskId : null;
242
+ if ("sourceTaskId" in body)
243
+ return { readable: false };
244
+ const rawSource = "sourceSessionId" in body ? body.sourceSessionId : null;
243
245
  if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
244
246
  return { readable: false };
245
- const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
247
+ const sourceSessionId = typeof rawSource === "string" ? rawSource : null;
246
248
  if (!("pendingAction" in body))
247
- return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId };
249
+ return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceSessionId };
248
250
  const pa = body.pendingAction;
249
251
  if (typeof pa !== "object" || pa === null)
250
252
  return { readable: false };
@@ -252,7 +254,7 @@ export function pendingActionToolCallId(body) {
252
254
  if (kind !== null && typeof kind !== "string")
253
255
  return { readable: false };
254
256
  if (!("toolCallId" in pa))
255
- return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId };
257
+ return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceSessionId };
256
258
  const id = pa.toolCallId;
257
259
  if (typeof id !== "string")
258
260
  return { readable: false };
@@ -261,7 +263,7 @@ export function pendingActionToolCallId(body) {
261
263
  const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
262
264
  if (bih !== null && typeof bih !== "string")
263
265
  return { readable: false };
264
- return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
266
+ return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
265
267
  }
266
268
  function pendingActionOfColumn(cell, where) {
267
269
  let body;
@@ -650,7 +652,7 @@ export class SqlCheckpointStore {
650
652
  status: String(r.status),
651
653
  createdAtMs: Number(r.created_at_ms),
652
654
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
653
- sourceTaskId: null,
655
+ sourceSessionId: null,
654
656
  };
655
657
  if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
656
658
  out.push({ ...base, boundCallId: null, unparseable: true });
@@ -673,7 +675,7 @@ export class SqlCheckpointStore {
673
675
  const effectiveHash = base.boundInputHash ?? derived.boundInputHash;
674
676
  const effective = col ?? derived.toolCallId;
675
677
  if (effective === toolCallId)
676
- out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
678
+ out.push({ ...base, boundInputHash: effectiveHash, sourceSessionId: derived.sourceSessionId, boundCallId: effective });
677
679
  }
678
680
  return out;
679
681
  }
@@ -2,7 +2,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
4
4
  import { type BlobBackend } from "./blob-backend.js";
5
- import type { SyncBlobPutResult } from "./store-backend.js";
5
+ import type { ServiceBlobExchange } from "./store-backend.js";
6
6
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
7
7
  /** The file-history blob domain's SQL table (BlobBackend `table`/MinIO index binding + the A7 assert). */
8
8
  export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
@@ -24,12 +24,10 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
24
24
  /** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on
25
25
  * its own string). Not used for this class's own statements (those go through `db`). */
26
26
  protected readonly pool: MySqlPool | PgPool;
27
- /** 2c sync blob-face MARKER (codex R2-high): the route/caps predicates gate on THIS, never on a
28
- * `typeof putBlob` duck-probe — core's local FileFileHistoryStore carries a PRIVATE synchronous
29
- * `putBlob(hash, bytes): void` helper whose name collides with the service face; a name probe
30
- * passes there, mutates core-owned blob storage out of contract, then dereferences `.ok` on
31
- * undefined. Presence of this literal = the full typed putBlob/getBlob/hasBlobs face exists. */
32
- readonly syncBlobFaces: true;
27
+ /** The blob exchange (core's `FileHistoryStore.blobExchange` group, see {@link ServiceBlobExchange}): how another machine's bytes
28
+ * come in ahead of an `importHistory`, go out to a puller, and are asked about. Its presence IS the capability — read through
29
+ * `blobExchangeOf` (`session-sync.ts`), which defers to core's judge; there is no second word for it on this class. */
30
+ readonly blobExchange: ServiceBlobExchange;
33
31
  protected readonly blobs: BlobBackend;
34
32
  /** §10 S-6 in-process lease state (design v1 D2: NOT persisted). Keys = `${scope}\x00${entryId}`. */
35
33
  private readonly spentAttempts;
@@ -58,15 +56,25 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
58
56
  /**
59
57
  * Per-scope WRITE-SERIALIZATION lock (codex R1-critical): `SELECT … FOR UPDATE` over an EMPTY result
60
58
  * locks nothing on PG, so neither the empty-target law (import/adopt) nor a multi-statement graph
61
- * write can be serialized by the data rows alone. Every graph-mutating tx therefore first ensures +
59
+ * write can be serialized by the data rows alone. Every graph transaction therefore first ensures +
62
60
  * row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
63
- * longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
64
- * the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
61
+ * longer interleave into a merged/torn graph.
65
62
  *
66
63
  * Shape A ([ref]): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
67
64
  * On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
68
65
  * below has to upgrade it to X — two concurrent scope writers each holding an S deadlock ([ref]). ON
69
66
  * DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
67
+ *
68
+ * **This is the transaction's first statement** (every caller: `begin()` and then this — nothing of the
69
+ * transaction exists before it), and a lost arbitration here restarts the transaction from the top
70
+ * (rollback + begin + the same ensure-and-lock), through the repo's one bound ({@link retryOnDeadlock}). Two shapes:
71
+ * · the engine picked this transaction as a deadlock victim. InnoDB only (REPEATABLE READ): when the sentinel
72
+ * physically disappears while ≥ 2 transactions are queued on it — its minter rolled back, or a whole-scope
73
+ * removal ({@link deleteBySession}) committed and purge took the delete-marked row — the queued locks are
74
+ * inherited as GAP locks on the next record, and each queued ensure's insert-intention waits on the others'
75
+ * gap locks. The engine kills all but one; the survivor's mint goes through and the others restart into it;
76
+ * · the sentinel was gone at the locking read ({@link ScopeSentinelVanishedError}; PG, see there).
77
+ * The lock order itself stays single: sentinels first (sorted), then the scope's rows.
70
78
  */
71
79
  private lockScope;
72
80
  /** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
@@ -147,14 +155,12 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
147
155
  /** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window.
148
156
  * Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */
149
157
  sweepOrphanBlobs(): Promise<number>;
150
- /** Upload ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). Store-level
151
- * integrity re-check (the route pre-verifies too — the internal caller might not). Never-throw.
152
- * The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
153
- * keeps its own code — the route answers it 413 (the sentence names that connection's walls), every other write failure 502 ("retry"). */
154
- putBlob(hash: string, bytes: Uint8Array): Promise<SyncBlobPutResult>;
155
- getBlob(hash: string): Promise<Uint8Array | undefined>;
156
- /** The subset of `hashes` already present — one SQL INDEX probe (the import presence pre-check). */
157
- hasBlobs(hashes: string[]): Promise<Set<string>>;
158
+ /** Store ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). The content address is judged HERE and
159
+ * nowhere else (core's `put` law: bytes whose sha256 is not `hash` are refused `adopt_failed` / `terminal`, nothing stored).
160
+ * Never-throw. The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
161
+ * is a terminal refusal that carries `blobCap` — the route answers it 413 with the judge's sentence; every other write failure is
162
+ * `snapshot_failed` with the store's judgment of the fault (the route answers 502, "retry"). */
163
+ private putExchangedBlob;
158
164
  adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
159
165
  private scopeEmpty;
160
166
  /** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { captureFileBackupViaEnv, applyFileRestoreViaEnv, fileHistoryModeProvenDifferent, worldStillMatchesMint, annulProofFailure, boundaryPublishVerdict, fileHistoryDescribeTrackKey, fileHistoryTrackKeyOf, fileHistoryResolveTrackKey, fileHistoryExportDigest, validateFileHistoryExport, resolveFileHistoryRetention, fileHistoryBoundariesToKeep, } from "@sema-agent/core";
3
3
  import { BlobTooLargeError, SqlBlobBackend } from "./blob-backend.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
- import { isMissingColumnError } from "./sql-errors.js";
5
+ import { isMissingColumnError, retryOnDeadlock } from "./sql-errors.js";
6
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
7
7
  import { recordFailOpen } from "../observability/fail-open.js";
8
8
  import { foldCaughtFailure } from "../file-history-disclosure.js";
@@ -12,6 +12,8 @@ const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
12
12
  const truthy = (v) => v === true || v === 1 || v === "1";
13
13
  class StagedRefGoneError extends Error {
14
14
  }
15
+ class ScopeSentinelVanishedError extends Error {
16
+ }
15
17
  function reapedEntryRefusal(entryId) {
16
18
  return {
17
19
  ok: false,
@@ -24,7 +26,11 @@ function reapedEntryRefusal(entryId) {
24
26
  export class SqlFileHistoryStore {
25
27
  db;
26
28
  pool;
27
- syncBlobFaces = true;
29
+ blobExchange = {
30
+ put: (hash, bytes) => this.putExchangedBlob(hash, bytes),
31
+ get: (hash) => this.blobs.getBlob(hash),
32
+ has: (hashes) => this.blobs.hasBlobs([...hashes]),
33
+ };
28
34
  blobs;
29
35
  spentAttempts = new Set();
30
36
  activeAttempts = new Set();
@@ -48,16 +54,21 @@ export class SqlFileHistoryStore {
48
54
  async beforeObserve(_scope, _keyHash) { }
49
55
  async beforePublish(_scope) { }
50
56
  async lockScope(conn, ...scopes) {
51
- for (const scope of [...new Set(scopes)].sort()) {
52
- let locked = false;
53
- for (let i = 0; i < 3 && !locked; i++) {
57
+ const ordered = [...new Set(scopes)].sort();
58
+ let restart = false;
59
+ await retryOnDeadlock(this.db.dialect, async () => {
60
+ if (restart) {
61
+ await conn.rollback().catch(() => undefined);
62
+ await conn.begin();
63
+ }
64
+ restart = true;
65
+ for (const scope of ordered) {
54
66
  await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
55
67
  const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
56
- locked = Boolean(r.rows[0]);
68
+ if (!r.rows[0])
69
+ throw new ScopeSentinelVanishedError(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
57
70
  }
58
- if (!locked)
59
- throw new Error(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
60
- }
71
+ }, (err) => err instanceof ScopeSentinelVanishedError);
61
72
  }
62
73
  async markObserved(scope, keyHash) {
63
74
  const r = await this.db.query(this.q("UPDATE file_history_tracked SET observed_later = 1 WHERE scope = ? AND key_hash = ? AND observed_later = 0", "UPDATE file_history_tracked SET observed_later = TRUE WHERE scope = $1 AND key_hash = $2 AND observed_later = FALSE"), [scope, keyHash]);
@@ -585,9 +596,9 @@ export class SqlFileHistoryStore {
585
596
  return 0;
586
597
  return this.blobs.deleteBlobs(orphans);
587
598
  }
588
- async putBlob(hash, bytes) {
599
+ async putExchangedBlob(hash, bytes) {
589
600
  if (sha256hex(bytes) !== hash) {
590
- return { ok: false, error: { code: "unsupported_format", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
601
+ return { ok: false, error: { code: "adopt_failed", message: `blob ${hash}: the bytes do not hash to this address — nothing stored`, permanence: "terminal" } };
591
602
  }
592
603
  try {
593
604
  await this.blobs.putBlob(hash, bytes);
@@ -595,17 +606,11 @@ export class SqlFileHistoryStore {
595
606
  }
596
607
  catch (err) {
597
608
  if (err instanceof BlobTooLargeError)
598
- return { ok: false, error: { code: err.code, message: err.message, wallSource: err.cap.source } };
609
+ return { ok: false, error: { code: "adopt_failed", message: err.message, permanence: "terminal", blobCap: { wallSource: err.cap.source } } };
599
610
  const f = foldCaughtFailure(err);
600
- return { ok: false, error: { code: "snapshot_failed", message: `putBlob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
611
+ return { ok: false, error: { code: "snapshot_failed", message: `blob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
601
612
  }
602
613
  }
603
- async getBlob(hash) {
604
- return this.blobs.getBlob(hash);
605
- }
606
- async hasBlobs(hashes) {
607
- return this.blobs.hasBlobs(hashes);
608
- }
609
614
  async adoptScope(fromScope, toScope) {
610
615
  try {
611
616
  if (fromScope === toScope)