@sema-agent/server 7.142.0 → 7.144.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 (301) hide show
  1. package/MIGRATION.md +608 -0
  2. package/USAGE.md +63 -36
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +155 -39
  9. package/dist/approval-card.js +111 -31
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/execution-env.js +1 -1
  17. package/dist/boot/governance-seams.d.ts +26 -41
  18. package/dist/boot/governance-seams.js +61 -28
  19. package/dist/boot/late-binds.d.ts +7 -4
  20. package/dist/boot/late-binds.js +3 -1
  21. package/dist/boot/leader.js +2 -4
  22. package/dist/boot/memory-consolidation.d.ts +0 -8
  23. package/dist/boot/memory-consolidation.js +3 -3
  24. package/dist/boot/reapers.js +5 -4
  25. package/dist/boot/resolve-spec.d.ts +1 -1
  26. package/dist/boot/resolve-spec.js +8 -8
  27. package/dist/boot/runner-deps.d.ts +14 -6
  28. package/dist/boot/runner-deps.js +6 -17
  29. package/dist/boot/shutdown.d.ts +9 -0
  30. package/dist/boot/shutdown.js +18 -2
  31. package/dist/boot/stage-01-config.js +2 -0
  32. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  33. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  34. package/dist/boot/stage-03-store-backend.js +1 -2
  35. package/dist/boot/stage-04-stores.d.ts +5 -2
  36. package/dist/boot/stage-04-stores.js +3 -5
  37. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  38. package/dist/boot/stage-06-runners.d.ts +5 -2
  39. package/dist/boot/stage-06-runners.js +1 -3
  40. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  41. package/dist/boot/stage-07-capability-layer.js +6 -2
  42. package/dist/boot/stage-08-reapers.d.ts +5 -2
  43. package/dist/boot/stage-09-leader.d.ts +5 -2
  44. package/dist/boot/stage-10-http-server.d.ts +5 -2
  45. package/dist/boot/stage-10-http-server.js +4 -1
  46. package/dist/boot/stores.d.ts +5 -2
  47. package/dist/boot/stores.js +7 -7
  48. package/dist/boot/web-admin.d.ts +5 -1
  49. package/dist/boot/web-admin.js +31 -19
  50. package/dist/capabilities/hands-lane.d.ts +9 -39
  51. package/dist/capabilities/hands-lane.js +8 -13
  52. package/dist/capabilities/mcp-probe.d.ts +3 -2
  53. package/dist/capabilities/scenarios.d.ts +23 -17
  54. package/dist/capabilities/scenarios.js +2 -2
  55. package/dist/capabilities/skills.d.ts +1 -17
  56. package/dist/capabilities/skills.js +0 -8
  57. package/dist/capability-facts.d.ts +8 -8
  58. package/dist/capability-facts.js +4 -4
  59. package/dist/card-void.d.ts +6 -9
  60. package/dist/config-catalog.js +1 -1
  61. package/dist/config-d-migration.d.ts +2 -0
  62. package/dist/config-dir-trust.js +58 -29
  63. package/dist/config-invariants.d.ts +7 -1
  64. package/dist/config-invariants.js +7 -0
  65. package/dist/config-provider.d.ts +10 -0
  66. package/dist/config-provider.js +15 -4
  67. package/dist/config-root-migration.d.ts +13 -0
  68. package/dist/config-root-migration.js +99 -0
  69. package/dist/config-types.d.ts +5 -5
  70. package/dist/config.d.ts +4 -3
  71. package/dist/config.js +1 -1
  72. package/dist/delegated-session-owner.d.ts +35 -151
  73. package/dist/delegated-session-owner.js +1 -83
  74. package/dist/device-ws-protocol.d.ts +1 -0
  75. package/dist/device-ws-protocol.js +2 -1
  76. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  77. package/dist/env-model-table-gate.d.ts +53 -0
  78. package/dist/env-model-table-gate.js +53 -0
  79. package/dist/execution-placement.d.ts +7 -4
  80. package/dist/fleet/fleet-bus.d.ts +28 -52
  81. package/dist/fleet/fleet-bus.js +35 -100
  82. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  83. package/dist/fleet/fleet-reconciler.js +1 -1
  84. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  85. package/dist/fleet/subagent-tail-bus.js +15 -28
  86. package/dist/hooks/classic-stations.d.ts +3 -1
  87. package/dist/hooks/classic-stations.js +2 -0
  88. package/dist/hooks/hook-runner.js +28 -7
  89. package/dist/host-decision.d.ts +54 -20
  90. package/dist/host-decision.js +42 -14
  91. package/dist/http/admission.d.ts +1 -1
  92. package/dist/http/admission.js +7 -3
  93. package/dist/http/approval-sweeps.js +11 -8
  94. package/dist/http/caller-trust.d.ts +1 -1
  95. package/dist/http/durable-run-leg.js +2 -2
  96. package/dist/http/engine-refusal.d.ts +16 -0
  97. package/dist/http/engine-refusal.js +17 -0
  98. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  99. package/dist/http/memory-compliance-failure-reply.js +4 -6
  100. package/dist/http/owner-access.d.ts +11 -0
  101. package/dist/http/owner-access.js +13 -0
  102. package/dist/http/principal-gate.d.ts +1 -1
  103. package/dist/http/refusal-body.d.ts +7 -3
  104. package/dist/http/refusal-body.js +1 -0
  105. package/dist/http/resume-legs.d.ts +21 -7
  106. package/dist/http/resume-legs.js +82 -53
  107. package/dist/http/resume-restatement.d.ts +39 -0
  108. package/dist/http/resume-restatement.js +25 -0
  109. package/dist/http/route-ctx.d.ts +35 -10
  110. package/dist/http/route-ctx.js +1 -1
  111. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  112. package/dist/http/routes/approvals-assistant.js +143 -31
  113. package/dist/http/routes/capabilities.js +11 -2
  114. package/dist/http/routes/memory-policy.d.ts +2 -2
  115. package/dist/http/routes/memory-policy.js +1 -1
  116. package/dist/http/routes/notify-wake.js +23 -25
  117. package/dist/http/routes/rules.js +48 -13
  118. package/dist/http/routes/runs.js +9 -8
  119. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  120. package/dist/http/routes/session-memory-sync.js +27 -10
  121. package/dist/http/routes/session-sync.js +13 -20
  122. package/dist/http/routes/sessions.d.ts +2 -2
  123. package/dist/http/routes/sessions.js +22 -23
  124. package/dist/http/routes/tasks.js +27 -49
  125. package/dist/http/server.d.ts +13 -7
  126. package/dist/http/server.js +2 -2
  127. package/dist/http/stored-body-replay.js +15 -4
  128. package/dist/http/wire-types.d.ts +17 -7
  129. package/dist/index.d.ts +1 -0
  130. package/dist/index.js +1 -0
  131. package/dist/lane-home-probe.d.ts +15 -0
  132. package/dist/lane-home-probe.js +4 -0
  133. package/dist/leader/leader.js +5 -2
  134. package/dist/leader/wire.d.ts +7 -1
  135. package/dist/leader/wire.js +6 -5
  136. package/dist/leg-facts.d.ts +37 -0
  137. package/dist/leg-facts.js +15 -0
  138. package/dist/memory-bundle-engine.d.ts +1 -4
  139. package/dist/memory-bundle-engine.js +2 -2
  140. package/dist/memory-operator-faces.d.ts +30 -66
  141. package/dist/memory-operator-faces.js +17 -88
  142. package/dist/memory-scope-stores.d.ts +38 -40
  143. package/dist/memory-scope-stores.js +51 -36
  144. package/dist/memory-scope.d.ts +0 -9
  145. package/dist/memory-scope.js +0 -9
  146. package/dist/memory-sync-client.js +14 -3
  147. package/dist/memory-sync.d.ts +20 -4
  148. package/dist/memory-sync.js +23 -4
  149. package/dist/model-plane-face.d.ts +13 -7
  150. package/dist/model-plane-face.js +2 -1
  151. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  152. package/dist/observability/corrupt-read-seat.js +1 -2
  153. package/dist/observability/fail-open.d.ts +30 -10
  154. package/dist/observability/fail-open.js +27 -10
  155. package/dist/observability/run-terminal.d.ts +1 -1
  156. package/dist/observability/run-terminal.js +6 -2
  157. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  158. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  159. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  160. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  161. package/dist/orchestration/workflow-park-decision.js +5 -3
  162. package/dist/org-delivery-gate.d.ts +11 -0
  163. package/dist/org-delivery-gate.js +19 -1
  164. package/dist/parked-decide.d.ts +13 -9
  165. package/dist/parked-decide.js +7 -7
  166. package/dist/parked-revive-surface.d.ts +6 -7
  167. package/dist/parked-revive-surface.js +1 -1
  168. package/dist/peer-inbound-face.d.ts +122 -0
  169. package/dist/peer-inbound-face.js +123 -0
  170. package/dist/permission-rule-list.d.ts +13 -9
  171. package/dist/permission-rule-list.js +7 -3
  172. package/dist/plugins/approval-ask-store-file.js +18 -6
  173. package/dist/plugins/approval-ask-store-memory.js +8 -6
  174. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  175. package/dist/plugins/approval-ask-store-sql.js +30 -11
  176. package/dist/plugins/caching-session-store.d.ts +0 -3
  177. package/dist/plugins/caching-session-store.js +0 -3
  178. package/dist/plugins/checkpoint-store-sql.d.ts +45 -38
  179. package/dist/plugins/checkpoint-store-sql.js +108 -45
  180. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  181. package/dist/plugins/file-history-store-sql.js +24 -19
  182. package/dist/plugins/file-run-store.d.ts +29 -0
  183. package/dist/plugins/file-run-store.js +97 -8
  184. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  185. package/dist/plugins/fork-routing-session-store.js +6 -0
  186. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  187. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  188. package/dist/plugins/local-checkpoint-store.d.ts +6 -8
  189. package/dist/plugins/local-checkpoint-store.js +5 -4
  190. package/dist/plugins/local-session-store.d.ts +69 -30
  191. package/dist/plugins/local-session-store.js +133 -57
  192. package/dist/plugins/local-session-window.d.ts +4 -2
  193. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  194. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  195. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  196. package/dist/plugins/mailbox-store-sql.js +76 -7
  197. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  198. package/dist/plugins/memory-sync-store-pg.js +21 -0
  199. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  200. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  201. package/dist/plugins/one-time-migrations.d.ts +44 -14
  202. package/dist/plugins/one-time-migrations.js +118 -15
  203. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  204. package/dist/plugins/org-delivery-quota-store.js +2 -12
  205. package/dist/plugins/park-commit-watch.d.ts +1 -1
  206. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  207. package/dist/plugins/permission-rule-store-file.js +15 -2
  208. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  209. package/dist/plugins/permission-rule-store-sql.js +35 -2
  210. package/dist/plugins/pg-session-storage.d.ts +1 -9
  211. package/dist/plugins/pg-session-storage.js +2 -19
  212. package/dist/plugins/remote-env-device.d.ts +8 -11
  213. package/dist/plugins/remote-env-device.js +6 -3
  214. package/dist/plugins/remote-env-host.d.ts +8 -4
  215. package/dist/plugins/remote-env-host.js +7 -9
  216. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  217. package/dist/plugins/remote-env-k8s.js +50 -17
  218. package/dist/plugins/remote-shell.js +12 -2
  219. package/dist/plugins/retention-store-sql.d.ts +2 -2
  220. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  221. package/dist/plugins/sandbox-read-floor.js +2 -7
  222. package/dist/plugins/schema-consistency.d.ts +5 -1
  223. package/dist/plugins/session-fork.d.ts +1 -1
  224. package/dist/plugins/session-ownership.d.ts +9 -0
  225. package/dist/plugins/session-ownership.js +6 -0
  226. package/dist/plugins/session-store.d.ts +13 -16
  227. package/dist/plugins/session-store.js +9 -10
  228. package/dist/plugins/sql-errors.d.ts +10 -1
  229. package/dist/plugins/sql-errors.js +13 -0
  230. package/dist/plugins/sql-json-column.d.ts +24 -2
  231. package/dist/plugins/sql-json-column.js +38 -3
  232. package/dist/plugins/store-backend.d.ts +23 -28
  233. package/dist/plugins/store-backend.js +12 -3
  234. package/dist/plugins/store-contracts.d.ts +29 -5
  235. package/dist/plugins/store-contracts.js +4 -1
  236. package/dist/plugins/task-attachment-store.d.ts +2 -2
  237. package/dist/plugins/task-attachment-store.js +37 -52
  238. package/dist/plugins/tidb-session-store.d.ts +2 -14
  239. package/dist/plugins/tidb-session-store.js +2 -20
  240. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  241. package/dist/plugins/tool-result-store-sql.js +23 -2
  242. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  243. package/dist/plugins/wiring-section-codec.js +55 -0
  244. package/dist/plugins/workspace-fill.d.ts +1 -1
  245. package/dist/rule-entry-list.d.ts +3 -2
  246. package/dist/rule-entry-list.js +5 -1
  247. package/dist/rules-consent.d.ts +28 -5
  248. package/dist/rules-consent.js +40 -28
  249. package/dist/run-local.js +11 -6
  250. package/dist/runs.d.ts +12 -8
  251. package/dist/runs.js +10 -7
  252. package/dist/runtime-governance.d.ts +1 -1
  253. package/dist/security.d.ts +0 -4
  254. package/dist/server-secret-env.d.ts +4 -0
  255. package/dist/session-error-wire.d.ts +34 -23
  256. package/dist/session-error-wire.js +8 -3
  257. package/dist/session-incarnation.d.ts +3 -3
  258. package/dist/session-notify-door.d.ts +106 -0
  259. package/dist/session-notify-door.js +97 -0
  260. package/dist/session-sync.d.ts +9 -14
  261. package/dist/session-sync.js +5 -5
  262. package/dist/session-workspace.d.ts +3 -13
  263. package/dist/session-workspace.js +0 -9
  264. package/dist/standing-answer-grant.d.ts +5 -0
  265. package/dist/standing-answer-grant.js +10 -0
  266. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  267. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  268. package/dist/task-mcp.d.ts +1 -1
  269. package/dist/task-settings.d.ts +11 -8
  270. package/dist/task-settings.js +31 -11
  271. package/dist/tool-approval.d.ts +55 -35
  272. package/dist/tool-approval.js +112 -74
  273. package/dist/trace/core-keyset-guard.d.ts +12 -9
  274. package/dist/trace/engine-notice-wire.d.ts +1 -1
  275. package/dist/trace/engine-notice-wire.js +4 -2
  276. package/dist/trace/exit-seat.d.ts +7 -1
  277. package/dist/trace/exit-seat.js +1 -0
  278. package/dist/trace/ledger-sink.d.ts +2 -2
  279. package/dist/trace/ledger-sink.js +12 -4
  280. package/dist/trace/model-segment.d.ts +74 -25
  281. package/dist/trace/model-segment.js +141 -32
  282. package/dist/trace/project.d.ts +15 -6
  283. package/dist/trace/project.js +12 -4
  284. package/dist/trace/redact.d.ts +72 -9
  285. package/dist/trace/redact.js +1509 -471
  286. package/dist/trace/self-minted-wire.d.ts +598 -47
  287. package/dist/trace/self-minted-wire.js +190 -20
  288. package/dist/trace/wire-projection-faces.d.ts +1 -1
  289. package/dist/trace/wire-projection-faces.js +1 -1
  290. package/dist/verifier-pause-card.d.ts +33 -5
  291. package/dist/verifier-pause-card.js +15 -10
  292. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  293. package/dist/web-admin-client/apply-effective.js +90 -10
  294. package/dist/workspace-space.d.ts +19 -3
  295. package/dist/workspace-space.js +8 -4
  296. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  297. package/package.json +3 -2
  298. package/wire-changes/server@7.143.0.json +1167 -0
  299. package/wire-changes/server@7.144.0.json +291 -0
  300. package/dist/session-records-skipped-notice.d.ts +0 -21
  301. package/dist/session-records-skipped-notice.js +0 -27
