@sema-agent/server 7.125.1 → 7.127.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 (275) hide show
  1. package/MIGRATION.md +229 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +346 -145
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +2 -1
  10. package/deploy/sema-up/chart/values.yaml +22 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +20 -13
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  18. package/deploy/sema-up/lib/parse-machines.py +2 -2
  19. package/deploy/sema-up/lib/shell-access.sh +1 -1
  20. package/deploy/sema-up/preflight.sh +4 -4
  21. package/deploy/sema-up/sema-up.sh +33 -33
  22. package/deploy/sema-up/smoke.sh +3 -3
  23. package/deploy/sema-up/stack/compose.yaml +1 -1
  24. package/deploy/sema-up/stack/sema-stack-cluster.yaml +26 -3
  25. package/dist/approval-card.d.ts +20 -4
  26. package/dist/approval-content-kind.d.ts +38 -22
  27. package/dist/approval-content-kind.js +20 -4
  28. package/dist/audit.js +3 -0
  29. package/dist/bench/s1/arms.d.ts +9 -37
  30. package/dist/bench/s1/arms.js +4 -61
  31. package/dist/bench/s1/live-deps.d.ts +4 -10
  32. package/dist/bench/s1/live-deps.js +1 -222
  33. package/dist/bench/s1/oracle.d.ts +1 -1
  34. package/dist/bench/s1/reviewer.d.ts +1 -1
  35. package/dist/bench/s1/row.d.ts +5 -7
  36. package/dist/bench/s1/row.js +0 -2
  37. package/dist/bench/s1/run-firm.d.ts +2 -2
  38. package/dist/bench/s1/run-firm.js +1 -4
  39. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  40. package/dist/bench/s1/runner-ctx.js +3 -7
  41. package/dist/bench/s1/tasks.d.ts +9 -46
  42. package/dist/bench/s1/tasks.js +4 -63
  43. package/dist/boot/execution-env.d.ts +38 -26
  44. package/dist/boot/execution-env.js +87 -41
  45. package/dist/boot/leader.d.ts +2 -1
  46. package/dist/boot/memory-boundary.d.ts +1 -1
  47. package/dist/boot/memory-consolidation.d.ts +37 -3
  48. package/dist/boot/memory-consolidation.js +18 -2
  49. package/dist/boot/parked-revive-gate.d.ts +0 -3
  50. package/dist/boot/parked-revive-gate.js +2 -2
  51. package/dist/boot/resolve-spec.d.ts +0 -2
  52. package/dist/boot/resolve-spec.js +12 -17
  53. package/dist/boot/retention-lane.d.ts +12 -1
  54. package/dist/boot/retention-lane.js +2 -0
  55. package/dist/boot/runner-deps.d.ts +7 -6
  56. package/dist/boot/runner-deps.js +5 -16
  57. package/dist/boot/session-faces.d.ts +7 -2
  58. package/dist/boot/session-faces.js +37 -1
  59. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  60. package/dist/boot/stage-03-store-backend.js +2 -2
  61. package/dist/boot/stage-04-stores.d.ts +1 -0
  62. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  63. package/dist/boot/stage-05-execution-env.js +7 -4
  64. package/dist/boot/stage-06-runners.d.ts +2 -3
  65. package/dist/boot/stage-06-runners.js +13 -9
  66. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  67. package/dist/boot/stage-07-capability-layer.js +3 -4
  68. package/dist/boot/stage-08-reapers.d.ts +2 -3
  69. package/dist/boot/stage-08-reapers.js +2 -2
  70. package/dist/boot/stage-09-leader.d.ts +2 -3
  71. package/dist/boot/stage-09-leader.js +2 -1
  72. package/dist/boot/stage-10-http-server.d.ts +2 -3
  73. package/dist/boot/stage-10-http-server.js +2 -2
  74. package/dist/boot/stores.d.ts +1 -0
  75. package/dist/boot/stores.js +4 -3
  76. package/dist/boot/web-admin.d.ts +5 -0
  77. package/dist/boot/web-admin.js +28 -14
  78. package/dist/brain.d.ts +2 -2
  79. package/dist/brain.js +1 -1
  80. package/dist/capabilities/builtin-tools.d.ts +2 -1
  81. package/dist/capabilities/composite-members.d.ts +1 -1
  82. package/dist/capabilities/composite-members.js +1 -1
  83. package/dist/capabilities/hands-lane.d.ts +9 -6
  84. package/dist/capabilities/hands-lane.js +2 -2
  85. package/dist/capabilities/memory-notice.d.ts +27 -22
  86. package/dist/capabilities/memory-notice.js +15 -17
  87. package/dist/capabilities/scenarios.d.ts +9 -14
  88. package/dist/capabilities/scenarios.js +5 -50
  89. package/dist/capability-facts.d.ts +22 -11
  90. package/dist/capability-facts.js +13 -6
  91. package/dist/config-catalog.js +14 -5
  92. package/dist/config-provider.d.ts +5 -1
  93. package/dist/config-provider.js +23 -4
  94. package/dist/config-types.d.ts +19 -2
  95. package/dist/config.js +41 -6
  96. package/dist/deployment-governance.d.ts +19 -21
  97. package/dist/deployment-governance.js +7 -8
  98. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  99. package/dist/env-name-allowlist-knobs.js +1 -0
  100. package/dist/execution-lane-caps.d.ts +12 -0
  101. package/dist/execution-lane-caps.js +18 -4
  102. package/dist/fleet/subagent-tail-bus.js +15 -5
  103. package/dist/hooks/hook-runner.d.ts +84 -30
  104. package/dist/hooks/hook-runner.js +74 -41
  105. package/dist/http/admission.js +17 -10
  106. package/dist/http/durable-run-leg.d.ts +5 -4
  107. package/dist/http/durable-run-leg.js +2 -4
  108. package/dist/http/idempotency.d.ts +28 -6
  109. package/dist/http/idempotency.js +10 -2
  110. package/dist/http/principal-gate.d.ts +14 -0
  111. package/dist/http/principal-gate.js +7 -0
  112. package/dist/http/request-guards.d.ts +2 -1
  113. package/dist/http/request-guards.js +14 -14
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +20 -10
  116. package/dist/http/route-ctx.d.ts +15 -7
  117. package/dist/http/routes/a2a-serve.js +11 -7
  118. package/dist/http/routes/approvals-assistant.js +18 -3
  119. package/dist/http/routes/background.d.ts +1 -1
  120. package/dist/http/routes/background.js +1 -1
  121. package/dist/http/routes/capabilities.js +10 -2
  122. package/dist/http/routes/mcp-reconnect.js +1 -1
  123. package/dist/http/routes/runs.d.ts +4 -3
  124. package/dist/http/routes/runs.js +35 -14
  125. package/dist/http/routes/session-sync.js +14 -11
  126. package/dist/http/routes/sessions.js +9 -9
  127. package/dist/http/routes/side-query.js +6 -19
  128. package/dist/http/routes/tasks.js +29 -19
  129. package/dist/http/routes/workflows.js +8 -0
  130. package/dist/http/server.d.ts +4 -0
  131. package/dist/http/server.js +3 -5
  132. package/dist/http/submit-replay.d.ts +15 -8
  133. package/dist/http/submit-replay.js +6 -0
  134. package/dist/http/wire-types.d.ts +7 -9
  135. package/dist/index.d.ts +1 -1
  136. package/dist/index.js +1 -1
  137. package/dist/internal-run-env.d.ts +30 -0
  138. package/dist/internal-run-env.js +11 -0
  139. package/dist/leader/wire.js +3 -2
  140. package/dist/lsp/manager.js +1 -1
  141. package/dist/memory-scope.d.ts +19 -7
  142. package/dist/memory-scope.js +25 -8
  143. package/dist/observability/fail-open.d.ts +31 -3
  144. package/dist/observability/fail-open.js +31 -3
  145. package/dist/observability/metrics.d.ts +0 -1
  146. package/dist/observability/metrics.js +1 -4
  147. package/dist/observability/run-terminal.js +72 -4
  148. package/dist/permission-rule-vocab.d.ts +3 -2
  149. package/dist/permission-rule-vocab.js +1 -1
  150. package/dist/plan-cache-probe.d.ts +3 -2
  151. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  152. package/dist/plugins/caching-session-store.d.ts +42 -22
  153. package/dist/plugins/caching-session-store.js +19 -9
  154. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  155. package/dist/plugins/checkpoint-store-sql.js +57 -11
  156. package/dist/plugins/file-run-store.d.ts +25 -21
  157. package/dist/plugins/file-run-store.js +70 -34
  158. package/dist/plugins/k8s-exec-protocol.js +3 -0
  159. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  160. package/dist/plugins/local-checkpoint-store.js +5 -15
  161. package/dist/plugins/local-session-store.d.ts +66 -7
  162. package/dist/plugins/local-session-store.js +168 -101
  163. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  164. package/dist/plugins/memory-run-store.d.ts +19 -3
  165. package/dist/plugins/memory-run-store.js +38 -3
  166. package/dist/plugins/one-time-migrations.d.ts +165 -39
  167. package/dist/plugins/one-time-migrations.js +51 -144
  168. package/dist/plugins/pg-pool.js +11 -1
  169. package/dist/plugins/pg-session-storage.d.ts +2 -0
  170. package/dist/plugins/pg-session-storage.js +9 -5
  171. package/dist/plugins/remote-env-adb.d.ts +3 -0
  172. package/dist/plugins/remote-env-adb.js +1 -0
  173. package/dist/plugins/remote-env-device.d.ts +3 -0
  174. package/dist/plugins/remote-env-device.js +1 -0
  175. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  176. package/dist/plugins/remote-env-e2b.js +2 -1
  177. package/dist/plugins/remote-env-host.d.ts +23 -1
  178. package/dist/plugins/remote-env-host.js +20 -2
  179. package/dist/plugins/remote-env-k8s.d.ts +264 -41
  180. package/dist/plugins/remote-env-k8s.js +551 -82
  181. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  182. package/dist/plugins/remote-env-local-docker.js +2 -1
  183. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  184. package/dist/plugins/remote-env-ssh.js +1 -0
  185. package/dist/plugins/retention-store-sql.d.ts +4 -1
  186. package/dist/plugins/retention-store-sql.js +11 -9
  187. package/dist/plugins/run-store-sql.d.ts +44 -6
  188. package/dist/plugins/run-store-sql.js +92 -27
  189. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  190. package/dist/plugins/session-store.d.ts +4 -3
  191. package/dist/plugins/session-store.js +1 -1
  192. package/dist/plugins/sql-driver.d.ts +2 -1
  193. package/dist/plugins/store-backend.d.ts +12 -5
  194. package/dist/plugins/store-backend.js +13 -4
  195. package/dist/plugins/store-contracts.d.ts +20 -3
  196. package/dist/plugins/store-open-wait.js +3 -0
  197. package/dist/plugins/tidb-pool.js +15 -3
  198. package/dist/plugins/tidb-session-storage.js +3 -2
  199. package/dist/plugins/tidb-session-store.d.ts +2 -0
  200. package/dist/plugins/tidb-session-store.js +6 -2
  201. package/dist/request-key-closure.d.ts +32 -7
  202. package/dist/request-key-closure.js +25 -9
  203. package/dist/router/route-orchestration.d.ts +3 -11
  204. package/dist/router/route-orchestration.js +0 -4
  205. package/dist/rule-entry-list.js +0 -4
  206. package/dist/rules-consent.js +3 -0
  207. package/dist/run-acceptance.d.ts +51 -0
  208. package/dist/run-acceptance.js +27 -0
  209. package/dist/run-local.d.ts +5 -4
  210. package/dist/run-local.js +23 -17
  211. package/dist/runs.d.ts +7 -11
  212. package/dist/runs.js +6 -16
  213. package/dist/sandbox-egress-policy.d.ts +61 -0
  214. package/dist/sandbox-egress-policy.js +235 -0
  215. package/dist/security.d.ts +7 -0
  216. package/dist/security.js +5 -0
  217. package/dist/server-secret-env.d.ts +10 -4
  218. package/dist/server-secret-env.js +4 -1
  219. package/dist/session-error-wire.d.ts +2 -2
  220. package/dist/session-incarnation.d.ts +46 -2
  221. package/dist/session-incarnation.js +14 -3
  222. package/dist/session-sync-kernel.d.ts +10 -3
  223. package/dist/session-titler.d.ts +5 -3
  224. package/dist/spec-fields.d.ts +24 -8
  225. package/dist/spec-fields.js +21 -2
  226. package/dist/task-workflow.d.ts +6 -4
  227. package/dist/task-workflow.js +3 -2
  228. package/dist/tool-approval.d.ts +78 -6
  229. package/dist/tool-approval.js +7 -3
  230. package/dist/trace/core-keyset-guard.d.ts +6 -5
  231. package/dist/trace/engine-notice-wire.d.ts +26 -1
  232. package/dist/trace/engine-notice-wire.js +7 -2
  233. package/dist/trace/ledger-events.d.ts +9 -3
  234. package/dist/trace/ledger-sink.d.ts +7 -0
  235. package/dist/trace/ledger-sink.js +14 -4
  236. package/dist/trace/project.d.ts +57 -11
  237. package/dist/trace/project.js +54 -8
  238. package/dist/trace/self-minted-wire.d.ts +94 -2
  239. package/dist/trace/self-minted-wire.js +28 -0
  240. package/dist/trace/tool-output-media.d.ts +26 -0
  241. package/dist/trace/tool-output-media.js +57 -0
  242. package/dist/trace/wire-projection-faces.d.ts +7 -1
  243. package/dist/trace/wire-projection-faces.js +2 -1
  244. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  245. package/dist/web-admin-client/apply-effective.js +11 -26
  246. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  247. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  248. package/dist/web-admin-client/candidate-gate.js +9 -2
  249. package/dist/web-admin-client/facade.d.ts +2 -2
  250. package/dist/web-admin-client/http-client.d.ts +3 -1
  251. package/dist/web-admin-client/http-client.js +4 -3
  252. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  253. package/dist/web-admin-client/read-warnings.js +41 -6
  254. package/dist/web-admin-client/types.d.ts +11 -5
  255. package/dist/workspace-cache.d.ts +45 -0
  256. package/dist/workspace-cache.js +30 -0
  257. package/dist/workspace-space-commit.d.ts +116 -0
  258. package/dist/workspace-space-commit.js +117 -0
  259. package/dist/workspace-space.d.ts +75 -9
  260. package/dist/workspace-space.js +52 -1
  261. package/package.json +4 -4
  262. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  263. package/dist/boot/session-incarnation-seat.js +0 -69
  264. package/dist/capabilities/code-review-council.d.ts +0 -49
  265. package/dist/capabilities/code-review-council.js +0 -111
  266. package/dist/capabilities/collab-wire.d.ts +0 -36
  267. package/dist/capabilities/collab-wire.js +0 -2
  268. package/dist/capabilities/collab-workflows.d.ts +0 -47
  269. package/dist/capabilities/collab-workflows.js +0 -90
  270. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  271. package/dist/capabilities/prompts/code-review.js +0 -39
  272. package/dist/capabilities/prompts/team.d.ts +0 -10
  273. package/dist/capabilities/prompts/team.js +0 -6
  274. package/dist/capabilities/team.d.ts +0 -113
  275. package/dist/capabilities/team.js +0 -70
