@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,6 +1,6 @@
1
- import { existsSync, realpathSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
3
- import { FileMemoryEngineBackend, readMemoryLedgerForm, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope, normalizeMemorySpec } from "@sema-agent/core";
3
+ import { FileMemoryEngineBackend, physicalPathOf, readMemoryLedgerForm, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope, normalizeMemorySpec } from "@sema-agent/core";
4
4
  import { capabilitiesFor, executionLaneOf } from "./execution-lane-caps.js";
5
5
  import { errorCodeOf, errorKindOf } from "./observability/err-text.js";
6
6
  import { assertMemoryScopeWidth } from "./plugins/memory-key-guards.js";
@@ -113,7 +113,7 @@ export function fileMemoryEngineRoot(config, fallbackRoot) {
113
113
  }
114
114
  export function canonicalPathOf(p) {
115
115
  if (isAbsolute(p) && existsSync(p))
116
- return realpathSync.native(p);
116
+ return physicalPathOf(p);
117
117
  const abs = isAbsolute(p) ? p : `${process.cwd()}${sep}${p}`;
118
118
  const { root } = parse(abs);
119
119
  let existing = root;
@@ -134,7 +134,7 @@ export function canonicalPathOf(p) {
134
134
  }
135
135
  const next = join(existing, seg);
136
136
  if (existsSync(next))
137
- existing = realpathSync.native(next);
137
+ existing = physicalPathOf(next);
138
138
  else
139
139
  tail.push(seg);
140
140
  }
@@ -1,4 +1,4 @@
1
- import { type memorySyncReportOf, type MemorySyncTransport } from "@sema-agent/core";
1
+ import { type memorySyncReportOf, type MemorySyncLayerReport, type MemorySyncTransport } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "./config-types.js";
3
3
  import type { MemoryScopeStores } from "./memory-scope-stores.js";
4
4
  import { type OneTimeMigrationWindow } from "./plugins/one-time-migrations.js";
@@ -48,10 +48,37 @@ export declare function memorySyncCursorDirFor(memoryEngineDir: string): string;
48
48
  * 加一个词 ⇒ {@link wayOutSentence} 的穷尽 switch 编译红(句子先写好,词才上得了 wire)。
49
49
  */
50
50
  export type MemorySyncWayOut = "delete_cursor";
51
- /** ⑦ 一层停驻的读数(读面与运维行同读这一只记录;不带盘路径 —— 路径只进运维行)。 */
51
+ /** 引擎游标拒的 `detail`(轴 + 游标在那一轴上是缺席还是别的值);型取自引擎的同步报告,本仓不另写一份。 */
52
+ type CursorRefusalDetail = NonNullable<Extract<MemorySyncLayerReport, {
53
+ readonly outcome: "refused";
54
+ }>["detail"]>;
55
+ /** 一层为什么停驻(铸记录的输入):`kind` 与引擎读数经记录上 wire,`why` 句只进运维行的 `note`(不在记录里)。 */
56
+ type ParkCause = {
57
+ readonly kind: "cursor_refused";
58
+ readonly detail: CursorRefusalDetail | undefined;
59
+ } | {
60
+ readonly kind: "cursor_unusable";
61
+ readonly why: string;
62
+ };
63
+ /**
64
+ * ⑦ 停驻成因的闭集(词 = {@link ParkCause} 的 `kind`):`cursor_refused` = 同步引擎在游标上拒了这一层(轴与读数见 `axis` / `found`);`cursor_unusable` = 游标文件读不成游标。
65
+ * 与运维行的 `reason`(本轮的触发原因)是两回事 —— 本词答「为什么停」,不答「哪一轮撞上它」。
66
+ */
67
+ export type MemorySyncParkCause = ParkCause["kind"];
68
+ /** 引擎游标拒读数的两个闭集(型取自引擎的同步报告,本仓不另写一份):被拒的是哪一轴;游标在那一轴上是缺席(`absent`)还是绑着别的值(`other`)。 */
69
+ export type MemorySyncCursorAxis = CursorRefusalDetail["axis"];
70
+ export type MemorySyncCursorFound = CursorRefusalDetail["found"];
71
+ /**
72
+ * ⑦ 一层停驻的记录(读面与运维行同读这一只;**整条**就是 `memory-status.memorySync.parked[]` 的条目,所以它不带盘路径 —— 路径只进运维行)。
73
+ * 在场律:`cause` 恒在;`axis` / `found` ⇔ 成因是 `cursor_refused` 且引擎的读数在闭集内(别的成因、读数不在闭集 ⇒ 两键缺席,不是 `undefined` 值)。
74
+ * 加一个键 ⇒ 它就上 wire:自铸登记表的键集(`memorySyncParkedKey`)按本型 `satisfies`,漏登是编译红。唯一铸点 = {@link parkedLayerOf}。
75
+ */
52
76
  export interface MemorySyncParkedLayer {
53
77
  readonly scope: string;
54
78
  readonly wayOut: MemorySyncWayOut;
79
+ readonly cause: MemorySyncParkCause;
80
+ readonly axis?: MemorySyncCursorAxis;
81
+ readonly found?: MemorySyncCursorFound;
55
82
  }
56
83
  /** M31 一次的读数(测试按它断言)。`window: "closed"` ⇒ 一行都没读。 */
57
84
  export interface SyncCursorRelocationM31Report {
@@ -134,4 +161,5 @@ export declare function startMemorySyncRunner(deps: {
134
161
  stores: MemoryScopeStores;
135
162
  log: MemorySyncLog;
136
163
  }): MemorySyncRunner;
164
+ export {};
137
165
  //# sourceMappingURL=memory-sync-client.d.ts.map
@@ -55,6 +55,18 @@ export function memorySyncCursorDirFor(memoryEngineDir) {
55
55
  }
56
56
  const CURSOR_AXES = { scope: true, peer: true, store: true, incarnation: true };
57
57
  const CURSOR_FOUND = { absent: true, other: true };
58
+ function parkedLayerOf(scope, cause, wayOut) {
59
+ switch (cause.kind) {
60
+ case "cursor_refused":
61
+ return Object.freeze({ scope, wayOut, cause: cause.kind, ...(cause.detail !== undefined ? { axis: cause.detail.axis, found: cause.detail.found } : {}) });
62
+ case "cursor_unusable":
63
+ return Object.freeze({ scope, wayOut, cause: cause.kind });
64
+ default: {
65
+ const unhandled = cause;
66
+ throw new Error(`memory sync: unknown park cause ${JSON.stringify(unhandled)}`);
67
+ }
68
+ }
69
+ }
58
70
  function wayOutSentence(wayOut, cursorPath) {
59
71
  switch (wayOut) {
60
72
  case "delete_cursor":
@@ -81,16 +93,12 @@ function parkCauseSentence(cause) {
81
93
  }
82
94
  }
83
95
  class MemorySyncLayerParked extends Error {
84
- scope;
85
96
  cursorPath;
86
- parkCause;
87
- wayOut;
97
+ layer;
88
98
  constructor(scope, cursorPath, parkCause, wayOut = "delete_cursor") {
89
99
  super(`memory sync: layer ${JSON.stringify(scope)} parked — ${parkCauseSentence(parkCause)}; ${wayOutSentence(wayOut, cursorPath)}`);
90
- this.scope = scope;
91
100
  this.cursorPath = cursorPath;
92
- this.parkCause = parkCause;
93
- this.wayOut = wayOut;
101
+ this.layer = parkedLayerOf(scope, parkCause, wayOut);
94
102
  }
95
103
  }
96
104
  function cursorRefusalOf(err) {
@@ -354,8 +362,7 @@ export function createMemorySyncRunner(opts) {
354
362
  let parkedLayers = [];
355
363
  const logLayerFailure = (reason, scope, err) => {
356
364
  if (err instanceof MemorySyncLayerParked) {
357
- const refused = err.parkCause.kind === "cursor_refused" ? err.parkCause.detail : undefined;
358
- opts.log.warn("memory_sync_layer_parked", { reason, scope, cursorPath: err.cursorPath, ...(refused !== undefined ? { axis: refused.axis, found: refused.found } : {}), wayOut: err.wayOut, note: err.message });
365
+ opts.log.warn("memory_sync_layer_parked", { reason, cursorPath: err.cursorPath, ...err.layer, note: err.message });
359
366
  }
360
367
  else {
361
368
  opts.log.warn("memory_sync_layer_failed", { reason, scope, cursorPath: memorySyncCursorPath(memoryEngineDir, scope), err: errorText(err) });
@@ -386,7 +393,7 @@ export function createMemorySyncRunner(opts) {
386
393
  catch (err) {
387
394
  calls.push({ scope, thrown: err });
388
395
  if (err instanceof MemorySyncLayerParked)
389
- parkedNow.push({ scope, wayOut: err.wayOut });
396
+ parkedNow.push(err.layer);
390
397
  logLayerFailure(reason, scope, err);
391
398
  }
392
399
  }
@@ -14,4 +14,11 @@ export declare function errorKindOf(err: unknown): string;
14
14
  * 不是串 ⇒ `undefined`(没有码,不编一个)。调用方按自己的形判(什么样的码配进一句拒句)再用它。
15
15
  */
16
16
  export declare function errorCodeOf(err: unknown): string | undefined;
17
+ /**
18
+ * 不可信对象上**一个成员**的读口(店边界读 `code` / `errno` / `cause` / `message` 的唯一一只;同一处兜底 {@link readOr}):不是对象 ⇒ 缺席;访问器 / 代理抛 ⇒ 缺席(`undefined`)。
19
+ * 三问(缺席倒向):错误落到哪 —— 成员被当作不在,不单独记(这一读不是任何一条路的判词);谁受害 —— 没有真实受害者(驱动错误是平面数据对象),只有 getter 会抛的对象读不出码 / 链 / 句;
20
+ * 缺席倒向 —— 码读不出 ⇒ 没有码扳机、`cause` 读不出 ⇒ 链在此处止步,而**驱动出身扳机按对象身份判、不读任何成员**(`plugins/error-origin.ts`),链深扳机数的是层数 —— 所以带驱动记号的错照样**触发**换形;
21
+ * 换形本身读 `code` 判类时,成员 getter 会抛的对象仍可能在那一步抛出(店边界既有的限制,不是本读口引入的;真实驱动错误是平面数据对象,不在射程内)。
22
+ */
23
+ export declare function errorMemberOf(o: unknown, key: string): unknown;
17
24
  //# sourceMappingURL=err-text.d.ts.map
@@ -30,4 +30,9 @@ export function errorCodeOf(err) {
30
30
  }, "");
31
31
  return code === "" ? undefined : code;
32
32
  }
33
+ export function errorMemberOf(o, key) {
34
+ if (o === null || typeof o !== "object")
35
+ return undefined;
36
+ return readOr(() => o[key], undefined);
37
+ }
33
38
  //# sourceMappingURL=err-text.js.map
@@ -55,13 +55,13 @@ export declare const FAIL_OPEN_TAGS: {
55
55
  readonly cls: "F";
56
56
  readonly note: "S-1173(`workflow-notify-journal.ts` 的 `ensurePointersFor`,park 指针的唯一写点):workflow run 记录的 `parks[]` **读不出**(在场却不是列表 / 某条目身份位或持有者两位形坏;读者 = `workflow-agent-session-index.ts` 的 `readRunParks`,整档成败制)⇒ 这一份指针投影派生不出来 ⇒ 写者这次**不写**指针,记录照写(已写过的指针只增不删,原样留着)。修前这一臂静默 `return`。**`parks` 缺席不进这一臂**:那是引擎契约里的合法形(更早引擎写的记录 / 准入没走完的续跑记录),读者答 `[]`(没有条目),没有要写的指针,不计。与指针**写**失败分家:写失败 ⇒ 记录写抛、记录不落(指针先于记录,不 fail-open);本臂没有丢任何一枚已写的指针 —— 读不出的是**记录本身**,引擎对条目形坏的记录整条拒续跑(`park_binding_broken`),读口对已有指针的 run 据记录响亮拒(`unproven/unreadable`),所以要的是查那条记录为什么读不出(投影把它写坏 / 手改)。放行的最坏后果:这条 run 首写就读不出时没有指针 ⇒ 这张卡没有别的指针时 `/decide` 落 legacy 腿、409 响亮拒,方向 fail-closed,不误配、不放行任何东西;例外 —— 同一张卡上另一支的指针在时分叉门判不出、决定投给那一支(引擎从不写坏形,要这一支在首写之前就被写坏);derived cache 没更新、真源读口自带响亮拒 ⇒ F 类。detail = `runId=`;计数非零 = 有记录的 `parks[]` 读不出,按 runId 去查记录。";
57
57
  };
58
- readonly "server.workflow-live-receipt.channel-unreadable": {
58
+ readonly "server.workflow-park-holder.read-failed": {
59
59
  readonly cls: "F";
60
- readonly note: "活 run 收下 workflow 停驻卡的决定之后,本进程回执簿(`parked-decide.ts` 的 `holdLiveReceipt`)等那条 run 的引擎观察通道收场来删簿;等的那一读拒 ⇒ 按收场删掉这条 run 的回执。放行的最坏后果:那条 run 仍在跑、驱动还在等并发槽时,同一决定重点一次答 409 `decide.claim_lost` 而不是首张回执(卡此刻仍 pending,重读列表再决);不重复执行、不放宽任何判定。";
60
+ readonly note: "workflow 停驻卡持有进程的拉取腿(`orchestration/workflow-park-holder.ts`,维护拍上读本进程执行中、持有停驻卡的 run 的决定行,把立住的决定在本地交给引擎)这一拍对某条候选读不出:决定行的主键读抛或超过期限(`step=decision`)、卡 / 持有者那一读超过期限(`step=card`),或执行器视图等那条 run 的引擎观察通道收场的那一读拒(`step=channel`,core 的观察队列只关不拒,结构上不可达)。放行的最坏后果:这条决定这一拍不交,行留着、卡仍 pending —— 下一拍再来;`channel` 那一形这条 run 出视图,它的行改由 run 的收场送达交给宿主会话的下一跑(交付晚到 run 收场,不丢、不错交)。不放宽任何判定:交不交、交给谁仍由行 + 卡状态 + 引擎判。detail = `step=<decision|card|channel> runId=<持有者> err=<原句,经记录器脱敏>`;计数持续非零 = 决定店 / 检查点店对这几条读不出,按 runId 查。";
61
61
  };
62
62
  readonly "server.workflow-park-decision.card-unreadable": {
63
63
  readonly cls: "F";
64
- readonly note: "起跑读者(`parked-decide.ts` 的 `heldWorkflowParkDecisions`,每一只起跑器开跑前一次)读某条已受理的 workflow 子代决定所属的那张卡时 checkpoint 店抛 ⇒ 这一跑**不带**这条决定,行留着给下一跑。放行的最坏后果:这一跑里宿主若续跑那条 workflow run,子代按同一个 token 再停驻一次(卡仍 pending,决定仍在,下一跑照带)—— 方向是少交付一次,不是错交付:这一读失败不代表引擎随后那一读也失败,而引擎对「卡已不 pending、却拿到一条决定」的序数会当确认结算,所以读不出时绝不照供。计数持续非零 = checkpoint 店对这张卡读不出(库故障 / 行坏 / 写者版本更新),按 detail 的 `runId=`(受理那一刻的持有者)去查。detail = `runId=<留存锚> err=<原句,经记录器脱敏>`。";
64
+ readonly note: "交付决定行的三方(起跑读者 `heldWorkflowParkDecisions`,每一只起跑器开跑前一次;workflow 停驻卡持有进程的拉取腿;run 收场送达)共用的逐行判(`parked-decide.ts` 的 `deliverableEntryOf`)读某条已受理的 workflow 子代决定所属的那张卡(`step=card`)或那张卡的当下持有者(`step=holder`:park 索引 / 记录读)时店抛 ⇒ 这一次**不交付**这条决定,行留着(起跑读者:这一跑不带它,下一跑照带;拉取腿:下一拍再来;收场送达:不 ack、`recover` 重投)。放行的最坏后果:这一跑里宿主若续跑那条 workflow run,子代按同一个 token 再停驻一次(卡仍 pending,决定仍在,下一跑照带)—— 方向是少交付一次,不是错交付:这一读失败不代表引擎随后那一读也失败,而引擎对「卡已不 pending、却拿到一条决定」的序数会当确认结算,所以读不出时绝不照供。计数持续非零 = checkpoint 店 / park 索引 / 记录店对这张卡读不出(库故障 / 行坏 / 写者版本更新),按 detail 的 `runId=`(受理那一刻的持有者)去查。detail = `step=<card|holder> runId=<留存锚> err=<原句,经记录器脱敏>`。";
65
65
  };
66
66
  readonly "server.workflow-park-decision.store-unreadable": {
67
67
  readonly cls: "F";
@@ -351,6 +351,10 @@ export declare const FAIL_OPEN_TAGS: {
351
351
  readonly cls: "F";
352
352
  readonly note: "#329 / #343(黑板 [5100] Q3):停驻行迟到决议的**受理回执表**(本副本对「200 丢包后重试」的幂等承诺 + 重叠请求的单飞句柄;键 = 行的键 askId,只在一次赎回真开飞时开格;容量帽只数受理之后留下的回执,在飞的单飞句柄永不被逐)**拒留一张回执**。放行的最坏后果 = 这**一次**受理(照旧生效)没有本副本的回执可依:200 丢包后的重试会再进一次赎回腿、拿到那条腿的 409(那张 checkpoint 已被消费)而不是幂等 200 回放(终局恒由赎回腿的 gate CAS 定,不会错兑也不会双兑);迟到受理本身、`GET /v1/approvals` 列表与 `/v1/approvals/:sessionId/decide` 一个字节不动 ⇒ F 类。为什么方向是「拒新」而不是 FIFO 挤最旧:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产受理即可把别家回执挤出,merge-rescan 744 confirmed 的同一病形)。必须留痕:「表被别人打满」与「这一次本来就没有回执」在 wire 上同形。detail 带 owner(判「压力来自谁」的第一手线索)。臂只有一条:全局回执帽满、本 principal 未到子帽、表里没有过期回执可收 ⇒ 拒留(detail 带 owner)。本 principal 到了子帽的常态臂逐自己最旧的一张回执(量产者自伤,设计内有界表纪律),不走本 tag;在飞的单飞句柄不占帽、永不被逐。";
353
353
  };
354
+ readonly "server.approval.parked-decision-not-recorded": {
355
+ readonly cls: "F";
356
+ readonly note: "S-1054 α(`tool-approval.ts` 的 `recordParkedOutcome`,停驻赎回那一飞的落行步):停驻门**已经受理**了这次决定(续跑已开始),而把它的记录(决议向 · 署名 · 理由 · 回决幂等键 · 出处)落到仍 `PARKED` 的问行上没成 —— 店错 / 超时 / 行出了门判过的属主域 / 这把键已被同 task 另一只问占着(detail 带 `reason`)。放行的最坏后果 = 这一问在耐久决断口上的**同键回放**读不到这把键:同键重发得到 `409 conflict.ask_decided`(回显门上站着的那次决定,决议向 / 时刻 / 理由取自门,无署名),而不是 200 回放;审计面少一份署名。决定本身零影响(终局恒在停驻门上,门已决);不改判、不重赎、不双兑。计数非零 = 问账店在受理之后写不动了,运维要去看店。";
357
+ };
354
358
  readonly "server.approvals.stale-echoed-token-unreadable": {
355
359
  readonly cls: "F";
356
360
  readonly note: "S-02 codex R1-[medium](验真后修):decide 的 409 `approval_stale` 臂对**调用方回显的旧 token** 做归属判定(`cs.get(binding.checkpointToken)`),那次读**抛了**(store 抖动 / 坏 checkpoint 行 / 行恰被清理)。修前这里是静默 `catch(()=>null)`,「读不出」被折成「确认不在库」——叠上 S-02 的 404 收口后,一次 store 抖动就会把合法旧 token 的 409+terminal 谎报成 404「没有这条审批」。修后:unreadable ⇒ **保持 409**(`terminal` 不披露——证不出归属就不说,与 2026-07-26 纪律同向;404 折叠只在读成功且未命中时走,探测方无法自控 store 故障 ⇒ 谕示封闭不破)。放行的最坏后果 = 该次拒体 `terminal` 诚实缺席(`currentPending` 由它自己的读口与属主复核另判,不受本臂牵连),壳退回重拉列表,judgment 面 fail-safe,故 F 类。留痕理由:这次读是 terminal 披露与 404 折叠两个判定的输入,长期抖动必须在遥测显形。";
@@ -36,13 +36,13 @@ export const FAIL_OPEN_TAGS = {
36
36
  cls: "F",
37
37
  note: "S-1173(`workflow-notify-journal.ts` 的 `ensurePointersFor`,park 指针的唯一写点):workflow run 记录的 `parks[]` **读不出**(在场却不是列表 / 某条目身份位或持有者两位形坏;读者 = `workflow-agent-session-index.ts` 的 `readRunParks`,整档成败制)⇒ 这一份指针投影派生不出来 ⇒ 写者这次**不写**指针,记录照写(已写过的指针只增不删,原样留着)。修前这一臂静默 `return`。**`parks` 缺席不进这一臂**:那是引擎契约里的合法形(更早引擎写的记录 / 准入没走完的续跑记录),读者答 `[]`(没有条目),没有要写的指针,不计。与指针**写**失败分家:写失败 ⇒ 记录写抛、记录不落(指针先于记录,不 fail-open);本臂没有丢任何一枚已写的指针 —— 读不出的是**记录本身**,引擎对条目形坏的记录整条拒续跑(`park_binding_broken`),读口对已有指针的 run 据记录响亮拒(`unproven/unreadable`),所以要的是查那条记录为什么读不出(投影把它写坏 / 手改)。放行的最坏后果:这条 run 首写就读不出时没有指针 ⇒ 这张卡没有别的指针时 `/decide` 落 legacy 腿、409 响亮拒,方向 fail-closed,不误配、不放行任何东西;例外 —— 同一张卡上另一支的指针在时分叉门判不出、决定投给那一支(引擎从不写坏形,要这一支在首写之前就被写坏);derived cache 没更新、真源读口自带响亮拒 ⇒ F 类。detail = `runId=`;计数非零 = 有记录的 `parks[]` 读不出,按 runId 去查记录。",
38
38
  },
39
- "server.workflow-live-receipt.channel-unreadable": {
39
+ "server.workflow-park-holder.read-failed": {
40
40
  cls: "F",
41
- note: "活 run 收下 workflow 停驻卡的决定之后,本进程回执簿(`parked-decide.ts` 的 `holdLiveReceipt`)等那条 run 的引擎观察通道收场来删簿;等的那一读拒 ⇒ 按收场删掉这条 run 的回执。放行的最坏后果:那条 run 仍在跑、驱动还在等并发槽时,同一决定重点一次答 409 `decide.claim_lost` 而不是首张回执(卡此刻仍 pending,重读列表再决);不重复执行、不放宽任何判定。",
41
+ note: "workflow 停驻卡持有进程的拉取腿(`orchestration/workflow-park-holder.ts`,维护拍上读本进程执行中、持有停驻卡的 run 的决定行,把立住的决定在本地交给引擎)这一拍对某条候选读不出:决定行的主键读抛或超过期限(`step=decision`)、卡 / 持有者那一读超过期限(`step=card`),或执行器视图等那条 run 的引擎观察通道收场的那一读拒(`step=channel`,core 的观察队列只关不拒,结构上不可达)。放行的最坏后果:这条决定这一拍不交,行留着、卡仍 pending —— 下一拍再来;`channel` 那一形这条 run 出视图,它的行改由 run 的收场送达交给宿主会话的下一跑(交付晚到 run 收场,不丢、不错交)。不放宽任何判定:交不交、交给谁仍由行 + 卡状态 + 引擎判。detail = `step=<decision|card|channel> runId=<持有者> err=<原句,经记录器脱敏>`;计数持续非零 = 决定店 / 检查点店对这几条读不出,按 runId 查。",
42
42
  },
43
43
  "server.workflow-park-decision.card-unreadable": {
44
44
  cls: "F",
45
- note: "起跑读者(`parked-decide.ts` 的 `heldWorkflowParkDecisions`,每一只起跑器开跑前一次)读某条已受理的 workflow 子代决定所属的那张卡时 checkpoint 店抛 ⇒ 这一跑**不带**这条决定,行留着给下一跑。放行的最坏后果:这一跑里宿主若续跑那条 workflow run,子代按同一个 token 再停驻一次(卡仍 pending,决定仍在,下一跑照带)—— 方向是少交付一次,不是错交付:这一读失败不代表引擎随后那一读也失败,而引擎对「卡已不 pending、却拿到一条决定」的序数会当确认结算,所以读不出时绝不照供。计数持续非零 = checkpoint 店对这张卡读不出(库故障 / 行坏 / 写者版本更新),按 detail 的 `runId=`(受理那一刻的持有者)去查。detail = `runId=<留存锚> err=<原句,经记录器脱敏>`。",
45
+ note: "交付决定行的三方(起跑读者 `heldWorkflowParkDecisions`,每一只起跑器开跑前一次;workflow 停驻卡持有进程的拉取腿;run 收场送达)共用的逐行判(`parked-decide.ts` 的 `deliverableEntryOf`)读某条已受理的 workflow 子代决定所属的那张卡(`step=card`)或那张卡的当下持有者(`step=holder`:park 索引 / 记录读)时店抛 ⇒ 这一次**不交付**这条决定,行留着(起跑读者:这一跑不带它,下一跑照带;拉取腿:下一拍再来;收场送达:不 ack、`recover` 重投)。放行的最坏后果:这一跑里宿主若续跑那条 workflow run,子代按同一个 token 再停驻一次(卡仍 pending,决定仍在,下一跑照带)—— 方向是少交付一次,不是错交付:这一读失败不代表引擎随后那一读也失败,而引擎对「卡已不 pending、却拿到一条决定」的序数会当确认结算,所以读不出时绝不照供。计数持续非零 = checkpoint 店 / park 索引 / 记录店对这张卡读不出(库故障 / 行坏 / 写者版本更新),按 detail 的 `runId=`(受理那一刻的持有者)去查。detail = `step=<card|holder> runId=<留存锚> err=<原句,经记录器脱敏>`。",
46
46
  },
47
47
  "server.workflow-park-decision.store-unreadable": {
48
48
  cls: "F",
@@ -332,6 +332,10 @@ export const FAIL_OPEN_TAGS = {
332
332
  cls: "F",
333
333
  note: "#329 / #343(黑板 [5100] Q3):停驻行迟到决议的**受理回执表**(本副本对「200 丢包后重试」的幂等承诺 + 重叠请求的单飞句柄;键 = 行的键 askId,只在一次赎回真开飞时开格;容量帽只数受理之后留下的回执,在飞的单飞句柄永不被逐)**拒留一张回执**。放行的最坏后果 = 这**一次**受理(照旧生效)没有本副本的回执可依:200 丢包后的重试会再进一次赎回腿、拿到那条腿的 409(那张 checkpoint 已被消费)而不是幂等 200 回放(终局恒由赎回腿的 gate CAS 定,不会错兑也不会双兑);迟到受理本身、`GET /v1/approvals` 列表与 `/v1/approvals/:sessionId/decide` 一个字节不动 ⇒ F 类。为什么方向是「拒新」而不是 FIFO 挤最旧:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产受理即可把别家回执挤出,merge-rescan 744 confirmed 的同一病形)。必须留痕:「表被别人打满」与「这一次本来就没有回执」在 wire 上同形。detail 带 owner(判「压力来自谁」的第一手线索)。臂只有一条:全局回执帽满、本 principal 未到子帽、表里没有过期回执可收 ⇒ 拒留(detail 带 owner)。本 principal 到了子帽的常态臂逐自己最旧的一张回执(量产者自伤,设计内有界表纪律),不走本 tag;在飞的单飞句柄不占帽、永不被逐。",
334
334
  },
335
+ "server.approval.parked-decision-not-recorded": {
336
+ cls: "F",
337
+ note: "S-1054 α(`tool-approval.ts` 的 `recordParkedOutcome`,停驻赎回那一飞的落行步):停驻门**已经受理**了这次决定(续跑已开始),而把它的记录(决议向 · 署名 · 理由 · 回决幂等键 · 出处)落到仍 `PARKED` 的问行上没成 —— 店错 / 超时 / 行出了门判过的属主域 / 这把键已被同 task 另一只问占着(detail 带 `reason`)。放行的最坏后果 = 这一问在耐久决断口上的**同键回放**读不到这把键:同键重发得到 `409 conflict.ask_decided`(回显门上站着的那次决定,决议向 / 时刻 / 理由取自门,无署名),而不是 200 回放;审计面少一份署名。决定本身零影响(终局恒在停驻门上,门已决);不改判、不重赎、不双兑。计数非零 = 问账店在受理之后写不动了,运维要去看店。",
338
+ },
335
339
  "server.approvals.stale-echoed-token-unreadable": {
336
340
  cls: "F",
337
341
  note: "S-02 codex R1-[medium](验真后修):decide 的 409 `approval_stale` 臂对**调用方回显的旧 token** 做归属判定(`cs.get(binding.checkpointToken)`),那次读**抛了**(store 抖动 / 坏 checkpoint 行 / 行恰被清理)。修前这里是静默 `catch(()=>null)`,「读不出」被折成「确认不在库」——叠上 S-02 的 404 收口后,一次 store 抖动就会把合法旧 token 的 409+terminal 谎报成 404「没有这条审批」。修后:unreadable ⇒ **保持 409**(`terminal` 不披露——证不出归属就不说,与 2026-07-26 纪律同向;404 折叠只在读成功且未命中时走,探测方无法自控 store 故障 ⇒ 谕示封闭不破)。放行的最坏后果 = 该次拒体 `terminal` 诚实缺席(`currentPending` 由它自己的读口与属主复核另判,不受本臂牵连),壳退回重拉列表,judgment 面 fail-safe,故 F 类。留痕理由:这次读是 terminal 披露与 404 折叠两个判定的输入,长期抖动必须在遥测显形。",
@@ -279,6 +279,7 @@ export function createMetrics() {
279
279
  m.counter("memory_knn_query_failed_total", "Memory KNN searchScored queries that failed at runtime (S22)");
280
280
  m.counter("memory_dedup_folded_total", "Memory writes folded into an existing near-duplicate row (LOW)");
281
281
  m.counter("memory_dedup_probe_failed_total", "Memory dedup KNN probes that failed and fell through to INSERT (LOW)");
282
+ m.counter("store_boundary_fault_total", "SQL-store failures whose driver error was rewritten at the store boundary (code + fixed sentence leave the store; the driver's own sentence goes to the at=store log line), by site (store.method) and class (store fault judge word)");
282
283
  m.counter("redactions_applied_total", "redactSecrets rewrites (S24), by pattern label and `arm` (S-88: full vs prefix-fragment arm — which RULE fired, NOT proof the text was upstream-truncated)");
283
284
  m.counter("session_cache_stale_hit_total", "Warm session-cache hits dropped because the store had moved past the cached leaf (written by another replica, or gone); the session was re-woken from the store");
284
285
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
@@ -278,6 +278,15 @@ export declare class InMemoryWorkflowNotifyJournalStore implements WorkflowNotif
278
278
  get(runId: string): Promise<WorkflowNotifyJournalEntry | null>;
279
279
  get size(): number;
280
280
  }
281
+ /** 一笔**落成**的记录写的观察者(执行器视图;型属主在这里,实现 = `workflow-park-holder.ts` 的 `HeldParksHere`)。 */
282
+ export interface LandedRecordObserver {
283
+ noteLanded(runId: string, run: WorkflowRun): void;
284
+ }
285
+ /**
286
+ * 「本服务的 workflow 记录写者经哪一只记录店写」的唯一读法:交给引擎的那只记录店是 journaling 装饰器 ⇒ 它里面那一只(notify 门的收养翻转也经它写);
287
+ * 否则就是它自己。起服判官(`workflow-park-decision.ts` 的 `parkDecisionStoreRefusal`)拿它与决定店的记录栅栏按身份比。
288
+ */
289
+ export declare function recordWriterOf(store: object): object;
281
290
  /**
282
291
  * A {@link WorkflowRunStore} DECORATOR that journals a run the instant core persists it at START. core's
283
292
  * `startWorkflow` `put`s the run row (`status: "running"`) synchronously before the body runs, carrying both the
@@ -304,6 +313,9 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
304
313
  * 的派生投影,写点必须与 fleet 行投影/notify journal 同一个(见 index 模块的文件头「单写者」段)。
305
314
  * 可选:缺席 ⇒ 一位不维护(与 fleet 半场同姿势,行为逐字不变)。 */
306
315
  private readonly agentSessionIndex?;
316
+ /** 本进程执行器视图(`workflow-park-holder.ts` 的 `HeldParksHere`):本装饰器在本进程引擎每一笔**落成**的记录写之后把记录交给它(它读其中本记录持有的停驻 token)。
317
+ * 写抛 / CAS 输 ⇒ 不交(视图保留上一份)。可选:缺席 ⇒ 不维护(没有 workflow 停驻车道的部署)。 */
318
+ private readonly landedRecords?;
307
319
  constructor(inner: WorkflowRunStore,
308
320
  /** [ref] optional since the fleet decoupling: undefined ⇒ no start-time journal (ephemeral memory
309
321
  * backend has nothing to recover), the FLEET half still publishes — the panel is replica-local UI
@@ -316,7 +328,12 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
316
328
  /** [ref] 车CM:workflow 出身 park 的 join 索引。**本装饰器是它全仓唯一的写者** —— 索引是 `WorkflowRun`
317
329
  * 的派生投影,写点必须与 fleet 行投影/notify journal 同一个(见 index 模块的文件头「单写者」段)。
318
330
  * 可选:缺席 ⇒ 一位不维护(与 fleet 半场同姿势,行为逐字不变)。 */
319
- agentSessionIndex?: WorkflowAgentSessionIndex | undefined);
331
+ agentSessionIndex?: WorkflowAgentSessionIndex | undefined,
332
+ /** 本进程执行器视图(`workflow-park-holder.ts` 的 `HeldParksHere`):本装饰器在本进程引擎每一笔**落成**的记录写之后把记录交给它(它读其中本记录持有的停驻 token)。
333
+ * 写抛 / CAS 输 ⇒ 不交(视图保留上一份)。可选:缺席 ⇒ 不维护(没有 workflow 停驻车道的部署)。 */
334
+ landedRecords?: LandedRecordObserver | undefined);
335
+ /** 本装饰器的每一笔记录写经它落下的那只记录店(起服判官按身份比:决定店的记录栅栏必须就是它 —— {@link recordWriterOf})。 */
336
+ get records(): WorkflowRunStore;
320
337
  /** 本进程**亲眼看着落进索引**的指针:run → 它点名过的子会话。只用来跳过重复的 upsert,不是缓存(指针只增不删,记着的必在索引里)。
321
338
  *
322
339
  * 为什么需要:workflow 的进度写是每只 agent 状态变化一次,而 park 是罕见事件 ⇒ 无条件 upsert 会给每一次写多打几发语句(SQL)
@@ -270,16 +270,24 @@ export class InMemoryWorkflowNotifyJournalStore {
270
270
  return this.entries.size;
271
271
  }
272
272
  }
273
+ export function recordWriterOf(store) {
274
+ return store instanceof JournalingWorkflowRunStore ? store.records : store;
275
+ }
273
276
  export class JournalingWorkflowRunStore {
274
277
  inner;
275
278
  gate;
276
279
  fleetBus;
277
280
  agentSessionIndex;
278
- constructor(inner, gate, fleetBus, agentSessionIndex) {
281
+ landedRecords;
282
+ constructor(inner, gate, fleetBus, agentSessionIndex, landedRecords) {
279
283
  this.inner = inner;
280
284
  this.gate = gate;
281
285
  this.fleetBus = fleetBus;
282
286
  this.agentSessionIndex = agentSessionIndex;
287
+ this.landedRecords = landedRecords;
288
+ }
289
+ get records() {
290
+ return this.inner;
283
291
  }
284
292
  pointedSessions = new Map();
285
293
  async ensurePointersFor(id, run) {
@@ -338,10 +346,13 @@ export class JournalingWorkflowRunStore {
338
346
  scope: run.scope,
339
347
  });
340
348
  await this.writeWithPointers(id, run, () => this.inner.put(id, run), () => true);
349
+ this.landedRecords?.noteLanded(id, run);
341
350
  this.publishFleet(id, run);
342
351
  }
343
352
  async update(id, scope, run, expect) {
344
353
  const ok = await this.writeWithPointers(id, run, () => this.inner.update(id, scope, run, expect), (won) => won);
354
+ if (ok)
355
+ this.landedRecords?.noteLanded(id, run);
345
356
  if (ok)
346
357
  this.publishFleet(id, run);
347
358
  return ok;
@@ -1,4 +1,4 @@
1
- import { type CheckpointToken, type ResumeOutcome } from "@sema-agent/core";
1
+ import { type CheckpointToken, type ResumeOutcome, type WorkflowRun } from "@sema-agent/core";
2
2
  import type { ResumeRestatement } from "../http/resume-restatement.js";
3
3
  import { type JsonColumnReader } from "../plugins/sql-json-column.js";
4
4
  import { type InProcessClaimFence } from "../plugins/store-contracts.js";
@@ -15,25 +15,68 @@ export interface WorkflowParkDecision {
15
15
  /** 带着它的会话 = 受理那一刻那条 run 的发起会话(宿主)。 */
16
16
  readonly hostSessionId: string;
17
17
  readonly outcome: WorkflowParkOutcome;
18
- /** {@link parkDecisionFingerprint}:同一决定的判据(回执幂等用)。 */
18
+ /** {@link parkDecisionFingerprint}:同一决定的判据(先受理者立 / 回执幂等 / 收场转交的条件)。 */
19
19
  readonly fingerprint: string;
20
- /** 受理它的那一次投递跑(回执上的 `taskId`)。 */
20
+ /** 承运者(回执上的 `taskId`):记录栅栏下 = 持有这张卡的那条 workflow run;认领栅栏下 = 投递那一跑。读者不按它的种类分支(文件头)。 */
21
21
  readonly deliveredTaskId: string;
22
- /** 受理时刻(受理那台副本的墙钟,记录值;不当任何条件 —— 序由认领栅栏的提交序定,见文件头律二)。 */
22
+ /** 受理时刻(受理那台副本的墙钟,记录值;不当任何条件)。 */
23
23
  readonly acceptedAtMs: number;
24
24
  }
25
25
  /**
26
- * 决定行的店契约(SQL 两方言 / local 文件 / 内存三形同律)。写者只有 `/decide` 的 workflow 车道(开跑之前那一笔);读者是起跑读者与
27
- * `/decide` 自己的回执幂等判;删除只有 SQL 收割的级联。
26
+ * 记录栅栏下一次决断的结局(闭集;`switch` 穷尽):
27
+ * · `not_running` —— 记录已不读作 `running`(或不在)⇒ 承运者换成续跑车道的投递那一跑,零写;
28
+ * · `not_held` —— 仍在跑,本记录的 `parks[]` 已不持有这个 token(或读不出)⇒ 零写;
29
+ * · `restated` —— 仍在跑、仍持有,请求带续跑重述键(`mcpServers` / `a2aPeers`)⇒ 零写(活 run 上的子代带着它停驻时的表继续,重述盖不上);
30
+ * · `other_decision` —— 行上已是**另一个**决定(先受理者立)⇒ 零写;
31
+ * · `stands` —— 本决定落下了,或行上就是同一决定(指纹相等)。
32
+ */
33
+ export type HolderFenceVerdict = "not_running" | "not_held" | "restated" | "other_decision" | "stands";
34
+ /** 记录栅栏的结局:`running: false` = 记录此刻不读作 `running`(或不在),`fn` 一次都没调。 */
35
+ export type RecordFenced<T> = {
36
+ readonly running: true;
37
+ readonly value: T;
38
+ } | {
39
+ readonly running: false;
40
+ };
41
+ /**
42
+ * **workflow 记录的「仍在跑」原子段 —— 进程内那一形**(属主 = `workflow-record-fence.ts` 的 `FencedWorkflowRecordStore`:本服务全部记录写者 ——
43
+ * 引擎经 journaling 装饰器、notify 门的收养翻转 —— 都经它写,它按 runId 互斥)。`whileRunning(runId, fn)`:在这条 run 的互斥段里读记录;读作 `running`
44
+ * 才调 `fn`(同步:判与写同一段,型上不许把写挪到段外)、答 `{ running: true, value }`,否则答 `{ running: false }`。
45
+ * SQL 孪生是一个事务(`SqlWorkflowRunStore.whileRunning`:锁住 `workflow_run` 行,`fn` 收那只事务连接),不是本型。
46
+ */
47
+ export interface InProcessRecordFence {
48
+ whileRunning<T>(runId: string, fn: (run: WorkflowRun) => T): Promise<RecordFenced<T>>;
49
+ }
50
+ /** 记录栅栏的两形(闭集,与认领栅栏 `ClaimFenceStore` 同形):SQL 孪生一只行锁事务 / 进程内按 runId 互斥。决定店的两形各绑各的那一形。 */
51
+ export type WorkflowRecordFenceStore = import("../plugins/workflow-run-store-sql.js").SqlWorkflowRunStore | InProcessRecordFence;
52
+ /**
53
+ * 记录栅栏判的那一半(两形同一只纯函数;写那一半各形自己的原子段):本记录持有这个 token 吗(`heldParksOf`,`parks[]` 读不出 ⇒ 证不出持有),
54
+ * 请求带重述键吗。交回 `land` ⇒ 进「行上没有就落、有就比指纹」那一步。
55
+ */
56
+ export declare function holderFenceAdmits(run: WorkflowRun, d: Pick<WorkflowParkDecision, "runId" | "token">, restated: boolean): "not_held" | "restated" | "land";
57
+ /**
58
+ * 决定行的店契约(SQL 两方言 / local 文件 / 内存三形同律)。写者两个:`/decide` 与应答口的 workflow 车道(记录栅栏那一笔 / 续跑车道开跑之前那一笔)、
59
+ * run 收场送达的转交(续跑车道那一笔,带条件);读者是起跑读者、持有进程的拉取腿、收场送达与 `/decide` 自己的回执幂等判;删除只有 SQL 收割的级联。
28
60
  */
29
61
  export interface WorkflowParkDecisionStore {
30
- /** 这只店每一笔写所在的认领栅栏 = 构造时绑定的那只 run 店。起服判官要它**就是**部署的 run 店({@link parkDecisionStoreRefusal})。 */
62
+ /** 认领栅栏 = 构造时绑定的那只 run 店。起服判官要它**就是**部署的 run 店({@link parkDecisionStoreRefusal})。 */
31
63
  readonly claims: object;
64
+ /** 记录栅栏 = 构造时绑定的那只 workflow 记录店。起服判官要它**就是**本服务记录写者共用的那一只。 */
65
+ readonly records: object;
66
+ /**
67
+ * 认领栅栏的原子段里写(律的第二个取值):投递那一跑此刻持有宿主会话的认领且在跑 ⇒ 写并答 `true`;否则什么都不写、答 `false`。
68
+ * `condition` 缺席 ⇒ 整行写(没有行插入、有行整行取代);在场 ⇒ 行上的指纹仍等于 `expect` 才整行写,否则不写、答 `false`(收场转交:一个更新的决定已在行上)。
69
+ */
70
+ record(decision: WorkflowParkDecision, condition?: {
71
+ readonly expect: string;
72
+ }): Promise<boolean>;
32
73
  /**
33
- * 认领栅栏的原子段里整行写(律二,文件头):投递那一跑此刻持有宿主会话的认领且在跑 ⇒ 写(没有行插入、有行整行取代)并答 `true`;
34
- * 否则什么都不写、答 `false`。
74
+ * 记录栅栏的原子段里判与写(律的第一个取值;`decision.deliveredTaskId` = `decision.runId` = 持有这张卡的那条 run):结局见 {@link HolderFenceVerdict}。
75
+ * 只有 `stands` 可能写过(行上本来没有时插入),其余零写。
35
76
  */
36
- record(decision: WorkflowParkDecision): Promise<boolean>;
77
+ recordForRunningHolder(decision: WorkflowParkDecision, request: {
78
+ readonly restated: boolean;
79
+ }): Promise<HolderFenceVerdict>;
37
80
  get(token: CheckpointToken): Promise<WorkflowParkDecision | null>;
38
81
  /** 带着这个会话的全部决定行(顺序不承重)。 */
39
82
  listByHostSession(hostSessionId: string): Promise<WorkflowParkDecision[]>;
@@ -41,15 +84,17 @@ export interface WorkflowParkDecisionStore {
41
84
  /** 起服判官读的那几座(`ServiceDeps` 的子集)。 */
42
85
  export interface ParkDecisionLaneSeats {
43
86
  readonly runStore?: object | undefined;
87
+ readonly workflowRunStore?: object | undefined;
44
88
  readonly workflowAgentSessionIndex?: unknown;
45
89
  readonly workflowParkDecisions?: WorkflowParkDecisionStore | undefined;
46
90
  }
47
91
  /**
48
- * 🔴 **「决定店绑对了 run 店吗」的唯一判官**(`createHttpServer` 起服时调;拒 ⇒ 拒启)。判的是**递进来的对象**(按身份比),不是配置组合 ——
92
+ * 🔴 **「决定店绑对了两只栅栏吗」的唯一判官**(`createHttpServer` 起服时调;拒 ⇒ 拒启)。判的是**递进来的对象**(按身份比),不是配置组合 ——
49
93
  * 本服务自己那一形(SQL 后端配 `WORKFLOW_RUN_STORE=file|memory` 且开自编排)由配置不变量 `workflow-park-decision-domain` 在第 1 段、一次性迁移之前拒,走不到这里;
50
94
  * 走到这里的不相等只来自嵌入方递错了座。
51
- * 期望的绑定:workflow 子代停驻车道装了(park 索引在场)且部署有 run 店 ⇒ 决定店在场、绑的**就是**那只 run 店;否则不该有决定店。不相等的三形各一句:
52
- * ①车道与 run 店都在而决定店缺席;②决定店绑的是别的 run 店(两只店各取各的:决定写进一只库、认领与读在另一只);③有决定店,而车道没装或部署没有 run 店。
95
+ * 期望的绑定:workflow 子代停驻车道装了(park 索引在场)且部署有 run 店 ⇒ 决定店在场、`claims` **就是**那只 run 店、`records` **就是**本服务记录写者经它写的那只记录店
96
+ * ({@link recordWriterOf}:journaling 装饰器里面那一只);否则不该有决定店。不相等的四形各一句:
97
+ * ①车道与 run 店都在而决定店缺席;②决定店绑的是别的 run 店;③有决定店,而车道没装或部署没有 run 店;④决定店的记录栅栏不是本服务记录写者经它写的那一只。
53
98
  */
54
99
  export declare function parkDecisionStoreRefusal(seats: ParkDecisionLaneSeats): string | undefined;
55
100
  /**
@@ -68,24 +113,31 @@ export declare function parkDecisionFingerprint(outcome: WorkflowParkOutcome, re
68
113
  */
69
114
  export declare const readWorkflowParkOutcome: JsonColumnReader<WorkflowParkOutcome>;
70
115
  /**
71
- * 单机车道的决定店(`LocalWorkflowAgentSessionIndex` 的同室孪生),绑在进程内的 run 店上({@link InProcessClaimFence}:Memory / File run 店)。
116
+ * 单机车道的决定店(`LocalWorkflowAgentSessionIndex` 的同室孪生),绑在进程内的两只栅栏上:run 店({@link InProcessClaimFence}:Memory / File run 店)与
117
+ * 记录栅栏装饰器({@link InProcessRecordFence})。
72
118
  * `root` 在场 ⇒ 追加式 JSONL 账本(0o700 目录 + 0o600 账本,一行 = 一条决定行的整份快照,每一笔写都追加);重放按行序、**末行胜**
73
- * (写序就是认领栅栏的序:每一笔都在栅栏的同步段里追加,同一宿主会话上的受理由认领串行);缺席 ⇒ 纯内存(`WORKFLOW_RUN_STORE=memory` 的易失车道)。
119
+ * (写序就是栅栏的序:每一笔都在栅栏的段里追加);缺席 ⇒ 纯内存(`WORKFLOW_RUN_STORE=memory` 的易失车道)。
74
120
  *
75
121
  * 🔴 **坏行 fail-closed**(登记座 `workflow-park-decisions`):读不出的一行可能正是**取代**了一条旧决定的那一笔 —— 跳过它重放其余行,
76
- * 会把被取代的旧决定复活成当下的决定,在下一次起跑时交给引擎。闸合上之后三个动词都响亮拒,直到有人处理那只文件;
122
+ * 会把被取代的旧决定复活成当下的决定,在下一次交付时交给引擎。闸合上之后四个动词都响亮拒,直到有人处理那只文件;
77
123
  * 起跑读者(`parked-decide.ts`)把这一拒读成「本店没有可供的行」(F tag `server.workflow-park-decision.store-unreadable`)—— 起跑不连带拒,写与按 token 读照旧拒。
78
124
  * SQL 孪生同律:某一行 `outcome` 坏体 ⇒ 读口拒,起跑读者同一臂。
79
125
  */
80
126
  export declare class LocalWorkflowParkDecisionStore implements WorkflowParkDecisionStore {
81
127
  readonly claims: InProcessClaimFence;
128
+ readonly records: InProcessRecordFence;
82
129
  private readonly fsyncEnabled;
83
130
  private readonly ledgerPath;
84
131
  private readonly gate;
85
132
  private readonly rows;
86
133
  private log;
87
- constructor(claims: InProcessClaimFence, root?: string, fsyncEnabled?: boolean);
88
- record(decision: WorkflowParkDecision): Promise<boolean>;
134
+ constructor(claims: InProcessClaimFence, records: InProcessRecordFence, root?: string, fsyncEnabled?: boolean);
135
+ record(decision: WorkflowParkDecision, condition?: {
136
+ readonly expect: string;
137
+ }): Promise<boolean>;
138
+ recordForRunningHolder(decision: WorkflowParkDecision, request: {
139
+ readonly restated: boolean;
140
+ }): Promise<HolderFenceVerdict>;
89
141
  /** 整行写(同步):落盘**先于**内存翻转(调用方观察到的状态背后一定有耐久记录)。交回栅栏的落定记号(同步交回,型上不许让出)。 */
90
142
  private landNow;
91
143
  get(token: CheckpointToken): Promise<WorkflowParkDecision | null>;
@@ -5,6 +5,14 @@ import { approvalPayloadFingerprint } from "../plugins/approval-payload-fingerpr
5
5
  import { createObjectColumnReader, isJsonObject } from "../plugins/sql-json-column.js";
6
6
  import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
7
7
  import { FENCE_LANDED } from "../plugins/store-contracts.js";
8
+ import { heldParksOf, readRunParks } from "./workflow-agent-session-index.js";
9
+ import { recordWriterOf } from "./workflow-notify-journal.js";
10
+ export function holderFenceAdmits(run, d, restated) {
11
+ const parks = readRunParks(run);
12
+ if (parks === undefined || !heldParksOf(d.runId, parks).some((p) => p.token === d.token))
13
+ return "not_held";
14
+ return restated ? "restated" : "land";
15
+ }
8
16
  function seatName(v) {
9
17
  if (v === undefined)
10
18
  return "none";
@@ -14,7 +22,8 @@ function seatName(v) {
14
22
  export function parkDecisionStoreRefusal(seats) {
15
23
  const wanted = seats.workflowAgentSessionIndex !== undefined ? seats.runStore : undefined;
16
24
  const decisions = seats.workflowParkDecisions;
17
- if (decisions?.claims === wanted)
25
+ const recordWriter = seats.workflowRunStore === undefined ? undefined : recordWriterOf(seats.workflowRunStore);
26
+ if (decisions?.claims === wanted && (decisions === undefined || decisions.records === recordWriter))
18
27
  return undefined;
19
28
  const pair = `run store: ${seatName(seats.runStore)}; workflow park decision store: ${decisions === undefined ? "none" : `${seatName(decisions)} bound to ${seatName(decisions.claims)}`}`;
20
29
  if (decisions === undefined) {
@@ -27,9 +36,14 @@ export function parkDecisionStoreRefusal(seats) {
27
36
  "a decision store is only read and written by that lane, which needs both the park index and the run store the decisions are fenced by; " +
28
37
  "wire the lane (park index and run store) or drop the decision store; refusing to start rather than keep a store no lane fences");
29
38
  }
30
- return (`the workflow park decision store is not bound to this server's run store (${pair}) — ` +
31
- "its writes are fenced by the run store it was built on, so a different run store means decisions recorded where the session claims are not checked; " +
32
- "take runStore and workflowParkDecisions from the same store backend (run() and workflowParkDecisions()); refusing to start rather than record decisions the session claim cannot fence");
39
+ if (decisions.claims !== wanted) {
40
+ return (`the workflow park decision store is not bound to this server's run store (${pair}) — ` +
41
+ "its writes are fenced by the run store it was built on, so a different run store means decisions recorded where the session claims are not checked; " +
42
+ "take runStore and workflowParkDecisions from the same store backend (run() and workflowParkDecisions()); refusing to start rather than record decisions the session claim cannot fence");
43
+ }
44
+ return (`the workflow park decision store is not bound to the workflow record store this server writes run records through (workflow record writer: ${seatName(recordWriter)}; decision store's record fence: ${seatName(decisions.records)}) — ` +
45
+ "while the run that holds a card is running, a decision row is written in the same atomic step that checks the run's record still reads running and still holds the card, so the fence must be the store every record write goes through; " +
46
+ "pass the record store the workflow run store wraps to the store backend's workflowParkDecisions(); refusing to start rather than record decisions a run's end cannot be ordered against");
33
47
  }
34
48
  export function parkDecisionFingerprint(outcome, restatement) {
35
49
  const payload = approvalPayloadFingerprint({
@@ -76,13 +90,15 @@ function readRow(raw) {
76
90
  const LEDGER_LINE_SHAPE = { what: "a workflow park decision row", read: readRow };
77
91
  export class LocalWorkflowParkDecisionStore {
78
92
  claims;
93
+ records;
79
94
  fsyncEnabled;
80
95
  ledgerPath;
81
96
  gate;
82
97
  rows = new Map();
83
98
  log;
84
- constructor(claims, root, fsyncEnabled = true) {
99
+ constructor(claims, records, root, fsyncEnabled = true) {
85
100
  this.claims = claims;
101
+ this.records = records;
86
102
  this.fsyncEnabled = fsyncEnabled;
87
103
  if (root === undefined) {
88
104
  this.ledgerPath = undefined;
@@ -92,14 +108,37 @@ export class LocalWorkflowParkDecisionStore {
92
108
  const dir = join(root, "workflow-park-decisions");
93
109
  ensureDir(dir);
94
110
  this.ledgerPath = join(dir, "decisions.jsonl");
95
- const { records, gate } = replayJsonlGuarded(this.ledgerPath, "workflow-park-decisions", LEDGER_LINE_SHAPE);
96
- this.gate = gate;
97
- for (const d of records)
111
+ const replayed = replayJsonlGuarded(this.ledgerPath, "workflow-park-decisions", LEDGER_LINE_SHAPE);
112
+ this.gate = replayed.gate;
113
+ for (const d of replayed.records)
98
114
  this.rows.set(d.token, d);
99
115
  }
100
- async record(decision) {
116
+ async record(decision, condition) {
117
+ this.gate?.assertUsable();
118
+ let superseded = false;
119
+ const held = this.claims.whileHoldingClaim(decision.hostSessionId, decision.deliveredTaskId, () => {
120
+ if (condition !== undefined && this.rows.get(decision.token)?.fingerprint !== condition.expect) {
121
+ superseded = true;
122
+ return FENCE_LANDED;
123
+ }
124
+ return this.landNow(decision);
125
+ });
126
+ return held && !superseded;
127
+ }
128
+ async recordForRunningHolder(decision, request) {
101
129
  this.gate?.assertUsable();
102
- return this.claims.whileHoldingClaim(decision.hostSessionId, decision.deliveredTaskId, () => this.landNow(decision));
130
+ const fenced = await this.records.whileRunning(decision.runId, (run) => {
131
+ const admits = holderFenceAdmits(run, decision, request.restated);
132
+ if (admits !== "land")
133
+ return admits;
134
+ const existing = this.rows.get(decision.token);
135
+ if (existing === undefined) {
136
+ this.landNow(decision);
137
+ return "stands";
138
+ }
139
+ return existing.fingerprint === decision.fingerprint ? "stands" : "other_decision";
140
+ });
141
+ return fenced.running ? fenced.value : "not_running";
103
142
  }
104
143
  landNow(decision) {
105
144
  if (this.ledgerPath !== undefined) {