@@ -1,10 +1,10 @@
1
1
  import { parkedApprovalCard } from "../approval-content-kind.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { CHECKPOINT_FACET_SQL, CHECKPOINT_SWEEP_ORDER_SQL, CheckpointIndexRefusedError, judgeCheckpointIndexCells } from "./checkpoint-index-columns.js";
4
- import { CheckpointError, isGateKind, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, winnerFromOutcome, summarizeCheckpoint, checkpointRevOf, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
4
+ import { CheckpointError, isGateKind, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, winnerFromOutcome, summarizeCheckpoint, checkpointRevOf, judgeCheckpointRow, CONTRACT_KIT_ENGINE_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
5
5
  import { redactDeep, redactSecrets } from "../trace/redact.js";
6
- import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, countKindlessBatchOffers, parkRowCarriesSkillCardFacts, parkRowFacts, readRuleOfferList, skillCardFacts } from "../approval-card.js";
7
- import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readJsonArrayColumn, readJsonObjectColumn, readNonNullJsonValueColumn, } from "./sql-json-column.js";
6
+ import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, countKindlessBatchOffers, parkRowFacts, readRuleOfferList, skillCardFacts } from "../approval-card.js";
7
+ import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, jsonColumnTextSha256, readJsonArrayColumn, readJsonColumnTextSha256, readJsonObjectColumn, readNonNullJsonValueColumn, } from "./sql-json-column.js";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { durableRowIdentity, withoutCheckpointToken } from "../checkpoint-identity.js";
10
10
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
@@ -21,6 +21,35 @@ export const TERMINAL_GRACE_MS = 3_600_000;
21
21
  export function tokenFingerprint(token) {
22
22
  return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
23
23
  }