@@ -8,24 +8,36 @@ export interface CachingOptions {
8
8
  /** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
9
9
  ttlSec: number;
10
10
  sweepIntervalMs?: number;
11
- /** S25 (SILENT-FALLBACK P1): called when a cross-instance write (import/replace) evicts a WARM entry —
12
- * the affinity-contract violation signal that previously had zero fingerprint. */
13
- onConflictEvict?: (sessionId: string) => void;
11
+ /** Called once per warm hit that turned out stale (the store's head no longer names the cached leaf, or the session is
12
+ * gone): the entry was dropped and the session woken from the store again. Not an error — the operator signal for
13
+ * "requests for one session alternate between replicas within the TTL" (the hit rate affinity would buy back). */
14
+ onStaleHit?: (sessionId: string) => void;
14
15
  }
16
+ /** The store's current head for one session — the one read every warm hit is checked against (`undefined` = no such
17
+ * session). The same single-row read `OwnerAwareSessionStore.getHead` names; this cache requires its inner store to have it. */
18
+ export type SessionHeadReader = Required<Pick<OwnerAwareSessionStore, "getHead">>;
15
19
  /**
16
- * Warm/sticky cache in front of a durable {@link SessionStore} (Option A).
20
+ * Warm cache in front of a durable {@link SessionStore}.
17
21
  *
18
- * Keeps a woken session in memory and **reuses the same storage instance across turns**, so a hot
19
- * session is not re-read from TiDB every short turn (treats the cattle like a warm pet while it's
20
- * active). Idle sessions expire after `ttlSec`; `release` evicts immediately.
22
+ * Keeps a woken session in memory and **reuses the same storage instance across turns**, so an active session is not
23
+ * woken from the store (meta row + event log) on every short turn. Idle sessions expire after `ttlSec`; `release` evicts.
21
24
  *
22
- * ⚠️ **Session affinity is REQUIRED** when this cache is enabled (`SESSION_CACHE_TTL_SEC>0`): route a
23
- * given sessionId to the same instance. Under affinity the cached storage is the sole writer, so its
24
- * in-memory leaf is always current and consistent with TiDB (write-through; F2 CAS still applies).
25
- * Without affinity a concurrent write from another instance makes this cache stale; that surfaces as
26
- * a `SessionError("conflict")` on the next append — the async run driver calls `evict()` on conflict
27
- * (defense-in-depth) and the entry also expires after `ttlSec`, but those are backstops, not the
28
- * guarantee. If you cannot guarantee affinity, set `SESSION_CACHE_TTL_SEC=0` to disable the cache.
25
+ * **One rule: a warm entry is served only while the store's head for that session still names the leaf the entry
26
+ * holds.** Every hit reads the head first (`getHead` — one primary-key row read on the SQL twins); head gone or leaf
27
+ * moved ⇒ the entry is dropped and the session is woken from the store again ({@link CachingOptions.onStaleHit}). The
28
+ * leaf is the token because it is exactly what the stores' conditional writes compare (`expectedLeafId`): a handle this
29
+ * cache hands out is never behind the store's leaf, so those writes fail only on a genuinely concurrent write — two runs
30
+ * writing one session at the same time. Any number of replicas may share one store under any routing; session affinity
31
+ * only raises the hit rate.
32
+ *
33
+ * Open item (the same before this rule): a write that rewrites history while leaving the final leaf id where it was —
34
+ * a session-sync replace / `overwrite-dst` staged commit on ANOTHER replica that changes entry payloads under the same
35
+ * ids — moves no leaf, so a warm entry elsewhere keeps its old payloads until it expires or is evicted. Its conditional
36
+ * writes still succeed (the leaf matches); what is stale is the content of the entries under it. Closing it needs a
37
+ * store revision bumped by every write path (append, leaf move, replace, staged commit) and carried by the woken handle.
38
+ *
39
+ * Writes that go around the cached handle through THIS wrapper (import / replace / staged commit / delete) also drop
40
+ * the entry at the write site, so the next acquire wakes from the written state.
29
41
  *
30
42
  * Orthogonal to the F3 branch-floor change: floor bounds the *cold wake* size; this cache removes
31
43
  * *repeated* re-reads of an active session.
@@ -36,12 +48,16 @@ export declare class CachingSessionStore implements SessionStore {
36
48
  private readonly pending;
37
49
  private readonly ttlMs;
38
50
  private readonly timer?;
51
+ private readonly onStaleHit?;
39
52
  /** Delegated tenant-ownership ops, present only if the inner store exposes them (the TiDB store does). */
