@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
@@ -1,5 +1,9 @@
1
1
  import { ExecutionError, FileError, SYMLINK_CHAIN_MAX_HOPS } from "@sema-agent/core";
2
+ import { posix } from "node:path";
3
+ import { anchorUnder } from "./exec-path-law.js";
4
+ import { isCanonicalBase64 } from "../device-ws-protocol.js";
2
5
  import { NAME_MAX_BYTES, classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
6
+ import { echoCallerText } from "../caller-text-echo.js";
3
7
  import { recordFailOpen } from "../observability/fail-open.js";
4
8
  import { createLogger } from "../observability/logger.js";
5
9
  import { seatText } from "../trace/exit-seat.js";
@@ -156,6 +160,220 @@ export async function runSymlinkChain(absPath, run) {
156
160
  return { ok: false, error: classifyFsStderr(r.value.stderr, absPath, "unknown") };
157
161
  return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout, absPath) } };
158
162
  }
163
+ export const BOUND_READ_EXITS = { link: 90, moved: 91, kind: 92, over: 93, platform: 94, name: 95 };
164
+ const BOUND_READ_TIMEOUT_EXITS = [124, 137, 143];
165
+ export const BOUND_READ_SCRIPT = [
166
+ "f=$1; op=$2; want=$3; wk=$4; max=$5; enc=$6; LC_ALL=C; export LC_ALL; nl=$(printf '\\n.'); nl=${nl%.}",
167
+ `[ -d /proc/self/fd ] || exit ${BOUND_READ_EXITS.platform}`,
168
+ `for t in readlink stat head; do command -v "$t" >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; done`,
169
+ `if [ "$enc" = base64 ]; then command -v base64 >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; fi`,
170
+ `if [ -L "$f" ]; then exit ${BOUND_READ_EXITS.link}; fi`,
171
+ `if [ -e "$f" ]; then if [ -f "$f" ] || [ -d "$f" ]; then :; else exit ${BOUND_READ_EXITS.kind}; fi; fi`,
172
+ "{",
173
+ ` p=$(readlink /proc/self/fd/0 && printf .) || exit ${BOUND_READ_EXITS.platform}`,
174
+ ' p=${p%.}; p=${p%"$nl"}',
175
+ ` s=$(stat -L -c '%d:%i %s %F' /proc/self/fd/0) || exit ${BOUND_READ_EXITS.platform}`,
176
+ ` [ "$p" = "$f" ] || exit ${BOUND_READ_EXITS.moved}`,
177
+ ' id=${s%% *}; r=${s#* }; sz=${r%% *}; t=${r#* }',
178
+ ` case $t in "regular file"|"regular empty file") k=file ;; directory) k=directory ;; *) exit ${BOUND_READ_EXITS.kind} ;; esac`,
179
+ ` if [ "$op" = open ]; then :; elif [ "$id" = "$want" ] && [ "$k" = "$wk" ]; then :; else exit ${BOUND_READ_EXITS.moved}; fi`,
180
+ " case $op in",
181
+ ` open) printf '%s %s\\n' "$id" "$k" ;;`,
182
+ ` size) printf '%s\\n' "$sz" ;;`,
183
+ ` readBytes) if [ "$sz" -le "$max" ]; then :; else printf '%s\\n' "$sz"; exit ${BOUND_READ_EXITS.over}; fi`,
184
+ ' n=$sz; [ "$sz" -gt 0 ] || n=$((max + 1))',
185
+ ` printf '%s\\n' "$sz"`,
186
+ ' if [ "$enc" = base64 ]; then { rc=$( { { head -c "$n"; echo "$?" >&4; } | base64 >&3; } 4>&1 ); } 3>&1; [ "$rc" = 0 ] || exit 1; else head -c "$n"; fi ;;',
187
+ " list) cd -P /proc/self/fd/0 || exit 1",
188
+ " set --",
189
+ ` for n in * .[!.]* ..?*; do case $n in *"$nl"*) exit ${BOUND_READ_EXITS.name} ;; esac; if [ -e "$n" ] || [ -L "$n" ]; then set -- "$@" "$n"; fi; done`,
190
+ ` if [ "$#" -gt 0 ]; then stat -c '%f %s %Y %n' -- "$@"; fi ;;`,
191
+ " *) exit 1 ;;",
192
+ " esac",
193
+ " exit",
194
+ '} < "$f"',
195
+ "exit 1",
196
+ ].join("\n");
197
+ export function buildBoundReadCommand(abs, op, args, payload, boundSec) {
198
+ const argv = [abs, op, args.bound?.identity ?? "-", args.bound?.kind ?? "-", String(args.max ?? 0), payload].map(shellQuote).join(" ");
199
+ return `{ command -v timeout >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; exec timeout ${Math.max(1, Math.floor(boundSec))} sh -c ${shellQuote(BOUND_READ_SCRIPT)} sema-bound-read ${argv}; }`;
200
+ }
201
+ const BOUND_SMALL_ANSWER_BYTES = 4096;
202
+ const BOUND_LIST_ANSWER_BYTES = 16 * 1024 * 1024;
203
+ const BOUND_IDENTITY_RE = /^\d+:\d+$/;
204
+ const BOUND_DECIMAL_RE = /^\d+$/;
205
+ function overCapError(abs, max, bytes) {
206
+ return new FileError("invalid", `The held file is over the ${max}-byte read cap (${bytes} bytes or more); it was not read`, abs);
207
+ }
208
+ function malformedAnswer(abs, what) {
209
+ return new FileError("unknown", `the target's bound-read answer was malformed (${what}); nothing was read`, abs);
210
+ }
211
+ function boundReadRefusal(word, abs, max, stdout) {
212
+ switch (word) {
213
+ case "link":
214
+ return new FileError("precondition_failed", "The path is now a link, not the object that was checked", abs);
215
+ case "moved":
216
+ return new FileError("precondition_failed", "The path named another object than the one opened", abs);
217
+ case "kind":
218
+ return new FileError("invalid", "Neither a regular file nor a directory", abs);
219
+ case "over":
220
+ return overCapError(abs, max, BOUND_DECIMAL_RE.test(stdout.trim()) ? stdout.trim() : "more than the cap");
221
+ case "platform":
222
+ return new FileError("not_supported", "This target cannot bind a read: it gives no /proc/self/fd observation of an opened file (not Linux, or /proc is not mounted), or it lacks timeout / readlink / stat / head / base64", abs);
223
+ case "name":
224
+ return new FileError("invalid", "A name in this directory contains a newline, which this listing cannot carry unambiguously; the directory was not listed", abs);
225
+ }
226
+ }
227
+ function boundReadExitOf(code) {
228
+ for (const word of Object.keys(BOUND_READ_EXITS))
229
+ if (BOUND_READ_EXITS[word] === code)
230
+ return word;
231
+ return undefined;
232
+ }
233
+ function answerCap(op, max, payload) {
234
+ switch (op) {
235
+ case "open":
236
+ case "size":
237
+ return BOUND_SMALL_ANSWER_BYTES;
238
+ case "list":
239
+ return BOUND_LIST_ANSWER_BYTES;
240
+ case "readBytes": {
241
+ const body = payload === "base64" ? Math.ceil((max + 1) / 3) * 4 : max + 1;
242
+ return BOUND_SMALL_ANSWER_BYTES + body + Math.ceil(body / 76) + 1;
243
+ }
244
+ }
245
+ }
246
+ async function runBoundOp(abs, op, args, lane, abortSignal) {
247
+ const max = args.max ?? 0;
248
+ const cap = answerCap(op, max, lane.payload);
249
+ const r = await lane.run(buildBoundReadCommand(abs, op, args, lane.payload, lane.boundSec), cap, abortSignal);
250
+ if (!r.ok)
251
+ return { ok: false, error: r.error.code === "timeout" ? new FileError("timeout", `the bound read ran past the lane's bound: ${r.error.message}`, abs, r.error) : fileErrorFromExec(r.error, abs) };
252
+ const { exitCode, stderr } = r.value;
253
+ const stdout = Buffer.from(r.value.stdout);
254
+ if (exitCode !== 0) {
255
+ const word = boundReadExitOf(exitCode);
256
+ if (word !== undefined)
257
+ return { ok: false, error: boundReadRefusal(word, abs, max, stdout.subarray(0, 64).toString("latin1")) };
258
+ if (BOUND_READ_TIMEOUT_EXITS.includes(exitCode))
259
+ return { ok: false, error: new FileError("timeout", `the bound read on the target ran past its ${lane.boundSec}s bound and was killed; nothing was read`, abs) };
260
+ return { ok: false, error: classifyFsStderr(stderr, abs, "unknown") };
261
+ }
262
+ if (stdout.length > cap)
263
+ return { ok: false, error: malformedAnswer(abs, `over the ${cap}-byte bound of a ${op} answer`) };
264
+ return { ok: true, value: stdout };
265
+ }
266
+ function bytesOfReadAnswer(abs, out, payload) {
267
+ const nl = out.indexOf(0x0a);
268
+ const head = nl < 0 ? "" : out.subarray(0, nl).toString("latin1");
269
+ if (!BOUND_DECIMAL_RE.test(head))
270
+ return { ok: false, error: malformedAnswer(abs, "no size line") };
271
+ const size = Number(head);
272
+ const rest = out.subarray(nl + 1);
273
+ let bytes;
274
+ if (payload === "base64") {
275
+ const text = rest.toString("latin1").replace(/\s+/g, "");
276
+ if (!isCanonicalBase64(text))
277
+ return { ok: false, error: malformedAnswer(abs, "the payload is not canonical base64") };
278
+ bytes = Buffer.from(text, "base64");
279
+ }
280
+ else {
281
+ bytes = rest;
282
+ }
283
+ if (size > 0 && bytes.length !== size) {
284
+ return { ok: false, error: new FileError("unknown", `the file was ${size} bytes when the read started but ${bytes.length} arrived (it changed while it was read, or the transfer was cut); nothing was read`, abs) };
285
+ }
286
+ return { ok: true, value: new Uint8Array(bytes) };
287
+ }
288
+ function entriesOfListAnswer(abs, out) {
289
+ const entries = [];
290
+ for (const line of out.toString("utf8").split("\n")) {
291
+ if (line === "")
292
+ continue;
293
+ const m = /^([0-9a-fA-F]+) (\d+) (\d+) (.+)$/.exec(line);
294
+ if (m === null)
295
+ return { ok: false, error: malformedAnswer(abs, "a listing line is not `<mode> <size> <mtime> <name>`") };
296
+ const type = parseInt(m[1], 16) & 0o170000;
297
+ const kind = type === 0o100000 ? "file" : type === 0o040000 ? "directory" : type === 0o120000 ? "symlink" : undefined;
298
+ if (kind === undefined)
299
+ continue;
300
+ entries.push({ name: m[4], kind, size: Number(m[2]), mtimeMs: Number(m[3]) * 1000 });
301
+ }
302
+ return { ok: true, value: entries };
303
+ }
304
+ export function createShellRebindingAnswers(abs, lane) {
305
+ return {
306
+ async open(abortSignal) {
307
+ const r = await runBoundOp(abs, "open", {}, lane, abortSignal);
308
+ if (!r.ok)
309
+ return r;
310
+ const [identity, kind, ...more] = r.value.toString("latin1").trim().split(" ");
311
+ if (identity === undefined || !BOUND_IDENTITY_RE.test(identity) || (kind !== "file" && kind !== "directory") || more.length > 0)
312
+ return { ok: false, error: malformedAnswer(abs, "the open answer is not `<dev>:<inode> <file|directory>`") };
313
+ return { ok: true, value: { identity, kind } };
314
+ },
315
+ async size(bound) {
316
+ const r = await runBoundOp(abs, "size", { bound }, lane);
317
+ if (!r.ok)
318
+ return r;
319
+ const text = r.value.toString("latin1").trim();
320
+ if (!BOUND_DECIMAL_RE.test(text))
321
+ return { ok: false, error: malformedAnswer(abs, "the size answer is not a decimal") };
322
+ return { ok: true, value: Number(text) };
323
+ },
324
+ async readBytes(bound, max, abortSignal) {
325
+ const r = await runBoundOp(abs, "readBytes", { bound, max }, lane, abortSignal);
326
+ return r.ok ? bytesOfReadAnswer(abs, r.value, lane.payload) : r;
327
+ },
328
+ async list(bound, abortSignal) {
329
+ const r = await runBoundOp(abs, "list", { bound }, lane, abortSignal);
330
+ return r.ok ? entriesOfListAnswer(abs, r.value) : r;
331
+ },
332
+ };
333
+ }
334
+ export function createRebindingBoundFile(abs, bound, answers) {
335
+ return {
336
+ identity: bound.identity,
337
+ kind: bound.kind,
338
+ size: () => answers.size(bound),
339
+ async readBytes(max, abortSignal) {
340
+ if (bound.kind !== "file")
341
+ return { ok: false, error: new FileError("is_directory", "The held object is a directory", abs) };
342
+ if (!Number.isSafeInteger(max) || max < 0)
343
+ return { ok: false, error: new FileError("invalid", `the read cap must be a non-negative integer (got ${String(max)})`, abs) };
344
+ const r = await answers.readBytes(bound, max, abortSignal);
345
+ if (!r.ok)
346
+ return r;
347
+ if (r.value.length > max)
348
+ return { ok: false, error: overCapError(abs, max, r.value.length) };
349
+ return r;
350
+ },
351
+ async list(abortSignal) {
352
+ if (bound.kind !== "directory")
353
+ return { ok: false, error: new FileError("not_directory", "The held object is a file", abs) };
354
+ const r = await answers.list(bound, abortSignal);
355
+ if (!r.ok)
356
+ return r;
357
+ const out = [];
358
+ for (const e of r.value) {
359
+ if (e.name === "" || e.name === "." || e.name === ".." || e.name.includes("/"))
360
+ return { ok: false, error: malformedAnswer(abs, `a listed name is not one path segment: ${JSON.stringify(echoCallerText(e.name, { maxChars: 80 }))}`) };
361
+ out.push({ ...e, path: anchorUnder(posix, abs, e.name) });
362
+ }
363
+ return { ok: true, value: out };
364
+ },
365
+ close: async () => undefined,
366
+ };
367
+ }
368
+ export async function openRebinding(abs, answers, abortSignal) {
369
+ const opened = await answers.open(abortSignal);
370
+ if (!opened.ok)
371
+ return opened;
372
+ return { ok: true, value: createRebindingBoundFile(abs, opened.value, answers) };
373
+ }
374
+ export function openBoundOverShell(abs, lane, abortSignal) {
375
+ return openRebinding(abs, createShellRebindingAnswers(abs, lane), abortSignal);
376
+ }
159
377
  const STAGED_FILE = "data";