24
+ const STORED_ROW_MEMO_CAP = 8192;
25
+ class StoredRowProjectionMemo {
26
+ rows = new Map();
27
+ keyOf(sha256) {
28
+ return `${sha256}@${CONTRACT_KIT_ENGINE_VERSION}`;
29
+ }
30
+ get(sha256) {
31
+ const key = this.keyOf(sha256);
32
+ const hit = this.rows.get(key);
33
+ if (hit !== undefined) {
34
+ this.rows.delete(key);
35
+ this.rows.set(key, hit);
36
+ }
37
+ return hit;
38
+ }
39
+ set(sha256, projection) {
40
+ const key = this.keyOf(sha256);
41
+ this.rows.delete(key);
42
+ this.rows.set(key, projection);
43
+ while (this.rows.size > STORED_ROW_MEMO_CAP) {
44
+ const oldest = this.rows.keys().next().value;
45
+ if (oldest === undefined)
46
+ break;
47
+ this.rows.delete(oldest);
48
+ }
49
+ }
50
+ }
51
+ const STORED_ROW_READ_BATCH = 100;
52
+ const CHECKPOINT_SHA256_AS = "checkpoint_sha256";
24
53
  const GATE_KIND_IS_LIVE_RUN_PARK = {
25
54
  human: true,
26
55
  irreversible_ask: true,
@@ -236,15 +265,17 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
236
265
  }
