@sema-agent/server 7.150.0 → 7.151.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 +185 -0
  2. package/USAGE.md +53 -25
  3. package/deploy/sema-up/README.md +7 -1
  4. package/deploy/sema-up/chart/Chart.yaml +1 -1
  5. package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
  6. package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
  8. package/deploy/sema-up/chart/templates/server.yaml +5 -3
  9. package/deploy/sema-up/chart/values-full-single.yaml +3 -1
  10. package/deploy/sema-up/chart/values.yaml +8 -1
  11. package/deploy/sema-up/cluster-up.sh +1 -0
  12. package/deploy/sema-up/kube-up.sh +1 -1
  13. package/deploy/sema-up/lib/flagshape.sh +1 -1
  14. package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
  15. package/deploy/sema-up/lib/helm-values-kube.py +2 -0
  16. package/dist/adoption/import.js +3 -3
  17. package/dist/approval-ask-machine.d.ts +1 -1
  18. package/dist/approval-card.d.ts +2 -2
  19. package/dist/approval-gate-currency.d.ts +1 -1
  20. package/dist/auth-bridge.d.ts +20 -0
  21. package/dist/auth-bridge.js +9 -0
  22. package/dist/background-session-reap.d.ts +1 -1
  23. package/dist/bake-runner/main.d.ts +4 -2
  24. package/dist/bind-host.d.ts +2 -3
  25. package/dist/bind-host.js +2 -4
  26. package/dist/boot/budget-tracing.js +1 -1
  27. package/dist/boot/engine-lease.d.ts +5 -0
  28. package/dist/boot/engine-lease.js +1 -0
  29. package/dist/boot/execution-env.d.ts +8 -14
  30. package/dist/boot/execution-env.js +54 -17
  31. package/dist/boot/execution-lane.js +32 -5
  32. package/dist/boot/late-binds.d.ts +8 -4
  33. package/dist/boot/late-binds.js +7 -1
  34. package/dist/boot/memory-consolidation.d.ts +3 -2
  35. package/dist/boot/memory-consolidation.js +6 -6
  36. package/dist/boot/reapers.d.ts +5 -0
  37. package/dist/boot/reapers.js +12 -1
  38. package/dist/boot/resolve-spec.d.ts +3 -3
  39. package/dist/boot/resolve-spec.js +31 -22
  40. package/dist/boot/runner-deps.d.ts +9 -2
  41. package/dist/boot/runner-deps.js +3 -3
  42. package/dist/boot/session-age-out.d.ts +36 -0
  43. package/dist/boot/session-age-out.js +101 -0
  44. package/dist/boot/session-faces.d.ts +5 -14
  45. package/dist/boot/session-faces.js +9 -5
  46. package/dist/boot/stage-01-config.js +2 -0
  47. package/dist/boot/stage-05-execution-env.d.ts +5 -3
  48. package/dist/boot/stage-05-execution-env.js +2 -2
  49. package/dist/boot/stage-06-runners.d.ts +7 -4
  50. package/dist/boot/stage-06-runners.js +15 -6
  51. package/dist/boot/stage-07-capability-layer.d.ts +7 -4
  52. package/dist/boot/stage-07-capability-layer.js +2 -1
  53. package/dist/boot/stage-08-reapers.d.ts +7 -4
  54. package/dist/boot/stage-08-reapers.js +4 -2
  55. package/dist/boot/stage-09-leader.d.ts +11 -5
  56. package/dist/boot/stage-09-leader.js +12 -3
  57. package/dist/boot/stage-10-http-server.d.ts +11 -5
  58. package/dist/boot/stage-10-http-server.js +3 -6
  59. package/dist/boot/web-admin.js +3 -1
  60. package/dist/boot/workflow-orchestration.d.ts +7 -2
  61. package/dist/boot/workflow-orchestration.js +24 -7
  62. package/dist/bounded-rotation.d.ts +29 -0
  63. package/dist/bounded-rotation.js +24 -0
  64. package/dist/capability-facts.d.ts +22 -13
  65. package/dist/capability-facts.js +28 -18
  66. package/dist/center-credential.d.ts +25 -6
  67. package/dist/center-credential.js +25 -5
  68. package/dist/config-catalog.d.ts +26 -7
  69. package/dist/config-catalog.js +14 -11
  70. package/dist/config-invariants.d.ts +2 -1
  71. package/dist/config-invariants.js +4 -4
  72. package/dist/config-types.d.ts +10 -4
  73. package/dist/config.d.ts +51 -2
  74. package/dist/config.js +35 -24
  75. package/dist/deployment-shape.d.ts +9 -1
  76. package/dist/deployment-shape.js +2 -1
  77. package/dist/device-protocol.d.ts +5 -4
  78. package/dist/device-protocol.js +1 -1
  79. package/dist/device-ws-hub.js +3 -2
  80. package/dist/device-ws-protocol.d.ts +63 -10
  81. package/dist/device-ws-protocol.js +56 -25
  82. package/dist/engine-identity.d.ts +19 -7
  83. package/dist/engine-identity.js +10 -2
  84. package/dist/entry-mode-clamp-notice.d.ts +2 -1
  85. package/dist/execution-env-refusal.d.ts +23 -0
  86. package/dist/execution-env-refusal.js +5 -0
  87. package/dist/execution-lane-caps.d.ts +3 -1
  88. package/dist/field-conflict.d.ts +1 -1
  89. package/dist/file-history-capture.d.ts +11 -4
  90. package/dist/file-history-capture.js +9 -6
  91. package/dist/file-history-disclosure.d.ts +7 -5
  92. package/dist/file-history-disclosure.js +7 -5
  93. package/dist/fleet/drain-entry.d.ts +4 -0
  94. package/dist/hooks/hook-runner.js +2 -2
  95. package/dist/host-lsp-manager.d.ts +2 -2
  96. package/dist/http/admission.js +17 -2
  97. package/dist/http/approval-sweeps.js +5 -5
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +2 -1
  101. package/dist/http/dispatch.js +34 -27
  102. package/dist/http/durable-run-leg.js +16 -16
  103. package/dist/http/health-self-report.d.ts +9 -1
  104. package/dist/http/health-self-report.js +2 -0
  105. package/dist/http/owner-access.d.ts +31 -11
  106. package/dist/http/owner-access.js +37 -6
  107. package/dist/http/resume-ctx-read.js +25 -3
  108. package/dist/http/resume-legs.d.ts +2 -1
  109. package/dist/http/resume-legs.js +259 -79
  110. package/dist/http/route-ctx.d.ts +18 -8
  111. package/dist/http/route-table.d.ts +1 -1
  112. package/dist/http/routes/admin-memory-optout.d.ts +1 -1
  113. package/dist/http/routes/agents-roster.js +2 -3
  114. package/dist/http/routes/approvals-assistant.js +4 -3
  115. package/dist/http/routes/background.d.ts +3 -3
  116. package/dist/http/routes/capabilities.js +8 -5
  117. package/dist/http/routes/fleet.js +10 -8
  118. package/dist/http/routes/projects.d.ts +3 -2
  119. package/dist/http/routes/projects.js +4 -5
  120. package/dist/http/routes/runs.js +102 -36
  121. package/dist/http/routes/session-sync.d.ts +4 -3
  122. package/dist/http/routes/session-sync.js +172 -77
  123. package/dist/http/routes/sessions-list.js +13 -7
  124. package/dist/http/routes/sessions.js +10 -6
  125. package/dist/http/routes/tasks.js +30 -16
  126. package/dist/http/routes/trace-usage.js +2 -2
  127. package/dist/http/server.d.ts +21 -27
  128. package/dist/http/server.js +2 -2
  129. package/dist/http/submit-ctx.js +5 -4
  130. package/dist/http/wire-types.d.ts +14 -4
  131. package/dist/identity-source.d.ts +20 -3
  132. package/dist/identity-source.js +14 -0
  133. package/dist/index.d.ts +4 -0
  134. package/dist/lane-home-probe.d.ts +3 -7
  135. package/dist/lane-home-probe.js +0 -1
  136. package/dist/leader/leader.d.ts +2 -1
  137. package/dist/leg-facts.d.ts +13 -5
  138. package/dist/leg-facts.js +2 -1
  139. package/dist/memory-scope.d.ts +5 -4
  140. package/dist/memory-scope.js +4 -4
  141. package/dist/memory-sync-client.d.ts +30 -2
  142. package/dist/memory-sync-client.js +16 -9
  143. package/dist/observability/err-text.d.ts +7 -0
  144. package/dist/observability/err-text.js +5 -0
  145. package/dist/observability/fail-open.d.ts +7 -3
  146. package/dist/observability/fail-open.js +7 -3
  147. package/dist/observability/metrics.js +1 -0
  148. package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
  149. package/dist/orchestration/workflow-notify-journal.js +12 -1
  150. package/dist/orchestration/workflow-park-decision.d.ts +70 -18
  151. package/dist/orchestration/workflow-park-decision.js +49 -10
  152. package/dist/orchestration/workflow-park-holder.d.ts +65 -0
  153. package/dist/orchestration/workflow-park-holder.js +98 -0
  154. package/dist/orchestration/workflow-record-fence.d.ts +36 -0
  155. package/dist/orchestration/workflow-record-fence.js +42 -0
  156. package/dist/owner-faces.d.ts +1 -1
  157. package/dist/parked-decide.d.ts +104 -53
  158. package/dist/parked-decide.js +153 -117
  159. package/dist/peer-directory.d.ts +2 -2
  160. package/dist/peer-inbound-face.d.ts +2 -6
  161. package/dist/peer-inbound-face.js +0 -3
  162. package/dist/permission-mode-admission.d.ts +41 -5
  163. package/dist/permission-mode-admission.js +29 -3
  164. package/dist/plugins/adoption-log-sql.d.ts +3 -1
  165. package/dist/plugins/adoption-log-sql.js +7 -1
  166. package/dist/plugins/approval-ask-store-file.d.ts +10 -2
  167. package/dist/plugins/approval-ask-store-file.js +8 -0
  168. package/dist/plugins/approval-ask-store-memory.d.ts +6 -1
  169. package/dist/plugins/approval-ask-store-memory.js +28 -9
  170. package/dist/plugins/approval-ask-store-sql.d.ts +43 -2
  171. package/dist/plugins/approval-ask-store-sql.js +34 -5
  172. package/dist/plugins/approval-exemption-store.js +3 -0
  173. package/dist/plugins/approval-nonce-store.js +2 -0
  174. package/dist/plugins/breaker-state-sql.js +2 -0
  175. package/dist/plugins/caching-session-store.d.ts +9 -15
  176. package/dist/plugins/caching-session-store.js +6 -0
  177. package/dist/plugins/device-store-sql.js +6 -0
  178. package/dist/plugins/error-origin.d.ts +18 -0
  179. package/dist/plugins/error-origin.js +10 -0
  180. package/dist/plugins/execution-placement-sql.js +6 -6
  181. package/dist/plugins/image-bake-store-sql.js +6 -0
  182. package/dist/plugins/image-index-sql.js +4 -0
  183. package/dist/plugins/landed-attendants.d.ts +212 -0
  184. package/dist/plugins/landed-attendants.js +161 -0
  185. package/dist/plugins/leader-run-store-sql.js +4 -0
  186. package/dist/plugins/local-session-live-reference.d.ts +40 -0
  187. package/dist/plugins/local-session-live-reference.js +39 -0
  188. package/dist/plugins/local-session-store.d.ts +71 -33
  189. package/dist/plugins/local-session-store.js +129 -27
  190. package/dist/plugins/memory-engine-pg.js +4 -0
  191. package/dist/plugins/memory-engine-sql-apply.js +5 -2
  192. package/dist/plugins/memory-engine-tidb.js +4 -0
  193. package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
  194. package/dist/plugins/memory-sync-store-pg.js +5 -0
  195. package/dist/plugins/memory-sync-store-tidb.js +5 -0
  196. package/dist/plugins/one-time-migrations.d.ts +57 -63
  197. package/dist/plugins/one-time-migrations.js +162 -90
  198. package/dist/plugins/org-delivery-quota-store.js +4 -1
  199. package/dist/plugins/outcome-ledger-sql.js +2 -0
  200. package/dist/plugins/permission-rule-store-sql.js +11 -4
  201. package/dist/plugins/pg-cost-quota.js +4 -0
  202. package/dist/plugins/pg-pool.js +24 -1
  203. package/dist/plugins/pg-rate-limiter.js +2 -0
  204. package/dist/plugins/posix-shell-fs.d.ts +6 -1
  205. package/dist/plugins/posix-shell-fs.js +4 -1
  206. package/dist/plugins/remote-env-adb.d.ts +19 -15
  207. package/dist/plugins/remote-env-adb.js +41 -15
  208. package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
  209. package/dist/plugins/remote-env-connect-seat.js +72 -0
  210. package/dist/plugins/remote-env-device.d.ts +22 -5
  211. package/dist/plugins/remote-env-device.js +106 -8
  212. package/dist/plugins/remote-env-e2b.d.ts +15 -6
  213. package/dist/plugins/remote-env-e2b.js +42 -32
  214. package/dist/plugins/remote-env-file-error.js +6 -2
  215. package/dist/plugins/remote-env-host.d.ts +17 -0
  216. package/dist/plugins/remote-env-host.js +30 -38
  217. package/dist/plugins/remote-env-k8s.d.ts +41 -39
  218. package/dist/plugins/remote-env-k8s.js +53 -30
  219. package/dist/plugins/remote-env-local-docker.d.ts +13 -8
  220. package/dist/plugins/remote-env-local-docker.js +34 -41
  221. package/dist/plugins/remote-env-ssh.d.ts +18 -12
  222. package/dist/plugins/remote-env-ssh.js +60 -59
  223. package/dist/plugins/remote-shell.d.ts +98 -1
  224. package/dist/plugins/remote-shell.js +218 -0
  225. package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
  226. package/dist/plugins/resume-anchor-store-sql.js +12 -5
  227. package/dist/plugins/retention-lane-store-sql.js +4 -0
  228. package/dist/plugins/retention-sql.d.ts +20 -2
  229. package/dist/plugins/retention-sql.js +11 -0
  230. package/dist/plugins/run-store-sql.d.ts +21 -2
  231. package/dist/plugins/run-store-sql.js +31 -7
  232. package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
  233. package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
  234. package/dist/plugins/schema-consistency.d.ts +4 -2
  235. package/dist/plugins/schema-consistency.js +19 -2
  236. package/dist/plugins/send-file-ledger.js +3 -0
  237. package/dist/plugins/serving-requirement.d.ts +26 -7
  238. package/dist/plugins/serving-requirement.js +11 -4
  239. package/dist/plugins/session-birth.d.ts +6 -2
  240. package/dist/plugins/session-capture-record-store-sql.js +2 -0
  241. package/dist/plugins/session-ext-table.d.ts +14 -1
  242. package/dist/plugins/session-ext-table.js +5 -1
  243. package/dist/plugins/session-row-sql.d.ts +49 -2
  244. package/dist/plugins/session-row-sql.js +56 -13
  245. package/dist/plugins/session-store-sql.d.ts +34 -18
  246. package/dist/plugins/session-store-sql.js +98 -17
  247. package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
  248. package/dist/plugins/shared-memory-store-sql.js +6 -2
  249. package/dist/plugins/sql-driver.d.ts +2 -1
  250. package/dist/plugins/sql-driver.js +18 -3
  251. package/dist/plugins/sql-errors.d.ts +11 -6
  252. package/dist/plugins/sql-errors.js +5 -6
  253. package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
  254. package/dist/plugins/stop-then-start-sentence.js +14 -10
  255. package/dist/plugins/stop-then-start.d.ts +109 -0
  256. package/dist/plugins/stop-then-start.js +104 -0
  257. package/dist/plugins/store-backend.d.ts +40 -13
  258. package/dist/plugins/store-backend.js +169 -27
  259. package/dist/plugins/store-boundary.d.ts +57 -0
  260. package/dist/plugins/store-boundary.js +139 -0
  261. package/dist/plugins/store-contracts.d.ts +17 -0
  262. package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
  263. package/dist/plugins/store-fault-sentence-m53.js +25 -0
  264. package/dist/plugins/store-fault.d.ts +26 -7
  265. package/dist/plugins/store-fault.js +18 -2
  266. package/dist/plugins/task-attachment-store.js +9 -0
  267. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  268. package/dist/plugins/task-list-store-sql.js +10 -8
  269. package/dist/plugins/tidb-cost-quota.js +4 -0
  270. package/dist/plugins/tidb-pool.js +2 -1
  271. package/dist/plugins/tidb-rate-limiter.js +4 -0
  272. package/dist/plugins/user-input-ledger-sql.js +4 -0
  273. package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
  274. package/dist/plugins/workflow-run-store-sql.js +64 -4
  275. package/dist/plugins/workspace-fill.d.ts +4 -7
  276. package/dist/plugins/workspace-fill.js +2 -1
  277. package/dist/plugins/write-behind-counter.js +5 -0
  278. package/dist/project-context.d.ts +0 -2
  279. package/dist/project-context.js +4 -8
  280. package/dist/project-registry.d.ts +46 -0
  281. package/dist/project-registry.js +44 -0
  282. package/dist/request-key-closure.js +1 -1
  283. package/dist/request-value-closure.d.ts +4 -0
  284. package/dist/request-value-closure.js +10 -0
  285. package/dist/run-acceptance.d.ts +3 -2
  286. package/dist/run-acceptance.js +4 -6
  287. package/dist/run-local.js +1 -0
  288. package/dist/runs.d.ts +37 -8
  289. package/dist/runs.js +53 -12
  290. package/dist/runtime-governance.d.ts +5 -4
  291. package/dist/security.d.ts +30 -32
  292. package/dist/security.js +41 -7
  293. package/dist/server-secret-env.d.ts +12 -10
  294. package/dist/server-secret-env.js +24 -15
  295. package/dist/session-bindings.d.ts +82 -0
  296. package/dist/session-bindings.js +18 -0
  297. package/dist/session-children-retire.d.ts +2 -2
  298. package/dist/session-children-retire.js +2 -6
  299. package/dist/session-deletion.d.ts +16 -1
  300. package/dist/session-error-wire.d.ts +3 -3
  301. package/dist/session-error-wire.js +3 -3
  302. package/dist/session-incarnation.d.ts +10 -0
  303. package/dist/session-incarnation.js +10 -1
  304. package/dist/session-kind.d.ts +57 -0
  305. package/dist/session-kind.js +22 -0
  306. package/dist/session-row.d.ts +21 -1
  307. package/dist/session-row.js +7 -1
  308. package/dist/session-sync-kernel.d.ts +24 -14
  309. package/dist/session-sync-kernel.js +3 -6
  310. package/dist/session-sync.d.ts +4 -98
  311. package/dist/session-sync.js +1 -71
  312. package/dist/session-titler.d.ts +10 -3
  313. package/dist/session-titler.js +19 -6
  314. package/dist/ssh-host-key.d.ts +5 -0
  315. package/dist/ssh-host-key.js +3 -0
  316. package/dist/stores-sql/file-history-store-sql.d.ts +52 -2
  317. package/dist/stores-sql/file-history-store-sql.js +108 -70
  318. package/dist/stores-sql/pg-session-storage.d.ts +34 -7
  319. package/dist/stores-sql/pg-session-storage.js +37 -13
  320. package/dist/stores-sql/retention-store-sql.d.ts +1 -9
  321. package/dist/stores-sql/retention-store-sql.js +2 -10
  322. package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
  323. package/dist/stores-sql/session-policy-store-sql.js +61 -44
  324. package/dist/stores-sql/session-write-hooks.d.ts +11 -0
  325. package/dist/stores-sql/tidb-session-store.d.ts +58 -7
  326. package/dist/stores-sql/tidb-session-store.js +37 -13
  327. package/dist/submission-exit.d.ts +3 -2
  328. package/dist/submission-exit.js +9 -1
  329. package/dist/task-cwd.d.ts +2 -2
  330. package/dist/task-settings.d.ts +6 -5
  331. package/dist/terminal.d.ts +33 -2
  332. package/dist/terminal.js +139 -9
  333. package/dist/tool-approval.d.ts +140 -6
  334. package/dist/tool-approval.js +165 -72
  335. package/dist/trace/self-minted-wire.d.ts +290 -27
  336. package/dist/trace/self-minted-wire.js +101 -13
  337. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +78 -27
  338. package/package.json +2 -2
  339. package/wire-changes/server@7.151.0.json +762 -0
  340. package/dist/http/session-inherit-env.d.ts +0 -36
  341. package/dist/http/session-inherit-env.js +0 -6
