@sema-agent/server 7.2.0 → 7.4.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 (140) hide show
  1. package/README.md +2 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +26 -1
  4. package/dist/approval-ask-machine.d.ts +39 -0
  5. package/dist/approval-ask-machine.js +101 -0
  6. package/dist/approval-card.d.ts +244 -0
  7. package/dist/approval-card.js +237 -0
  8. package/dist/approval-deny-reasons.d.ts +56 -0
  9. package/dist/approval-deny-reasons.js +54 -0
  10. package/dist/approval-reconciler.d.ts +174 -0
  11. package/dist/approval-reconciler.js +307 -0
  12. package/dist/boot/coordinators.d.ts +1 -0
  13. package/dist/boot/coordinators.js +39 -4
  14. package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
  15. package/dist/boot/deferred-sandbox-path-env.js +279 -0
  16. package/dist/boot/execution-env.js +11 -1
  17. package/dist/boot/lexical-path-env.d.ts +10 -0
  18. package/dist/boot/lexical-path-env.js +88 -0
  19. package/dist/boot/reapers.d.ts +34 -0
  20. package/dist/boot/reapers.js +198 -23
  21. package/dist/boot/resolve-spec.js +97 -33
  22. package/dist/capabilities/center-prompts.js +4 -1
  23. package/dist/capabilities/oa-tools.d.ts +15 -0
  24. package/dist/capabilities/oa-tools.js +54 -0
  25. package/dist/config-types.d.ts +68 -1
  26. package/dist/config.d.ts +1 -0
  27. package/dist/config.js +138 -2
  28. package/dist/elicitation.d.ts +4 -0
  29. package/dist/elicitation.js +7 -3
  30. package/dist/finance/cost-taxonomy.d.ts +34 -0
  31. package/dist/finance/cost-taxonomy.js +26 -0
  32. package/dist/hooks/hook-runner.js +32 -0
  33. package/dist/http/routes/capabilities.js +14 -0
  34. package/dist/http/routes/diagnostics.d.ts +84 -0
  35. package/dist/http/routes/diagnostics.js +140 -0
  36. package/dist/http/routes/runs.d.ts +1 -0
  37. package/dist/http/routes/runs.js +548 -16
  38. package/dist/http/routes/tasks.js +175 -12
  39. package/dist/http/server.d.ts +6 -1
  40. package/dist/http/server.js +120 -4
  41. package/dist/http/sse-log.d.ts +51 -0
  42. package/dist/http/sse-log.js +64 -0
  43. package/dist/http/wire-types.d.ts +20 -5
  44. package/dist/main.js +35 -4
  45. package/dist/observability/fail-open.d.ts +98 -0
  46. package/dist/observability/fail-open.js +216 -0
  47. package/dist/observability/prompt-manifest.d.ts +13 -0
  48. package/dist/observability/prompt-manifest.js +8 -0
  49. package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
  50. package/dist/plugins/approval-ask-store-memory.js +299 -0
  51. package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
  52. package/dist/plugins/approval-ask-store-sql.js +705 -0
  53. package/dist/plugins/approval-store-sql.d.ts +116 -0
  54. package/dist/plugins/approval-store-sql.js +151 -0
  55. package/dist/plugins/background-agent-store-sql.js +20 -1
  56. package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
  57. package/dist/plugins/checkpoint-store-sql.js +297 -16
  58. package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
  59. package/dist/plugins/file-workflow-journal-store.js +12 -0
  60. package/dist/plugins/local-checkpoint-store.d.ts +6 -5
  61. package/dist/plugins/local-checkpoint-store.js +4 -0
  62. package/dist/plugins/pg-approval-store.d.ts +9 -0
  63. package/dist/plugins/pg-approval-store.js +9 -0
  64. package/dist/plugins/pg-breaker-state.d.ts +8 -0
  65. package/dist/plugins/pg-breaker-state.js +8 -0
  66. package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
  67. package/dist/plugins/pg-checkpoint-store.js +10 -0
  68. package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
  69. package/dist/plugins/pg-file-snapshot-store.js +8 -0
  70. package/dist/plugins/pg-image-bake.d.ts +12 -0
  71. package/dist/plugins/pg-image-bake.js +11 -0
  72. package/dist/plugins/pg-image-index.d.ts +12 -0
  73. package/dist/plugins/pg-image-index.js +11 -0
  74. package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
  75. package/dist/plugins/pg-outcome-ledger.js +11 -0
  76. package/dist/plugins/pg-pool.js +11 -0
  77. package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
  78. package/dist/plugins/pg-resume-anchor-store.js +7 -0
  79. package/dist/plugins/pg-run-store.d.ts +9 -0
  80. package/dist/plugins/pg-run-store.js +9 -0
  81. package/dist/plugins/pg-session-policy-store.d.ts +7 -0
  82. package/dist/plugins/pg-session-policy-store.js +7 -0
  83. package/dist/plugins/pg-session-store.d.ts +12 -0
  84. package/dist/plugins/pg-session-store.js +12 -0
  85. package/dist/plugins/pg-tool-result-store.d.ts +9 -0
  86. package/dist/plugins/pg-tool-result-store.js +9 -0
  87. package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
  88. package/dist/plugins/pg-workflow-journal-store.js +9 -0
  89. package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
  90. package/dist/plugins/pg-workflow-run-store.js +9 -0
  91. package/dist/plugins/store-backend.d.ts +18 -0
  92. package/dist/plugins/store-backend.js +10 -0
  93. package/dist/plugins/tidb-approval-store.d.ts +8 -0
  94. package/dist/plugins/tidb-approval-store.js +8 -0
  95. package/dist/plugins/tidb-breaker-state.d.ts +7 -0
  96. package/dist/plugins/tidb-breaker-state.js +7 -0
  97. package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
  98. package/dist/plugins/tidb-checkpoint-store.js +9 -0
  99. package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
  100. package/dist/plugins/tidb-file-snapshot-store.js +8 -0
  101. package/dist/plugins/tidb-image-bake.d.ts +12 -0
  102. package/dist/plugins/tidb-image-bake.js +11 -0
  103. package/dist/plugins/tidb-image-index.d.ts +12 -0
  104. package/dist/plugins/tidb-image-index.js +11 -0
  105. package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
  106. package/dist/plugins/tidb-outcome-ledger.js +12 -0
  107. package/dist/plugins/tidb-pool.js +27 -4
  108. package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
  109. package/dist/plugins/tidb-resume-anchor-store.js +7 -0
  110. package/dist/plugins/tidb-run-store.d.ts +10 -0
  111. package/dist/plugins/tidb-run-store.js +9 -0
  112. package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
  113. package/dist/plugins/tidb-session-policy-store.js +7 -0
  114. package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
  115. package/dist/plugins/tidb-tool-result-store.js +10 -0
  116. package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
  117. package/dist/plugins/tidb-workflow-journal-store.js +9 -0
  118. package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
  119. package/dist/plugins/tidb-workflow-run-store.js +10 -0
  120. package/dist/plugins/workflow-journal-limits.d.ts +12 -0
  121. package/dist/plugins/workflow-journal-limits.js +12 -0
  122. package/dist/question.d.ts +21 -14
  123. package/dist/question.js +83 -34
  124. package/dist/runs.d.ts +16 -1
  125. package/dist/runs.js +61 -3
  126. package/dist/runtime-caps-resolver.d.ts +7 -1
  127. package/dist/runtime-caps-resolver.js +65 -3
  128. package/dist/sema-registry.d.ts +41 -0
  129. package/dist/sema-registry.js +40 -0
  130. package/dist/spec-fields.d.ts +4 -0
  131. package/dist/spec-fields.js +6 -0
  132. package/dist/task-settings.d.ts +36 -15
  133. package/dist/task-settings.js +19 -5
  134. package/dist/tool-approval.d.ts +296 -3
  135. package/dist/tool-approval.js +1074 -50
  136. package/dist/trace/core-keyset-guard.d.ts +2 -2
  137. package/dist/trace/ledger-sink.js +14 -1
  138. package/dist/trace/project.d.ts +90 -0
  139. package/dist/trace/project.js +188 -0
  140. package/package.json +5 -4
