@sema-agent/server 7.50.0 → 7.52.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 (222) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/device-lane.d.ts +79 -0
  21. package/dist/boot/device-lane.js +63 -0
  22. package/dist/boot/engine-lease.d.ts +1 -1
  23. package/dist/boot/execution-env.d.ts +48 -2
  24. package/dist/boot/execution-env.js +57 -5
  25. package/dist/boot/governance-seams.d.ts +2 -2
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-boundary.d.ts +2 -2
  28. package/dist/boot/memory-consolidation.d.ts +6 -6
  29. package/dist/boot/org-memory.d.ts +1 -1
  30. package/dist/boot/parked-revive-gate.d.ts +3 -3
  31. package/dist/boot/reapers.d.ts +2 -2
  32. package/dist/boot/reapers.js +17 -4
  33. package/dist/boot/resolve-spec.d.ts +15 -0
  34. package/dist/boot/resolve-spec.js +40 -15
  35. package/dist/boot/retention-lane.d.ts +2 -2
  36. package/dist/boot/runner-deps.d.ts +17 -7
  37. package/dist/boot/runner-deps.js +4 -2
  38. package/dist/boot/runtime-caps.d.ts +23 -0
  39. package/dist/boot/runtime-caps.js +24 -2
  40. package/dist/boot/session-faces.d.ts +6 -1
  41. package/dist/boot/session-faces.js +17 -3
  42. package/dist/boot/shutdown.d.ts +8 -0
  43. package/dist/boot/shutdown.js +3 -1
  44. package/dist/boot/stores.js +40 -1
  45. package/dist/boot/task-list-lane.d.ts +2 -2
  46. package/dist/brain.js +51 -31
  47. package/dist/budget.d.ts +2 -2
  48. package/dist/capabilities/center-plugins.d.ts +1 -1
  49. package/dist/capabilities/collab-workflows.d.ts +1 -1
  50. package/dist/capabilities/hands-lane.d.ts +3 -3
  51. package/dist/capabilities/scenarios.d.ts +3 -3
  52. package/dist/capabilities/skills.d.ts +1 -1
  53. package/dist/config-center/hot-keys-registry.js +5 -0
  54. package/dist/config-center/http-client.d.ts +2 -2
  55. package/dist/config-center/read-face.d.ts +1 -1
  56. package/dist/config-center/restart-signal.d.ts +1 -1
  57. package/dist/config-center/types.d.ts +10 -3
  58. package/dist/config-invariants.d.ts +2 -2
  59. package/dist/config-invariants.js +18 -0
  60. package/dist/config-types.d.ts +90 -18
  61. package/dist/config.js +91 -19
  62. package/dist/deployment-governance.d.ts +1 -1
  63. package/dist/device-enrollment.d.ts +125 -0
  64. package/dist/device-enrollment.js +156 -0
  65. package/dist/device-store.d.ts +385 -0
  66. package/dist/device-store.js +407 -0
  67. package/dist/device-ws-hub.d.ts +182 -0
  68. package/dist/device-ws-hub.js +1012 -0
  69. package/dist/device-ws-protocol.d.ts +429 -0
  70. package/dist/device-ws-protocol.js +464 -0
  71. package/dist/env-facts.d.ts +5 -2
  72. package/dist/env-facts.js +1 -0
  73. package/dist/execution-lane-caps.d.ts +152 -0
  74. package/dist/execution-lane-caps.js +166 -0
  75. package/dist/fleet/fleet-bus.d.ts +1 -1
  76. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  77. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  78. package/dist/http/active-run-conflict.d.ts +2 -2
  79. package/dist/http/principal-gate.d.ts +1 -1
  80. package/dist/http/route-ctx.d.ts +24 -4
  81. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  82. package/dist/http/routes/admin-memory-optout.js +75 -0
  83. package/dist/http/routes/agents-roster.d.ts +1 -1
  84. package/dist/http/routes/approvals-assistant.js +21 -21
  85. package/dist/http/routes/capabilities.js +27 -11
  86. package/dist/http/routes/diagnostics.d.ts +1 -1
  87. package/dist/http/routes/leader.d.ts +1 -1
  88. package/dist/http/routes/memory-bundle.d.ts +1 -1
  89. package/dist/http/routes/memory-bundle.js +3 -1
  90. package/dist/http/routes/memory-compliance.d.ts +4 -4
  91. package/dist/http/routes/memory-origin.d.ts +4 -4
  92. package/dist/http/routes/memory-origin.js +3 -1
  93. package/dist/http/routes/memory-policy.js +6 -2
  94. package/dist/http/routes/retention-ops.d.ts +1 -1
  95. package/dist/http/routes/rules.d.ts +5 -2
  96. package/dist/http/routes/rules.js +84 -5
  97. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  98. package/dist/http/routes/run-memory-capture.js +97 -0
  99. package/dist/http/routes/runs.d.ts +1 -0
  100. package/dist/http/routes/runs.js +212 -6
  101. package/dist/http/routes/session-sync.d.ts +4 -6
  102. package/dist/http/routes/session-sync.js +60 -47
  103. package/dist/http/routes/sessions.js +61 -188
  104. package/dist/http/routes/shared-memory.d.ts +1 -1
  105. package/dist/http/routes/tasks.js +1 -1
  106. package/dist/http/routes/workflows.js +19 -0
  107. package/dist/http/server.d.ts +49 -13
  108. package/dist/http/server.js +109 -13
  109. package/dist/http/verify-rounds.d.ts +1 -1
  110. package/dist/http/wire-types.d.ts +41 -8
  111. package/dist/leader/endpoint.d.ts +1 -1
  112. package/dist/leader/leader.d.ts +1 -1
  113. package/dist/leader/repair-oracle.d.ts +2 -2
  114. package/dist/leader/repair-oracle.js +1 -1
  115. package/dist/leader/wire.js +4 -2
  116. package/dist/main.js +23 -9
  117. package/dist/memory-bundle-engine.d.ts +1 -1
  118. package/dist/memory-operator-faces.d.ts +5 -5
  119. package/dist/memory-origin-fence.d.ts +50 -0
  120. package/dist/memory-origin-fence.js +15 -0
  121. package/dist/memory-posture.d.ts +13 -1
  122. package/dist/memory-posture.js +1 -0
  123. package/dist/memory-scope.d.ts +6 -1
  124. package/dist/memory-scope.js +3 -2
  125. package/dist/model-select.d.ts +2 -2
  126. package/dist/observability/fail-open.d.ts +17 -5
  127. package/dist/observability/fail-open.js +17 -5
  128. package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
  129. package/dist/orchestration/hardened-vm-runner.js +11 -1
  130. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  131. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  132. package/dist/plugins/background-shell-support.d.ts +2 -2
  133. package/dist/plugins/blob-backend.d.ts +7 -2
  134. package/dist/plugins/blob-backend.js +22 -18
  135. package/dist/plugins/caching-session-store.d.ts +7 -3
  136. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  137. package/dist/plugins/device-store-sql.d.ts +130 -0
  138. package/dist/plugins/device-store-sql.js +574 -0
  139. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  140. package/dist/plugins/file-history-store-sql.js +827 -0
  141. package/dist/plugins/file-run-store.d.ts +44 -5
  142. package/dist/plugins/file-run-store.js +85 -13
  143. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  144. package/dist/plugins/fork-routing-session-store.js +2 -2
  145. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  146. package/dist/plugins/local-session-store.d.ts +9 -1
  147. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  148. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  149. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  150. package/dist/plugins/memory-origin-law.d.ts +2 -2
  151. package/dist/plugins/memory-run-store.d.ts +4 -3
  152. package/dist/plugins/memory-run-store.js +2 -1
  153. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  154. package/dist/plugins/permission-rule-store-file.js +77 -13
  155. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  156. package/dist/plugins/permission-rule-store-sql.js +123 -21
  157. package/dist/plugins/pg-pool.js +59 -0
  158. package/dist/plugins/pg-safe-json.d.ts +1 -1
  159. package/dist/plugins/remote-env-device.d.ts +271 -0
  160. package/dist/plugins/remote-env-device.js +727 -0
  161. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  162. package/dist/plugins/remote-scratchpad.js +1 -1
  163. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  164. package/dist/plugins/retention-store-sql.d.ts +7 -7
  165. package/dist/plugins/retention-store-sql.js +6 -0
  166. package/dist/plugins/run-store-sql.d.ts +9 -3
  167. package/dist/plugins/run-store-sql.js +2 -2
  168. package/dist/plugins/send-user-file.d.ts +1 -1
  169. package/dist/plugins/session-placement.d.ts +1 -1
  170. package/dist/plugins/sql-errors.d.ts +1 -1
  171. package/dist/plugins/store-backend.d.ts +68 -29
  172. package/dist/plugins/store-backend.js +78 -15
  173. package/dist/plugins/store-contracts.d.ts +5 -3
  174. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  175. package/dist/plugins/tidb-pool.js +59 -0
  176. package/dist/plugins/web-search.d.ts +1 -1
  177. package/dist/prompts-domain-validate.js +1 -1
  178. package/dist/resource-suspend.d.ts +1 -1
  179. package/dist/resource-window.d.ts +3 -3
  180. package/dist/rules-consent.d.ts +231 -9
  181. package/dist/rules-consent.js +411 -13
  182. package/dist/run-local.js +1 -0
  183. package/dist/runs.d.ts +15 -1
  184. package/dist/runs.js +19 -0
  185. package/dist/runtime-caps-resolver.d.ts +36 -1
  186. package/dist/runtime-caps-resolver.js +25 -0
  187. package/dist/runtime-governance.d.ts +1 -1
  188. package/dist/security.d.ts +5 -5
  189. package/dist/security.js +1 -1
  190. package/dist/session-sync.d.ts +52 -37
  191. package/dist/session-sync.js +19 -16
  192. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  193. package/dist/sighup-idle.d.ts +1 -1
  194. package/dist/spec-fields.d.ts +22 -1
  195. package/dist/spec-fields.js +11 -0
  196. package/dist/store-live-probe.d.ts +2 -2
  197. package/dist/task-a2a.d.ts +1 -1
  198. package/dist/task-cwd.d.ts +25 -0
  199. package/dist/task-cwd.js +3 -0
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/task-settings.d.ts +3 -3
  202. package/dist/task-settings.js +3 -1
  203. package/dist/task-workflow.d.ts +1 -1
  204. package/dist/text-bidi.d.ts +2 -2
  205. package/dist/tool-approval.d.ts +191 -9
  206. package/dist/tool-approval.js +248 -29
  207. package/dist/trace/core-keyset-guard.d.ts +1 -1
  208. package/dist/trace/engine-notice-wire.d.ts +6 -4
  209. package/dist/trace/engine-notice-wire.js +2 -0
  210. package/dist/trace/ledger-events.d.ts +1 -1
  211. package/dist/trace/ledger-sink.d.ts +2 -2
  212. package/dist/trace/project.d.ts +3 -2
  213. package/dist/trace/project.js +1 -0
  214. package/dist/turn-activity.d.ts +1 -1
  215. package/dist/wall-clock-jump-guard.d.ts +3 -3
  216. package/package.json +2 -2
  217. package/dist/http/tar.d.ts +0 -17
  218. package/dist/http/tar.js +0 -53
  219. package/dist/http/workspace-content.d.ts +0 -11
  220. package/dist/http/workspace-content.js +0 -20
  221. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  222. package/dist/plugins/file-snapshot-store-sql.js +0 -213