@@ -87,11 +87,11 @@ export async function eraseSessionPolicyRowsOn(conn, dialect, sessionIds) {
87
87
  }
88
88
  export class SqlSessionPolicyStore {
89
89
  db;
90
- countClientWrite;
90
+ root;
91
91
  logger;
92
- constructor(db, countClientWrite, logger) {
92
+ constructor(db, root, logger) {
93
93
  this.db = db;
94
- this.countClientWrite = countClientWrite;
94
+ this.root = root;
95
95
  this.logger = logger;
96
96
  }
97
97
  q(mysql, pg) {
@@ -124,50 +124,67 @@ export class SqlSessionPolicyStore {
124
124
  }));
125
125
  }
126
126
  async putRules(sessionId, principal, rules, opts) {
127
- const key = policyKey(sessionId, principal);
128
127
  return this.retrying(() => this.tx(async (conn) => {
129
- const root = await conn.query(this.q("SELECT session_id FROM session_meta WHERE session_id = ? FOR UPDATE", "SELECT session_id FROM session_meta WHERE session_id = $1 FOR UPDATE"), [sessionId]);
130
- const { rows } = await conn.query(this.q(`SELECT ${jsonColumnText("mysql", RULES)}, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE`, `SELECT ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE`), [key]);
131
- const prior = rows[0] ? rowToStored(rows[0], `for session ${JSON.stringify(sessionId)} (pre-write snapshot)`) : undefined;
132
- const priorRev = prior?.rev ?? 0;
133
- if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
134
- throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${priorRev})`);
135
- }
136
- const { rows: markRows } = await conn.query(this.q("SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = ? FOR UPDATE", "SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = $1 FOR UPDATE"), [key]);
137
- const markRev = markRows[0] ? Number(markRows[0].rev) : 0;
138
- const markGen = markRows[0] ? Number(markRows[0].gen) : 0;
139
- if (markRows[0] !== undefined && (!isStoredCounter(markGen, 0) || !isStoredCounter(markRev, 0))) {
140
- throw new SessionPolicyError("corrupt", `the session rules rev mark for session ${JSON.stringify(sessionId)} is unreadable (rev ${String(markRows[0].rev)}, gen ${String(markRows[0].gen)}) — refusing rather than stamp a rev/lineage nothing proves`);
141
- }
142
- const clean = judgeSessionRulesWrite(prior ?? null, rules, opts);
143
- if (this.db.dialect === "pg" && pgHasUnstorable(clean)) {
144
- throw new SessionPolicyError("loosen_forbidden", "rules contain unstorable bytes (NUL / lone surrogate) — refused; strip them at the source (authorization rules are never rewritten by the store)");
145
- }
146
- const nextRev = Math.max(priorRev, markRev) + 1;
147
- const nextGen = prior?.gen ?? markGen;
148
- if (!isStoredCounter(nextRev, 1) || !isStoredCounter(nextGen, 0)) {
149
- throw new SessionPolicyError("corrupt", `session rules for session ${JSON.stringify(sessionId)}: the next stamp (rev ${nextRev}, gen ${nextGen}) would leave the safe-integer range this store reads back — refusing before writing a row nothing could read`);
150
- }
151
- if (root.rows.length > 0)
152
- await this.countClientWrite(conn, this.db.dialect, sessionId);
153
- const json = JSON.stringify(clean);
154
- const now = new Date();
155
- if (prior) {
156
- const upd = await conn.query(this.q(`UPDATE session_policy SET principal = ?, ${RULES} = ?, rev = ?, gen = ?, updated_at = ? WHERE policy_key = ? AND rev = ?`, `UPDATE session_policy SET principal = $1, ${RULES} = $2::jsonb, rev = $3, gen = $4, updated_at = $5 WHERE policy_key = $6 AND rev = $7`), [principal ?? null, json, nextRev, nextGen, now, key, priorRev]);
157
- if (upd.affected !== 1)
158
- throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts?.expectedRev ?? priorRev}, have ${priorRev})`);
159
- }
160
- else {
161
- await conn.query(this.q(`INSERT INTO session_policy (policy_key, session_id, principal, ${RULES}, rev, gen, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`, `INSERT INTO session_policy (policy_key, session_id, principal, ${RULES}, rev, gen, created_at, updated_at) VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8)`), [key, sessionId, principal ?? null, json, nextRev, nextGen, now, now]);
162
- }
163
- return structuredClone({ ...clean, rev: nextRev, gen: nextGen });
128
+ const present = await this.root.lockRoot(conn, this.db.dialect, sessionId);
129
+ const out = await this.putRulesOn(conn, sessionId, principal, rules, opts, { count: present });
130
+ if ("skipped" in out)
131
+ throw new Error("SqlSessionPolicyStore.putRules: no skip predicate was given, yet the write was skipped");
132
+ return out;
164
133
  }));