237
266
  }
238
267
  }
239
- export function pendingActionToolCallId(body) {
268
+ function pendingActionToolCallId(body) {
240
269
  if (typeof body !== "object" || body === null)
241
270
  return { readable: false };
242
- const rawSource = "sourceTaskId" in body ? body.sourceTaskId : null;
271
+ if ("sourceTaskId" in body)
272
+ return { readable: false };
273
+ const rawSource = "sourceSessionId" in body ? body.sourceSessionId : null;
243
274
  if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
244
275
  return { readable: false };
245
- const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
276
+ const sourceSessionId = typeof rawSource === "string" ? rawSource : null;
246
277
  if (!("pendingAction" in body))
247
- return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId };
278
+ return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceSessionId };
248
279
  const pa = body.pendingAction;
249
280
  if (typeof pa !== "object" || pa === null)
250
281
  return { readable: false };
@@ -252,7 +283,7 @@ export function pendingActionToolCallId(body) {
252
283
  if (kind !== null && typeof kind !== "string")
253
284
  return { readable: false };
254
285
  if (!("toolCallId" in pa))
255
- return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId };
286
+ return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceSessionId };
256
287
  const id = pa.toolCallId;
257
288
  if (typeof id !== "string")
258
289
  return { readable: false };
@@ -261,19 +292,24 @@ export function pendingActionToolCallId(body) {
261
292
  const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
262
293
  if (bih !== null && typeof bih !== "string")
263
294
  return { readable: false };
264
- return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
295
+ return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
265
296
  }