40
53
  readonly ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
41
54
  /** S-790:属主 + 化身一次读,纯转发(不经暖缓存 —— 化身判的就是「此刻坐在座位上的是谁」,缓存的旧答案正是要消灭的那一形)。
42
55
  * 🔴 必须转发(与 retention / placements / instruction-source 同一条教训):生产默认把真店包进本类,不转发 = parked 决定腿
43
56
  * 与记忆座在默认部署上读不到化身。 */
44
57
  readonly sessionFaceOf?: SessionFaceReader;
58
+ /** core `SessionStore.incarnationOf`(按 id 读住客)—— 同一条纯转发、同一条理由:答的是行上那一格,绝不从暖缓存里那只会话的元数据答
59
+ * (缓存的会话活得比「另一只店删掉又重建了这一行」长)。🔴 必须转发:k8s 工作区空间的每 run 读与起服门读的就是本类。 */
60
+ readonly incarnationOf?: SessionStore["incarnationOf"];
45
61
  readonly register?: (sessionId: string, owner: string | null) => Promise<void>;
46
62
  /** Delegated SessionRepo seam (§0.5) — list/fork/delete operate on the DURABLE tables directly (they bypass the
47
63
  * warm cache by design; a fork/delete must read/write durable state, not a possibly-stale cached view). */