165
134
  }
135
+ async putRulesOn(conn, sessionId, principal, rules, opts, how) {
136
+ const key = policyKey(sessionId, principal);
137
+ const { rows } = await conn.query(this.q(`SELECT ${jsonColumnText("mysql", RULES)}, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE`, `SELECT ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE`), [key]);
138
+ const prior = rows[0] ? rowToStored(rows[0], `for session ${JSON.stringify(sessionId)} (pre-write snapshot)`) : undefined;
139
+ if (how.skip?.(prior ?? null) === true)
140
+ return { skipped: true };
141
+ const priorRev = prior?.rev ?? 0;
142
+ if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
143
+ throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${priorRev})`);
144
+ }
145
+ const { rows: markRows } = await conn.query(this.q("SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = ? FOR UPDATE", "SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = $1 FOR UPDATE"), [key]);
146
+ const markRev = markRows[0] ? Number(markRows[0].rev) : 0;
147
+ const markGen = markRows[0] ? Number(markRows[0].gen) : 0;
148
+ if (markRows[0] !== undefined && (!isStoredCounter(markGen, 0) || !isStoredCounter(markRev, 0))) {
149
+ throw new SessionPolicyError("corrupt", `the session rules rev mark for session ${JSON.stringify(sessionId)} is unreadable (rev ${String(markRows[0].rev)}, gen ${String(markRows[0].gen)}) — refusing rather than stamp a rev/lineage nothing proves`);
150
+ }
151
+ const clean = judgeSessionRulesWrite(prior ?? null, rules, opts);
152
+ if (this.db.dialect === "pg" && pgHasUnstorable(clean)) {
153
+ throw new SessionPolicyError("loosen_forbidden", "rules contain unstorable bytes (NUL / lone surrogate) — refused; strip them at the source (authorization rules are never rewritten by the store)");
154
+ }
155
+ const nextRev = Math.max(priorRev, markRev) + 1;
156
+ const nextGen = prior?.gen ?? markGen;
157
+ if (!isStoredCounter(nextRev, 1) || !isStoredCounter(nextGen, 0)) {
158
+ throw new SessionPolicyError("corrupt", `session rules for session ${JSON.stringify(sessionId)}: the next stamp (rev ${nextRev}, gen ${nextGen}) would leave the safe-integer range this store reads back — refusing before writing a row nothing could read`);
159
+ }
160
+ if (how.count)
161
+ await this.root.countClientWrite(conn, this.db.dialect, sessionId);
162
+ const json = JSON.stringify(clean);
163
+ const now = new Date();
164
+ if (prior) {
165
+ const upd = await conn.query(this.q(`UPDATE session_policy SET principal = ?, ${RULES} = ?, rev = ?, gen = ?, updated_at = ? WHERE policy_key = ? AND rev = ?`, `UPDATE session_policy SET principal = $1, ${RULES} = $2::jsonb, rev = $3, gen = $4, updated_at = $5 WHERE policy_key = $6 AND rev = $7`), [principal ?? null, json, nextRev, nextGen, now, key, priorRev]);
166
+ if (upd.affected !== 1)
167
+ throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts?.expectedRev ?? priorRev}, have ${priorRev})`);
168
+ }
169
+ else {
170
+ await conn.query(this.q(`INSERT INTO session_policy (policy_key, session_id, principal, ${RULES}, rev, gen, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`, `INSERT INTO session_policy (policy_key, session_id, principal, ${RULES}, rev, gen, created_at, updated_at) VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8)`), [key, sessionId, principal ?? null, json, nextRev, nextGen, now, now]);
171
+ }
172
+ return structuredClone({ ...clean, rev: nextRev, gen: nextGen });
173
+ }
174
+ isDupKeyError(e) {
175
+ return this.isDupKey(e);
176
+ }
166
177
  retrying(attempt) {
167
178
  return retryOnDeadlock(this.db.dialect, attempt, (e) => this.isDupKey(e));
168
179
  }