160
378
  const STAGE_DIR_REFUSED_EXIT = 3;
161
379
  const STAGE_DIR_UNSEALED_EXIT = 4;
@@ -17,14 +17,23 @@
17
17
  */
18
18
  import type { Pool as MySqlPool } from "mysql2/promise";
19
19
  import type { Pool as PgPool } from "pg";
20
- import { type SqlDriver } from "./sql-driver.js";
20
+ import { type SqlDialect, type SqlDriver, type SqlExec } from "./sql-driver.js";
21
+ /**
22
+ * 一条锚的写(幂等 upsert,主键 `(session_id, event_id)`)—— **语句源**,跑在调用方给的执行口上:落定段(`landed-attendants.ts`:段首锁根行判过住客,
23
+ * 同一事务)是生产里唯一的调用方;店的 {@link SqlResumeAnchorStore.put} 是夹具 / 契约面的同一句。
24
+ */
25
+ export declare function putAnchorOn(exec: Pick<SqlExec, "query">, dialect: SqlDialect, sessionId: string, eventId: string, entryId: string, owner: string | null): Promise<void>;
26
+ /** 一只会话的锚全删(按 `session_id` 定址)—— 语句源(落定段的擦除腿与 E21 删除同一句)。返回删掉的行数。 */
27
+ export declare function deleteAnchorsOn(exec: Pick<SqlExec, "query">, dialect: SqlDialect, sessionId: string): Promise<number>;
21
28
  /** Dual-dialect durable resume-at anchor map. See the file header for the dialect-delta ledger. */