@@ -73,11 +89,8 @@ export declare class CachingSessionStore implements SessionStore {
73
89
  /** E18 — delegate the leaf read straight to inner (a cache-bypassing single SELECT; the resume-at turn-capture
74
90
  * reads the DURABLE leaf, not a possibly-stale cached view — same discipline as the §0.5 repo ops above). */
75
91
  readonly getLeafId?: (sessionId: string) => Promise<string | null>;
76
- /** [ref] 六轮复审:owner+leafId 原子快照透传(cache-bypassing,getLeafId 同姿势)。 */
77
- readonly getHead?: (sessionId: string) => Promise<{
78
- owner: string | null;
79
- leafId: string | null;
80
- } | undefined>;
92
+ /** [ref] 六轮复审:owner+leafId 原子快照透传(cache-bypassing,getLeafId 同姿势)。内层店必有(本类的暖命中按它核新鲜)。 */
93
+ readonly getHead: SessionHeadReader["getHead"];
81
94
  /** 2c session-sync — delegated export/import seam, DURABLE (bypasses the warm cache exactly like
82
95
  * list/fork: an export reads the full durable log, an import writes durable state). */
83
96
  readonly exportEntries?: (sessionId: string) => Promise<SessionTreeEntry[] | null>;
@@ -96,7 +109,7 @@ export declare class CachingSessionStore implements SessionStore {
96
109
  * 转发白名单里 ⇒ 默认部署(SESSION_CACHE_TTL_SEC=300 把真店包进本类)PUSH Phase A 恒 501
97
110
  * `capability.session_store_required`,而 capabilities().sessionSync 谓词三项(backend/exportEntries/
98
111
  * fileHistoryStore)都被转发了 ⇒ 照报 true(能力面谎)。转发形:handle 的 `commit` 是唯一写真 id 的
99
- * 时刻,成功后逐出该 id 的暖 handle + S25 指纹(importEntries/replaceEntries 同纪律);`abort` 只丢
112
+ * 时刻,成功后逐出该 id 的暖 handle(importEntries/replaceEntries 同纪律);`abort` 只丢
100
113
  * staging 行、不碰真 id ⇒ 不逐出。 */
101
114
  readonly beginImportStaging?: (realSessionId: string, token: string) => StagingHandle;
102
115
  /** Staging 清扫面(kernel `ImportStagingStore.sweepStagingSessions`)。当下 reaper 走 `backend.session()`
@@ -133,11 +146,18 @@ export declare class CachingSessionStore implements SessionStore {
133
146
  scope?: string;
134
147
  }) => Promise<PlacedSessionRow[]>;
135
148
  readonly placementOf?: (sessionId: string) => Promise<SessionPlacementRecord | undefined>;
136
- constructor(inner: SessionStore, opts: CachingOptions);
149
+ constructor(inner: SessionStore & SessionHeadReader, opts: CachingOptions);
137
150
  acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
138
151
  touch(sessionId: string): Promise<void>;
139
152
  release(sessionId: string): Promise<void>;
140
- /** Drop a session from the warm cache (e.g. after a cross-instance write conflict). */
153
+ /**
154
+ * The cache's one rule: the warm entry for `sessionId`, if the store's head still names the leaf it holds; otherwise
155
+ * the entry is dropped (counted once via `onStaleHit`) and `undefined` sends the caller to the store. The head is read
156
+ * first, the cached leaf second: a write this replica commits in between can only turn a fresh verdict into a stale
157
+ * one (an extra wake), never the reverse.
158
+ */
159
+ private freshHit;
160
+ /** Drop a session from the warm cache. */
141
161
  evict(sessionId: string): void;
142
162
  get size(): number;
143
163
  dispose(): void;
@@ -5,8 +5,10 @@ export class CachingSessionStore {
5
5
  pending = new Map();
6
6
  ttlMs;
7
7
  timer;
8
+ onStaleHit;
8
9
  ownerOf;
9
10
  sessionFaceOf;
11
+ incarnationOf;
10
12
  register;
11
13
  listSessions;
12
14
  fork;
@@ -34,6 +36,8 @@ export class CachingSessionStore {
34
36
  constructor(inner, opts) {
35
37
  this.inner = inner;
36
38
  this.ttlMs = Math.max(0, opts.ttlSec) * 1000;
39
+ this.onStaleHit = opts.onStaleHit;
40
+ this.getHead = inner.getHead.bind(inner);
37
41
  const declared = inner.retention;
38
42
  if (declared !== undefined)
39
43
  this.retention = declared;
@@ -49,6 +53,8 @@ export class CachingSessionStore {
49
53
  this.ownerOf = ownerAware.ownerOf.bind(inner);
50
54
  if (ownerAware.sessionFaceOf)
51
55
  this.sessionFaceOf = ownerAware.sessionFaceOf.bind(inner);
56
+ if (typeof inner.incarnationOf === "function")
57
+ this.incarnationOf = inner.incarnationOf.bind(inner);
52
58
  if (ownerAware.register)
53
59
  this.register = ownerAware.register.bind(inner);
54
60
  if (ownerAware.listSessions)
@@ -69,8 +75,6 @@ export class CachingSessionStore {
69
75
  this.fork = ownerAware.fork.bind(inner);
70
76
  if (ownerAware.getLeafId)
71
77
  this.getLeafId = ownerAware.getLeafId.bind(inner);
72
- if (ownerAware.getHead)
73
- this.getHead = ownerAware.getHead.bind(inner);
74
78
  if (ownerAware.exportEntries)
75
79
  this.exportEntries = ownerAware.exportEntries.bind(inner);
76
80
  if (ownerAware.listEntryIds)
@@ -85,8 +89,6 @@ export class CachingSessionStore {
85
89
  const imp = ownerAware.importEntries.bind(inner);
86
90
  this.importEntries = async (sessionId, owner, entries) => {
87
91
  await imp(sessionId, owner, entries);
88
- if (this.cache.has(sessionId))
89
- opts.onConflictEvict?.(sessionId);
90
92
  this.evict(sessionId);
91
93
  };
92
94
  }
@@ -94,8 +96,6 @@ export class CachingSessionStore {
94
96
  const rep = ownerAware.replaceEntries.bind(inner);
95
97
  this.replaceEntries = async (sessionId, owner, entries) => {
96
98
  await rep(sessionId, owner, entries);
97
- if (this.cache.has(sessionId))
98
- opts.onConflictEvict?.(sessionId);
99
99
  this.evict(sessionId);
100
100
  };
101
101
  }
@@ -109,8 +109,6 @@ export class CachingSessionStore {
109
109
  appendBatch: (entries) => h.appendBatch(entries),
110
110
  commit: async (owner, o) => {
111
111
  const r = await h.commit(owner, o);
112
- if (this.cache.has(realSessionId))
113
- opts.onConflictEvict?.(realSessionId);
114
112
  this.evict(realSessionId);
115
113
  return r;
116
114
  },
@@ -141,7 +139,7 @@ export class CachingSessionStore {
141
139
  this.store(fresh);
142
140
  return fresh;
143
141
  }
144
- const hit = this.cache.get(sessionId);
142
+ const hit = await this.freshHit(sessionId);
145
143
  if (hit && opts?.requireExisting !== true && this.placements !== undefined && this.placementOf !== undefined) {
146
144
  assertPlacementAdmission(sessionId, await this.placementOf(sessionId), opts);
147
145
  }
@@ -182,6 +180,18 @@ export class CachingSessionStore {
182
180
  this.cache.delete(sessionId);
183
181
  await this.inner.release(sessionId);
184
182
  }
183
+ async freshHit(sessionId) {
184
+ const hit = this.cache.get(sessionId);
185
+ if (hit === undefined)
186
+ return undefined;
187
+ const head = await this.inner.getHead(sessionId);
188
+ if (head !== undefined && head.leafId === (await hit.acquired.session.getLeafId()))
189
+ return hit;
190
+ if (this.cache.get(sessionId) === hit)
191
+ this.cache.delete(sessionId);
192
+ this.onStaleHit?.(sessionId);
193
+ return undefined;
194
+ }
185
195
  evict(sessionId) {
186
196
  this.cache.delete(sessionId);
187
197
  }
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
3
+ import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity, type ToolApprovalCard } from "@sema-agent/core";
4
4
  import { type ParkRowFacts, type RuleOfferProjection } from "../approval-card.js";
5
5
  import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
6
6
  export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
@@ -127,17 +127,14 @@ export interface PendingCheckpoint extends ParkRowFacts {
127
127
  */
128
128
  hasBidiControls?: true;
129
129
  /**
130
- * [ref]([ref]② core 点名 / [ref] 认领):**内容问句分型键** —— `"content_ask"` 当这条 tool_approval
131
- * park 门住的是保留工具 AskUserQuestion(问用户的问题,不是副作用工具),消费端(cli/壳审批面)据它
132
- * 渲问句 UI 而不用嗅探 `toolName`。词与判据 = core `summarizeCheckpoint` 的同一行规则(行上 core 铸的
133
- * `toolName` 的**全函数**派生;词属主 `approval-content-kind.ts`,parity 钉看漂移)——**不是**
134
- * `hasBidiControls` 那类「server 不重算」的扫描位:派生 ≠ 重扫。
135
- * 🔴 `"content_ask"` 或**缺席**([ref]② OMIT 纪律,恒不铸 `null`/`false`):缺席 = 非内容问句门
136
- * (普通工具门 / 非工具 park / toolName 缺席的存量行)。展示/分诊用,永不参与 resume / gate / CAS。
137
- * 两条 durable 读面(`GET /v1/approvals` 行 + `/stream` `pending` 帧)经同一投影同时携带;live 腿的
138
- * 同名键在 `LivePendingRow.contentKind`(同一词属主)。
130
+ * core 7.49.0 #1414:**这一行渲在哪张卡上** —— 门上引擎铸的 `gate.approvalCard`(闭集 core `TOOL_APPROVAL_CARDS`;`question` = 问用户的
131
+ * 问题,不是副作用工具),即 core `summarizeCheckpoint` 投给 `CheckpointSummary.approvalCard` 的同一个词。读法单点 = `approval-content-kind.ts`
132
+ * 的 `parkedApprovalCard`(只在审批门型上读、闭集窄读;两只店同调它:SQL 孪生读 `checkpoint` 列的 `gate` 子树,LOCAL 孪生读 blob 上的门)。
133
+ * 🔴 缺席即缺席([ref]② OMIT 纪律):非审批门、7.49.0 之前停驻的存量行(门上无卡 —— core 的读法「无卡 = 不是一问」,本仓**不**从
134
+ * 工具名回推)。两条 durable 读面(`GET /v1/approvals` 行 + `/stream` `pending` 帧)经同一投影同时携带;live 腿的同名键在
135
+ * `LivePendingRow.approvalCard`。展示/分诊用,永不参与 resume / gate / CAS。(此前此处是按 `tool_name` 列派生的 `contentKind`,随 #1414 删。)
139
136
  */
140
- contentKind?: "content_ask";
137
+ approvalCard?: ToolApprovalCard;
141
138
  }
142
139
  /**
143
140
  * [ref]([ref] / DEBTS [ref]):session 键读口(`findPendingTokenBySession` / `peekPendingScope`)的**行绑定**。
@@ -416,8 +413,8 @@ interface SteerColumnRead {
416
413
  * **一列一形**:`pending_steer_queue` 存 `PendingSteerEntry[]`。
417
414
  *
418
415
  * 🔴 7.74.0 起只剩这**一**列。队列化之前的裸单座列 `pending_steer` 随它的席位一起退役了(core 7.17.0
419
- * [ref] d:引擎对带席的行 PRE-CAS 拒 `seat_retired`),存量列由启动期的一次性迁移删掉
420
- * (`plugins/one-time-migrations.ts`:列上还有非空值 ⇒ **响亮拒启**,清空后 `DROP COLUMN`)。
416
+ * [ref] d:引擎对带席的行 PRE-CAS 拒 `seat_retired`),存量列 7.74.0 起由启动期的一次性迁移删掉;迁移窗关后删码,
417
+ * 仍带着这一列的跳版升级库起服**响亮拒**(`plugins/one-time-migrations.ts` 的退役 schema 拒启臂)。
421
418
  * ⇒ 这里刻意**没有**第二条读腿:双读臂的寿命就是那一次迁移,不是永远。
422
419
  *
423
420
  * 列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
@@ -623,29 +620,46 @@ export declare class SqlCheckpointStore implements CheckpointStore {
623
620
  *
624
621
  * [ref] D-D: the deadline-branch EXPIRES (≈ abort) every kind EXCEPT a tool-approval human/irreversible_ask
625
622
  * gate — those are resolve-DENIED gracefully by the SLA deny-sweep (listExpiredApprovalGates →
626
- * resumeCheckpoint(deny)), so expiring them here too would race + lose the graceful denial. 🔴 A durable
627
- * AskUserQuestion ALSO mints gate.kind='human' (no question-specific kind in core) — but DENYING a question is
628
- * incoherent (the model gets a "denied" tool-result, not an answer), so it is carved BACK INTO the expire path
629
- * (COALESCE(tool_name,'')='AskUserQuestion') to abort-expire on timeout instead. Legacy rows (gate_kind NULL)
630
- * stay on the expire path. The terminal_at_ms-branch is the crash-safe backstop for ANY kind (incl. a human gate
631
- * whose deny-resume keeps failing) — it always abort-expires past the absolute cap (which is now STRICTLY after
632
- * the deadline, so it never races the deny-sweep at the deadline instant).
623
+ * resumeCheckpoint(deny)), so expiring them here too would race + lose the graceful denial. Legacy rows (gate_kind
624
+ * NULL) stay on the expire path. The terminal_at_ms-branch is the crash-safe backstop for ANY kind (incl. a human
625
+ * gate whose deny-resume keeps failing) — it always abort-expires past the absolute cap (which is now STRICTLY
626
+ * after the deadline, so it never races the deny-sweep at the deadline instant).
627
+ *
628
+ * core 7.49.0 #1414 / #1468 A-S4: among the approval kinds, which rows the deny-sweep owns has ONE judge — core
629
+ * `facetOf(gate)`. A gate carrying the `question` card is facet `B` (a question's deadline voids it; denying a question
630
+ * is incoherent), so step ② expires it here row by row (token CAS, `status='pending'` guarded — a concurrent decide or
631
+ * another replica wins cleanly). The card lives only in the `checkpoint` blob's `gate` subtree, so the split cannot be
632
+ * one SQL predicate without writing core's judgment a second time in SQL; the old `tool_name` literal arm is gone. A
633
+ * gate with no card (parked before 7.49.0) is no question — core's reading — and stays with the deny-sweep.
633
634
  */
634
635
  reapExpired(cutoff: number): Promise<number>;
636
+ /** reapExpired ②'s position in the overdue approval rows, per store instance (see there). Absent = start from the earliest deadline. */
637
+ private reapCursor;
635
638
  /**
636
- * [ref] D-D (SLA-timer): the sessions of pending human/irreversible_ask checkpoints past their `deadline`
637
- * (the per-approval SLA). The reaper resolve-DENIES each (re-resume with decision=deny → the model continues
638
- * with the denial) — graceful, vs the abort that expire() gives resource_limit/needs_review. Excludes legacy
639
- * rows (gate_kind NULL — they predate the split and fall through to reapExpired's expire path). LIMIT bounds
640
- * the per-tick deny-resume work (each is a model run). Scope is read off the COMMITTED row (invariant #4).
641
- * 🔴 EXCLUDES AskUserQuestion gates (which also mint gate.kind='human'): denying a question is incoherent —
642
- * the model would get a "denied" tool-result instead of an answer — so a timed-out durable question falls to
643
- * reapExpired's abort-expire path instead of this graceful-deny path.
639
+ * [ref] D-D (SLA-timer): the sessions of pending approval-kind checkpoints past their `deadline` (the per-approval
640
+ * SLA) that the deny-sweep owns ({@link denySweepOwns} — core `facetOf` reads them as facet `A`) — the reaper resolve-DENIES
641
+ * each (re-resume with decision=deny → the model continues with the denial) — graceful, vs the abort that expire() gives
642
+ * resource_limit/needs_review. Excludes legacy rows (gate_kind NULL — they predate the split and fall through to
643
+ * reapExpired's expire path). `limit` bounds the per-tick deny-resume work (each is a model run).
644
+ * 🔴 core 7.49.0 #1414 / #1468 A-S4: a gate carrying the `question` card (facet `B`) is NOT deny-swept — core refuses
645
+ * `sla_timeout` on it pre-CAS — reapExpired voids it instead. The card lives only in the blob's `gate` subtree and the
646
+ * judge is core's (the split is not one SQL predicate — see {@link reapExpired}), so the filter runs on the rows read;
647
+ * the read is therefore PAGED, filter first and `limit` after: pages of {@link REAP_CLASSIFY_BATCH} rows in the stable
648
+ * (deadline, token) order, the cursor advancing, until `limit` rows are collected or the overdue rows run out — the same
649
+ * "filter, then take `limit`" the LOCAL twin does. A backlog of question rows ahead of the deny-sweep's rows costs extra
650
+ * bounded pages, never a hidden deny-sweep row (they used to hold slots of a `LIMIT limit` window until voided).
644
651
  */
645
652
  listExpiredApprovalGates(cutoff: number, limit?: number): Promise<Array<{
646
653
  sessionId: string;
647
654
  scope: string;
648
655
  }>>;
656
+ /**
657
+ * core 7.49.0 #1414 / #1468 A-S4 —— 审批门型里过了 deadline 的 pending 行(deadline 升序),每行带 core 对它的到期读法
658
+ * (`denySweepOwns` = 审批门型 ∧ core `facetOf` 读作 `A`,门型取列、卡取 `gate` 子树)。到期两条腿(reapExpired 的 ②、拒扫腿的选行)读同一只:
659
+ * 对审批门型「谁拒扫、谁作废」本仓不写第二份判据。读恒有界(每次一页 `limit` 行),序 = (deadline, token) 稳定序;`after` = 从这一位置之后读
660
+ * (reapExpired 的跨拍游标;拒扫腿选行一次之内的翻页游标)。
661
+ */
662
+ private overdueApprovalRows;
649
663
  /** Pending checkpoints for the operator queue — NON-secret fields only (never the token). `taskId` is
650
664
  * joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
651
665
  * session claim, so the join is live for every pending row; null only in pathological windows). */
@@ -1,6 +1,6 @@
1
- import { ASK_USER_QUESTION_SQL_LITERAL, contentKindOf } from "../approval-content-kind.js";
1
+ import { parkedApprovalCard, parkedFacetOf } from "../approval-content-kind.js";
2
2
  import { createHash } from "node:crypto";
3
- import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
3
+ import { APPROVAL_GATE_KINDS_SQL_IN, denySweepOwns } from "../tool-approval.js";
4
4
  import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
5
5
  import { redactDeep, redactSecrets } from "../trace/redact.js";
6
6
  import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema, backfillLegacyBatchMemberKind, parkRowFacts, screenUncoveredDetailSeat } from "../approval-card.js";
@@ -10,6 +10,7 @@ import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.
10
10
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
11
11
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
12
12
  import { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
13
+ const REAP_CLASSIFY_BATCH = 200;
13
14
  export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
14
15
  const MAX_TOOL_INPUT_CHARS = 8192;
15
16
  export const TERMINAL_BACKSTOP_MS = Math.max(60_000, (Number.isFinite(Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS)) ? Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) : 0) || 30 * 86_400_000);
@@ -182,6 +183,22 @@ function readParkRowFactsCell(cell) {
182
183
  return parkRowFacts(pendingAction);
183
184
  }
184
185
  const PARK_ROW_PENDING_ACTION = { key: "pendingAction", as: "pending_action" };
186
+ const PARK_ROW_GATE = { key: "gate", as: "park_gate" };
187
+ function readParkGateCell(cell) {
188
+ try {
189
+ return decodeNullableJsonColumn(cell, readParkGateMemberBody, "checkpoint.checkpoint $.gate (approval card / expiry facet)");
190
+ }
191
+ catch (e) {
192
+ if (!isJsonColumnBodyProblem(e))
193
+ throw e;
194
+ recordFailOpen("server.approvals.park-row-blob-unreadable", "member=gate");
195
+ return undefined;
196
+ }
197
+ }
198
+ const readParkGateMemberBody = createObjectColumnReader();
199
+ function parkedGateOfRow(r) {
200
+ return { kind: r.gate_kind ?? undefined, approvalCard: readParkGateCell(r[PARK_ROW_GATE.as])?.approvalCard };
201
+ }
185
202
  const readPendingActionMemberBody = createObjectColumnReader();
186
203
  export function boundedToolInput(args) {
187
204
  if (args === undefined)
@@ -518,21 +535,49 @@ export class SqlCheckpointStore {
518
535
  }
519
536
  async reapExpired(cutoff) {
520
537
  const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at_ms = ? " +
521
- `WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') = ${ASK_USER_QUESTION_SQL_LITERAL})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= ?))`, "UPDATE checkpoint SET status = 'expired', decided_at_ms = $1 " +
522
- `WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') = ${ASK_USER_QUESTION_SQL_LITERAL})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= $3))`), [Date.now(), cutoff, cutoff]);
523
- return res.affected;
538
+ `WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= ?))`, "UPDATE checkpoint SET status = 'expired', decided_at_ms = $1 " +
539
+ `WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= $3))`), [Date.now(), cutoff, cutoff]);
540
+ let n = res.affected;
541
+ const batch = await this.overdueApprovalRows(cutoff, REAP_CLASSIFY_BATCH, this.reapCursor);
542
+ for (const row of batch) {
543
+ if (!row.denySweeps && (await this.expire(row.token, row.scope)))
544
+ n++;
545
+ }
546
+ const last = batch.at(-1);
547
+ this.reapCursor = batch.length < REAP_CLASSIFY_BATCH || last === undefined ? undefined : { deadline: last.deadline, token: last.token };
548
+ return n;
524
549
  }
550
+ reapCursor;
525
551
  async listExpiredApprovalGates(cutoff, limit = 100) {
526
- const { rows } = await this.db.query(this.q(`SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ${APPROVAL_GATE_KINDS_SQL_IN} ` +
527
- `AND COALESCE(tool_name,'') <> ${ASK_USER_QUESTION_SQL_LITERAL} AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?`, `SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ${APPROVAL_GATE_KINDS_SQL_IN} ` +
528
- `AND COALESCE(tool_name,'') <> ${ASK_USER_QUESTION_SQL_LITERAL} AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2`), [cutoff, limit]);
529
- return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
552
+ const hits = [];
553
+ let after;
554
+ while (hits.length < limit) {
555
+ const page = await this.overdueApprovalRows(cutoff, REAP_CLASSIFY_BATCH, after);
556
+ for (const r of page) {
557
+ if (hits.length < limit && r.denySweeps)
558
+ hits.push({ sessionId: r.sessionId, scope: r.scope });
559
+ }
560
+ const last = page.at(-1);
561
+ if (page.length < REAP_CLASSIFY_BATCH || last === undefined)
562
+ break;
563
+ after = { deadline: last.deadline, token: last.token };
564
+ }
565
+ return hits;
566
+ }
567
+ async overdueApprovalRows(cutoff, limit, after) {
568
+ const head = `SELECT c.token, c.session_id, c.scope, c.gate_kind, c.deadline, ${jsonColumnText(this.db.dialect, "checkpoint", PARK_ROW_GATE)} FROM checkpoint c ` +
569
+ `WHERE c.status = 'pending' AND c.gate_kind IN ${APPROVAL_GATE_KINDS_SQL_IN} AND c.deadline IS NOT NULL AND `;
570
+ const { rows } = await this.db.query(after === undefined
571
+ ? head + this.q("c.deadline <= ? ORDER BY c.deadline ASC, c.token ASC LIMIT ?", "c.deadline <= $1 ORDER BY c.deadline ASC, c.token ASC LIMIT $2")
572
+ : head +
573
+ this.q("c.deadline <= ? AND (c.deadline > ? OR (c.deadline = ? AND c.token > ?)) ORDER BY c.deadline ASC, c.token ASC LIMIT ?", "c.deadline <= $1 AND (c.deadline > $2 OR (c.deadline = $3 AND c.token > $4)) ORDER BY c.deadline ASC, c.token ASC LIMIT $5"), after === undefined ? [cutoff, limit] : [cutoff, after.deadline, after.deadline, after.token, limit]);
574
+ return rows.map((r) => ({ token: String(r.token), sessionId: String(r.session_id), scope: String(r.scope), deadline: Number(r.deadline), denySweeps: denySweepOwns(parkedGateOfRow(r)) }));
530
575
  }
531
576
  async listPending(scope) {
532
577
  const d = this.db.dialect;
533
578
  const base = `SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, ${jsonColumnText(d, "tool_input")}, c.bound_input_hash, ` +
534
579
  `${jsonColumnText(d, "risk_descriptor")}, ${jsonColumnText(d, "rule_suggestions")}, c.has_bidi_controls, c.created_at_ms, c.deadline, c.gate_kind, ta.task_id, ` +
535
- `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)} ` +
580
+ `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_GATE)} ` +
536
581
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
537
582
  const { rows } = scope
538
583
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
@@ -543,6 +588,7 @@ export class SqlCheckpointStore {
543
588
  const boundInputHash = r.bound_input_hash ?? null;
544
589
  const gateKind = r.gate_kind ?? null;
545
590
  const ruleOffers = readRuleOffersCell(r.rule_suggestions);
591
+ const approvalCard = parkedApprovalCard(parkedGateOfRow(r));
546
592
  return {
547
593
  sessionId: String(r.session_id),
548
594
  scope: String(r.scope),
@@ -558,7 +604,7 @@ export class SqlCheckpointStore {
558
604
  riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
559
605
  ...(ruleOffers !== undefined ? { ruleOffers } : {}),
560
606
  ...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
561
- ...(contentKindOf(toolName) !== undefined ? { contentKind: "content_ask" } : {}),
607
+ ...(approvalCard !== undefined ? { approvalCard } : {}),
562
608
  ...readParkRowFactsCell(r[PARK_ROW_PENDING_ACTION.as]),
563
609
  };
564
610
  });
@@ -1,8 +1,9 @@
1
1
  import { type UsageRow } from "../usage-analytics.js";
2
2
  import type { TaskStatus } from "@sema-agent/core";
3
- import { StoreBootRefusalError, type CreateRunOutcome, type NewRunMeta, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
3
+ import { StoreBootRefusalError, type CreateRunOutcome, type NewRunMeta, type RunIdemBinding, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
4
4
  import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
5
5
  import type { LedgerEventType } from "../trace/ledger-events.js";
6
+ import { type SpaceCommitClaim, type SpaceCommitReading, type SpaceCommitState } from "../workspace-space-commit.js";
6
7
  /**
7
8
  * `RUN_STORE_STRICT_HYDRATE=on` 下 hydrate 列不出 `runs/` 时的**拒启**类型([ref] 件B;codex 对抗复审
8
9
  * R1-[high] 验真后从裸 `Error` 升成具名类型)。
@@ -25,6 +26,11 @@ export declare class FileRunStore {
25
26
  private readonly runsDir;
26
27
  private readonly activeDir;
27
28
  private readonly tmpDir;
29
+ /** `<root>/space-commits/` — the twin of `workspace_space_commit`: one record per space key (file name = the key,
30
+ * checked by `sanitizePathComponent`; written by `atomicWriteFile`). */
31
+ private readonly spaceCommitsDir;
32
+ /** The k8s workspace space's commit fence (the rule: `workspace-space-commit.ts`) over the record files. */
33
+ private readonly spaceCommits;
28
34
  /** taskId → RunRow (the run registry index). */
29
35
  private readonly runs;
30
36
  /** sessionId → taskId — the single-active-run claim (the task_active table; index of active/*). */
@@ -55,6 +61,15 @@ export declare class FileRunStore {
55
61
  private runJsonPath;
56
62
  private eventsPath;
57
63
  private activePath;
64
+ private spaceCommitPath;
65
+ /** One space's commit record, or `undefined` (no file). A record that is not this store's shape is a loud read failure
66
+ * (the reader — the next run's connect — refuses; it never guesses that no commit is in flight). */
67
+ private loadSpaceCommit;
68
+ /** The k8s workspace space's commit fence rows (SQL twin: `workspace_space_commit`; the rule: `workspace-space-commit.ts`). */
69
+ readSpaceCommit(spaceKey: string): Promise<SpaceCommitReading | undefined>;
70
+ claimSpaceCommit(spaceKey: string, taskId: string, state: SpaceCommitState, claim: SpaceCommitClaim): Promise<boolean>;
71
+ endSpaceCommit(spaceKey: string, taskId: string): Promise<void>;
72
+ abandonSpaceCommit(spaceKey: string, taskId: string): Promise<boolean>;
58
73
  private toRow;
59
74
  private toDisk;
60
75
  private toRecord;
@@ -120,6 +135,9 @@ export declare class FileRunStore {
120
135
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
121
136
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
122
137
  setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
138
+ /** The terminal write's one synchronous section (SQL twin: one transaction): the k8s space's pending commit record (when
139
+ * offered) → the terminal row → the active-run claim release. Returns whether the pending record is now this run's. */
140
+ private writeTerminal;
123
141
  /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
124
142
  * S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
125
143
  setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
@@ -131,11 +149,15 @@ export declare class FileRunStore {
131
149
  markResuming(taskId: string, instanceId: string): Promise<boolean>;
132
150
  getActiveTaskId(sessionId: string): Promise<string | undefined>;
133
151
  getRun(taskId: string): Promise<RunRecord | undefined>;
134
- /** Undo THIS submission's key binding (`idem_key` → NULL) — called only where the submit surface refuses the submission
152
+ /** Undo THIS submission's binding (key + body fingerprint → null) — called only where the submit surface refuses the submission
135
153
  * before any work and terminalizes the row it just claimed (the sync leg's zero-work refusal, a device-placement reject):
136
154
  * the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
137
- * is the refusal's record); only the key is freed. */
155
+ * is the refusal's record); only the binding is freed. */
138
156
  releaseIdemKey(taskId: string): Promise<void>;
157
+ /** The body fingerprint this row's binding of `idem.key` holds — binding `idem.bodyFingerprint` first when it holds none (first
158
+ * arrival binds and is persisted, everyone after reads; the file twin of the SQL conditional UPDATE — one writer per data root,
159
+ * single-threaded between the check and the write). `null` = no such row, or the row is not bound to this key (never written). */
160
+ bindIdemBodyFingerprint(taskId: string, idem: RunIdemBinding): Promise<string | null>;
139
161
  /** The run a submission's scoped `Idempotency-Key` names (the file twin of `uq_task_run_idem`), if any. */
140
162
  getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
141
163
  /** Runs newest-first, keyset on (createdAt, taskId) DESC; optional exact-match filters (owner is exact, like the SQL). */
@@ -312,24 +334,6 @@ export declare class FileRunStore {
312
334
  private releaseClaim;
313
335
  /** Drop a run entirely (registry + event log + open fd + on-disk dir) — used by deleteBySession. */
314
336
  private dropRun;
315
- /**
316
- * 🔴 **一次性迁移 M3 的 local 腿(窗口见 `one-time-migrations.ts` 登记表,随 M3 一起删)** —— 退役词旧拼法的账本改写,
317
- * SQL 两孪生那条整表 UPDATE 的 local 形。逐条走**同一只**纯改写函数,所以三条车道改出来的字节一致。
318
- *
319
- * 为什么是 store 上的一只动词、而不是外面一个文件走查:本店的账本有**两份**在场 —— 盘上的 jsonl
320
- * 与 hydrate 进来的内存索引。外部走查只改得到前者,读腿读的却是后者(`getEvents` 从 Map 取),
321
- * 于是迁移在本次进程里完全不生效、下次重启才"忽然"生效。两份一起改的唯一正确位置就是这里。
322
- *
323
- * 写盘用整文件原子替换(不是追加):改的是**已有行**,而 `AppendLog` 只会往后写。替换之前把**全部**
324
- * append fd 撤掉(rename 之后旧 fd 指向的是被 unlink 的 inode,继续往那儿追加 = 之后的事件全部写进一个
325
- * 没人看得见的文件),下一次 `appendEvent` 会按惰性规则重新开。撤 fd 复用**既有的那一只** {@link dispose}
326
- * —— 刻意不在这里另写一段 close+catch:那会是本文件第二处同形的兜底臂,而规则要变少不要变多。
327
- * 常态下这一撤是空操作:本动词跑在 boot 期,那时一个 append fd 都还没惰性开出来。
328
- *
329
- * 幂等:零命中 ⇒ 一个字节都不写、一只 fd 都不撤(第二跑零 I/O)。
330
- * @returns 被改写的账本**行**数(不是 run 数)。
331
- */
332
- migrateRetiredAskOriginRows(): Promise<number>;
333
337
  /**
334
338
  * 一次性迁移 **M6 的 local 腿**(窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):把 ≤7.115.x 写下的死亡判决行
335
339
  * (`failed` ∧ 码列空 ∧ 句子是那两句,判据 = `readLegacyHolderDeathCode`)补上机读码,盘上(`run.json` 整份原子替换,同 `persist`)与内存同改。