169
180
  async deleteBySession(sessionId) {
170
- const sealed = await this.retrying(() => this.tx((conn) => eraseSessionPolicyRowsOn(conn, this.db.dialect, [sessionId])));
181
+ const sealed = await this.retrying(() => this.tx((conn) => this.eraseOn(conn, sessionId)));
182
+ this.discloseErased(sessionId, sealed);
183
+ }
184
+ eraseOn(conn, sessionId) {
185
+ return eraseSessionPolicyRowsOn(conn, this.db.dialect, [sessionId]);
186
+ }
187
+ discloseErased(sessionId, sealed) {
171
188
  if (sealed.length > 0)
172
189
  this.discloseSealed(sessionId, sealed);
173
190
  }
@@ -206,13 +223,13 @@ export class SqlSessionPolicyStore {
206
223
  }
207
224
  }
208
225
  export class TiDBSessionPolicyStore extends SqlSessionPolicyStore {
209
- constructor(pool, countClientWrite, logger) {
210
- super(mysqlDriver(pool), countClientWrite, logger);
226
+ constructor(pool, root, logger) {
227
+ super(mysqlDriver(pool), root, logger);
211
228
  }
212
229
  }
213
230
  export class PgSessionPolicyStore extends SqlSessionPolicyStore {
214
- constructor(pool, countClientWrite, logger) {
215
- super(pgDriver(pool), countClientWrite, logger);
231
+ constructor(pool, root, logger) {
232
+ super(pgDriver(pool), root, logger);
216
233
  }
217
234
  }
218
235
  //# sourceMappingURL=session-policy-store-sql.js.map
@@ -13,6 +13,10 @@
13
13
  * 不对别的表的行加锁,也不在 EvalPlanQual 里复判它们。所以是**两句**:店先 `FOR UPDATE` 锁根行(删除 / 重登记 / 改属主都要先拿这把锁),
14
14
  * 再在**同一连接**上交出语句面,部署用新语句锁定读(PG 新快照、TiDB 当前读)。钩里的写随店的事务提交或回滚(店拒、或任何一步抛,一起撤)。
15
15
  *
16
+ * ## 建行钩(服务扩展,包里没有)
17
+ * 带种类的登记(`registerWithReceipt` 的第三参;会话种类的律在 `session-kind.ts`):店在一只事务里 INSERT、锁定读回化身,读回的 = 本次铸的(这一笔真建了行)⇒
18
+ * {@link SessionWriteHooks.birth} 在同一事务里记下种类;别的(已在的行 / 并发登记里输的那一只)⇒ 零写。包随店搬家时这一组同是向引擎的请托。
19
+ *
16
20
  * ## 键控 fork(服务扩展,包里没有)
17
21
  * 调用方给目标 id 与键的 fork(`forkTo`)是本服务的扩展:键是部署格,在建目标行的同一事务里由 {@link SessionWriteHooks.keyedFork} 记下;撞上已在的目标时
18
22
  * 读它的属主与键。包随店搬家时这一组是向引擎的请托(带目标 id 与键的 fork,或钩表多一组)。
@@ -61,5 +65,12 @@ export interface SessionWriteHooks {
61
65
  }): Promise<void>;
62
66
  targetOf(sessionId: string): Promise<ForkTargetReading | undefined>;
63
67
  };
68
+ /** 建行钩(服务扩展,与键控 fork 同族):带种类的登记里,本事务的 INSERT 真建了这一行(锁定读回的化身 = 本次铸的)⇒ 在同一事务里记下它的种类(部署格的值,座原样转交、不解读;律在部署那一侧)。 */
69
+ readonly birth: {
70
+ record(txn: SessionTxStatements, born: {
71
+ readonly rootSessionId: string;
72
+ readonly kind: string;
73
+ }): Promise<void>;
74
+ };
64
75
  }
65
76
  //# sourceMappingURL=session-write-hooks.d.ts.map