@@ -268,7 +268,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
268
268
  maxLines?: number;
269
269
  abortSignal?: AbortSignal;
270
270
  }): Promise<Result<string[], FileError>>;
271
- /** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**绕过**它:
271
+ /** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**跳过**它:
272
272
  * 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
273
273
  * 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
274
274
  * ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
@@ -1,4 +1,4 @@
1
- const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh"]);
1
+ const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh", "device"]);
2
2
  export function isRemoteScratchpadLane(provider) {
3
3
  return provider !== undefined && REMOTE_SCRATCHPAD_LANES.has(provider);
4
4
  }
@@ -147,7 +147,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
147
147
  * 判据三合取写进**一条 CAS 的 WHERE**:holder 是我 ∧ fencing token 还是本轮那个 ∧ **尚未过期**。
148
148
  * · 第三项是承重的:租约过期而 holder 还写着我,必须读成**不再属于我**(fail-closed)——另一个副本
149
149
  * 随时可能抢走,两个副本同时跑三方法是这条腿最不能出的事。
150
- * · 「复核」与「续租」合成一条语句而不是先读后写(codex 对抗复审 R1-[high],验真后改):
150
+ * · 「复核」与「续租」合成一条语句而不是先读后写(codex 交叉复审 R1-[high],验真后改):
151
151
  * ① 读+写两条语句之间有窗口,而 CAS 的谓词与更新在引擎里是原子的;
152
152
  * ② 更要紧的是**续租本身**:`startRetentionLane` 的重入守卫会跳过下一 tick,于是一轮的续租机会
153
153
  * 只有轮首那一次 —— 一轮跑满 2×interval 之后租约自己到期,而本副本毫不知情、继续删下去。
@@ -196,7 +196,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
196
196
  nowMs: number;
197
197
  }): Promise<void>;
198
198
  /**
199
- * 放置/解除 **+ 治理审计行,同一个事务**(codex 对抗复审 R1-[high],验真后加)。
199
+ * 放置/解除 **+ 治理审计行,同一个事务**(codex 交叉复审 R1-[high],验真后加)。
200
200
  *
201
201
  * 🔴 为什么必须原子(与 §6 F5 的「删除与审计同事务」是同一条判据,只是换了一张表):两次独立提交下,
202
202
  * 第二步失败会留下一次**无审计的状态变更**。PUT 那一支是「冻结了但账上没有」;**DELETE 那一支更重**
@@ -231,7 +231,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
231
231
  *
232
232
  * `before` = 上一页最后一行的 `id`(严格小于)。
233
233
  *
234
- * 🔴 读上限是 `MAX_LIMIT + 1` 而不是 `MAX_LIMIT`(codex 对抗复审 R1-[medium],验真属实):调用方要判
234
+ * 🔴 读上限是 `MAX_LIMIT + 1` 而不是 `MAX_LIMIT`(codex 交叉复审 R1-[medium],验真属实):调用方要判
235
235
  * 「还有没有下一页」就得多取一行,而**页**的上限是 `MAX_LIMIT`。旧形把 limit 无条件夹到 `MAX_LIMIT`
236
236
  * ⇒ 请求 `limit=200` 时 scanned 恒 ≤ 200 ⇒ `hasMore` 恒 false ⇒ **第 201 行之后的审计对这条分页链
237
237
  * 永久不可达**。这一格是「窗的上限」与「探路的那一行」两个不同的数被写成同一个数造成的,
@@ -217,7 +217,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
217
217
  * 一条既存的 hold(两方言的这两个动词都只在缺行时写),只保证**接下来那把行锁有东西可锁**;
218
218
  * ② `SELECT held … FOR UPDATE` —— 现在这行必定存在,于是锁真的拿到了。
219
219
  *
220
- * 🔴 为什么必须先造行(codex 对抗复审 R1-[high],验真后改):`FOR UPDATE` 锁不住缺席行(TiDB 无 gap
220
+ * 🔴 为什么必须先造行(codex 交叉复审 R1-[high],验真后改):`FOR UPDATE` 锁不住缺席行(TiDB 无 gap
221
221
  * lock;PG READ COMMITTED 同样)。presence 形(「行在 = 冻结」)下,operator 的 PUT 与本事务可以**同时**
222
222
  * 都读到「没有 hold」,PUT 提交返回成功之后本事务照删不误 —— 设计稿 §5 声称由行锁给出的串行化在那一形
223
223
  * 上根本不存在。哨兵行把 hold 的写点与删除的读点压到**同一把行锁**上,那句承诺才成立。
@@ -309,7 +309,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
309
309
  /**
310
310
  * **可过期**的候选 token(保护谓词写在 SQL 里、扣在 `LIMIT` **之前**)。
311
311
  *
312
- * 🔴 为什么保护必须下推(codex 对抗复审 R1-[medium],验真后改):旧形先按时间取 N 行、再在 JS 里剔除
312
+ * 🔴 为什么保护必须下推(codex 交叉复审 R1-[medium],验真后改):旧形先按时间取 N 行、再在 JS 里剔除
313
313
  * pinned —— 只要最老的一批持续被占着,每轮取回的都是同一批、每轮删 0,**它们后面**那些同样过期、又
314
314
  * 没有任何引用的行**永远轮不到**。那不是「下一拍自然收敛」,那是一个域的留存静默停摆(而且读数一直
315
315
  * 是 deleted=0,看上去像"没有候选")。判据下推之后 LIMIT 扣的是**真能动的行**,头部被保护多少行都不
@@ -319,7 +319,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
319
319
  /**
320
320
  * 过期视界之内、**当下被 live-pin 护住**的 pending 行数 = receipt 的 `skipped`。
321
321
  *
322
- * 🔴 账法定谳(两轮对抗复审各推了一次,最终落在这一形):
322
+ * 🔴 账法定谳(两轮交叉复审各推了一次,最终落在这一形):
323
323
  * · 曾经用「候选 + pinned 两读相加当分母、差值当 skipped」——被 codex R2 抓到会把中途翻转 pin 的行数两遍;
324
324
  * · 改成「视界总数当分母、差值当 skipped」——被 codex R3 抓到在 PG 的 READ COMMITTED 下可以**为负**
325
325
  * (分母读完之后又有行被 put/reopen 成 pending,候选与 UPDATE 都看得见它 ⇒ expired > 分母),而一条
@@ -356,7 +356,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
356
356
  * **可删**的会话 id:本域、过视界、且四条活引用腿**一条都不命中**(保护谓词下推,扣在 LIMIT 之前 ——
357
357
  * 理由与 {@link expiryCandidates} 逐字相同:头部被保护的树不许饿死它后面的树)。
358
358
  *
359
- * `lock = true`(删除事务里的读)追加 `FOR UPDATE`,这一格是**承重**的(codex 对抗复审 R1-[high]):
359
+ * `lock = true`(删除事务里的读)追加 `FOR UPDATE`,这一格是**承重**的(codex 交叉复审 R1-[high]):
360
360
  * · 会话行是**存在**的行 ⇒ 这把锁与 hold 哨兵不同,它真的锁得住;
361
361
  * · 锁住之后,并发的 `touch()`(把会话重新拉进视界)与 session-sync 的 **owner 改写**(importEntries /
362
362
  * replaceEntries / staged commit 都会重写 `session_meta.owner`)必须排队等我们提交 —— 否则「按旧候选集
@@ -371,11 +371,11 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
371
371
  /**
372
372
  * 本域**可归属**的孤儿卸载结果(属主 session 已亡 + 墓碑归属本域 + 归属**无歧义**)。
373
373
  *
374
- * 🔴 第三个条件是 fail-closed 的归属门(codex 对抗复审 R3-[high],验真后加):`tool_result` 没有域列,
374
+ * 🔴 第三个条件是 fail-closed 的归属门(codex 交叉复审 R3-[high],验真后加):`tool_result` 没有域列,
375
375
  * 它的域完全靠「属主 sessionId 的墓碑」推。而会话 id 是**调用方可自选**的 ⇒ 两个租户先后用过同一个 id
376
376
  * 完全合法;两边都删过之后,同一个 row_key 上会有**两个域**的墓碑,这时任何一边的 sweep 拿自己的墓碑
377
377
  * 去认领这些字节都只是猜。判据因此是:**存在第二个域的同名墓碑 ⇒ 这批行不可归属 ⇒ 一根不删**,
378
- * 实体清理退回既有的 TTL 腿(`reapOlderThan`)。少删一次是可接受的;删掉另一个租户的字节、还绕过它的
378
+ * 实体清理退回既有的 TTL 腿(`reapOlderThan`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
379
379
  * legal hold 与审计域,不可接受。
380
380
  */