266
- function pendingActionOfColumn(cell, where) {
267
- let body;
297
+ export function candidateIdentityOf(cp) {
298
+ if (judgeCheckpointRow(cp).kind === "lacking")
299
+ return { readable: false };
300
+ return pendingActionToolCallId(cp);
301
+ }
302
+ function candidateIdentityOfColumn(cell, where) {
303
+ let cp;
268
304
  try {
269
- body = decodeJsonColumn(cell, readJsonObjectColumn, `checkpoint.checkpoint ${where}`);
305
+ cp = decodeCheckpointColumn(cell, where);
270
306
  }
271
307
  catch (e) {
272
308
  if (!isJsonColumnBodyProblem(e))
273
309
  throw e;
274
310
  return { readable: false };
275
311
  }
276
- return pendingActionToolCallId(body);
312
+ return candidateIdentityOf(cp);
277
313
  }
278
314
  function readActorAssertion(v) {
279
315
  if (typeof v !== "object" || v === null)
@@ -344,6 +380,7 @@ export class SqlCheckpointStore {
344
380
  fidelity = "json";
345
381
  execution = { outcome: true };
346
382
  claims;
383
+ storedRowMemo = new StoredRowProjectionMemo();
347
384
  constructor(db, logger, opts = {}) {
348
385
  this.db = db;
349
386
  this.logger = logger;
@@ -431,16 +468,12 @@ export class SqlCheckpointStore {
431
468
  }
432
469
  }
433
470
  async get(token) {
434
- const { rows } = await this.db.query(this.q(`SELECT version, status, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnText("mysql", "outcome")}, reopen_reason, rev, ` +
435
- `${jsonColumnText("mysql", "pending_steer_queue")}, ${jsonColumnText("mysql", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = ?`, `SELECT version, status, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnText("pg", "outcome")}, reopen_reason, rev, ` +
471
+ const { rows } = await this.db.query(this.q(`SELECT status, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnText("mysql", "outcome")}, reopen_reason, rev, ` +
472
+ `${jsonColumnText("mysql", "pending_steer_queue")}, ${jsonColumnText("mysql", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = ?`, `SELECT status, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnText("pg", "outcome")}, reopen_reason, rev, ` +
436
473
  `${jsonColumnText("pg", "pending_steer_queue")}, ${jsonColumnText("pg", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = $1`), [token]);
437
474
  const r = rows[0];
438
475
  if (!r)
439
476
  return null;
440
- const version = Number(r.version);
441
- if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
442
- throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
443
- }
444
477
  const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `for token ${tokenFingerprint(token)}`));
445
478
  cp.status = r.status;
446
479
  const outcomeRaw = refusing("a decision record nobody can read", () => decodeOutcomeColumn(r.outcome, `for token ${tokenFingerprint(token)}`));
@@ -466,7 +499,6 @@ export class SqlCheckpointStore {
466
499
  this.logger?.info?.("checkpoint_get", {
467
500
  tokenFp: tokenFingerprint(token),
468
501
  status: cp.status,
469
- version,
470
502
  hasWorkspaceHandle: !!cp.state?.workspaceHandle,
471
503
  snapshotId: cp.state?.workspaceHandle?.snapshotId,
472
504
  });
@@ -556,7 +588,8 @@ export class SqlCheckpointStore {
556
588
  const d = this.db.dialect;
557
589
  const base = `SELECT c.session_id, c.token, c.scope, c.tool_name, c.tool_call_id, ${jsonColumnText(d, "tool_input")}, c.bound_input_hash, ` +
558
590
  `${jsonColumnText(d, "risk_descriptor")}, ${jsonColumnText(d, "rule_suggestions")}, c.has_bidi_controls, c.created_at_ms, c.deadline, c.gate_kind, c.approval_card, ta.task_id, ` +
559
- `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)} ` +
591
+ `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)}, ` +
592
+ `${jsonColumnTextSha256(d, "checkpoint", CHECKPOINT_SHA256_AS)} ` +
560
593
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
561
594
  const { rows } = scope
562
595
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
@@ -590,43 +623,77 @@ export class SqlCheckpointStore {
590
623
  ...(approvalCard !== undefined ? { approvalCard } : {}),
591
624
  ...parkRowFacts(pendingAction),
592
625
  };
593
- return [{ row, token: String(r.token), readSkillCard: parkRowCarriesSkillCardFacts(pendingAction) }];
626
+ return [{ row, token: String(r.token), sha256: readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (operator queue row)") }];
627
+ });
628
+ const projections = await this.storedRowProjections(entries);
629
+ const out = entries.map((e) => {
630
+ const p = projections.get(e.token);
631
+ return p === undefined ? e.row : { ...e.row, ...p.skillCardFacts, rowVerdict: p.rowVerdict };
594
632
  });
595
- const out = [];
596
- for (const e of entries)
597
- out.push(e.readSkillCard ? { ...e.row, ...(await this.readParkedSkillCardFacts(e.token)) } : e.row);
598
633
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
599
634
  }
600
- async readParkedSkillCardFacts(token) {
601
- const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint WHERE token = ? AND status = 'pending'`, `SELECT ${jsonColumnText("pg", "checkpoint")} FROM checkpoint WHERE token = $1 AND status = 'pending'`), [token]);
602
- const r = rows[0];
603
- if (r === undefined)
604
- return {};
635
+ async storedRowProjections(rows) {
636
+ const out = new Map();
637
+ const misses = [];
638
+ const expected = new Map();
639
+ for (const { token, sha256 } of rows) {
640
+ expected.set(token, sha256);
641
+ const hit = this.storedRowMemo.get(sha256);
642
+ if (hit === undefined)
643
+ misses.push(token);
644
+ else
645
+ out.set(token, hit);
646
+ }
647
+ for (let i = 0; i < misses.length; i += STORED_ROW_READ_BATCH) {
648
+ const batch = misses.slice(i, i + STORED_ROW_READ_BATCH);
649
+ const { rows: read } = await this.db.query(this.q(`SELECT token, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnTextSha256("mysql", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
650
+ `WHERE status = 'pending' AND token IN (${batch.map(() => "?").join(", ")})`, `SELECT token, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnTextSha256("pg", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
651
+ `WHERE status = 'pending' AND token IN (${batch.map((_, j) => `$${j + 1}`).join(", ")})`), batch);
652
+ for (const r of read) {
653
+ const token = String(r.token);
654
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (whole-row read)");
655
+ const projection = this.projectStoredRow(r, sha256);
656
+ if (projection !== undefined && sha256 === expected.get(token))
657
+ out.set(token, projection);
658
+ }
659
+ }
660
+ return out;
661
+ }
662
+ projectStoredRow(r, sha256) {
605
663
  let cp;
606
664
  try {
607
- cp = decodeCheckpointColumn(r.checkpoint, "(operator queue row, skill-card facts)");
665
+ cp = decodeCheckpointColumn(r.checkpoint, "(operator queue row, whole-row read)");
608
666
  }
609
667
  catch (e) {
610
668
  if (!isJsonColumnBodyProblem(e))
611
669
  throw e;
612
670
  recordFailOpen("server.approvals.park-row-blob-unreadable");
613
- return {};
671
+ return undefined;
614
672
  }
673
+ let summary;
615
674
  try {
616
- return skillCardFacts(summarizeCheckpoint(cp));
675
+ summary = summarizeCheckpoint(cp);
617
676
  }
618
677
  catch {
619
678
  recordFailOpen("server.approvals.park-row-blob-unreadable");
620
- return {};
679
+ return { rowVerdict: judgeCheckpointRow(cp), skillCardFacts: {} };
621
680
  }
681
+ const projection = { rowVerdict: summary.rowVerdict, skillCardFacts: skillCardFacts(summary) };
682
+ this.storedRowMemo.set(sha256, projection);
683
+ return projection;
622
684
  }
623
685
  async listByScope(scope) {
624
- const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")}, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at_ms ASC LIMIT 500`, `SELECT ${jsonColumnText("pg", "checkpoint")}, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at_ms ASC LIMIT 500`), [scope]);
686
+ const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnTextSha256("mysql", "checkpoint", CHECKPOINT_SHA256_AS)}, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at_ms ASC LIMIT 500`, `SELECT ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnTextSha256("pg", "checkpoint", CHECKPOINT_SHA256_AS)}, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at_ms ASC LIMIT 500`), [scope]);
625
687
  const out = [];
626
688
  for (const r of rows) {
689
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], `checkpoint.checkpoint_sha256 in the ${scope} scheduler view`);
627
690
  const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `in the ${scope} scheduler view`));
628
691
  cp.status = r.status;
629
- out.push(summarizeCheckpoint(cp, (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token))));
692
+ const memo = this.storedRowMemo.get(sha256);
693
+ const summary = summarizeCheckpoint(cp, (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token)), memo?.rowVerdict);
694
+ if (memo === undefined)
695
+ this.storedRowMemo.set(sha256, { rowVerdict: summary.rowVerdict, skillCardFacts: skillCardFacts(summary) });
696
+ out.push(summary);
630
697
  }
631
698
  return out;
632
699
  }
@@ -640,8 +707,8 @@ export class SqlCheckpointStore {
640
707
  return rows[0] ? String(rows[0].token) : null;
641
708
  }
642
709
  async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
643
- const { rows } = await this.db.query(this.q(`SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, version, ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint ` +
644
- "WHERE scope=? AND session_id=? AND created_at_ms>=? AND (tool_call_id=? OR tool_call_id IS NULL) ORDER BY created_at_ms ASC", `SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, version, ${jsonColumnText("pg", "checkpoint")} FROM checkpoint ` +
710
+ const { rows } = await this.db.query(this.q(`SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint ` +
711
+ "WHERE scope=? AND session_id=? AND created_at_ms>=? AND (tool_call_id=? OR tool_call_id IS NULL) ORDER BY created_at_ms ASC", `SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, ${jsonColumnText("pg", "checkpoint")} FROM checkpoint ` +
645
712
  "WHERE scope=$1 AND session_id=$2 AND created_at_ms>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at_ms ASC"), [scope, sessionId, sinceMs, toolCallId]);
646
713
  const out = [];
647
714
  for (const r of rows) {
@@ -650,13 +717,9 @@ export class SqlCheckpointStore {
650
717
  status: String(r.status),
651
718
  createdAtMs: Number(r.created_at_ms),
652
719
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
653
- sourceTaskId: null,
720
+ sourceSessionId: null,
654
721
  };
655
- if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
656
- out.push({ ...base, boundCallId: null, unparseable: true });
657
- continue;
658
- }
659
- const derived = pendingActionOfColumn(r.checkpoint, `for token ${tokenFingerprint(String(r.token))}`);
722
+ const derived = candidateIdentityOfColumn(r.checkpoint, `for token ${tokenFingerprint(String(r.token))}`);
660
723
  if (!derived.readable) {
661
724
  out.push({ ...base, boundCallId: null, unparseable: true });
662
725
  continue;
@@ -673,7 +736,7 @@ export class SqlCheckpointStore {
673
736
  const effectiveHash = base.boundInputHash ?? derived.boundInputHash;
674
737
  const effective = col ?? derived.toolCallId;
675
738
  if (effective === toolCallId)
676
- out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
739
+ out.push({ ...base, boundInputHash: effectiveHash, sourceSessionId: derived.sourceSessionId, boundCallId: effective });
677
740
  }
678
741
  return out;
679
742
  }
@@ -2,7 +2,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
4
4
  import { type BlobBackend } from "./blob-backend.js";
5
- import type { SyncBlobPutResult } from "./store-backend.js";
5
+ import type { ServiceBlobExchange } from "./store-backend.js";
6
6
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
7
7
  /** The file-history blob domain's SQL table (BlobBackend `table`/MinIO index binding + the A7 assert). */
8
8
  export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
@@ -24,12 +24,10 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
24
24
  /** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on
25
25
  * its own string). Not used for this class's own statements (those go through `db`). */
26
26
  protected readonly pool: MySqlPool | PgPool;
27
- /** 2c sync blob-face MARKER (codex R2-high): the route/caps predicates gate on THIS, never on a
28
- * `typeof putBlob` duck-probe — core's local FileFileHistoryStore carries a PRIVATE synchronous
29
- * `putBlob(hash, bytes): void` helper whose name collides with the service face; a name probe
30
- * passes there, mutates core-owned blob storage out of contract, then dereferences `.ok` on
31
- * undefined. Presence of this literal = the full typed putBlob/getBlob/hasBlobs face exists. */
32
- readonly syncBlobFaces: true;
27
+ /** The blob exchange (core's `FileHistoryStore.blobExchange` group, see {@link ServiceBlobExchange}): how another machine's bytes
28
+ * come in ahead of an `importHistory`, go out to a puller, and are asked about. Its presence IS the capability — read through
29
+ * `blobExchangeOf` (`session-sync.ts`), which defers to core's judge; there is no second word for it on this class. */
30
+ readonly blobExchange: ServiceBlobExchange;
33
31
  protected readonly blobs: BlobBackend;
34
32
  /** §10 S-6 in-process lease state (design v1 D2: NOT persisted). Keys = `${scope}\x00${entryId}`. */
35
33
  private readonly spentAttempts;
@@ -58,15 +56,25 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
58
56
  /**
59
57
  * Per-scope WRITE-SERIALIZATION lock (codex R1-critical): `SELECT … FOR UPDATE` over an EMPTY result
60
58
  * locks nothing on PG, so neither the empty-target law (import/adopt) nor a multi-statement graph
61
- * write can be serialized by the data rows alone. Every graph-mutating tx therefore first ensures +
59
+ * write can be serialized by the data rows alone. Every graph transaction therefore first ensures +
62
60
  * row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
63
- * longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
64
- * the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
61
+ * longer interleave into a merged/torn graph.
65
62
  *
66
63
  * Shape A ([ref]): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
67
64
  * On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
68
65
  * below has to upgrade it to X — two concurrent scope writers each holding an S deadlock ([ref]). ON
69
66
  * DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
67
+ *
68
+ * **This is the transaction's first statement** (every caller: `begin()` and then this — nothing of the
69
+ * transaction exists before it), and a lost arbitration here restarts the transaction from the top
70
+ * (rollback + begin + the same ensure-and-lock), through the repo's one bound ({@link retryOnDeadlock}). Two shapes:
71
+ * · the engine picked this transaction as a deadlock victim. InnoDB only (REPEATABLE READ): when the sentinel
72
+ * physically disappears while ≥ 2 transactions are queued on it — its minter rolled back, or a whole-scope
73
+ * removal ({@link deleteBySession}) committed and purge took the delete-marked row — the queued locks are
74
+ * inherited as GAP locks on the next record, and each queued ensure's insert-intention waits on the others'
75
+ * gap locks. The engine kills all but one; the survivor's mint goes through and the others restart into it;
76
+ * · the sentinel was gone at the locking read ({@link ScopeSentinelVanishedError}; PG, see there).
77
+ * The lock order itself stays single: sentinels first (sorted), then the scope's rows.
70
78
  */
71
79
  private lockScope;
72
80
  /** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
@@ -147,14 +155,12 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
147
155
  /** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window.
148
156
  * Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */
149
157
  sweepOrphanBlobs(): Promise<number>;
150
- /** Upload ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). Store-level
151
- * integrity re-check (the route pre-verifies too — the internal caller might not). Never-throw.
152
- * The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
153
- * keeps its own code — the route answers it 413 (the sentence names that connection's walls), every other write failure 502 ("retry"). */
154
- putBlob(hash: string, bytes: Uint8Array): Promise<SyncBlobPutResult>;
155
- getBlob(hash: string): Promise<Uint8Array | undefined>;
156
- /** The subset of `hashes` already present — one SQL INDEX probe (the import presence pre-check). */
157
- hasBlobs(hashes: string[]): Promise<Set<string>>;
158
+ /** Store ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). The content address is judged HERE and
159
+ * nowhere else (core's `put` law: bytes whose sha256 is not `hash` are refused `adopt_failed` / `terminal`, nothing stored).
160
+ * Never-throw. The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
161
+ * is a terminal refusal that carries `blobCap` — the route answers it 413 with the judge's sentence; every other write failure is
162
+ * `snapshot_failed` with the store's judgment of the fault (the route answers 502, "retry"). */
163
+ private putExchangedBlob;
158
164
  adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
159
165
  private scopeEmpty;
160
166
  /** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { captureFileBackupViaEnv, applyFileRestoreViaEnv, fileHistoryModeProvenDifferent, worldStillMatchesMint, annulProofFailure, boundaryPublishVerdict, fileHistoryDescribeTrackKey, fileHistoryTrackKeyOf, fileHistoryResolveTrackKey, fileHistoryExportDigest, validateFileHistoryExport, resolveFileHistoryRetention, fileHistoryBoundariesToKeep, } from "@sema-agent/core";
3
3
  import { BlobTooLargeError, SqlBlobBackend } from "./blob-backend.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
- import { isMissingColumnError } from "./sql-errors.js";
5
+ import { isMissingColumnError, retryOnDeadlock } from "./sql-errors.js";
6
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
7
7
  import { recordFailOpen } from "../observability/fail-open.js";
8
8
  import { foldCaughtFailure } from "../file-history-disclosure.js";
@@ -12,6 +12,8 @@ const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
12
12
  const truthy = (v) => v === true || v === 1 || v === "1";
13
13
  class StagedRefGoneError extends Error {
14
14
  }
15
+ class ScopeSentinelVanishedError extends Error {
16
+ }
15
17
  function reapedEntryRefusal(entryId) {
16
18
  return {
17
19
  ok: false,
@@ -24,7 +26,11 @@ function reapedEntryRefusal(entryId) {
24
26
  export class SqlFileHistoryStore {
25
27
  db;
26
28
  pool;
27
- syncBlobFaces = true;
29
+ blobExchange = {
30
+ put: (hash, bytes) => this.putExchangedBlob(hash, bytes),
31
+ get: (hash) => this.blobs.getBlob(hash),
32
+ has: (hashes) => this.blobs.hasBlobs([...hashes]),
33
+ };
28
34
  blobs;
29
35
  spentAttempts = new Set();
30
36
  activeAttempts = new Set();
@@ -48,16 +54,21 @@ export class SqlFileHistoryStore {
48
54
  async beforeObserve(_scope, _keyHash) { }
49
55
  async beforePublish(_scope) { }
50
56
  async lockScope(conn, ...scopes) {
51
- for (const scope of [...new Set(scopes)].sort()) {
52
- let locked = false;
53
- for (let i = 0; i < 3 && !locked; i++) {
57
+ const ordered = [...new Set(scopes)].sort();
58
+ let restart = false;
59
+ await retryOnDeadlock(this.db.dialect, async () => {
60
+ if (restart) {
61
+ await conn.rollback().catch(() => undefined);
62
+ await conn.begin();
63
+ }
64
+ restart = true;
65
+ for (const scope of ordered) {
54
66
  await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
55
67
  const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
56
- locked = Boolean(r.rows[0]);
68
+ if (!r.rows[0])
69
+ throw new ScopeSentinelVanishedError(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
57
70
  }
58
- if (!locked)
59
- throw new Error(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
60
- }
71
+ }, (err) => err instanceof ScopeSentinelVanishedError);
61
72
  }
62
73
  async markObserved(scope, keyHash) {
63
74
  const r = await this.db.query(this.q("UPDATE file_history_tracked SET observed_later = 1 WHERE scope = ? AND key_hash = ? AND observed_later = 0", "UPDATE file_history_tracked SET observed_later = TRUE WHERE scope = $1 AND key_hash = $2 AND observed_later = FALSE"), [scope, keyHash]);
@@ -585,9 +596,9 @@ export class SqlFileHistoryStore {
585
596
  return 0;
586
597
  return this.blobs.deleteBlobs(orphans);
587
598
  }
588
- async putBlob(hash, bytes) {
599
+ async putExchangedBlob(hash, bytes) {
589
600
  if (sha256hex(bytes) !== hash) {
590
- return { ok: false, error: { code: "unsupported_format", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
601
+ return { ok: false, error: { code: "adopt_failed", message: `blob ${hash}: the bytes do not hash to this address — nothing stored`, permanence: "terminal" } };
591
602
  }
592
603
  try {
593
604
  await this.blobs.putBlob(hash, bytes);
@@ -595,17 +606,11 @@ export class SqlFileHistoryStore {
595
606
  }
596
607
  catch (err) {
597
608
  if (err instanceof BlobTooLargeError)
598
- return { ok: false, error: { code: err.code, message: err.message, wallSource: err.cap.source } };
609
+ return { ok: false, error: { code: "adopt_failed", message: err.message, permanence: "terminal", blobCap: { wallSource: err.cap.source } } };
599
610
  const f = foldCaughtFailure(err);
600
- return { ok: false, error: { code: "snapshot_failed", message: `putBlob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
611
+ return { ok: false, error: { code: "snapshot_failed", message: `blob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
601
612
  }
602
613
  }
603
- async getBlob(hash) {
604
- return this.blobs.getBlob(hash);
605
- }
606
- async hasBlobs(hashes) {
607
- return this.blobs.hasBlobs(hashes);
608
- }
609
614
  async adoptScope(fromScope, toScope) {
610
615
  try {
611
616
  if (fromScope === toScope)
@@ -41,6 +41,14 @@ export declare class FileRunStore implements InProcessClaimFence {
41
41
  private readonly events;
42
42
  /** taskId → the open append log (one O_APPEND fd per run, lazily opened, held for lifetime). */
43
43
  private readonly eventLogs;
44
+ /** `<root>/run-blobs/` — one section-blob ledger per session (plane 4 of the header). */
45
+ private readonly blobsDir;
46
+ /** sessionId → (digest → section body): the index of plane 4 (hydrated per session that has run rows). */
47
+ private readonly sectionBlobs;
48
+ /** sessionId → the open section-blob ledger (lazily opened, held until the session is deleted / dispose). */
49
+ private readonly blobLogs;
50
+ /** `(taskId, seq)` of manifest rows already reported as unresolvable (one report per row per store instance = per process). */
51
+ private readonly unresolvedReported;
44
52
  /** [ref] the checkpoint-table stand-in (see memory-run-store.ts {@link RunStoreCheckpointProbe} — the full
45
53
  * rationale lives on the interface); absent ⇒ the suspended-run reapers stay honest NO-OPs. */
46
54
  private checkpointProbe?;
@@ -61,6 +69,11 @@ export declare class FileRunStore implements InProcessClaimFence {
61
69
  private runJsonPath;
62
70
  private eventsPath;
63
71
  private activePath;
72
+ /** A session's section-blob ledger. The file name is core's `sanitizeScope` form (readable prefix + sha256 of the id), not the
73
+ * bare id: two ids that differ only in letter case are one file on a case-insensitive file system (macOS by default), and a
74
+ * deleted session must never take another session's blocks with it. `undefined` only for a run row read back from a damaged
75
+ * run.json whose session id is not a string — such a row has no ledger (its manifest rows are stored whole). */
76
+ private blobLedgerPath;
64
77
  private spaceCommitPath;
65
78
  /** One space's commit record, or `undefined` (no file). A record that is not this store's shape is a loud read failure
66
79
  * (the reader — the next run's connect — refuses; it never guesses that no commit is in flight). */
@@ -129,6 +142,22 @@ export declare class FileRunStore implements InProcessClaimFence {
129
142
  isPreemptRequested(taskId: string, owner: string | null): Promise<boolean>;
130
143
  heartbeat(taskId: string, owner: string | null): Promise<void>;
131
144
  appendEvent(taskId: string, seq: number, type: LedgerEventType, data: unknown): Promise<void>;
145
+ /**
146
+ * 写口里第四个盘面的那一段(与事件行的追加在**同一同步调用**里,块在前):帧经编解码拆成引用形 + 段体,本会话块账本里还没有的段体先追加
147
+ * (一批一次 fsync:事件行本身 fsync:false,而一只块被同会话之后每一条清单行引用 —— 丢一行丢一帧,丢一块丢一串帧),再把引用形交回给
148
+ * 事件行的追加。块追加抛 ⇒ 原样上抛:事件行不写,不退回整帧(与事件行自己写失败同一条路,汇点的串行写口留 seq 洞)。
149
+ * 这条 run 没有行(会话不详)⇒ 没有块账本可挂 ⇒ 帧原样存:和类型里「没有一段用引用」的合法实例,读口原样交出。
150
+ */
151
+ private storeSections;
152
+ private blobLogFor;
153
+ /** Drop a session's section-blob ledger: the file first, then the open fd and the index. The file removal is NOT best-effort
154
+ * (unlike {@link dropRun}'s directory): it is the delete's first destructive step, so a removal that fails throws before
155
+ * anything else changed — the session delete answers an error and a retry starts from the same state, instead of leaving
156
+ * an index that no longer matches the disk. (`AppendLog.close` never throws: it swallows its own `closeSync`.) */
157
+ private dropSectionBlobs;
158
+ /** 读口的悬空引用臂:清单行有一段装不回 ⇒ 这一帧这一次不交出(永不交出缺段的清单),每 `(taskId, seq)` 每只店实例(= 每进程)留一次 F 类痕;
159
+ * 帧仍在索引里,`maxSeq` / `retainedFrom` 照旧按存位算(下一个写者的 seq 不会撞上它)。 */
160
+ private reportUnresolved;
132
161
  maxSeq(taskId: string): Promise<number>;
133
162
  retainedFrom(taskId: string): Promise<number>;
134
163
  getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;