@@ -13,10 +13,10 @@
13
13
  import { type AcquiredSession, type PlacedSessionRow, type SessionHead, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
14
14
  import type { Pool } from "mysql2/promise";
15
15
  import type { RetentionDeclaration } from "@sema-agent/core";
16
- import { type RegisterReceipt, type WithdrawJudgeVerdict, type WithdrawOutcome } from "../session-incarnation.js";
16
+ import { type RegisterReceipt, type SessionFace, type WithdrawJudgeVerdict, type WithdrawOutcome } from "../session-incarnation.js";
17
17
  import type { SessionTxStatements, SessionWriteHooks } from "./session-write-hooks.js";
18
18
  import { type PlacementAcquireOpts } from "../plugins/session-placement.js";
19
- import { type StagingHandle } from "../session-sync-kernel.js";
19
+ import { type EntriesStreamOptions, type StagedCommitOptions, type StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
20
20
  import { type ForkPlacement, type ForkTarget, type ForkToOutcome } from "../plugins/session-fork.js";
21
21
  /**
22
22
  * **条件撤回的店那一半**(两方言一形;TiDB 孪生与 PG 孪生 `pg-session-storage.ts` 同调;撤回的规则在 `plugins/session-birth.ts` 头注)。
@@ -75,7 +75,7 @@ export declare class TiDBSessionStore implements SessionStore {
75
75
  * 出生回执面(`plugins/session-birth.ts`):与 {@link register} 同一笔 INSERT;候选化身 = 这一次 INSERT 入参里铸的那一只(不复读)。
76
76
  * 已在的行上 INSERT 是 no-op 臂 ⇒ 行上的化身不是候选 ⇒ 撤回在锁内比对时判「留」—— 出生事实只在那里判,这里不分新旧行。
77
77
  */
78
- registerWithReceipt(sessionId: string, owner: string | null): Promise<RegisterReceipt>;
78
+ registerWithReceipt(sessionId: string, owner: string | null, kind?: string): Promise<RegisterReceipt>;
79
79
  /** 条件删除(`plugins/session-birth.ts` 的撤回;本连接开事务 → 店与部署两半同一语句面 → 撤回才提交,否则回滚)。 */
80
80
  deleteSessionIf(sessionId: string, cond: SessionDeleteCondition): Promise<WithdrawOutcome>;
81
81
  /**
@@ -88,6 +88,8 @@ export declare class TiDBSessionStore implements SessionStore {
88
88
  * **永不被重写**(first-write 不可变,义务 5 的写侧半场)。
89
89
  * [ref]:化身与属主同一条 first-write 律 —— 新行铸一只(`mintSessionIncarnation`),已存在的行 no-op 臂一个字节不动。 */
90
90
  private insertMeta;
91
+ /** {@link insertMeta} 那一句,发在给定的连接上(带种类的登记在自己的事务连接上发,与锁定读、建行钩同一事务)。 */
92
+ private insertMetaOn;
91
93
  /**
92
94
  * [ref] 义务 6 的廉价探针 —— 一行 PK SELECT:`undefined` = 普通会话或无此行。
93
95
  * (release 的冷缓存腿、包装店的暖命中准入判、诊断面共用这一只。)
@@ -194,10 +196,7 @@ export declare class TiDBSessionStore implements SessionStore {
194
196
  * (按文本投影 + `decodeSessionEventPayload` 恰好解一次码,坏体当场拒). The check is eager (the method is async) because a generator body is lazy — it can't decide
195
197
  * `null` vs. iterable on its own first pull.
196
198
  */
197
- exportEntriesStream(sessionId: string, opts?: {
198
- afterSeq?: number;
199
- batchSize?: number;
200
- }): Promise<AsyncIterable<SessionTreeEntry> | null>;
199
+ exportEntriesStream(sessionId: string, opts: EntriesStreamOptions): Promise<AsyncIterable<SessionTreeEntry> | null>;
201
200
  /**
202
201
  * 2c session-sync — IMPORT a verbatim entry log (from another backend's {@link exportEntries}) into a
203
202
  * (typically NEW) `sessionId`. This is the WRITE half of {@link fork}, surfaced as `SessionRepo.importEntries`,
@@ -269,5 +268,57 @@ export declare class TiDBSessionStore implements SessionStore {
269
268
  get size(): number;
270
269
  dispose(): void;
271
270
  }
271
+ /**
272
+ * 2c session-sync P1d-β (PUSH streaming, §15 + §4) — a TiDB {@link StagingHandle}: stream entry batches into a SHADOW
273
+ * `session_event` partition (the staging id, NO meta row → invisible to wake), then atomically SWAP them into the real
274
+ * session id. Server memory is bounded by the in-flight batch (one txn per batch), not the whole log. The swap is a
275
+ * row-key RENAME (`UPDATE session_event SET session_id`) — metadata-only, NO payload re-copy — so the commit cost is
276
+ * the rename + a DELETE of the (purged) dst rows, in ONE txn; the session_meta row is locked (or, for a fresh push, claimed) first and
277
+ * settled last, and COMMIT is the sole commit point.
278
+ */
279
+ /** 暂存句柄(会话同步的推送腿逐批写、提交、撤销;由本店的 `stageImport` 交出)。导出只为组合座给它封口(店边界)。 */
280
+ export declare class TiDBStagingHandle implements StagingHandle {
281
+ private readonly pool;
282
+ private readonly hooks;
283
+ readonly realSessionId: string;
284
+ readonly stagingId: string;
285
+ /** Running DENSE seq counter for the staging rows. Advanced ONLY for a logically-NEW entry id (tracked in `seen`), so
286
+ * a re-streamed entry (Phase-B resume) is skipped and the staged seqs stay dense 0..N-1 (NO holes that would collide
287
+ * with a future F2 leaf_seq append on the committed session). */
288
+ private nextSeq;
289
+ /** Entry ids already staged by THIS handle — a resume re-streams from the start, so dedup keeps seqs dense + idempotent. */
290
+ private readonly seen;
291
+ constructor(pool: Pool, hooks: SessionWriteHooks, realSessionId: string, stagingId: string);
292
+ /** Append a batch to the staging id, re-seq'd DENSE from the running counter (skipping ids already staged), IDEMPOTENTLY
293
+ * (ON DUPLICATE KEY UPDATE on the `(session_id, entry_id)` uk is the durable backstop too). One txn per batch
294
+ * (bounded). NO meta row is written (the staging id stays invisible). ts is the entry's own ISO timestamp (verbatim). */
295
+ appendBatch(entries: SessionTreeEntry[]): Promise<void>;
296
+ /**
297
+ * The ATOMIC swap (ONE txn), §15 commit:
298
+ * 0. OWNER CAS FIRST ([ref] — one gate predicate, two phases): the session's owner cell must still be the one the import's gate
299
+ * judged (`opts.expectedOccupant`: owner ∧ incarnation). Like every other writer of a session (append, replace, delete) the staged commit takes the
300
+ * session_meta row FIRST, then the logs — one lock order. A present cell: one conditioned UPDATE (current read; takes the row lock
301
+ * until COMMIT); it bumps `leaf_seq` only as the CAS's always-changing column (a same-value UPDATE can read 0 affected rows on the
302
+ * MySQL protocol depending on driver flags), and step 4 overwrites that column with the real value in the same txn. An absent cell
303
+ * (a fresh push) cannot be locked, so it is CLAIMED: insert this commit's row now (a no-op on an existing key), then a locking read
304
+ * must show the incarnation this commit minted — anything else is a row somebody else created. Either miss ⇒ ROLLBACK → throw
305
+ * StagedCommitOccupantChangedError (route → 409), before anything about the row is read or reported (no placement, no relation,
306
+ * no entry ids).
307
+ * 1. IN-COMMIT RE-CLASSIFY (closes the Phase-A plan→commit TOCTOU): read the dst's CURRENT entry ids + the staged
308
+ * ids under `FOR UPDATE` (row-locks serialize a concurrent commit/run) → classifySyncRelationshipByIds. A flip
309
+ * to fork/stale WITHOUT `resolution:"overwrite-dst"` → ROLLBACK → throw SyncConflictError (route → 409).
310
+ * 2. DELETE the dst's existing session_event rows (the replaceEntries purge).
311
+ * 3. UPDATE session_event SET session_id = real WHERE session_id = staging (the row-key RENAME — metadata only).
312
+ * 4. UPDATE session_meta(owner, leaf_id=replayedLeaf, leaf_seq=N) on the row step 0 locked or claimed; COMMIT is the sole commit point.
313
+ * A crash before COMMIT: the DELETE+rename roll back (dst untouched) and the staging rows keep no meta = invisible
314
+ * orphans (reaped by sweepStagingSessions). `owner` is the re-stamped importing principal (§9).
315
+ */
316
+ commit(owner: string | null, opts: StagedCommitOptions): Promise<{
317
+ relation: SyncRelation["relation"];
318
+ face: SessionFace;
319
+ }>;
320
+ /** Drop the staging rows (Phase B aborted before commit). Idempotent (a missing staging id deletes 0). */
321
+ abort(): Promise<void>;
322
+ }
272
323
  export {};
273
324
  //# sourceMappingURL=tidb-session-store.d.ts.map
@@ -6,7 +6,7 @@ import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "../plugins/retention-dec
6
6
  import { headFromRow, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
7
7
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
8
8
  import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertCells, SESSION_DURABILITY, SESSION_PLACEMENTS, } from "../plugins/session-placement.js";
9
- import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
9
+ import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOccupantChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
10
10
  import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue, placedSessionOwnerOf } from "../plugins/session-ownership.js";
11
11
  import { forkRowCells, selectForkRows } from "@sema-agent/core";
12
12
  import { FORK_LOG_ROW_SHAPE, ForkTargetUnsettledError, classifyForkTarget, forkByFreshId, forkPointOf, sameSourceGeneration } from "../plugins/session-fork.js";
@@ -104,14 +104,33 @@ export class TiDBSessionStore {
104
104
  async register(sessionId, owner) {
105
105
  await this.registerWithReceipt(sessionId, owner);
106
106
  }
107
- async registerWithReceipt(sessionId, owner) {
107
+ async registerWithReceipt(sessionId, owner, kind) {
108
108
  if (!isSessionOwnerValue(owner)) {
109
109
  if ((await this.headOf(sessionId)) !== undefined)
110
110
  return {};
111
111
  assertFirstOwnerWritable(sessionId, owner);
112
112
  }
113
113
  const incarnation = mintSessionIncarnation();
114
- await this.insertMeta(sessionId, owner, undefined, incarnation);
114
+ if (kind === undefined) {
115
+ await this.insertMeta(sessionId, owner, undefined, incarnation);
116
+ return { minted: { sessionId, owner, incarnation } };
117
+ }
118
+ const conn = await this.pool.getConnection();
119
+ try {
120
+ await conn.beginTransaction();
121
+ await this.insertMetaOn(conn, sessionId, owner, undefined, incarnation);
122
+ const [rows] = await conn.query("SELECT incarnation FROM session_meta WHERE session_id = ? FOR UPDATE", [sessionId]);
123
+ if (rows[0]?.incarnation === incarnation)
124
+ await this.hooks.birth.record(statementsOn(conn), { rootSessionId: sessionId, kind });
125
+ await conn.commit();
126
+ }
127
+ catch (err) {
128
+ await conn.rollback().catch(() => undefined);
129
+ throw err;
130
+ }
131
+ finally {
132
+ conn.release();
133
+ }
115
134
  return { minted: { sessionId, owner, incarnation } };
116
135
  }
117
136
  async deleteSessionIf(sessionId, cond) {
@@ -137,9 +156,12 @@ export class TiDBSessionStore {
137
156
  await this.insertMeta(sessionId, placement === undefined ? null : placedSessionOwnerOf(sessionId, placement), placement);
138
157
  }
139
158
  async insertMeta(sessionId, owner, placement, incarnation = mintSessionIncarnation()) {
159
+ await this.insertMetaOn(this.pool, sessionId, owner, placement, incarnation);
160
+ }
161
+ async insertMetaOn(on, sessionId, owner, placement, incarnation) {
140
162
  const now = new Date();
141
163
  const placed = placementInsertCells(placement, now);
142
- await this.pool.query(`INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, ${placed.columns}) ` +
164
+ await on.query(`INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, ${placed.columns}) ` +
143
165
  "VALUES (?,?,?,?,NULL,0,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE leaf_seq = leaf_seq", [sessionId, owner, now, now, incarnation, ...placed.values]);
144
166
  }
145
167
  async placementOf(sessionId) {
@@ -202,7 +224,7 @@ export class TiDBSessionStore {
202
224
  }
203
225
  throw err instanceof SessionError
204
226
  ? err
205
- : new SessionError("storage", `placed session ${sessionId} could not be deleted: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : undefined);
227
+ : new SessionError("storage", `placed session ${sessionId} could not be deleted`, err instanceof Error ? err : undefined);
206
228
  }
207
229
  finally {
208
230
  conn.release();
@@ -298,11 +320,13 @@ export class TiDBSessionStore {
298
320
  if (!metaRows[0])
299
321
  return null;
300
322
  const pool = this.pool;
301
- const batchSize = opts?.batchSize ?? 500;
323
+ const batchSize = opts.batchSize ?? 500;
302
324
  async function* gen() {
303
- let cursor = opts?.afterSeq ?? -1;
325
+ let cursor = opts.afterSeq ?? -1;
304
326
  for (;;) {
305
327
  const [rows] = await pool.query(`SELECT seq, ${jsonColumnText("mysql", "payload")} FROM session_event WHERE session_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?`, [sessionId, cursor, batchSize]);
328
+ if (rows.length > 0)
329
+ await opts.recheck();
306
330
  for (const r of rows)
307
331
  yield decodeSessionEventPayload(r.payload, sessionId, r.seq);
308
332
  if (rows.length < batchSize)
@@ -426,7 +450,7 @@ export class TiDBSessionStore {
426
450
  this.pending.clear();
427
451
  }
428
452
  }
429
- class TiDBStagingHandle {
453
+ export class TiDBStagingHandle {
430
454
  pool;
431
455
  hooks;
432
456
  realSessionId;
@@ -466,11 +490,11 @@ class TiDBStagingHandle {
466
490
  await conn.beginTransaction();
467
491
  const now = new Date();
468
492
  const minted = mintSessionIncarnation();
469
- if (opts.expectedOwner !== undefined) {
470
- const [cas] = await conn.query("UPDATE session_meta SET leaf_seq = leaf_seq + 1 WHERE session_id = ? AND owner <=> ?", [this.realSessionId, opts.expectedOwner]);
493
+ if (opts.expectedOccupant !== undefined) {
494
+ const [cas] = await conn.query("UPDATE session_meta SET leaf_seq = leaf_seq + 1 WHERE session_id = ? AND owner <=> ? AND incarnation = ?", [this.realSessionId, opts.expectedOccupant.owner, opts.expectedOccupant.incarnation]);
471
495
  if (cas.affectedRows !== 1) {
472
496
  await conn.rollback();
473
- throw new StagedCommitOwnerChangedError(this.realSessionId);
497
+ throw new StagedCommitOccupantChangedError(this.realSessionId);
474
498
  }
475
499
  }
476
500
  else {
@@ -478,7 +502,7 @@ class TiDBStagingHandle {
478
502
  const [claimed] = await conn.query("SELECT incarnation FROM session_meta WHERE session_id = ? FOR UPDATE", [this.realSessionId]);
479
503
  if (String(claimed[0]?.incarnation) !== minted) {
480
504
  await conn.rollback();
481
- throw new StagedCommitOwnerChangedError(this.realSessionId);
505
+ throw new StagedCommitOccupantChangedError(this.realSessionId);
482
506
  }
483
507
  }
484
508
  settleHookVerdict(await this.hooks.beforeCommit(statementsOn(conn), { op: "import", rootSessionId: this.realSessionId }));
@@ -490,7 +514,7 @@ class TiDBStagingHandle {
490
514
  throw new SessionError("placement_refused", `session ${this.realSessionId} belongs to the ${String(dstMeta[0].placement_kind)} placement partition — a session-sync staged commit cannot overwrite a delegation transcript`);
491
515
  }
492
516
  const srcIds = stagedRows.map((r) => String(r.entry_id));
493
- const dstIds = opts.expectedOwner !== undefined ? dstRows.map((r) => String(r.entry_id)) : null;
517
+ const dstIds = opts.expectedOccupant !== undefined ? dstRows.map((r) => String(r.entry_id)) : null;
494
518
  const rel = classifySyncRelationshipByIds(srcIds, dstIds);
495
519
  if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
496
520
  await conn.rollback();
@@ -2,6 +2,7 @@ import type { DeploymentShapeInput } from "./deployment-shape.js";
2
2
  import type { RunAcceptance } from "./run-acceptance.js";
3
3
  import type { SessionWithdrawFace } from "./plugins/session-birth.js";
4
4
  import type { SessionBirth, StoreKeptWord } from "./session-incarnation.js";
5
+ import type { BindingReceiptSink } from "./session-bindings.js";
5
6
  /** 收场词(闭集;加词 ⇒ {@link SubmissionSeat.close} 的穷尽 switch 编译红)。 */
6
7
  export type SubmissionEnd = "accepted" | "record" | "refused";
7
8
  /** 「留」的词:店内七词 + 座内四词(`withdraw_failed` = 撤回事务抛了,按留答)。 */
@@ -25,7 +26,7 @@ export interface SubmissionSeatDeps {
25
26
  };
26
27
  }
27
28
  /** 一次提交的出口座(提交门在授权之前铸;三条提交腿只喂它、收它)。 */
28
- export interface SubmissionSeat {
29
+ export interface SubmissionSeat extends BindingReceiptSink {
29
30
  /** 授权器登记新行时交来的候选与店的撤回面(只有新行臂调;调多次以第一次为准)。 */
30
31
  noteBirth(birth: SessionBirth, face: SessionWithdrawFace): void;
31
32
  /** 提交门铸好 spec 之后挂上受理座(闩的唯一读者在 {@link close})。 */
@@ -36,7 +37,7 @@ export interface SubmissionSeat {
36
37
  markRecord(): void;
37
38
  /** 执行环境为这次提交留下了按会话键的持久物。 */
38
39
  noteEnvWitness(): void;
39
- /** 此刻判不判得出「留」而不必走店(闩上 / record / 没有候选 / 有见证)。 */
40
+ /** 此刻判不判得出「什么都不撤」而不必收场(闩上 / record / 没有候选也没有回执 / 有见证也没有回执)。 */
40
41
  readonly keepsWithoutWithdraw: boolean;
41
42
  /** 收场(幂等、先到者定;不抛)。 */
42
43
  close(): Promise<SubmissionVerdict>;
@@ -4,6 +4,7 @@ import { retireSessionId } from "./deleted-session-ids.js";
4
4
  import { seatText } from "./trace/exit-seat.js";
5
5
  export function createSubmissionSeat(deps) {
6
6
  let birth;
7
+ let bindings;
7
8
  let acceptance;
8
9
  let ownTaskId;
9
10
  let recorded = false;
@@ -16,12 +17,15 @@ export function createSubmissionSeat(deps) {
16
17
  let verdict;
17
18
  switch (end) {
18
19
  case "accepted":
20
+ bindings?.face.settleSessionBindings(bindings.receipt);
19
21
  verdict = "kept_accepted";
20
22
  break;
21
23
  case "record":
24
+ bindings?.face.settleSessionBindings(bindings.receipt);
22
25
  verdict = "kept_record";
23
26
  break;
24
27
  case "refused":
28
+ bindings?.face.withdrawSessionBindings(bindings.receipt);
25
29
  verdict = await withdrawOrKeep();
26
30
  break;
27
31
  default: {
@@ -60,6 +64,10 @@ export function createSubmissionSeat(deps) {
60
64
  if (birth === undefined)
61
65
  birth = { birth: b, face };
62
66
  },
67
+ noteBindings(receipt, face) {
68
+ if (bindings === undefined)
69
+ bindings = { receipt, face };
70
+ },
63
71
  attachAcceptance(a) {
64
72
  acceptance = a;
65
73
  },
@@ -77,7 +85,7 @@ export function createSubmissionSeat(deps) {
77
85
  witnessed = true;
78
86
  },
79
87
  get keepsWithoutWithdraw() {
80
- return birth === undefined || witnessed || endNow() !== "refused";
88
+ return endNow() !== "refused" || (bindings === undefined && (birth === undefined || witnessed));
81
89
  },
82
90
  close() {
83
91
  if (closing === undefined) {
@@ -115,8 +115,8 @@ export declare function effectiveHostWorkspace(sessionCwd: string | undefined, c
115
115
  * 为什么组合腿要它:梯子的每一级是在引擎**现铸的会话**里冷跑(core `runCascade` 剥掉 spec 的 `sessionId`),验证门的验证器腿的 spec 不带会话 ——
116
116
  * 工厂按腿的会话查登记表(第 1 臂)查不到提交登记在根会话上的那一枚,不交就落第 3 臂(临时目录):用户让梯子在项目里改文件,改到了别处;验证器验的是一只空目录。
117
117
  * 跑在根会话上的腿(验证门的实现腿 / 修复腿)第 1 臂先命中,同一个值。
118
- * 没登记(`registeredCwd` 缺席:非 host / device 车道、多租户、根会话的提交从没带过 `cwd`、嵌入方装配器没铸事实)⇒ 空对象 —— 每一条腿照旧落临时目录、收场由托管表删。
119
- * 登记粘在会话上(`setSessionCwd` 只在请求带 `cwd` 时写):同一会话之前带过、这次不带 ⇒ 交的仍是那一枚 —— 与第 1 臂对同一会话上普通 run 的读法一致。
118
+ * 没登记(这条腿的会话绑定没有工作目录:非 host / device 车道、多租户、根会话的提交从没带过 `cwd`、嵌入方装配器没铸事实)⇒ 空对象 —— 每一条腿照旧落临时目录、收场由托管表删。
119
+ * 登记粘在会话上(会话绑定的 `cwd` 一格 = 这条腿跑在的那一枚 —— 请求带的 ?? 项目继承来的 ?? 会话上已登记的 —— 受理之后写回会话,`session-bindings.ts`):同一会话之前带过、这次不带 ⇒ 交的仍是那一枚 —— 与第 1 臂对同一会话上普通 run 的读法一致。
120
120
  */
121
121
  export declare function compositionLegRoot(registeredCwd: string | undefined): Pick<RunInternals, "parentCwd">;
122
122
  /** {@link launchWorkspaceOf} 读到的那一格出自哪里(闭集;起服那一行日志与随包文档的 `workspaceFrom` 词)。 */
@@ -122,11 +122,12 @@ export declare function planReviewDecisionExitsPlan(decision: "approve" | "edit"
122
122
  * 新体 `{ ...body, permissionMode: entry.effective }`,调用方的对象不改;bundle 的 `settings.permissions.defaultMode` 不动 —— 顶层赢过它
123
123
  * ({@link effectivePermissionMode} 的优先序),再改它就是同一语义的第二个写者。披露这一折的通告(`entry-mode-clamp-notice.ts`)读的也是同一份 `entry`。
124
124
  *
125
- * 调用点一个:提交入口的那一步 `permission-mode-admission.ts` `stampSubmissionEntryMode`(受理判官的入口一步;`http/admission.ts` 的 `prepareSpec` 调它:值形门全部判完之后、`resolveSpec` 之前)。那一份体就是首跑装配读的体,也是
126
- * `buildCheckpointCtx` 持久化的体 ⇒ 一处盖章同时覆盖首跑与每一次续跑重建;装配腿与续跑腿都不读地板(两处读 = 两个源)。
127
- * 另外两个写者是 `plan_review` 写回(`http/resume-legs.ts` `drivePlanReview`,判据 {@link planReviewDecisionExitsPlan})与在飞 run 的切档口写回
128
- * (`http/routes/run-permission-mode.ts`,写的是同一只判官准入的词)。
129
- * 热改 `AUTONOMY` 只影响之后提交的任务与之后的切档:续跑腿保持存储体里的词。地板的属主是 `runtime-governance.ts` 的 `autonomyModeFloor`。
125
+ * 调用点两个,都是受理判官的入口一步(`permission-mode-admission.ts`,同一只 `bodyEntry`):提交口 `stampSubmissionEntryMode`(`http/admission.ts` 的 `prepareSpec` 调它:
126
+ * 值形门全部判完之后、`resolveSpec` 之前 —— 那一份体就是首跑装配读的体,也是 `buildCheckpointCtx` 持久化的体)与续跑口 `stampReplayEntryMode`(`http/resume-legs.ts`
127
+ * 的 `replayCtxOf`:停驻 run 的存储体被重放之前过一次**现**地板,地板的出口词原样;钳过的体随 CAS 赢点写回)。装配腿不读地板;每一腿读一次地板、写进体,
128
+ * run 的模式词仍只有存储体一个源。另外两个写者是 `plan_review` 写回(`http/resume-legs.ts` `drivePlanReview`,判据 {@link planReviewDecisionExitsPlan};
129
+ * 写下的是 plan 地板的出口词)与在飞 run 的切档口写回(`http/routes/run-permission-mode.ts`,写的是同一只判官准入的词)。
130
+ * 热改 `AUTONOMY` 对之后的提交、切档与停驻 run 的下一次续跑都生效。地板的属主是 `runtime-governance.ts` 的 `autonomyModeFloor`。
130
131
  */
131
132
  export declare function stampEntryPermissionMode<B extends {
132
133
  permissionMode?: unknown;
@@ -1,4 +1,5 @@
1
1
  import { terminalProjection, type PausedCause, type TaskStatus, type TerminalCause } from "@sema-agent/core";
2
+ import { StoreBoundaryFault, type StoreFaultClass } from "./plugins/store-fault.js";
2
3
  /**
3
4
  * 这次失败带的**机器码**(`terminal.failed.code`);非失败终局 ⇒ `undefined`。
4
5
  *
@@ -129,9 +130,12 @@ export declare function mapPauseSeats<T>(result: T, f: (seat: SeatObject, keys:
129
130
  *
130
131
  * ── 折码的运维线索:一句定形字段,两只入口各说一只词 ──
131
132
  * 字段 `original=<码 | <not-a-code>> outward=<对外词> messageBytes=<n> at=<观看点>`,只有闭集词、数字与码({@link isCodeToken});码形座里装着文字的、长得像码的秘密同理只报 `<not-a-code>`。
132
- * 失败句(库驱动原句,自由文本:可以有连接串 / 口令 / 库账号 / 表名)**不上 wire**({@link outwardStoreFailure}),只进**写入口**那一行的末尾格 `message=<JSON 串>` ——
133
- * 经出口座的一句话行(`operator-log-text`:凭据形换记号、剥控制字符、有界),读座的再观看不带(同一行存量行每读一次就重放一次)。`at` 说这是哪一次观看,**一个入口一只词**(类型给定,不靠记得传):
133
+ * 失败句**不上 wire**({@link outwardStoreFailure});店起源的失败到这里时句子已是店边界换过的对外句({@link storeBoundaryFault}:库驱动原句 —— 自由文本,可以有连接串 / 口令 / 库账号 / 表名 ——
134
+ * 只进店边界那一行 `at=store`),写入口那一行的末尾格 `message=<JSON 串>` 对它们带的就是对外句;不经店边界的抛出物(fs / 网络 / 第三方码)带的仍是它自己的句子 ——
135
+ * 都经出口座的一句话行(`operator-log-text`:凭据形换记号、剥控制字符、有界),读座的再观看不带(同一行存量行每读一次就重放一次)。`at` 说这是哪一次观看,**一个入口一只词**(类型给定,不靠记得传):
134
136
  * · 本函数 = 结果**第一次**离开引擎(店的写口 / 出口铸体)⇒ `at=exit`,每折一次一行 `[terminal] outward_failure_code_folded <字段>[ message=<句>]` —— 每次都是一件新的失败;
137
+ * · {@link storeBoundaryFault} = 失败**离开本服务的店**(SQL 店的出口)⇒ `at=store`,每次一行,多两格 `site=<店>.<方法> class=<判官词>`,`message=` 是原错与其 `cause` 链各层的句子(逐层过座);
138
+ * 查库的原话读这一行,按 `original=` 与时间对 `at=exit` / `request_error` 行;
135
139
  * · {@link sealStoredResult} = 存量行的**再观看**(读座必填)⇒ 走 fail-open 纪律(登记 tag `server.terminal.stored-result-code-folded`):
136
140
  * 逐读计数 + 逐读探针行 + 每进程一次 warn,detail 即上面那句字段。读面每次轮询 / 每个观看者 / 每次重连都重放同一批行 ——
137
141
  * 计数让「旧失败被读了多少次」看得见,一次 warn 让日志不随读次放大;去重归记录器一只,不另造表。
@@ -148,12 +152,39 @@ export declare function sealResultForExit<T extends {
148
152
  export declare function sealStoredResult<T extends {
149
153
  readonly terminal?: unknown;
150
154
  }>(result: T, seat: StoredResultReadSeat): T;
155
+ /**
156
+ * 一个折掉的码在出口上的对外句(纯函数、无副作用)—— 出口四入口的**唯一**读法:{@link outwardStoreFailure}(封口 / 抛出臂,带运维线索)、{@link outwardPlane}
157
+ * (投影出口,纯读、不出线索)与 {@link persistedPlaneOf} 的旧形臂。句子已是「某个类别句 + 本码尾注」⇒ 原样(店边界换过的句子,词在原错上判,见 {@link storeBoundaryFault});
158
+ * 否则 = 判官读 `{ code }` 的类别句 + 尾注(拼法 `outwardStoreSentence`,一只拼法两种输入)。任何一张脸上折过码的 `errorMessage` 都不可能是驱动的原句,不论这条因由封过口没有、存的是哪一代形。
159
+ * 店边界之前落盘的存量子代句的一次性改写(M53,`store-fault-sentence-m53.ts`)读同一只。
160
+ */
161
+ export declare function outwardStoreMessageOf(code: string, message: unknown): string;
151
162
  /**
152
163
  * 腿**抛出**的错落进 run 行 `error` 列时的对外句(没有终局结果、没有因由的那一臂:`runs.ts` 后台腿的兜底腿与它的孪生 `resume-legs.ts` 兜底腿)—— 与终局因由是**同一只**
153
164
  * 铸点({@link outwardStoreFailure}):抛出物自带的 `code`({@link errorCodeOf},字符串)出了本仓词法 ⇒ 同码同句;没有码 / 码在词法内(本仓与引擎自己的错、普通 `Error`)⇒ 文本逐字,非库错误照旧。
154
165
  * `message` 是调用方已取出的抛出文本({@link errorText} 的产物,不抛);本函数不抛(读码口不抛)。
155
166
  */
156
167
  export declare function outwardThrownMessage(err: unknown, message: string): string;
168
+ /**
169
+ * 🔴 **店边界 —— 驱动原句越出 SQL 店的唯一读者**。律:一只 SQL 店的任何出口(抛出值、包装错误的 `cause`、抛出或返回值里的失败文本、交给调用方的可调用对象)
170
+ * 只带「原码 + 稳定句」({@link StoreBoundaryFault});驱动的原句在这里记一次运维日志,不随错误走。店的出口在 `plugins/store-boundary.ts` 封口(逐生产者、编译期穷尽的出口表),
171
+ * 店里自己拼句 / 拼值 / 包 `cause` 的地方先读本函数、再用它交回的形。交回:
172
+ * · 已是边界形 ⇒ 同一引用(嵌套店:最里层出口换过、记过,外层同一引用放行、不再记);
173
+ * · typed 失败(顶层自带词法内的码:`SessionError("storage")`、`store.row_too_large` …)⇒ 同一引用,它的类、码与句子原样;它 `cause` 链上的驱动错**就地**换成边界形
174
+ * (typed 层往下走,第一只不是 typed 而带驱动证据的那一段整只换;那一段的原句照样记一行 `at=store`);
175
+ * · 调用方出身(`error-origin.ts` 的 `caller`:店把调用方钩子的抛出物原样还给调用方)⇒ 同一引用、不记 —— 它不是店的失败;
176
+ * · 不触发扳机 ⇒ 同一引用(不带码 / 码在词法内、不带驱动出身记号的错照旧);
177
+ * · 触发 ⇒ 一行运维日志(`at=store site=<店>.<方法> class=<判官词>`,`message=` 是原错与其 `cause` 链各层的句子,逐层过出口座)+ 计数,交回新铸的边界形。
178
+ * 扳机两条(任一):① **码出词法**:码在场且 {@link outwardFailureCode} 折它 —— fail-closed,词法外的码一律换句(驱动、套接字、对象存储、fs 都算);
179
+ * ② **驱动出身**:原错或其 `cause` 链上任一层带池座打的记号(`plugins/error-origin.ts` 的 `driver`)—— 补词法看不见的无码驱动错。
180
+ * 链读法一律深 ≤ {@link CAUSE_CHAIN_DEPTH};链比这更深 ⇒ 当作带驱动证据(fail-closed:看不到底的链不放行)。
181
+ * 码的读法({@link driverCodeOf}):沿 `cause` 链(深 ≤ 8)取第一个非空串 `code`,`"unknown"` 只作兜底(与 core 的码链读法同读);一条串码都没有、而某一层带整数 `errno`
182
+ * ⇒ 码 = 那个 `errno` 的十进制串 —— 这是本仓的规则(引擎不读 errno):只带 errno 的驱动错(TiDB 自有号,mysql2 的码表里没有符号)于是进同一只扳机、说同一句,出口照折。
183
+ * 判官(`storeFaultClassOf`)在**原错**上判一次(连接类整句、errno 臂、1005 内层号都只在原错上读得到),词随形带走。不抛。
184
+ * `site` = 封口调用点的字面店词 + 方法名(源码里的标识符,不是外部输入)。
185
+ */
186
+ export declare function storeBoundaryFault<E>(err: E, site: string): E | StoreBoundaryFault;
187
+ export declare function setStoreBoundaryObserver(fn: ((site: string, faultClass: StoreFaultClass) => void) | undefined): void;
157
188
  /**
158
189
  * **投影出口的唯一属主**([ref])—— core 的 `terminalProjection` + 对外码归一,一次。
159
190
  *