@@ -27,7 +27,7 @@
27
27
  * - schema ownership: TiDB DDL in tidb-pool.ts, PG DDL centrally in pg-pool.ts (neither store creates tables).
28
28
  */
29
29
  import { createHash } from "node:crypto";
30
- import { CheckpointError, validatePendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
30
+ import { CheckpointError, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
31
31
  import { redactDeep } from "../trace/redact.js";
32
32
  import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
33
33
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
@@ -76,8 +76,154 @@ export function boundedToolInput(args) {
76
76
  return { truncated: true, bytes: json.length };
77
77
  return redacted;
78
78
  }
79
+ /**
80
+ * checkpoint blob → `pendingAction.toolCallId`。
81
+ *
82
+ * 🔴 三态、**不是**两态(真双库实跑抓到的缺陷,2026-08-06):「读不出」与「读出来了、但这条 park 本来就
83
+ * 没有 toolCallId」是两件事,压成同一个 `undefined` 会把每一条 `plan_review`/`task_done`/`resource_limit`
84
+ * 腿(core 的 `PendingAction` 联合里三个成员结构上就没有 toolCallId)都误报成坏行。前者应标 `unparseable`
85
+ * 交给收敛器当「不确定」,后者是**确定的不匹配**——干净地不是候选。
86
+ * - `{ readable: false }` —— JSON 坏 / 形状根本不是对象:真的读不出。
87
+ * - `{ readable: true, toolCallId: null }` —— 读出来了,这条 park 无工具动作:确定不匹配。
88
+ * - `{ readable: true, toolCallId: "…" }` —— 读出来了,拿去比。
89
+ *
90
+ * 逐层 `typeof` 收窄,不做裸 `as` 断言:blob 是持久层读回来的 `unknown`,是信任边界(宪法 [2704])。
91
+ */
92
+ function pendingActionToolCallId(blob) {
93
+ let parsed;
94
+ try {
95
+ parsed = typeof blob === "string" ? JSON.parse(blob) : blob;
96
+ }
97
+ catch {
98
+ return { readable: false };
99
+ }
100
+ if (typeof parsed !== "object" || parsed === null)
101
+ return { readable: false };
102
+ if (!("pendingAction" in parsed))
103
+ return { readable: true, toolCallId: null, boundInputHash: null, kind: null }; // 无 pendingAction 的 park:确定不匹配
104
+ const pa = parsed.pendingAction;
105
+ if (typeof pa !== "object" || pa === null)
106
+ return { readable: false };
107
+ const kind = "kind" in pa ? pa.kind : null;
108
+ if (kind !== null && typeof kind !== "string")
109
+ return { readable: false };
110
+ if (!("toolCallId" in pa))
111
+ return { readable: true, toolCallId: null, boundInputHash: null, kind }; // plan_review / task_done / resource_limit 腿
112
+ const id = pa.toolCallId;
113
+ if (typeof id !== "string")
114
+ return { readable: false };
115
+ // 🔴(属主兜底轮 F1)带 toolCallId 却挂着词表外的 kind = 结构漂移,不是「读得出」——这条 blob 声称
116
+ // 自己是一次工具审批 park(有 callId)又否认(kind 不是 tool_approval)。让它带着匹配的 callId 混进
117
+ // 健康候选,等于给「外部改库 / schema 偏斜」开一条错配 PARK 的路。词表判据窄取 "tool_approval":
118
+ // 其余合法 kind(plan_review 等)在 core 的类型里结构上**没有** toolCallId,走不到这里。
119
+ if (kind !== "tool_approval")
120
+ return { readable: false };
121
+ // hash 只做逐层收窄,不在这层与列比对(比对是调用处的列/blob 一致性门)。缺席 = null(旧行/无 hash 形)。
122
+ const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
123
+ if (bih !== null && typeof bih !== "string")
124
+ return { readable: false };
125
+ return { readable: true, toolCallId: id, boundInputHash: bih, kind };
126
+ }
79
127
  const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
80
128
  const PG_UNIQUE_VIOLATION = "23505";
129
+ function readActorAssertion(v) {
130
+ if (typeof v !== "object" || v === null)
131
+ return undefined;
132
+ const id = "id" in v ? v.id : undefined;
133
+ const hostAsserted = "hostAsserted" in v ? v.hostAsserted : undefined;
134
+ if (typeof id !== "string" || typeof hostAsserted !== "boolean")
135
+ return undefined;
136
+ const issuer = "issuer" in v ? v.issuer : undefined;
137
+ return { id, hostAsserted, ...(typeof issuer === "string" ? { issuer } : {}) };
138
+ }
139
+ /** 队列化之前的**裸单座**形(`{text,trusted}`)—— 旧行原样躺在库里,core 的 `readPendingSteerQueue`
140
+ * 会把它折成 member 0,所以读出来照原样保留、不改写、不迁移。 */
141
+ function readLegacySeat(v) {
142
+ if (typeof v !== "object" || v === null)
143
+ return undefined;
144
+ const text = "text" in v ? v.text : undefined;
145
+ const trusted = "trusted" in v ? v.trusted : undefined;
146
+ if (typeof text !== "string" || typeof trusted !== "boolean")
147
+ return undefined;
148
+ return { text, trusted };
149
+ }
150
+ /** 队列条目的逐层窄读(持久层读回的是 `unknown`,是信任边界 —— 禁裸 `as`)。四个必备键任一不合形 ⇒
151
+ * 整条不认(返回 undefined,由调用方判 lossy);`actor`/`priority` 可选,不合形即缺席。 */
152
+ function readQueueEntry(v) {
153
+ if (typeof v !== "object" || v === null)
154
+ return undefined;
155
+ const text = "text" in v ? v.text : undefined;
156
+ const trusted = "trusted" in v ? v.trusted : undefined;
157
+ const seq = "seq" in v ? v.seq : undefined;
158
+ const inputId = "inputId" in v ? v.inputId : undefined;
159
+ if (typeof text !== "string" || typeof trusted !== "boolean")
160
+ return undefined;
161
+ if (typeof seq !== "number" || !Number.isFinite(seq))
162
+ return undefined;
163
+ if (typeof inputId !== "string" || inputId === "")
164
+ return undefined;
165
+ const priority = "priority" in v ? v.priority : undefined;
166
+ const actor = readActorAssertion("actor" in v ? v.actor : undefined);
167
+ return {
168
+ text,
169
+ trusted,
170
+ seq,
171
+ inputId,
172
+ ...(actor !== undefined ? { actor } : {}),
173
+ ...(priority === "now" || priority === "next" || priority === "later" ? { priority } : {}),
174
+ };
175
+ }
176
+ /**
177
+ * 两个 steering 列 → `{pendingSteer?, pendingSteerQueue?}`(= core `appendPendingSteer`/`readPendingSteerQueue`
178
+ * 的入参形)。**两列各管一形**,不是一列两形:
179
+ * · `pending_steer`(旧列,本服务只读不写)—— 裸单座 `{text, trusted}`。来源有二:队列化之前的存量行,
180
+ * 以及**滚动升级窗里仍在跑的旧副本**写下的座。core 的 `readPendingSteerQueue` 把它折成 member 0。
181
+ * · `pending_steer_queue`(新列)—— `PendingSteerEntry[]`。
182
+ * 任一列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
183
+ */
184
+ export function readPendingSteerColumns(seatRaw, queueRaw) {
185
+ let lossy = false;
186
+ const parse = (raw) => {
187
+ if (raw === null || raw === undefined)
188
+ return undefined;
189
+ if (typeof raw !== "string")
190
+ return raw;
191
+ try {
192
+ return JSON.parse(raw);
193
+ }
194
+ catch {
195
+ lossy = true;
196
+ return undefined;
197
+ }
198
+ };
199
+ const seatParsed = parse(seatRaw);
200
+ const seat = seatParsed === undefined ? undefined : readLegacySeat(seatParsed);
201
+ if (seatParsed !== undefined && seat === undefined)
202
+ lossy = true;
203
+ const queueParsed = parse(queueRaw);
204
+ let queue;
205
+ if (queueParsed !== undefined) {
206
+ if (!Array.isArray(queueParsed))
207
+ lossy = true;
208
+ else {
209
+ queue = [];
210
+ for (const item of queueParsed) {
211
+ const entry = readQueueEntry(item);
212
+ if (entry === undefined)
213
+ lossy = true;
214
+ else
215
+ queue.push(entry);
216
+ }
217
+ }
218
+ }
219
+ return {
220
+ state: { ...(seat !== undefined ? { pendingSteer: seat } : {}), ...(queue !== undefined ? { pendingSteerQueue: queue } : {}) },
221
+ lossy,
222
+ };
223
+ }
224
+ /** 读-改-写的 CAS 重试上限。队列本身只有 `MAX_PENDING_STEER_ENTRIES`(= 3)个位子,再多就 fail-loud,
225
+ * 所以真实争用者数量天然很小;给足冗余后仍然全败 ⇒ 是异常,必须 fail-loud(见 setPendingSteer 尾注)。 */
226
+ const STEER_CAS_ATTEMPTS = 8;
81
227
  /** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
82
228
  export class SqlCheckpointStore {
83
229
  db;
@@ -136,7 +282,7 @@ export class SqlCheckpointStore {
136
282
  }
137
283
  }
138
284
  async get(token) {
139
- const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = $1"), [token]);
285
+ const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue FROM checkpoint WHERE token = $1"), [token]);
140
286
  const r = rows[0];
141
287
  if (!r)
142
288
  return null;
@@ -163,9 +309,22 @@ export class SqlCheckpointStore {
163
309
  cp.rev = r.rev == null ? 0 : Number(r.rev); // legacy/pre-migration rows ⇒ 0 (the .d.ts "Absent ⇒ legacy 0")
164
310
  // design/80 D-A: overlay the parked steer off the AUTHORITATIVE column (set by setPendingSteer AFTER the
165
311
  // suspend-time blob was written) so core injects it on resume. The blob never carries it.
166
- const steerRaw = parseJson(r.pending_steer);
167
- if (steerRaw && cp.state)
168
- cp.state.pendingSteer = steerRaw;
312
+ // core 5.14.0(#147):列现在承载**两个**字段 —— 队列本体 + 旧行折下来的裸单座。两者都要盖上去,
313
+ // core `readPendingSteerQueue` 才能把「legacy 座 = member 0,其余按 seq」拼成完整投递序。
314
+ // 🔴 坏列在这里**只丢 steer、不抛**:resume 是这条挂起的 run 唯一的出路,为一条读不出的转向把整条
315
+ // run 变成不可 resume 是错误的失败方向(写侧相反,见 setPendingSteer)。
316
+ const steer = readPendingSteerColumns(r.pending_steer, r.pending_steer_queue);
317
+ if (steer.lossy) {
318
+ // 「静默」到此为止:读不出的部分确实丢了(投递腿不能为一条坏列把整条 run 变成不可 resume),
319
+ // 但它必须**留痕** —— 丢的是操作员指令,而 #147 的全部意义就是这类丢失不许无声。
320
+ this.logger?.info?.("checkpoint_pending_steer_unreadable", { tokenFp: tokenFingerprint(token) });
321
+ }
322
+ if (cp.state) {
323
+ if (steer.state.pendingSteer !== undefined)
324
+ cp.state.pendingSteer = steer.state.pendingSteer;
325
+ if (steer.state.pendingSteerQueue !== undefined)
326
+ cp.state.pendingSteerQueue = steer.state.pendingSteerQueue;
327
+ }
169
328
  // Diagnostic: log what the REAL runner.resume store.get returns for the remote
170
329
  // workspaceHandle (the isolated round-trip test can't prove the real-path get). undefined here ⇒ store/get
171
330
  // drops it; present ⇒ core's 910→614 threading drops it. Cheap, on the resume read-path only.
@@ -227,20 +386,61 @@ export class SqlCheckpointStore {
227
386
  return res.affected === 1;
228
387
  }
229
388
  /**
230
- * design/80 D-A (durable steering): park a steer message on a STILL-PENDING checkpoint (last-writer-wins),
231
- * surfaced via get() as `state.pendingSteer` and injected on resume. CAS on `status='pending'` (+ scope) so a
232
- * steer NEVER lands on a resolved/expired checkpoint and NEVER touches status/the resolve path (§3 inv #4).
233
- * Returns the CAS winner (`true` = landed on a live pending checkpoint; `false` = no pending row to steer).
389
+ * design/80 D-A (durable steering) —— **core 5.14.0(#147 BREAKING)起是「追加进一条有界有序队列」**,
390
+ * 不再是「盖掉单座」。两个人同时给一条挂起的 run 转向,旧语义会让第二条**静默销毁**第一条(一条没人
391
+ * 知道丢了的操作员指令);新语义是两条都在,resume `seq` 全数投递。
392
+ *
393
+ * 语义与边界**全部铸在 core**,本 store 一个都不自铸(契约原文:`appendPendingSteer` is "the shared
394
+ * append every backend must build its next queue with: bounds + `seq` mint live there, not once per store"):
395
+ * · `validatePendingSteer` —— 每个 impl 内部必跑的 fail-closed 不变式(M2,接入审计 2026-07-08):内容
396
+ * 越狱/控制字符 `steering.invalid_content`;**未知字段现在是 REFUSE 而不是丢弃**(领先一个版本的
397
+ * producer 必须知道自己的新字段活不过这条挂起的腿);缺席的 `inputId` 在这里铸(uuidv7)。
398
+ * · `appendPendingSteer` —— 铸 `seq`、判 `inputId` 幂等(同 id 同载荷 = no-op 返回 true;同 id **不同**
399
+ * 载荷 = `steering.duplicate_input_id`,吞掉它就等于把队列要消灭的那种静默丢失原样请回来)、
400
+ * 执行两条上界(条数 + 48000 字节整信封),越界一律 `steering.queue_full` fail-loud,**绝不驱逐**
401
+ * 一条已被接受的指令。
402
+ *
403
+ * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
404
+ * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
405
+ * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
406
+ * CAS 假命中;也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
407
+ * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
408
+ * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
409
+ *
410
+ * 返回:`true` = 落在一条活的 pending 行上(含幂等重放);`false` = 没有该 (token,scope) 的 pending 行。
234
411
  */
235
412
  async setPendingSteer(token, scope, steer) {
236
- // M2(接入审计 2026-07-08):契约把 validatePendingSteer 定为【每个 impl 内部】必跑的 fail-closed 不变式
237
- // ("EVERY CheckpointStore.setPendingSteer impl runs [it] so the reject is IDENTICAL across the in-memory and
238
- // durable backends")——core 自家 File/InMemory/Pg 三 impl 全有;此前这里裸写 UPDATE=靠调用方自觉(HTTP 层
239
- // 三处前置校验只是碰巧全覆盖,下一个内部调用方不一定记得)。clean = 冻结 {text,trusted} 两字段拷贝
240
- // (调用方对象上的多余字段绝不落库,契约同款)。
241
413
  const clean = validatePendingSteer(steer); // throws CheckpointError(steering.invalid_content) — per-impl 一致拒绝
242
- const res = await this.db.query(this.q("UPDATE checkpoint SET pending_steer = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET pending_steer = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [this.json(clean, "pending steer"), token, scope]);
243
- return res.affected > 0;
414
+ for (let attempt = 0; attempt < STEER_CAS_ATTEMPTS; attempt++) {
415
+ const { rows } = await this.db.query(this.q("SELECT status, pending_steer, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = ? AND scope = ?", "SELECT status, pending_steer, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = $1 AND scope = $2"), [token, scope]);
416
+ const row = rows[0];
417
+ if (!row || row.status !== "pending")
418
+ return false; // 没有活的 pending 行(与旧的 CAS 落空同义)
419
+ const read = readPendingSteerColumns(row.pending_steer, row.pending_steer_queue);
420
+ if (read.lossy) {
421
+ // 🔴 写侧与 get() 的失败方向刻意相反:往一份读不全的队列上追加,写回时就会把读不出的那部分
422
+ // **永久抹掉** —— 那正是 #147 存在的理由(静默丢一条已 park 的操作员指令)。宁可这一次转向失败。
423
+ throw new Error(`the parked steering queue on checkpoint ${tokenFingerprint(token)} is unreadable — refusing to append onto it ` +
424
+ `(appending would silently drop the entries this build cannot parse)`);
425
+ }
426
+ const before = read.state.pendingSteerQueue?.length ?? 0;
427
+ const next = appendPendingSteer(read.state, clean); // throws steering.queue_full / steering.duplicate_input_id
428
+ // 幂等重放(同 inputId 同载荷):core 原样奉还既有队列 ⇒ 一个字都不用写,也不必烧一个 rev。
429
+ if (next.length === before)
430
+ return true;
431
+ const rev = Number(row.pending_steer_rev ?? 0);
432
+ // 🔴 只写队列列 —— `pending_steer`(旧单座列)本服务**从不写**:滚动升级窗里旧副本对那一列是无条件
433
+ // 覆盖写,两边写同一列 = 互相清零(codex 复审 F3)。分列之后旧副本的座与新队列共存,读侧合并。
434
+ const res = await this.db.query(this.q("UPDATE checkpoint SET pending_steer_queue = ?, pending_steer_rev = pending_steer_rev + 1 " +
435
+ "WHERE token = ? AND scope = ? AND status = 'pending' AND pending_steer_rev = ?", "UPDATE checkpoint SET pending_steer_queue = $1, pending_steer_rev = pending_steer_rev + 1 " +
436
+ "WHERE token = $2 AND scope = $3 AND status = 'pending' AND pending_steer_rev = $4"), [this.json(next, "pending steer queue"), token, scope, rev]);
437
+ if (res.affected > 0)
438
+ return true;
439
+ // CAS 输了:另一个 steer 抢先追加(或这行刚被 resolve/expire)⇒ 重读重算。下一轮的 SELECT 会
440
+ // 分辨这两种情形:行还 pending 就再追加一次,不 pending 就返回 false。
441
+ }
442
+ throw new Error(`setPendingSteer lost the pending_steer_rev CAS ${STEER_CAS_ATTEMPTS} times on checkpoint ${tokenFingerprint(token)} — ` +
443
+ `refusing to report it as "no pending checkpoint" (that would silently discard an operator instruction)`);
244
444
  }
245
445
  /**
246
446
  * CAS-expire a SINGLE checkpoint by token (core 1.71 `CheckpointStore.expire`, for `TaskStream.destroy`'s
@@ -377,6 +577,87 @@ export class SqlCheckpointStore {
377
577
  : await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
378
578
  return rows[0] ? String(rows[0].token) : null;
379
579
  }
580
+ /**
581
+ * 🔴 #151 车5 §9 C4 —— 流内审批对账收敛器判据 1 的**窄读口**(店面承载;收敛器本体不在本车)。
582
+ *
583
+ * 「这条 PARKING 的 ask 究竟 park 成了哪张 checkpoint?」的唯一读法。为什么不是「按 session 翻历史页」:
584
+ * 分页宽读会漏匹配,而漏匹配在收敛器那侧的后果是**假阴性 ⇒ 落一条不可逆的 DENIED**。所以这里改成
585
+ * 谓词精确查——`(scope, session_id, tool_call_id, created_at ≥ sinceMs)` 这组条件下的行数天然极小,
586
+ * 一次全量返回,结构上没有分页假阴性。
587
+ *
588
+ * 三条口径,逐条都是判据:
589
+ * - **`scope` 必填**(不是 optional):对账是跨腿读,租户门是读口自己的责任,不靠调用方记得过滤
590
+ * (同 `listPendingBySession` 姿势;`resolve`/`reap` 的 scope enforced 同族)。
591
+ * - **不按 status 过滤**:已 `resolved`/`expired` 的行照样返回。后到的 resume 会把行翻成 resolved,
592
+ * 若只看 pending 就会把「park 成功过」这个既成事实读成「没 park 过」⇒ 又是一条假阴性 DENIED
593
+ * (对账三约束③)。park 成功是事实,gate 的活性由 `status` 表达,交给收敛器自己判。
594
+ * - **单行读不出 ⇒ 标记返回,不抛**:`unparseable` 的行由收敛器视同「不匹配」,一条坏行不许打断整段
595
+ * 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
596
+ * 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
597
+ *
598
+ * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,命中它
599
+ * 的行零解析即候选;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)才回落去解 blob——解得出且
600
+ * 相等才算候选,解不出就标 `unparseable`。
601
+ */
602
+ async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
603
+ const { rows } = await this.db.query(this.q("SELECT token, status, created_at, tool_call_id, bound_input_hash, version, checkpoint FROM checkpoint " +
604
+ "WHERE scope=? AND session_id=? AND created_at>=? AND (tool_call_id=? OR tool_call_id IS NULL) ORDER BY created_at ASC", "SELECT token, status, created_at, tool_call_id, bound_input_hash, version, checkpoint FROM checkpoint " +
605
+ "WHERE scope=$1 AND session_id=$2 AND created_at>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at ASC"), [scope, sessionId, sinceMs, toolCallId]);
606
+ const out = [];
607
+ for (const r of rows) {
608
+ const base = {
609
+ token: String(r.token),
610
+ status: String(r.status),
611
+ createdAtMs: Number(r.created_at),
612
+ boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
613
+ };
614
+ // 🔴 前向兼容门必须在**任何**命中路径之前(codex 复审 F3,2026-08-06 真缺陷):版本超前的行是
615
+ // `get()` 明确拒读的行,而 `tool_call_id` 列命中与否跟能不能读懂 blob 毫无关系。若让列的快路径
616
+ // 绕过这道门,收敛器就会把它当健康候选去 `bindBatch` ⇒ ask 被钉成 **PARKED + 一张本进程读不出的
617
+ // checkpoint 的 resume 坐标**,而 PARKED 不可回滚。判据只能是「宁可不命中,绝不错配」(§8 D-2)。
618
+ // 门在这里**不抛**(见头注):标 unparseable 交给收敛器当「不确定」,单行不打断整段扫描。
619
+ if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
620
+ out.push({ ...base, boundCallId: null, unparseable: true });
621
+ continue;
622
+ }
623
+ // 🔴 blob 可读性同样门在**所有**命中路径之前(codex 确认轮补抓):`tool_call_id` 列长得对,不代表
624
+ // 这张 checkpoint 读得出来——列是 put() 时的投影,blob 才是 resume 真正要吃的东西。让「列命中」
625
+ // 跳过可读性检查,等于给「blob 损坏 / schema 偏斜」留了一条与 F3 完全相同的不可逆后果:
626
+ // 收敛器把 ask 钉成 PARKED + 这张 checkpoint 的坐标,而后续 `get()` 必然失败。
627
+ // 读口的契约只有一条——**读不出就标记**,没有「有一列长得对就豁免」的例外。
628
+ const derived = pendingActionToolCallId(r.checkpoint);
629
+ if (!derived.readable) {
630
+ out.push({ ...base, boundCallId: null, unparseable: true });
631
+ continue;
632
+ }
633
+ const col = r.tool_call_id == null ? null : String(r.tool_call_id);
634
+ // 列与 blob **互相矛盾** ⇒ 判别不出谁对,标 unparseable(「宁可不命中,绝不错配」,§8 D-2)。
635
+ // 🔴 矛盾的定义是「列说得出、而 blob 不同意」——同时覆盖两形:blob 给了**别的** callId,以及
636
+ // blob 说这条 park **根本没有工具动作**(`toolCallId === null`)。后者一样是漂移(列声称有一次
637
+ // 工具调用、blob 否认),早先只判「两边都非空且不等」会把它漏成健康候选。
638
+ // 现网 put() 把列与 blob 同源同刻写下,矛盾只可能来自外部改库或 schema 偏斜——正是该保守的场合。
639
+ if (col !== null && col !== derived.toolCallId) {
640
+ out.push({ ...base, boundCallId: null, unparseable: true });
641
+ continue;
642
+ }
643
+ // 🔴 bound_input_hash 的列/blob 一致性门(属主兜底轮 F1)——与上面 toolCallId 的矛盾门同款:
644
+ // 返回给收敛器的 hash 是「identity ∧ hash 双等」硬谓词的**输入**,列被外部改成漂移值(乃至恰好
645
+ // 改成另一只 ask 的 hash)时,只信列 = 把双等谓词架空成单等。两边都在场且不等 ⇒ unparseable;
646
+ // 列 NULL 而 blob 有值 ⇒ 用 blob 值补齐(旧行回落,与 toolCallId 的「列 NULL 回落 blob」同规)。
647
+ if (base.boundInputHash !== null && derived.boundInputHash !== null && base.boundInputHash !== derived.boundInputHash) {
648
+ out.push({ ...base, boundCallId: null, unparseable: true });
649
+ continue;
650
+ }
651
+ const effectiveHash = base.boundInputHash ?? derived.boundInputHash;
652
+ // 列在场时以列为准(权威投影);列为 NULL 的旧行回落用 blob 解出来的值。
653
+ const effective = col ?? derived.toolCallId;
654
+ if (effective === toolCallId)
655
+ out.push({ ...base, boundInputHash: effectiveHash, boundCallId: effective });
656
+ // 解得出、但不是这只 ask 的 callId(或这条 park 本来就没有工具动作)⇒ **确定**不是候选:
657
+ // 既不返回也不标坏行(标坏行会让收敛器把一条明确的「不匹配」当成「不确定」)。
658
+ }
659
+ return out;
660
+ }
380
661
  /**
381
662
  * The owner SCOPE of a session's pending checkpoint (the multi-tenant key === the owner principal in the
382
663
  * BFF/non-operator flow, the same key listPending filters by). For the /decide owner-gate: a non-operator
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SVC-2 — FILE-backed durable WorkflowJournalStore for the LOCAL (TOC) backend.
3
+ *
4
+ * Now a RE-EXPORT of core's copy (the write-here-then-core-adopts 剧本): the implementation was written here for server
5
+ * 1.89.0, then adopted verbatim by core (`@sema-agent/core` 1.223.0, file
6
+ * `src/stores/file/workflow-journal-store.ts` — crash-safe per-run JSONL, scope-guarded [CORE-9],
7
+ * idempotent-per-ordinal, oversize skip-journal, traversal-runId load degrade, 0o600/0o700). One source of
8
+ * truth in core; the SQL twins (tidb/pg) stay service-side (dialect = deployment glue). The service test
9
+ * suite (test/file-workflow-journal-store.test.ts) keeps pinning the contract through this re-export.
10
+ */
11
+ export { FileWorkflowJournalStore } from "@sema-agent/core";
12
+ //# sourceMappingURL=file-workflow-journal-store.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SVC-2 — FILE-backed durable WorkflowJournalStore for the LOCAL (TOC) backend.
3
+ *
4
+ * Now a RE-EXPORT of core's copy (the write-here-then-core-adopts 剧本): the implementation was written here for server
5
+ * 1.89.0, then adopted verbatim by core (`@sema-agent/core` 1.223.0, file
6
+ * `src/stores/file/workflow-journal-store.ts` — crash-safe per-run JSONL, scope-guarded [CORE-9],
7
+ * idempotent-per-ordinal, oversize skip-journal, traversal-runId load degrade, 0o600/0o700). One source of
8
+ * truth in core; the SQL twins (tidb/pg) stay service-side (dialect = deployment glue). The service test
9
+ * suite (test/file-workflow-journal-store.test.ts) keeps pinning the contract through this re-export.
10
+ */
11
+ export { FileWorkflowJournalStore } from "@sema-agent/core";
12
+ //# sourceMappingURL=file-workflow-journal-store.js.map
@@ -1,4 +1,4 @@
1
- import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "@sema-agent/core";
1
+ import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "@sema-agent/core";
2
2
  import { type PendingCheckpoint } from "./checkpoint-store-sql.js";
3
3
  export interface LocalCheckpointStoreOptions {
4
4
  /** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
@@ -31,10 +31,11 @@ export declare class LocalCheckpointStore {
31
31
  get(token: CheckpointToken): Promise<Checkpoint | null>;
32
32
  resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
33
33
  reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
34
- setPendingSteer(token: CheckpointToken, scope: string, steer: {
35
- text: string;
36
- trusted: boolean;
37
- }): Promise<boolean>;
34
+ /** core 5.14.0(#147):入参是 `PendingSteerInput`(text/trusted + 可选 actor/inputId/priority),不是
35
+ * 两键单座 —— 形参窄成 `{text,trusted}` 会**静默丢掉**署名与幂等键(TS 的方法参数是双变的,窄形不
36
+ * 报错,只是把三个可选键从类型上抹掉,调用点再也传不进来)。队列语义与边界全在被包的 core
37
+ * `FileCheckpointStore` 里,本 wrapper 只做转发。 */
38
+ setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
38
39
  expire(token: CheckpointToken, scope: string): Promise<boolean>;
39
40
  reap(scope: string, cutoff: number): Promise<number>;
40
41
  listByScope(scope: string): Promise<CheckpointSummary[]>;
@@ -133,6 +133,10 @@ export class LocalCheckpointStore {
133
133
  reopen(token, scope, reason) {
134
134
  return this.inner.reopen(token, scope, reason);
135
135
  }
136
+ /** core 5.14.0(#147):入参是 `PendingSteerInput`(text/trusted + 可选 actor/inputId/priority),不是
137
+ * 两键单座 —— 形参窄成 `{text,trusted}` 会**静默丢掉**署名与幂等键(TS 的方法参数是双变的,窄形不
138
+ * 报错,只是把三个可选键从类型上抹掉,调用点再也传不进来)。队列语义与边界全在被包的 core
139
+ * `FileCheckpointStore` 里,本 wrapper 只做转发。 */
136
140
  setPendingSteer(token, scope, steer) {
137
141
  return this.inner.setPendingSteer(token, scope, steer);
138
142
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Thin re-export shim (design/158 A12 收编 — see src/plugins/approval-store-sql.ts for the
3
+ * implementation, now SINGLE-FILE DUAL-DIALECT alongside the TiDB twin). Class name, ctor arity,
4
+ * and every exported type/const/helper stay EXACTLY as they were so no consumer (pg-pool.ts's
5
+ * central schema composition, store-backend.ts, test/pg-approval-store-integration.test.ts) has
6
+ * to move.
7
+ */
8
+ export { PgApprovalStore, PG_APPROVAL_SCHEMA, ensureSchema, type ApprovalRow, type ApprovalStatus, } from "./approval-store-sql.js";
9
+ //# sourceMappingURL=pg-approval-store.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Thin re-export shim (design/158 A12 收编 — see src/plugins/approval-store-sql.ts for the
3
+ * implementation, now SINGLE-FILE DUAL-DIALECT alongside the TiDB twin). Class name, ctor arity,
4
+ * and every exported type/const/helper stay EXACTLY as they were so no consumer (pg-pool.ts's
5
+ * central schema composition, store-backend.ts, test/pg-approval-store-integration.test.ts) has
6
+ * to move.
7
+ */
8
+ export { PgApprovalStore, PG_APPROVAL_SCHEMA, ensureSchema, } from "./approval-store-sql.js";
9
+ //# sourceMappingURL=pg-approval-store.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Re-export shim (design/158 A12 收编) — the real implementation moved to breaker-state-sql.ts
3
+ * (single-file dual-dialect: `SqlBreakerState` + the `TiDBBreakerState`/`PgBreakerState` ctor subclasses).
4
+ * Kept so every consumer's `from "./pg-breaker-state.js"` import (incl. pg-pool.ts's central ensureSchema
5
+ * composition and the real dual-DB integration suite) keeps working unchanged.
6
+ */
7
+ export { PgBreakerState, PG_BREAKER_STATE_SCHEMA, ensureSchema } from "./breaker-state-sql.js";
8
+ //# sourceMappingURL=pg-breaker-state.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Re-export shim (design/158 A12 收编) — the real implementation moved to breaker-state-sql.ts
3
+ * (single-file dual-dialect: `SqlBreakerState` + the `TiDBBreakerState`/`PgBreakerState` ctor subclasses).
4
+ * Kept so every consumer's `from "./pg-breaker-state.js"` import (incl. pg-pool.ts's central ensureSchema
5
+ * composition and the real dual-DB integration suite) keeps working unchanged.
6
+ */
7
+ export { PgBreakerState, PG_BREAKER_STATE_SCHEMA, ensureSchema } from "./breaker-state-sql.js";
8
+ //# sourceMappingURL=pg-breaker-state.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PostgreSQL `CheckpointStore` — the implementation now lives in the single-file dual-dialect
3
+ * `checkpoint-store-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, ON CONFLICT, `IS NOT DISTINCT FROM` + its placeholder-arity consequence, the lossless
5
+ * JSON envelope) read side by side instead of drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name and ctor shape unchanged.
8
+ */
9
+ export { PgCheckpointStore } from "./checkpoint-store-sql.js";
10
+ //# sourceMappingURL=pg-checkpoint-store.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PostgreSQL `CheckpointStore` — the implementation now lives in the single-file dual-dialect
3
+ * `checkpoint-store-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, ON CONFLICT, `IS NOT DISTINCT FROM` + its placeholder-arity consequence, the lossless
5
+ * JSON envelope) read side by side instead of drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name and ctor shape unchanged.
8
+ */
9
+ export { PgCheckpointStore } from "./checkpoint-store-sql.js";
10
+ //# sourceMappingURL=pg-checkpoint-store.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Thin re-export shim (design/158 A12 定型半场) — the real implementation collapsed into
3
+ * `file-snapshot-store-sql.ts` (single file, dual dialect). Kept so every existing import path
4
+ * (store-backend.ts, the migration script, the contract/integration/minio-equivalence suites) is
5
+ * untouched: same class name, same ctor arity, same exported surface.
6
+ */
7
+ export { PgFileSnapshotStore } from "./file-snapshot-store-sql.js";
8
+ //# sourceMappingURL=pg-file-snapshot-store.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Thin re-export shim (design/158 A12 定型半场) — the real implementation collapsed into
3
+ * `file-snapshot-store-sql.ts` (single file, dual dialect). Kept so every existing import path
4
+ * (store-backend.ts, the migration script, the contract/integration/minio-equivalence suites) is
5
+ * untouched: same class name, same ctor arity, same exported surface.
6
+ */
7
+ export { PgFileSnapshotStore } from "./file-snapshot-store-sql.js";
8
+ //# sourceMappingURL=pg-file-snapshot-store.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PostgreSQL BAKE store — the implementation now lives in the single-file dual-dialect
3
+ * `image-bake-store-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, ON CONFLICT, jsonb casts, the admission-statement divergence, the PG-only
5
+ * protocol-byte defenses) read side by side instead of drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name, ctor shape, `PG_IMAGE_BAKE_SCHEMA` and
8
+ * `ensureSchema` are unchanged, so pg-pool.ts / store-backend.ts / the integration suite are untouched.
9
+ */
10
+ export { PgImageBake, PG_IMAGE_BAKE_SCHEMA, ensurePgImageBakeSchema as ensureSchema } from "./image-bake-store-sql.js";
11
+ export type { BakeStatus, BakeState, BakeErrorCode, BakeRecord, BakeEvent, CreateBakeInput, BakeTerminal, } from "./store-contracts.js";
12
+ //# sourceMappingURL=pg-image-bake.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * PostgreSQL BAKE store — the implementation now lives in the single-file dual-dialect
3
+ * `image-bake-store-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, ON CONFLICT, jsonb casts, the admission-statement divergence, the PG-only
5
+ * protocol-byte defenses) read side by side instead of drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name, ctor shape, `PG_IMAGE_BAKE_SCHEMA` and
8
+ * `ensureSchema` are unchanged, so pg-pool.ts / store-backend.ts / the integration suite are untouched.
9
+ */
10
+ export { PgImageBake, PG_IMAGE_BAKE_SCHEMA, ensurePgImageBakeSchema as ensureSchema } from "./image-bake-store-sql.js";
11
+ //# sourceMappingURL=pg-image-bake.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PostgreSQL-backed sandbox-image-pool INDEX store — the PG twin of {@link TiDBImageIndex}
3
+ * (src/plugins/tidb-image-index.ts).
4
+ *
5
+ * Thin re-export shim (design/158 A12 定型半场): the real implementation is the single-file dual-dialect
6
+ * `image-index-sql.ts` (`SqlImageIndex` + this file's `PgImageIndex` ctor subclass). Class name, ctor
7
+ * arity, and the exported type/schema surface are UNCHANGED — every consumer (store-backend.ts, pg-pool.ts,
8
+ * the real-DB integration suite) imports from here exactly as before.
9
+ */
10
+ export { PgImageIndex, PG_IMAGE_INDEX_SCHEMA, ensureSchema } from "./image-index-sql.js";
11
+ export type { ImageNestedBuildMode, ImageCapabilities, ImagePodContract, ImageStatus, ImageVisibility, ImageIndexEntry, ImageIndexUpsert, ImageViewer, ImageListFilter, } from "./store-contracts.js";
12
+ //# sourceMappingURL=pg-image-index.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * PostgreSQL-backed sandbox-image-pool INDEX store — the PG twin of {@link TiDBImageIndex}
3
+ * (src/plugins/tidb-image-index.ts).
4
+ *
5
+ * Thin re-export shim (design/158 A12 定型半场): the real implementation is the single-file dual-dialect
6
+ * `image-index-sql.ts` (`SqlImageIndex` + this file's `PgImageIndex` ctor subclass). Class name, ctor
7
+ * arity, and the exported type/schema surface are UNCHANGED — every consumer (store-backend.ts, pg-pool.ts,
8
+ * the real-DB integration suite) imports from here exactly as before.
9
+ */
10
+ export { PgImageIndex, PG_IMAGE_INDEX_SCHEMA, ensureSchema } from "./image-index-sql.js";
11
+ //# sourceMappingURL=pg-image-index.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PostgreSQL outcome ledger — the implementation now lives in the single-file dual-dialect
3
+ * `outcome-ledger-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, JSON encode, the AVG boolean-coercion divergence) read side by side instead of
5
+ * drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name, `PG_OUTCOME_LEDGER_SCHEMA` and `ensureSchema`
8
+ * are unchanged, so pg-pool.ts / store-backend.ts / the integration suite are untouched.
9
+ */
10
+ export { PgOutcomeLedger, PG_OUTCOME_LEDGER_SCHEMA, ensurePgOutcomeLedgerSchema as ensureSchema, } from "./outcome-ledger-sql.js";
11
+ export type { TaskOutcome, RunStatus } from "./outcome-ledger-sql.js";
12
+ //# sourceMappingURL=pg-outcome-ledger.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * PostgreSQL outcome ledger — the implementation now lives in the single-file dual-dialect
3
+ * `outcome-ledger-sql.ts` (design/158 A12 定型半场), alongside its TiDB twin so the dialect deltas
4
+ * (placeholders, JSON encode, the AVG boolean-coercion divergence) read side by side instead of
5
+ * drifting across two files.
6
+ *
7
+ * This file is the historical import path: class name, `PG_OUTCOME_LEDGER_SCHEMA` and `ensureSchema`
8
+ * are unchanged, so pg-pool.ts / store-backend.ts / the integration suite are untouched.
9
+ */
10
+ export { PgOutcomeLedger, PG_OUTCOME_LEDGER_SCHEMA, ensurePgOutcomeLedgerSchema as ensureSchema, } from "./outcome-ledger-sql.js";
11
+ //# sourceMappingURL=pg-outcome-ledger.js.map
@@ -35,6 +35,7 @@ import { ensureSchema as ensureBreakerStateSchema } from "./breaker-state-sql.js
35
35
  import { ensurePgOutcomeLedgerSchema as ensureOutcomeLedgerSchema } from "./outcome-ledger-sql.js";
36
36
  import { ensureSchema as ensureImageIndexSchema } from "./image-index-sql.js";
37
37
  import { ensurePgImageBakeSchema as ensureImageBakeSchema } from "./image-bake-store-sql.js";
38
+ import { ensurePgApprovalAskSchema } from "./approval-ask-store-sql.js";
38
39
  export const PG_SCHEMA_STATEMENTS = [
39
40
  `CREATE TABLE IF NOT EXISTS task_run (
40
41
  task_id VARCHAR(64) NOT NULL,
@@ -93,7 +94,13 @@ export const PG_SCHEMA_STATEMENTS = [
93
94
  terminal_at BIGINT,
94
95
  gate_kind VARCHAR(32),
95
96
  bound_input_hash VARCHAR(190),
97
+ -- pending_steer / pending_steer_queue / pending_steer_rev:队列化后的 durable steering(core 5.14.0
98
+ -- #147)。三列的语义、「为什么队列另起一列而不是覆盖旧列」(滚动升级窗里旧副本的无条件覆盖写)、
99
+ -- 48000 字节预算、以及「为什么另起一个整数 rev 列而不拿 JSON 旧值当 CAS 守卫」的完整由来见 MySQL
100
+ -- 孪生(tidb-pool.ts 同三列的行内注)。PG text 无长度上限,预算由 core 侧统一执法。
96
101
  pending_steer TEXT,
102
+ pending_steer_queue TEXT,
103
+ pending_steer_rev BIGINT NOT NULL DEFAULT 0,
97
104
  risk_descriptor TEXT,
98
105
  PRIMARY KEY (token)
99
106
  )`,
@@ -334,6 +341,10 @@ export async function ensurePgSchema(pool) {
334
341
  await ensureOutcomeLedgerSchema(client);
335
342
  await ensureImageIndexSchema(client);
336
343
  await ensureImageBakeSchema(client);
344
+ // #151 流内审批协议持久层(MySQL twin 走 tidb-pool 的 SCHEMA_STATEMENTS 展开)。该 ensure 收的是裸
345
+ // `PgQueryFn`(而非兄弟们收的 client 形),故在这里绑一层 —— DDL 仍然跑在**同一条** client 上,
346
+ // advisory lock 的 session 语义不受影响(见本函数头注:路由到别的池连接会静默作废那把锁)。
347
+ await ensurePgApprovalAskSchema((text, params) => client.query(text, params));
337
348
  }
338
349
  finally {
339
350
  if (locked) {