381
381
  protected orphanToolResultCandidates(exec: {
@@ -394,7 +394,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
394
394
  * {@link SqlRetentionStore.sessionCandidates});
395
395
  * ③ 按 E21 顺序逐表删 → 墓碑 → 审计行(同事务)→ COMMIT。
396
396
  *
397
- * 🔴 活引用重查为什么是**候选查询本身**而不是第二条查询(codex 对抗复审 R1 的两条 high 合并采纳):
397
+ * 🔴 活引用重查为什么是**候选查询本身**而不是第二条查询(codex 交叉复审 R1 的两条 high 合并采纳):
398
398
  * · 两条查询之间有窗口,而下推之后「被查到的」按构造就是「四条腿一条都不命中的」;
399
399
  * · `FOR UPDATE` 让这些会话行在本事务期间不可被 `touch()` 拉回视界、也不可被 session-sync 改写属主 ——
400
400
  * 旧形(不加锁 + 尾部无条件删 `session_event`)可以在属主被并发改写后,把**别的租户**的历史删掉;
@@ -373,6 +373,12 @@ export class SqlRetentionStore {
373
373
  ["session_policy", "session_id"],
374
374
  ...(this.opts.taskAttachmentTable ? [["task_attachment", "session_id"]] : []),
375
375
  ["snapshot_manifest", "scope"],
376
+ ["file_history_tracked", "scope"],
377
+ ["file_history_version", "scope"],
378
+ ["file_history_boundary", "scope"],
379
+ ["file_history_boundary_file", "scope"],
380
+ ["file_history_scope", "scope"],
381
+ ["file_history_reaped_entry", "scope"],
376
382
  ]) {
377
383
  const s = set(col, 1);
378
384
  await conn.query(`DELETE FROM ${table} WHERE ${s.sql}`, s.params);
@@ -152,8 +152,14 @@ export declare class SqlRunStore {
152
152
  * this the row sits at `suspended`/updated_at=suspend-time for the WHOLE resume execution, so a resume that
153
153
  * crosses the TTL is marked `failed` mid-run and its session lock (`task_active`) is released. Returns true
154
154
  * if it claimed the flip (false ⇒ the reaper already expired it, or it isn't suspended).
155
+ *
156
+ * S-51(codex R1 [medium],红先双库证):认领 = **接管 `instance_id`**。此前该列只在 createRun 铸、此处不动,
157
+ * 于是 A 创建、C 认领 resume 的行永远记 A —— 而 steer/interrupt 的「他副本」臂与 sendHandleMiss 都拿这列当
158
+ * 「当前持有者」证据:verify 形 resume 腿(不登记 steerable)在 C 上跑时,C 读到 A≠C 就谎报「run 在别的副本」。
159
+ * 语义自此成文:`instance_id` = **当前持有副本**(createRun 铸、markResuming 认领接管);CAS 输不改列
160
+ * (一个输掉的认领不许覆盖赢家的持有记录——同一条条件 UPDATE 天然保证)。
155
161
  */
156
- markResuming(taskId: string): Promise<boolean>;
162
+ markResuming(taskId: string, instanceId: string): Promise<boolean>;
157
163
  /** The active run's taskId for a session (the `task_active` claim) — lets resume find the suspended run row. */
158
164
  getActiveTaskId(sessionId: string): Promise<string | undefined>;
159
165
  /** Shared row→record mapper — byte-identical between dialects (both drivers hand back the same column
@@ -215,7 +221,7 @@ export declare class SqlRunStore {
215
221
  * 右删失效**(anchors 腿的注逐字写着这句)。删除决策的属主门在**路由层**(DELETE /v1/sessions/:id 先
216
222
  * `ownerOf` 再比对 scope,fleet-wide 臂同门),本方法的唯一生产调用点是那条路由后面的 purge 协调器。
217
223
  *
218
- * 🔴 **化身围栏**(`expectedSessionOwner`;codex 对抗复审 R1-[high],验真后修)—— 属主门下来了,但会话
224
+ * 🔴 **化身围栏**(`expectedSessionOwner`;codex 交叉复审 R1-[high],验真后修)—— 属主门下来了,但会话
219
225
  * **身份**必须在同一个事务里重新确认。会话 id 由调用方自选、且删除之后**可被别人重新登记**
220
226
  * (`register` 是 INSERT … ON DUPLICATE KEY / ON CONFLICT,老行没了就是一条新行,新 owner):于是
221
227
  * 「路由 `ownerOf` 已验权」这句话在两只并发 purge 交错时会过期 —— A 与 A' 同时通过属主门,A' 跑完整条
@@ -280,7 +286,7 @@ export declare class SqlRunStore {
280
286
  * the run + RELEASE its task_active (unlock the session). Mirrors the checkpoint reaper's CAS-expire (≈ deny);
281
287
  * idempotent across replicas. Returns count.
282
288
  *
283
- * 🔴 design/80 D-D (adversarial blocker fix): SKIPS any row whose checkpoint is STILL pending — that row is
289
+ * 🔴 design/80 D-D (blocker fix): SKIPS any row whose checkpoint is STILL pending — that row is
284
290
  * owned by a checkpoint-state-driven sweep (the SLA deny-sweep for human/irreversible_ask, or
285
291
  * failSuspendedWithExpiredCheckpoint once the checkpoint is expired). Without this guard, this time-based
286
292
  * UPDATE fired at the SAME instant as the deny-sweep (deadline == createdAt + ttl, both keyed on the TTL),
@@ -132,8 +132,8 @@ export class SqlRunStore {
132
132
  async setNeedsReview(taskId) {
133
133
  await this.db.query(this.q("UPDATE task_run SET status = 'needs_review', updated_at = ? WHERE task_id = ? AND status IN ('running','needs_review')", "UPDATE task_run SET status = 'needs_review', updated_at = $1 WHERE task_id = $2 AND status IN ('running','needs_review')"), [new Date(), taskId]);
134
134
  }
135
- async markResuming(taskId) {
136
- const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, updated_at = $1 WHERE task_id = $2 AND status IN ('suspended','needs_review')"), [new Date(), taskId]);
135
+ async markResuming(taskId, instanceId) {
136
+ const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = ?, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = $1, updated_at = $2 WHERE task_id = $3 AND status IN ('suspended','needs_review')"), [instanceId, new Date(), taskId]);
137
137
  return affected === 1;
138
138
  }
139
139
  async getActiveTaskId(sessionId) {
@@ -8,7 +8,7 @@ export declare const PRESIGN_MAX_TTL_SEC = 604800;
8
8
  * - keyed on the S3 SECRET KEY — server-held and stable, so the mapping is deterministic per deployment but
9
9
  * unforgeable/unenumerable from outside (a URL holder cannot test principal guesses without the key);
10
10
  * - the "sendfile-scope:" DOMAIN SEPARATOR pins this HMAC use — the same secret signs SigV4 requests, and a
11
- * cross-protocol collision between "scope segment" and any other HMAC-of-attacker-controlled-string use is
11
+ * cross-protocol collision between "scope segment" and any other HMAC-of-caller-controlled-string use is
12
12
  * exactly what domain separation exists to prevent;
13
13
  * - 32 hex chars (128 bits) is a NAMESPACE label, not a secret: the cross-tenant BIRTHDAY bound at even a
14
14
  * billion tenants is ~n²/2^129 ≈ 2^-69 — negligible. (The earlier 16-hex/64-bit cut had a ~2.7% birthday
@@ -96,7 +96,7 @@ export interface PlacementAcquireOpts {
96
96
  * · 方向①(claim 关门):`persisted` 在场 + 非 `requireExisting` + 调用方没带创建 placement ⇒ 拒。
97
97
  * `requireExisting`(读/复活形,义务 5)与「创建调用及其幂等重取」(带 placement 的可信内部链)
98
98
  * 是仅有的两个合法形。placement 不可变意味着带 placement 的参数**永远重写不了**任何东西——持久化
99
- * 的那一份恒赢,所以放行它不开任何后门。
99
+ * 的那一份恒赢,所以放行它不开任何口子。
100
100
  * · 方向②(镜像/squat):带 placement 的创建形回读到**缺席**或**异组**元组 ⇒ 拒。缺席 = 并发的
101
101
  * claim 形抢先建出了普通会话(或带外写把元组抹了);异组 = 同 id 上坐着另一条委派链的转录。
102
102
  * 两者都不许让「声明了 durable 分区」这句话在静默中变成谎。
@@ -50,7 +50,7 @@ export declare const MYSQL_ER_BAD_FIELD_CODE = "ER_BAD_FIELD_ERROR";
50
50
  * 🔴 单一属主的理由与 {@link isDupKeyError} 逐字同源(本文件顶注的 single-semantic-multi-site-drift):
51
51
  * 判据一旦复制,两处的识别集就会各自漂,而这条谓词的错判方向**特别贵** —— 它守的是一句**破坏性**的
52
52
  * 错误指路(「去 DROP TABLE」)。超时、连接重置、取消、资源不足、列级权限被拒、表整个不存在,每一种
53
- * 都会让探针抛错;把它们诊断成「删表重建」比它要挡的缺陷更贵(#340 codex 对抗复审 round2/round3
53
+ * 都会让探针抛错;把它们诊断成「删表重建」比它要挡的缺陷更贵(#340 codex 交叉复审 round2/round3
54
54
  * [high] 两轮收窄后的终形)。认不出的一律**不是**缺列(fail-safe:宁可把一次真缺列报成原始错误)。
55
55
  *
56
56
  * MySQL 侧两个归因键都认(mysql2 的 `code`/`errno` 是同一件事的两种写法),与 dup-key 那条同姿势。
@@ -1,7 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import type { SessionStore, SessionPolicyStore, FileSnapshotStore, WorkflowJournalStore, ToolResultStore } from "@sema-agent/core";
4
- import { type FileSnapshotBounds } from "@sema-agent/core";
3
+ import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore } from "@sema-agent/core";
5
4
  import { type OutcomeSink } from "./file-outcome-sink.js";
6
5
  import { type InboxWarn } from "./workflow-run-store-sql.js";
7
6
  import { FileRunStore } from "./file-run-store.js";
@@ -9,6 +8,7 @@ import { LocalCheckpointStore } from "./local-checkpoint-store.js";
9
8
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
10
9
  import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
11
10
  import { type ApprovalAskStore as ApprovalAskStoreType } from "./approval-ask-store-sql.js";
11
+ import { type MemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
12
12
  import { type SendFileLedger } from "./send-file-ledger.js";
13
13
  import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
14
14
  import type { ServiceConfig } from "../config-types.js";
@@ -17,6 +17,7 @@ import { TiDBCheckpointStore, PgCheckpointStore } from "./checkpoint-store-sql.j
17
17
  import { TiDBImageIndex, PgImageIndex } from "./image-index-sql.js";
18
18
  import { TiDBImageBake, PgImageBake } from "./image-bake-store-sql.js";
19
19
  import { TiDBLeaderRunStore, PgLeaderRunStore } from "./leader-run-store-sql.js";
20
+ import type { DeviceStore } from "../device-store.js";
20
21
  import { TiDBBreakerState, PgBreakerState } from "./breaker-state-sql.js";
21
22
  import { TiDBCostQuota } from "./tidb-cost-quota.js";
22
23
  import { PgCostQuota } from "./pg-cost-quota.js";
@@ -44,22 +45,27 @@ export type { ApprovalAskStore, AskRow, NewAskRow, AskTransitionPatch, DecideAsk
44
45
  * it, core's InMemory omits it → optional). tidb/pg = durable twins; local = core's InMemorySessionPolicyStore (works
45
46
  * local — rules need no cloud-only infra). The shared env-gated equivalence suite keeps the twins byte-matched to InMemory. */
46
47
  export type ServiceSessionPolicyStore = SessionPolicyStore;
47
- /** E19 rewind-files FileSnapshotStore — core's interface PLUS the service `deleteBySession` (E21 purge; the durable
48
- * twins carry it, core's InMemory omits it → optional). tidb/pg = durable twins (manifest+blobs, reuse core
49
- * captureManifest/applyManifest); local = core's FileFileSnapshotStore(file-backed content-addressed,跨重启
50
- * 存活——接入审计 L6 订正:旧注释写 InMemory 会误导 local 快照持久性判断). Also the 2c work-artifact transfer store. */
51
- export type ServiceFileSnapshotStore = FileSnapshotStore & {
52
- /** [805]③a observability tail (SQL twins only — core's local stores drop the count internally, a core-side
53
- * gap): captureManifest's tolerated-vanished counter metric sink, assigned by main.ts when present.
54
- * codex F8: async sinks admitted; the twins swallow a rejected promise (never-throw seam holds). */
55
- onSkippedVanished?: (count: number) => void | Promise<void>;
48
+ /** S-15 per-edited-file rewind FileHistoryStore — core's interface PLUS the service extras only the SQL twins
49
+ * carry (optional so the local lane's core `FileFileHistoryStore` still fits). Replaces the retired E19
50
+ * whole-tree `ServiceFileSnapshotStore` (design/381: the boundary covers the TRACKED set only — files this
51
+ * session edited; there is no whole-tree manifest anymore). Also the 2c history-transfer store
52
+ * (exportHistory/importHistory + the blob negotiation faces below). */
53
+ export type ServiceFileHistoryStore = FileHistoryStore & {
54
+ /** 2c sync blob-face marker see SqlFileHistoryStore.syncBlobFaces (codex R2-high: name-probing
55
+ * `putBlob` on an arbitrary store collides with the local core store's PRIVATE void helper). */
56
+ syncBlobFaces?: true;
57
+ /** E21 purge / overwrite-dst sync wipe: drop the WHOLE scope incl. v1 baselines + DV-14 markers
58
+ * (`reap(scope, [])` deliberately retains those while the scope lives — not a purge). SQL twins only. */
56
59
  deleteBySession?(scope: string): Promise<number>;
57
- /** 2c session-sync DoS bound GLOBALLY GC grace-window orphan blobs (standalone PUTs never imported, plus the
58
- * blobs reap()/deleteBySession() left orphaned byte deletion is ASYNC, never on the sync purge path). The durable
59
- * twins carry it (scheduled by the main.ts reaper, error-tolerant → eventual consistency); core's InMemory omits it. */
60
+ /** Async byte-GC for the file_history_blob domain (orphans left by reap/purge/lost mint races; grace-window
61
+ * protectednever a synchronous external-store delete). Reaper-driven; SQL twins only. */
60
62
  sweepOrphanBlobs?(): Promise<number>;
61
- /** The subset of `hashes` already present a cheap SQL INDEX probe (BOTH backends). Used by the §10 import
62
- * blob-presence pre-check (a HEAD/index check, NOT N full getBlob GETs). The durable twins carry it; optional. */
63
+ /** 2c blob negotiation: upload one content-addressed blob ahead of importHistory. SQL twins only. */
64
+ putBlob?(hash: string, bytes: Uint8Array): Promise<import("@sema-agent/core").FileHistoryResult>;
65
+ /** 2c blob read face (scope-checked by the route against the session's history graph). SQL twins only. */
66
+ getBlob?(hash: string): Promise<Uint8Array | undefined>;
67
+ /** The subset of `hashes` already present — a cheap SQL INDEX probe (the import presence pre-check,
68
+ * never N full getBlob GETs). SQL twins only. */
63
69
  hasBlobs?(hashes: string[]): Promise<Set<string>>;
64
70
  };
65
71
  /** SVC-2 (core CORE-7) WorkflowJournalStore — core's interface PLUS the service `deleteByRun` (GC extra). tidb/pg =
@@ -159,9 +165,38 @@ export interface StoreBackend {
159
165
  /** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). REQUIRED on all backends — works local
160
166
  * (local = core's InMemorySessionPolicyStore), like resumeAnchor. Backs core's RunnerDeps.sessionPolicyStore. */
161
167
  sessionPolicy(): ServiceSessionPolicyStore;
162
- /** E19 rewind-files FileSnapshotStore (working-tree snapshot/restore keyed by entryId; also the 2c artifact-transfer
163
- * store). REQUIRED on all backends (local = core's InMemoryFileSnapshotStore). Backs core's RunnerDeps.fileSnapshotStore. */
164
- fileSnapshot(): ServiceFileSnapshotStore;
168
+ /** S-15 per-edited-file rewind FileHistoryStore (trackEdit/annulTrack/snapshot/restore over the tracked set;
169
+ * also the 2c history-transfer store). REQUIRED on all backends (local = core's file-backed
170
+ * `FileFileHistoryStore` under the data root). Backs core's RunnerDeps.fileHistoryStore. */
171
+ fileHistory(): ServiceFileHistoryStore;
172
+ /** design/383 §3.1 / S-50 §一(S-49 S-2):per-principal **memory-capture opt-out 授权表**
173
+ * (`memory_optout_grant`)—— `RuntimeCaps.allowMemoryOptOut` 的本地 verdict 源(三层折叠:per-principal
174
+ * 行 → 部署缺省哨兵行 → 代码缺省 allow)+ `/v1/admin/memory-optout` 四端点的持久层。
175
+ *
176
+ * 🔴 **可选**(与 permissionRule 同族):SQL 后端 = 双方言 twin;`local` 车道 **undefined**,且缺席是一个
177
+ * 真答案 —— 没有 verdict 源时 `allowMemoryOptOut` 键不合成(core 读「无 per-principal 限制」,`open` 姿态
178
+ * 下 opt-out 照常生效),而 `MEMORY_CAPTURE_POLICY=governed` 在启动期对无源部署拒启
179
+ * (`boot/runtime-caps.ts` `assertMemoryCapturePolicyWirable`)。刻意**不给 local 一个 in-memory twin**:
180
+ * 一条授权行是「operator 写下的合规事实」,进程内 Map 会在重启时静默丢掉它,下一次解析读到的是
181
+ * 「没有行 ⇒ 代码缺省 allow」—— 那是**放宽面**上的静默降级(permissionRule 那条禁令的同一理由)。
182
+ * File 形候真实的 local+governed 部署需求出现再落(候需求档)。 */
183
+ memoryOptOutGrant(): MemoryOptOutGrantStore | undefined;
184
+ /** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): does the RETIRED whole-tree
185
+ * snapshot epoch hold a boundary for (sessionId, entryId)? Reads the legacy `snapshot_manifest` table.
186
+ * SQL backends only (local never had a whole-tree epoch table). Diagnostic input on the refusal path —
187
+ * a probe that cannot tell answers undefined (reads exactly like an absent probe), never throws. */
188
+ legacyRewindBoundaryProbe?(query: {
189
+ sessionId: string;
190
+ entryId: string;
191
+ }): Promise<boolean | undefined>;
192
+ /** Retired-epoch byte hygiene: GC grace-passed orphan `snapshot_blob` rows/objects (rows the retention
193
+ * lane's `snapshot_manifest` sweeps keep orphaning — without this leg their BYTES would outlive every
194
+ * manifest reference forever, incl. MinIO objects). SQL backends only; reaper-driven. */
195
+ legacySnapshotBlobSweep?(): Promise<number>;
196
+ /** Retired-epoch right-to-delete: drop the session's LEGACY `snapshot_manifest` rows on the online E21
197
+ * purge (the retention lane already sweeps them on expiry — the two delete legs must converge on the
198
+ * same row set; a user's DELETE must reach old-epoch data too). Bytes go via the async legacy sweep. */
199
+ legacySnapshotPurge?(sessionId: string): Promise<number>;
165
200
  /** SVC-2 (core CORE-7) durable WorkflowJournalStore — the cross-replica resume journal for an LLM-authored workflow
166
201
  * (RunWorkflowOptions.journalStore). REQUIRED on all backends (tidb/pg = durable twins; local = core's
167
202
  * InMemoryWorkflowJournalStore, single-replica/in-process).
@@ -212,6 +247,14 @@ export interface StoreBackend {
212
247
  * 缺席 ⇒ endpoint 落到显式的单副本内存降级臂(见 `leader/endpoint.ts` 的 store 缺席注)。
213
248
  */
214
249
  leaderRun?(): LeaderRunStore;
250
+ /**
251
+ * device lane 的四表店(device-executor-lane-v2 §6,车A-2 交店 / 车A-4 接线)。**SQL 后端专有**,
252
+ * `local` 刻意省略 —— 与 {@link leaderRun} 逐字同一条理由:config 层已经把「`REMOTE_EXEC=device` 且
253
+ * 无外部 SQL 店」整个组合拒启了(`device-durable-store` 不变量),所以 local 车道上根本不存在一个需要
254
+ * 这四张表的 device worker。消费点按 `backend?.device?.()` 取,缺席 ⇒ `boot/device-lane.ts` 的
255
+ * `assertDeviceLaneWired` 响亮拒启(**不是**静默落进程内 stub)。
256
+ */
257
+ device?(): DeviceStore;
215
258
  /**
216
259
  * #270 —— 托管留存的**破坏性**三方法(core `ManagedRetentionCapability` 的 server 侧真身)。
217
260
  * **SQL 后端专有**,`local` 刻意省略:那三条腿是跨十余张表的单事务级联,file/in-memory 形没有事务面
@@ -252,17 +295,13 @@ export interface StoreBackend {
252
295
  * splits a fleet window into disjoint buckets = soft-limit leak). OPTIONAL: local backend omits (no fleet). */
253
296
  dbNowMs?(): Promise<number>;
254
297
  }
255
- /** (c)(clay 拍 a+c,2026-07-27)云形快照 blob 姿势门——boot 时调用(main.ts),纯函数可单测。
256
- * SQL 后端(mysql|pg)把 blob 字节落 `snapshot_blob` 单行的形已被两役实证撞 mysql-协议包墙
257
- * (D-1 附件 → 对象存储裁定;snapshot_blob >31MiB 同墙):大字节归对象存储。云形缺 MinIO
258
- * fail-loud(启动了才是假安全);`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true` = 显式接受 SQL +
259
- * per-blob 帽(单机/测试台形)。local 后端不经此门(file/in-memory 快照店,无 SQL 包墙) */
298
+ /** (c)(clay 拍 a+c,2026-07-27;S-15 换店后域=file_history_blob,门与旋钮名不变)云形 blob 姿势门——
299
+ * boot 时调用(boot/stores.ts),纯函数可单测。SQL 后端(mysql|pg)把 blob 字节落单行的形已被两役
300
+ * 实证撞 mysql-协议包墙(D-1 附件 → 对象存储裁定):大字节归对象存储。per-file 历史的 blob=单文件
301
+ * 字节,同一堵墙(>6MiB 的被 track 文件在 TiDB 上落不进 SQL ⇒ trackEdit 拒 ⇒ 默认 DV-14 下该次
302
+ * 编辑被拒)。云形缺 MinIO fail-loud(启动了才是假安全);`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true` =
303
+ * 显式接受 SQL 店 + per-blob 帽(单机/测试台形)。local 后端不经此门(file 店,无 SQL 包墙)。 */
260
304
  export declare function assertCloudSnapshotBlobPosture(kind: "mysql" | "pg" | "local", config: Pick<ServiceConfig, "snapshotBlobStore" | "snapshotBlobAllowSql">): void;
261
- /** REWIND_SNAPSHOT_MAX_MB overrides ONLY the byte cap of core's DEFAULT_SNAPSHOT_BOUNDS (raise for
262
- * big monorepos, lower for tight DBs). File-count / ignoreDirs stay core defaults — the knob answers the one
263
- * field the 20G-workspace case needed; more knobs when a case shows up. Exported for tests
264
- * (fractional-MB rounding + partial-merge semantics need a regression lock). */
265
- export declare function snapshotBoundsFromConfig(config: ServiceConfig): FileSnapshotBounds;
266
305
  /**
267
306
  * 启动日志里「跨副本 or 进程内」那一格的标签。
268
307
  *
@@ -1,22 +1,23 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { AdoptionError, FileStorageBackend, FileSessionRepo, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
3
+ import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
4
4
  import { FileWorkflowJournalStore } from "@sema-agent/core";
5
5
  import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
6
6
  import { FileOutcomeSink } from "./file-outcome-sink.js";
7
7
  import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore } from "./workflow-run-store-sql.js";
8
- import { FileRunStore } from "./file-run-store.js";
8
+ import { FileRunStore, RunStoreStrictHydrateError } from "./file-run-store.js";
9
9
  import { LocalSessionStore } from "./local-session-store.js";
10
10
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
11
11
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
12
12
  import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
13
13
  import { TiDBApprovalAskStore, PgApprovalAskStore } from "./approval-ask-store-sql.js";
14
14
  import { InMemoryApprovalAskStore } from "./approval-ask-store-memory.js";
15
+ import { TiDBMemoryOptOutGrantStore, PgMemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
15
16
  import { TiDBSendFileLedger, PgSendFileLedger, FileSendFileLedger } from "./send-file-ledger.js";
16
17
  import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
17
18
  import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-store-sql.js";
18
19
  import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
19
- import { TiDBFileSnapshotStore, PgFileSnapshotStore } from "./file-snapshot-store-sql.js";
20
+ import { TiDBFileHistoryStore, PgFileHistoryStore, FILE_HISTORY_BLOB_TABLE, FILE_HISTORY_BLOB_KEY_PREFIX } from "./file-history-store-sql.js";
20
21
  import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
21
22
  import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
22
23
  import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
@@ -26,6 +27,7 @@ import { TiDBToolResultStore, PgToolResultStore } from "./tool-result-store-sql.
26
27
  import { TiDBImageIndex, PgImageIndex } from "./image-index-sql.js";
27
28
  import { TiDBImageBake, PgImageBake } from "./image-bake-store-sql.js";
28
29
  import { TiDBLeaderRunStore, PgLeaderRunStore } from "./leader-run-store-sql.js";
30
+ import { TiDBDeviceStore, PgDeviceStore } from "./device-store-sql.js";
29
31
  import { TiDBBreakerState, PgBreakerState } from "./breaker-state-sql.js";
30
32
  import { TiDBCostQuota } from "./tidb-cost-quota.js";
31
33
  import { PgCostQuota } from "./pg-cost-quota.js";
@@ -40,7 +42,21 @@ import { TiDBRetentionLaneStore, PgRetentionLaneStore } from "./retention-lane-s
40
42
  import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
41
43
  import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
42
44
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
43
- function snapshotBlobBackend(config, dialect, pool) {
45
+ function historyBlobBackend(config, dialect, pool) {
46
+ const s = config.snapshotBlobStore;
47
+ if (!s)
48
+ return new SqlBlobBackend(dialect, pool, config.snapshotBlobSqlMaxBytes, FILE_HISTORY_BLOB_TABLE);
49
+ return new MinioBlobBackend({
50
+ endpoint: s.endpoint,
51
+ bucket: s.bucket,
52
+ accessKey: s.accessKey,
53
+ secretKey: s.secretKey,
54
+ ...(s.region ? { region: s.region } : {}),
55
+ keyPrefix: `${s.keyPrefix ?? ""}${FILE_HISTORY_BLOB_KEY_PREFIX}`,
56
+ ...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
57
+ }, { dialect, pool, table: FILE_HISTORY_BLOB_TABLE });
58
+ }
59
+ function legacyBlobBackend(config, dialect, pool) {
44
60
  const s = config.snapshotBlobStore;
45
61
  if (!s)
46
62
  return new SqlBlobBackend(dialect, pool, config.snapshotBlobSqlMaxBytes);
@@ -54,18 +70,51 @@ function snapshotBlobBackend(config, dialect, pool) {
54
70
  ...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
55
71
  }, { dialect, pool });
56
72
  }
73
+ async function legacyProbeImpl(dialect, pool, query) {
74
+ try {
75
+ if (dialect === "tidb") {
76
+ const [rows] = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", [query.sessionId, query.entryId]);
77
+ return rows.length > 0;
78
+ }
79
+ const r = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1", [query.sessionId, query.entryId]);
80
+ return r.rows.length > 0;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ async function legacyPurgeImpl(dialect, pool, sessionId) {
87
+ if (dialect === "tidb") {
88
+ const [r] = await pool.query("DELETE FROM snapshot_manifest WHERE scope = ?", [sessionId]);
89
+ return Number(r.affectedRows ?? 0);
90
+ }
91
+ const r = await pool.query("DELETE FROM snapshot_manifest WHERE scope = $1", [sessionId]);
92
+ return r.rowCount ?? 0;
93
+ }
94
+ async function legacySweepImpl(config, dialect, pool) {
95
+ const sql = "SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)";
96
+ let orphans;
97
+ if (dialect === "tidb") {
98
+ const [rows] = await pool.query(sql);
99
+ orphans = rows.map((r) => String(r.blob_hash));
100
+ }
101
+ else {
102
+ const r = await pool.query(sql);
103
+ orphans = r.rows.map((row) => row.blob_hash);
104
+ }
105
+ if (orphans.length === 0)
106
+ return 0;
107
+ return legacyBlobBackend(config, dialect, pool).deleteBlobs(orphans);
108
+ }
57
109
  export function assertCloudSnapshotBlobPosture(kind, config) {
58
110
  if (kind === "local" || config.snapshotBlobStore || config.snapshotBlobAllowSql)
59
111
  return;
60
- throw new Error("cloud deployments (DB_BACKEND=mysql|pg) must configure object storage for snapshot blobs " +
112
+ throw new Error("cloud deployments (DB_BACKEND=mysql|pg) must configure object storage for rewind file-history blobs " +
61
113
  "(MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY): single-row SQL blob writes hit TiDB's " +
62
114
  "txn-entry-size-limit (default 6MiB) / the mysql-protocol packet limit (bytes belong in object storage — " +
63
115
  "same ruling as the D-1 attachment store). Single-box/test rigs may explicitly opt out with " +
64
- "SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true (a per-blob cap then applies; oversized blobs are rejected 413).");
65
- }
66
- export function snapshotBoundsFromConfig(config) {
67
- const mb = config.rewindSnapshotMaxMb;
68
- return mb !== undefined ? { ...DEFAULT_SNAPSHOT_BOUNDS, maxBytes: Math.round(mb * 1024 * 1024) } : DEFAULT_SNAPSHOT_BOUNDS;
116
+ "SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true (a per-blob cap then applies; an over-cap tracked file's first-touch " +
117
+ "backup is refused typed, which under the default DV-14 policy refuses that edit).");
69
118
  }
70
119
  class TiDBBackend {
71
120
  pool;
@@ -84,7 +133,11 @@ class TiDBBackend {
84
133
  permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); }
85
134
  sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
86
135
  sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
87
- fileSnapshot() { return new TiDBFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "tidb", this.pool), snapshotBoundsFromConfig(this.config)); }
136
+ fileHistory() { return new TiDBFileHistoryStore(this.pool, historyBlobBackend(this.config, "tidb", this.pool), this.config.fileHistory); }
137
+ memoryOptOutGrant() { return new TiDBMemoryOptOutGrantStore(this.pool); }
138
+ legacyRewindBoundaryProbe(query) { return legacyProbeImpl("tidb", this.pool, query); }
139
+ legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "tidb", this.pool); }
140
+ legacySnapshotPurge(sessionId) { return legacyPurgeImpl("tidb", this.pool, sessionId); }
88
141
  workflowJournal() { return new TiDBWorkflowJournalStore(this.pool); }
89
142
  outcomeSink() { return new TiDBOutcomeLedger(this.pool); }
90
143
  workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); }
@@ -96,6 +149,7 @@ class TiDBBackend {
96
149
  imageIndex() { return new TiDBImageIndex(this.pool); }
97
150
  imageBake() { return new TiDBImageBake(this.pool); }
98
151
  leaderRun() { return new TiDBLeaderRunStore(this.pool); }
152
+ device() { return new TiDBDeviceStore(this.pool); }
99
153
  retention(opts) { return new TiDBRetentionStore(this.pool, opts); }
100
154
  retentionLane() { return new TiDBRetentionLaneStore(this.pool); }
101
155
  breaker(onWriteFail) { return new TiDBBreakerState(this.pool, onWriteFail); }
@@ -124,7 +178,11 @@ class PgBackend {
124
178
  permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); }
125
179
  sendFileLedger() { return new PgSendFileLedger(this.pool); }
126
180
  sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
127
- fileSnapshot() { return new PgFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "pg", this.pool), snapshotBoundsFromConfig(this.config)); }
181
+ fileHistory() { return new PgFileHistoryStore(this.pool, historyBlobBackend(this.config, "pg", this.pool), this.config.fileHistory); }
182
+ memoryOptOutGrant() { return new PgMemoryOptOutGrantStore(this.pool); }
183
+ legacyRewindBoundaryProbe(query) { return legacyProbeImpl("pg", this.pool, query); }
184
+ legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "pg", this.pool); }
185
+ legacySnapshotPurge(sessionId) { return legacyPurgeImpl("pg", this.pool, sessionId); }
128
186
  workflowJournal() { return new PgWorkflowJournalStore(this.pool); }
129
187
  outcomeSink() { return new PgOutcomeLedger(this.pool); }
130
188
  workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); }
@@ -136,6 +194,7 @@ class PgBackend {
136
194
  imageIndex() { return new PgImageIndex(this.pool); }
137
195
  imageBake() { return new PgImageBake(this.pool); }
138
196
  leaderRun() { return new PgLeaderRunStore(this.pool); }
197
+ device() { return new PgDeviceStore(this.pool); }
139
198
  retention(opts) { return new PgRetentionStore(this.pool, opts); }
140
199
  retentionLane() { return new PgRetentionLaneStore(this.pool); }
141
200
  breaker(onWriteFail) { return new PgBreakerState(this.pool, onWriteFail); }
@@ -166,7 +225,7 @@ class LocalBackend {
166
225
  try {
167
226
  this.fileBackend = new FileStorageBackend({
168
227
  root,
169
- ...(config.rewindSnapshotMaxMb !== undefined ? { snapshotBounds: { maxBytes: Math.round(config.rewindSnapshotMaxMb * 1024 * 1024) } } : {}),
228
+ ...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
170
229
  onCorruptRead: (info) => this.onWarn?.("file_store_corrupt_read", {
171
230
  path: info.path,
172
231
  reason: info.reason,
@@ -175,7 +234,7 @@ class LocalBackend {
175
234
  note: "a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
176
235
  "Consequence depends on which store read it: session-policy = that run lost its SUBTRACTIVE session-rule " +
177
236
  "layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
178
- "rows; file-snapshot = a snapshot or scope read as missing. Inspect the named path before deciding.",
237
+ "rows; file-history = a rewind boundary or scope read as missing. Inspect the named path before deciding.",
179
238
  }),
180
239
  });
181
240
  }
@@ -188,6 +247,7 @@ class LocalBackend {
188
247
  this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root), this.fileBackend.root);
189
248
  this.runStore = new FileRunStore(this.fileBackend.root, {
190
249
  error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
250
+ strictHydrate: config.runStoreStrictHydrate,
191
251
  });
192
252
  this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root);
193
253
  this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root);
@@ -237,7 +297,8 @@ class LocalBackend {
237
297
  }
238
298
  checkpointStoreInst;
239
299
  sessionPolicy() { return this.fileBackend.sessionPolicyStore; }
240
- fileSnapshot() { return this.fileBackend.fileSnapshotStore; }
300
+ fileHistory() { return this.fileBackend.fileHistoryStore; }
301
+ memoryOptOutGrant() { return undefined; }
241
302
  workflowJournal() { return this.workflowJournalStore; }
242
303
  outcomeSink() { return this.outcomeSinkInst; }
243
304
  session() { return this.sessionStore; }
@@ -272,6 +333,8 @@ export async function openStoreBackendWithFallback(config, logger) {
272
333
  catch (err) {
273
334
  if (err instanceof AdoptionError)
274
335
  throw err;
336
+ if (err instanceof RunStoreStrictHydrateError)
337
+ throw err;
275
338
  if (!mayFallback)
276
339
  throw err;
277
340
  logger.warn("store_db_unreachable_fallback_memory", {
@@ -30,9 +30,11 @@ export interface RunRecord {
30
30
  /** Which SYSTEM's credential submitted the run (oa/cc-mcp/portal…), derived at the auth gate — never
31
31
  * self-declared. null = legacy single-token deployment (authenticated, no system identity). */
32
32
  source: string | null;
33
- /** The replica that OWNS this run (the instanceId that created it). Lets a preempt/cancel handler tell
34
- * "this run is on MY replica" (consult the in-process inflight/preemptable map authoritatively) from "owned
35
- * elsewhere" (fall back to the durable cross-replica flag). null for pre-column rows. */
33
+ /** The replica currently HOLDING this run written at createRun and TAKEN OVER by markResuming (S-51: a
34
+ * cross-replica resume moves the seat, so a reader comparing it with its own instanceId is asking "is the
35
+ * live leg here NOW", not "who minted the row"). Lets a preempt/cancel/steer handler tell "this run is on
36
+ * MY replica" (consult the in-process inflight/steerable map authoritatively) from "held elsewhere" (fall
37
+ * back to the durable cross-replica flag). null for pre-column rows. */
36
38
  instanceId: string | null;
37
39
  /** First ~120 chars of the objective (secret-redacted at write) — the list's "what is this task" glance.
38
40
  * null for rows predating the column. */
@@ -33,7 +33,7 @@ export declare function ensurePgTaskListSchema(q: (text: string, params?: unknow
33
33
  * 语义 = 「这个 listKey 的清单**不存在过**」:item 行 + meta 行(高水位 `next_id` / 插入序
34
34
  * `next_sort`)一并抹掉。⚠️ **meta 行必须一起删**:留着它,同一个 listKey 被重新登记后
35
35
  * (会话 id 由调用方自选、删除后可被别人 `register`)新主的第一条任务会从旧主的高水位续号 ——
36
- * 那是一条跨化身的信息泄漏(「上一位在这里建过 47 条」)。
36
+ * 那是一条跨化身的信息外泄(「上一位在这里建过 47 条」)。
37
37
  *
38
38
  * 事务内先对 meta 行 `FOR UPDATE`(与 `mutate` 同一把分区锁)⇒ 与并发的清单写串行,不会删到
39
39
  * 一半被插回新行。meta 行不在(从未写过任何任务)= 无锁可拿,DELETE 命中 0 行,幂等。