22
29
  export declare class SqlResumeAnchorStore {
23
30
  protected readonly db: SqlDriver;
24
31
  constructor(db: SqlDriver);
25
32
  /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
26
33
  private q;
27
- /** Record/overwrite the anchor for one settled message boundary (idempotent upsert on the (session_id,event_id) PK). */
34
+ /** Record/overwrite the anchor for one settled message boundary (idempotent upsert on the (session_id,event_id) PK). **Not a production writer**:
35
+ * the deployment's read face (`StoreBackend.resumeAnchor()`'s `ResumeAnchorStore`) carries no `put` — every production anchor write goes through the landed section
36
+ * (`landed-attendants.ts`), which runs {@link putAnchorOn} after locking the session row and judging the occupant. Fixtures seed through this verb. */
28
37
  put(sessionId: string, eventId: string, entryId: string, owner: string | null): Promise<void>;
29
38
  /** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id; undefined = unknown anchor. */
30
39
  resolve(sessionId: string, eventId: string, owner: string | null): Promise<string | undefined>;
@@ -1,4 +1,13 @@
1
1
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
+ import { sealStoreClass } from "./store-boundary.js";
3
+ export async function putAnchorOn(exec, dialect, sessionId, eventId, entryId, owner) {
4
+ await exec.query(dialect === "mysql"
5
+ ? "INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE entry_id = VALUES(entry_id), owner = VALUES(owner)"
6
+ : "INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (session_id, event_id) DO UPDATE SET entry_id = EXCLUDED.entry_id, owner = EXCLUDED.owner", [sessionId, eventId, entryId, owner, new Date()]);
7
+ }
8
+ export async function deleteAnchorsOn(exec, dialect, sessionId) {
9
+ return (await exec.query(dialect === "mysql" ? "DELETE FROM resume_anchor WHERE session_id = ?" : "DELETE FROM resume_anchor WHERE session_id = $1", [sessionId])).affected;
10
+ }
2
11
  export class SqlResumeAnchorStore {
3
12
  db;
4
13
  constructor(db) {
@@ -8,9 +17,7 @@ export class SqlResumeAnchorStore {
8
17
  return this.db.dialect === "mysql" ? mysql : pg;
9
18
  }
10
19
  async put(sessionId, eventId, entryId, owner) {
11
- await this.db.query(this.q("INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES (?,?,?,?,?) " +
12
- "ON DUPLICATE KEY UPDATE entry_id = VALUES(entry_id), owner = VALUES(owner)", "INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES ($1,$2,$3,$4,$5) " +
13
- "ON CONFLICT (session_id, event_id) DO UPDATE SET entry_id = EXCLUDED.entry_id, owner = EXCLUDED.owner"), [sessionId, eventId, entryId, owner, new Date()]);
20
+ await putAnchorOn(this.db, this.db.dialect, sessionId, eventId, entryId, owner);
14
21
  }
15
22
  async resolve(sessionId, eventId, owner) {
16
23
  const { rows } = await this.db.query(this.q("SELECT entry_id FROM resume_anchor WHERE session_id = ? AND event_id = ? AND owner <=> ?", "SELECT entry_id FROM resume_anchor WHERE session_id = $1 AND event_id = $2 AND owner IS NOT DISTINCT FROM $3"), [sessionId, eventId, owner]);
@@ -21,10 +28,10 @@ export class SqlResumeAnchorStore {
21
28
  return rows.map((r) => ({ eventId: String(r.event_id), entryId: String(r.entry_id), owner: r.owner == null ? null : String(r.owner) }));
22
29
  }
23
30
  async deleteBySession(sessionId) {
24
- const res = await this.db.query(this.q("DELETE FROM resume_anchor WHERE session_id = ?", "DELETE FROM resume_anchor WHERE session_id = $1"), [sessionId]);
25
- return res.affected;
31
+ return deleteAnchorsOn(this.db, this.db.dialect, sessionId);
26
32
  }
27
33
  }
34
+ sealStoreClass(SqlResumeAnchorStore, "sql-resume-anchor-store", { put: "exit", resolve: "exit", listBySession: "exit", deleteBySession: "exit" });
28
35
  export class TiDBResumeAnchorStore extends SqlResumeAnchorStore {
29
36
  constructor(pool) {
30
37
  super(mysqlDriver(pool));
@@ -1,4 +1,5 @@
1
1
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
+ import { sealStoreClass } from "./store-boundary.js";
2
3
  export const RETENTION_AUDIT_TABLE = "retention_audit";
3
4
  export const RETENTION_HOLD_TABLE = "retention_hold";
4
5
  export const RETENTION_LEASE_TABLE = "retention_lease";
@@ -217,6 +218,9 @@ export class SqlRetentionLaneStore {
217
218
  }));
218
219
  }
219
220
  }
221
+ sealStoreClass(SqlRetentionLaneStore, "sql-retention-lane-store", {
222
+ acquire: "exit", renew: "exit", release: "exit", setHold: "exit", setHoldAudited: "exit", holdInForce: "exit", appendAudit: "exit", listAudit: "exit",
223
+ });
220
224
  export class TiDBRetentionLaneStore extends SqlRetentionLaneStore {
221
225
  constructor(pool) {
222
226
  super(mysqlDriver(pool));
@@ -1,6 +1,7 @@
1
1
  /**
2
- * 托管留存的两只 SQL 谓词 —— 执行器(`stores-sql/retention-store-sql.ts`,引擎座表那一半)与部署的事务内钩(`plugins/retention-deployment.ts`,
3
- * 部署表那一半)对**同一条域律**发谓词,只从这里取:一条律一个读法,两半不各写一份(分家的方向是多删或漏删)。
2
+ * 托管留存的三只 SQL 谓词 —— 执行器(`stores-sql/retention-store-sql.ts`,引擎座表那一半)与部署的事务内钩(`plugins/retention-deployment.ts`,
3
+ * 部署表那一半)对**同一条域律**发谓词,只从这里取:一条律一个读法,两半不各写一份(分家的方向是多删或漏删)。第三只 —— 会话树的活引用
4
+ * ({@link sessionLiveReferenceSql})—— 另有会话老化的两个读者(`plugins/session-store-sql.ts` 的老化候选面与老化围栏):自动老化不许比托管留存松。
4
5
  *
5
6
  * 域 = 租户键:`COALESCE(session_meta.owner, '')` / `checkpoint.scope` 原样;**空串 `''` = 无主(anonymous/dev)桶**,谓词写成
6
7
  * `(owner IS NULL OR owner = '')` —— 合并只发生在无主侧,永远伸不进一个真租户;拆成两个桶才会漏(NULL 那半永不被枚举)。
@@ -26,4 +27,21 @@ export declare function retentionIdSet(dialect: SqlDialect, col: string, ids: re
26
27
  sql: string;
27
28
  params: unknown[];
28
29
  };
30
+ /**
31
+ * **会话树的活引用 —— SQL 谓词的唯一写法**(`sm` = `session_meta` 的别名;两方言一只函数)。
32
+ *
33
+ * 读者三只,同调这一只:托管留存的「可删候选」(否定)与「被保护计数」(肯定)(`stores-sql/retention-store-sql.ts` 的 `liveReferenceExists`)、会话老化的候选面与
34
+ * 老化围栏的锁内重判(`plugins/session-store-sql.ts`;会话种类的律在 `src/session-kind.ts`)。两份手抄必然在某次改动后分家,而分家的方向是**多删**(候选放宽、
35
+ * 计数收紧,两边都不会有人红)。local 店的同义判是 `plugins/local-session-live-reference.ts`。
36
+ *
37
+ * 腿的词表:活跑 claim / 挂起 park(pending 检查点)/ 活着的后台子代(running|parked,三根会话锚)/ 在跑的 workflow(`originatingSessionId` 住在 run blob 里 ⇒
38
+ * JSON 提取是真方言分叉)。fork 子代**结构上没有边**(理由见 `SqlRetentionStore.deleteExpiredSessions` 的头注)。
39
+ *
40
+ * 🔴 [ref] C18 追加的**第五条腿**(placed 分区,core 5.48 [ref] 义务 4 的 server 侧兑现):一只 `placement_kind` 在场的会话是某条委派的**转录**,它的删除权
41
+ * **独家**属于联合 reap(`reapDurableAgents`:先按 rev 条件删 agent 行、赢了才 `release` 终结转录;以及行已不在时的分区收割腿)。让视界腿也去删它会造出两个删除
42
+ * 权威,而它们的交叠恰好是这条义务要关死的那个状态:行还在、指着一份已经不存在的转录(core 的 `delegation.transcript_integrity` 通告说的就是它)。⇒ 把「placed」当成
43
+ * 一条**活引用**:候选谓词排除它,被保护计数同时把它记成 skipped(两侧共用本函数 ⇒ 不可能分家)。它不会因此永生:分区收割腿按 `backgroundAgentRetentionMs` 的
44
+ * 年龄 + 「行确实不在」联合判定后真删。
45
+ */
46
+ export declare function sessionLiveReferenceSql(dialect: SqlDialect): string;
29
47
  //# sourceMappingURL=retention-sql.d.ts.map
@@ -5,4 +5,15 @@ export function retentionDomainWhere(dialect, col, domain, pgIndex) {
5
5
  export function retentionIdSet(dialect, col, ids, pgIndex) {
6
6
  return dialect === "mysql" ? { sql: `${col} IN (${ids.map(() => "?").join(",")})`, params: [...ids] } : { sql: `${col} = ANY($${pgIndex}::text[])`, params: [ids] };
7
7
  }
8
+ export function sessionLiveReferenceSql(dialect) {
9
+ const workflow = dialect === "mysql"
10
+ ? `EXISTS (SELECT 1 FROM workflow_run wr WHERE wr.status = 'running' AND JSON_UNQUOTE(JSON_EXTRACT(wr.run, '$.originatingSessionId')) = sm.session_id)`
11
+ : `EXISTS (SELECT 1 FROM workflow_run wr WHERE wr.status = 'running' AND wr.run::json->>'originatingSessionId' = sm.session_id)`;
12
+ return (`(sm.placement_kind IS NOT NULL` +
13
+ ` OR EXISTS (SELECT 1 FROM task_active ta WHERE ta.session_id = sm.session_id)` +
14
+ ` OR EXISTS (SELECT 1 FROM checkpoint c2 WHERE c2.session_id = sm.session_id AND c2.status = 'pending')` +
15
+ ` OR EXISTS (SELECT 1 FROM background_agent ba WHERE ba.status IN ('running','parked')` +
16
+ ` AND (ba.session_id = sm.session_id OR ba.parent_session_id = sm.session_id OR ba.root_session_id = sm.session_id))` +
17
+ ` OR ${workflow})`);
18
+ }
8
19
  //# sourceMappingURL=retention-sql.js.map
@@ -3,7 +3,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
3
3
  import type { Pool as PgPool } from "pg";
4
4
  import { type UsageRow } from "../usage-analytics.js";
5
5
  import { type RunStallWiring, type ClaimFenced, type CreateRunOutcome, type FencedRun, type NewRunMeta, type RunIdemBinding, type RunListQuery, type RunRecord, type RunEvent, type PersistedTaskResult, type LatestRunRef, type SettledLegMemoryObservation } from "./store-contracts.js";
6
- import { type SqlDriver, type SqlTxConn } from "./sql-driver.js";
6
+ import { type SqlDialect, type SqlDriver, type SqlTxConn } from "./sql-driver.js";
7
7
  import type { LedgerEventType } from "../trace/ledger-events.js";
8
8
  import { type SpaceCommitClaim, type SpaceCommitReading, type SpaceCommitState } from "../workspace-space-commit.js";
9
9
  export type { RunRecord, RunEvent, LatestRunRef } from "./store-contracts.js";
@@ -12,6 +12,25 @@ export type { RunRecord, RunEvent, LatestRunRef } from "./store-contracts.js";
12
12
  * ({@link decodeResultColumn},投影须经 `jsonColumnText`);读不出 ⇒ `"unreadable"`(迁移跳过那一行、码列不动 —— 店的读腿对它照旧响亮拒)。
13
13
  */
14
14
  export declare function decodeRunResultCellM44(v: unknown, taskId: string): PersistedTaskResult | null | "unreadable";
15
+ /** 「`running` × 心跳 × 判活窗」谓词的哪一侧:`live` = 窗内心跳过(停-迁-起证据读法选的那一批),`stale` = 窗外(死亡判决落的那一批)。 */
16
+ export type RunHeartbeatSide = "live" | "stale";
17
+ /**
18
+ * 🔴 **「`running` × 心跳 × 判活窗」谓词的唯一构造**(SQL 两方言):死亡判决的那一句({@link SqlRunStore} 的 `landStallVerdicts`:收割器与两处单行落判决)
19
+ * 与停-迁-起证据读法(`stop-then-start.ts` 的 `readOlderReplicaEvidence`)同调它 —— 同一个窗界、同一处 `>=` / `<` 边界,`task_run` 心跳比较在本仓只有这一处文本
20
+ * (源码门 `test/run-liveness-single-source-gate.test.ts`)。交回 WHERE 片段与它唯一的绑定参数(窗界)。
21
+ *
22
+ * **钟**:心跳由写者在绑定参数里送它自己的进程钟(本店每一句 `updated_at = ?` 都送 `new Date()`),池把它编成库里的值(MySQL 协议:池选项 `timezone: "Z"`
23
+ * ⇒ `DATETIME(3)` 里存 UTC 墙钟,`tidb-pool.ts` 的 `tidbPoolOptions`;PG:`TIMESTAMPTZ` 存绝对时刻)。窗界取同一种钟、经同一只池编码 —— `nowMs − windowMs` 作绑定参数,
24
+ * 不用库侧的 `NOW(3)`:它按会话时区答墙钟,会话时区不是 UTC 的库上与写者存下的 UTC 墙钟差出整个时区偏移(证据读法因此读空 = 守卫静默放行)。
25
+ * `placeholder` = 这一片段在整句里的 PG 占位符序号(MySQL 协议恒 `?`)。
26
+ */
27
+ export declare function runningHeartbeatPredicate(dialect: SqlDialect, side: RunHeartbeatSide, at: {
28
+ readonly nowMs: number;
29
+ readonly windowMs: number;
30
+ }, placeholder: number): {
31
+ readonly sql: string;
32
+ readonly param: Date;
33
+ };
15
34
  export declare class SqlRunStore {
16
35
  readonly db: SqlDriver;
17
36
  private readonly stall;
@@ -70,7 +89,7 @@ export declare class SqlRunStore {
70
89
  */
71
90
  private settleDueHolderOn;
72
91
  /**
73
- * 🔴 **死亡判决的那一句**(收割器与两处单行落判决共用一只语句构造):`running ∧ updated_at < cutoff` ⇒ `failed` / `result = NULL` / {@link RUN_STALLED_ERROR} /
92
+ * 🔴 **死亡判决的那一句**(收割器与两处单行落判决共用一只语句构造;条件 = {@link runningHeartbeatPredicate} 的 `stale` 一侧,与停-迁-起证据读法同一只):`running ∧ updated_at < 窗界` ⇒ `failed` / `result = NULL` / {@link RUN_STALLED_ERROR} /
74
93
  * `error_code = run.stalled`,同一事务放认领。两种作用域:
75
94
  * · **全表**(`taskId` 缺席,收割器):放掉每条非活非停驻行的认领 + 孤儿认领(语义同修前);
76
95
  * · **单行**(`AND task_id = ?`,观察口 / 认领冲突):只有命中 1 行才 `DELETE FROM task_active WHERE task_id = ?` —— 心跳或停驻抢先赢了 CAS ⇒ 0 行 ⇒ 认领不动。
@@ -12,6 +12,7 @@ import { isDupKeyError } from "./sql-errors.js";
12
12
  import { readDeletingTokenOn } from "./session-ext-table.js";
13
13
  import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
14
14
  import { decideSpaceCommitAbandon, decideSpaceCommitClaim, fenceSpaceCommitAtTerminal, spaceCommitStateOf } from "../workspace-space-commit.js";
15
+ import { sealStoreClass } from "./store-boundary.js";
15
16
  function runRowCols(dialect) {
16
17
  return (`task_id, session_id, owner, status, ${jsonColumnText(dialect, "result")}, error, error_code, job_id, source, ` +
17
18
  "identity_source, identity_scope, identity_principal_inc, instance_id, leg_id, cancel_requested, objective_preview, created_at, updated_at");
@@ -60,6 +61,20 @@ function m44ErrorCodeReadSide(status, result, column) {
60
61
  return column;
61
62
  return persistedPlaneOf(result).errorCode ?? null;
62
63
  }
64
+ export function runningHeartbeatPredicate(dialect, side, at, placeholder) {
65
+ const p = dialect === "mysql" ? "?" : `$${placeholder}`;
66
+ const param = new Date(at.nowMs - at.windowMs);
67
+ switch (side) {
68
+ case "live":
69
+ return { sql: `status = 'running' AND updated_at >= ${p}`, param };
70
+ case "stale":
71
+ return { sql: `status = 'running' AND updated_at < ${p}`, param };
72
+ default: {
73
+ const unhandled = side;
74
+ throw new Error(`runningHeartbeatPredicate: unknown side ${String(unhandled)}`);
75
+ }
76
+ }
77
+ }
63
78
  export class SqlRunStore {
64
79
  db;
65
80
  stall;
@@ -188,7 +203,7 @@ export class SqlRunStore {
188
203
  let landed = 0;
189
204
  try {
190
205
  await this.txOn(conn, async (c) => {
191
- landed = await this.landStallVerdicts(c, new Date(nowMs - windowMs), holderTaskId);
206
+ landed = await this.landStallVerdicts(c, { nowMs, windowMs }, holderTaskId);
192
207
  });
193
208
  }
194
209
  catch (err) {
@@ -199,11 +214,12 @@ export class SqlRunStore {
199
214
  this.stall.onVerdicts(landed);
200
215
  return landed > 0;
201
216
  }
202
- async landStallVerdicts(conn, cutoff, taskId) {
217
+ async landStallVerdicts(conn, at, taskId) {
203
218
  const one = taskId !== undefined;
219
+ const stale = { mysql: runningHeartbeatPredicate("mysql", "stale", at, 2), pg: runningHeartbeatPredicate("pg", "stale", at, 2) };
204
220
  const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = ? ` +
205
- `WHERE status = 'running' AND updated_at < ?${one ? " AND task_id = ?" : ""}`, `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = $1 ` +
206
- `WHERE status = 'running' AND updated_at < $2${one ? " AND task_id = $3" : ""}`), one ? [new Date(), cutoff, taskId] : [new Date(), cutoff]);
221
+ `WHERE ${stale.mysql.sql}${one ? " AND task_id = ?" : ""}`, `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = $1 ` +
222
+ `WHERE ${stale.pg.sql}${one ? " AND task_id = $3" : ""}`), one ? [new Date(), stale[this.db.dialect].param, taskId] : [new Date(), stale[this.db.dialect].param]);
207
223
  if (one) {
208
224
  if (affected === 1)
209
225
  await conn.query(this.q("DELETE FROM task_active WHERE task_id = ?", "DELETE FROM task_active WHERE task_id = $1"), [taskId]);
@@ -221,7 +237,7 @@ export class SqlRunStore {
221
237
  let landed = 0;
222
238
  try {
223
239
  await this.tx(async (conn) => {
224
- landed = await this.landStallVerdicts(conn, new Date(nowMs - windowMs), row.taskId);
240
+ landed = await this.landStallVerdicts(conn, { nowMs, windowMs }, row.taskId);
225
241
  });
226
242
  }
227
243
  catch (err) {
@@ -596,10 +612,10 @@ export class SqlRunStore {
596
612
  }));
597
613
  }
598
614
  async reapStale() {
599
- const cutoff = new Date(Date.now() - this.stall.windowMs());
615
+ const at = { nowMs: Date.now(), windowMs: this.stall.windowMs() };
600
616
  let reaped = 0;
601
617
  await this.tx(async (conn) => {
602
- reaped = await this.landStallVerdicts(conn, cutoff);
618
+ reaped = await this.landStallVerdicts(conn, at);
603
619
  });
604
620
  if (reaped > 0)
605
621
  this.stall.onVerdicts(reaped);
@@ -635,6 +651,14 @@ export class SqlRunStore {
635
651
  return reaped;
636
652
  }
637
653
  }
654
+ sealStoreClass(SqlRunStore, "sql-run-store", {
655
+ createRun: "exit", observeRun: "exit", fillObjectivePreview: "exit", requestCancel: "exit", isCancelRequested: "exit", requestPreempt: "exit",
656
+ isPreemptRequested: "exit", heartbeat: "exit", appendEvent: "exit", maxSeq: "exit", setTerminal: "exit", readSpaceCommit: "exit", claimSpaceCommit: "exit",
657
+ endSpaceCommit: "exit", abandonSpaceCommit: "exit", setSuspended: "exit", setNeedsReview: "exit", markResuming: "exit", whileHoldingClaim: "exit",
658
+ getActiveTaskId: "exit", getRun: "exit", getRunByIdem: "exit", releaseIdemKey: "exit", withdrawMintedRun: "exit", bindIdemBodyFingerprint: "exit",
659
+ listRuns: "exit", latestRunForSession: "exit", latestMemoryObservationForSession: "exit", deleteBySession: "exit", sourceSummary: "exit", usageScan: "exit",
660
+ retainedFrom: "exit", getEvents: "exit", reapStale: "exit", reapSuspended: "exit", failSuspendedWithExpiredCheckpoint: "exit",
661
+ });
638
662
  export class TiDBRunStore extends SqlRunStore {
639
663
  constructor(pool, stall) {
640
664
  super(mysqlDriver(pool), stall);
@@ -24,7 +24,7 @@ export type ScenarioPrefixRefusalReason = "stored_prefix" | "older_replica";
24
24
  /**
25
25
  * M42 的起服拒。具名类型 + 机读码 + 拒因;继承 {@link StoreBootRefusalError}(SQL 开库的等待窗不重试它;隐式 local 的降级臂不吞)。一个字节没改(扫描只读,标记没写)。
26
26
  * · `stored_prefix` —— 存量里有以保留前缀开头的身份(`hits` 逐来源点名);
27
- * · `older_replica` —— 零命中,但有停-迁-起证据:更早版本的副本可能还在服务,扫描之后它还会收下这类身份 ⇒ 全停之后再起(`live` = 证据,带时刻)。
27
+ * · `older_replica` —— 标记不在,而有停-迁-起证据(守卫段在扫描之前判):更早版本的副本可能还在服务,扫描之后它还会收下这类身份 ⇒ 全停之后再起(`live` = 证据,带时刻)。
28
28
  */
29
29
  export declare class ScenarioPrefixSwitchRefusedError extends StoreBootRefusalError {
30
30
  readonly reason: ScenarioPrefixRefusalReason;
@@ -31,9 +31,9 @@ export class ScenarioPrefixSwitchRefusedError extends StoreBootRefusalError {
31
31
  case "older_replica":
32
32
  if (live === undefined)
33
33
  throw new Error("scenario-prefix-switch: an older_replica refusal carries its evidence");
34
- return (`${SCENARIO_PREFIX_SWITCH_REFUSED_CODE}: no stored identity starts with the reserved prefix "${SCENARIO_SUBJECT_PREFIX}", but ${olderReplicaEvidenceSentence(live)} ` +
34
+ return (`${SCENARIO_PREFIX_SWITCH_REFUSED_CODE}: this version reserves the prefix "${SCENARIO_SUBJECT_PREFIX}" and checks stored identities for it once before recording a marker, but ${olderReplicaEvidenceSentence(live)} ` +
35
35
  `— a replica of an earlier version may still be serving, and it accepts principals with that prefix, so a clean check now would not stay clean. ` +
36
- `Nothing was changed and no marker was written. ${stopThenStartAction(live.after)}`);
36
+ `Nothing was scanned or changed and no marker was written. ${stopThenStartAction(live.after)}`);
37
37
  default: {
38
38
  const unhandled = reason;
39
39
  throw new Error(`scenario-prefix-switch: unknown refusal reason ${String(unhandled)}`);
@@ -185,8 +185,10 @@ export declare function describeMismatchedTables(dialect: SqlDialect, mismatches
185
185
  /**
186
186
  * **升级清单**(多行;`[]` = 没有语句要部署者执行,清单不出):一次起服的拒句里「这只库此刻对本构建还差什么、先后顺序」一段。
187
187
  * 缺列拒、判活拒、一次性迁移的拒本来各报各的一段 —— 运维补一句再起、才撞上下一族(列册对账在迁移段之后时,第四次起服才拒出其余全部 ALTER)。
188
- * 清单把本构建对这一只库的全部动作写成一张:① 逐表的 ALTER(其下 {@link describeMismatchedTables} 的逐表段,全库一次列全)② 这只库**还欠**的停-迁-起迁移
189
- * ③ 起服。第 ② 步逐行问登记行的 `stopThenStart.owedOn`(一次性迁移文件静态依赖本文件,所以惰性载入)—— 那条迁移的守卫自己问的同一只判据,清单不另判:
188
+ * 清单把本构建对这一只库的全部动作写成一张:⓪ 这只库要求、本构建没有的能力(起服要求表 `serving_requirement` 里本构建能力名单之外的行;有才出,排在一切语句之前 ——
189
+ * 照清单补完语句也不会让本构建能在这里服务:库形对账与「谁可以服务」是两个事实,补列是在把那条迁移改掉的形补回去)① 逐表的 ALTER(其下 {@link describeMismatchedTables} 的逐表段,
190
+ * 全库一次列全)② 这只库**还欠**的停-迁-起迁移 ③ 起服。第 ⓪ 步与准入同一只读法(`serving-requirement.ts` 的 `unmetServingRequirements`);要求表不在 ⇒ 不出(那是准入的 `table_absent`,
191
+ * 清单不判);读抛 ⇒ 照出并如实说没读到。第 ② 步逐行问登记行的 `stopThenStart.owedOn`(一次性迁移文件静态依赖本文件,所以惰性载入)—— 那条迁移的守卫段自己问的同一只判据,清单不另判:
190
192
  * 已经走过它们的库(标记落了 / 部署格已搬 / 零行旧词)一条不欠 ⇒ 第 ② 步与它的「不可滚动」动作段都不出,起服是第 ② 步。某一条读不出欠不欠(探针读抛)⇒ 照列并如实说
191
193
  * 没读到(多停一次,不漏停)。`read` = 列册对账读列册的同一只无参读口。
192
194
  */
@@ -2,7 +2,9 @@ import { recordEnsureStatements } from "./schema-ensure-legs.js";
2
2
  import { readLiveColumns } from "./schema-live-columns.js";
3
3
  import { SCHEMA_MISMATCH_WAYS_OUT } from "./schema-drift-sentence.js";
4
4
  import { SCHEMA_MISMATCH_ERROR_CODE, SchemaMismatchRefusedError, StoreBootRefusalError } from "./store-contracts.js";
5
- import { stopThenStartAction } from "./stop-then-start-sentence.js";
5
+ import { stopThenStartAction, unmetRequirementChecklistStep } from "./stop-then-start-sentence.js";
6
+ import { SERVING_REQUIREMENT_TABLE, unmetServingRequirements } from "./serving-requirement.js";
7
+ import { REPLICA_CAPABILITIES } from "./replica-registration.js";
6
8
  const QUOTES = new Set(["'", '"', "`"]);
7
9
  function stripComments(sql) {
8
10
  let out = "";
@@ -469,7 +471,11 @@ export async function describeUpgradeChecklist(read, dialect, mismatches) {
469
471
  if (owes)
470
472
  owed.push(` - ${m.id}: ${m.reason}`);
471
473
  }
472
- const steps = [["Run every statement printed below, each recipe in the order it is printed (a printed comment line marks a value only you can choose) — all tables at once, before anything else (this build never alters a database for you)."]];
474
+ const steps = [];
475
+ const requirements = await unmetRequirementsForChecklist(read, dialect);
476
+ if (requirements !== undefined)
477
+ steps.push(requirements);
478
+ steps.push(["Run every statement printed below, each recipe in the order it is printed (a printed comment line marks a value only you can choose) — all tables at once, before anything else (this build never alters a database for you)."]);
473
479
  if (owed.length > 0) {
474
480
  steps.push([
475
481
  "Stop-then-start one-time migrations this database still owes: this build runs them itself on its first boot (no statement from you), and each refuses to start — writing nothing — while a run on this database has",
@@ -484,6 +490,17 @@ export async function describeUpgradeChecklist(read, dialect, mismatches) {
484
490
  ...steps.flatMap((lines, i) => lines.map((l, k) => (k === 0 ? ` ${i + 1}. ${l}` : ` ${l}`))),
485
491
  ];
486
492
  }
493
+ async function unmetRequirementsForChecklist(read, dialect) {
494
+ try {
495
+ if (!(await readLiveColumns(read, dialect)).has(SERVING_REQUIREMENT_TABLE))
496
+ return undefined;
497
+ const unmet = await unmetServingRequirements(read, { capabilities: REPLICA_CAPABILITIES });
498
+ return unmet.length === 0 ? undefined : unmetRequirementChecklistStep(unmet);
499
+ }
500
+ catch (err) {
501
+ return unmetRequirementChecklistStep([], err instanceof Error ? err.message : String(err));
502
+ }
503
+ }
487
504
  export function describeSchemaMismatch(dialect, mismatches, checklist = []) {
488
505
  const lines = [
489
506
  `${SCHEMA_MISMATCH_ERROR_CODE}: the ${dialect === "pg" ? "PostgreSQL" : "MySQL-protocol"} database does not match this build's schema — refusing to start.`,
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { AppendLog, ensureDir, uuidv7 } from "@sema-agent/core";
3
3
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
4
+ import { sealStoreClass } from "./store-boundary.js";
4
5
  export const SENDFILE_SINGLE_USER_SCOPE = "_";
5
6
  export class TiDBSendFileLedger {
6
7
  pool;
@@ -17,6 +18,7 @@ export class TiDBSendFileLedger {
17
18
  return rows.map((r) => rowToEntry(r));
18
19
  }
19
20
  }
21
+ sealStoreClass(TiDBSendFileLedger, "tidb-send-file-ledger", { record: "exit", listByScope: "exit" });
20
22
  export class PgSendFileLedger {
21
23
  pool;
22
24
  constructor(pool) {
@@ -32,6 +34,7 @@ export class PgSendFileLedger {
32
34
  return res.rows.map(rowToEntry);
33
35
  }
34
36
  }
37
+ sealStoreClass(PgSendFileLedger, "pg-send-file-ledger", { record: "exit", listByScope: "exit" });
35
38
  function rowToEntry(r) {
36
39
  return {
37
40
  id: String(r.id),