@sema-agent/server 7.7.1 → 7.8.1

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 (36) hide show
  1. package/dist/approval-card.d.ts +2 -2
  2. package/dist/approval-card.js +2 -2
  3. package/dist/approval-deny-reasons.d.ts +1 -1
  4. package/dist/approval-deny-reasons.js +1 -1
  5. package/dist/approval-reconciler.js +2 -2
  6. package/dist/config-types.d.ts +1 -1
  7. package/dist/http/routes/runs.js +1 -1
  8. package/dist/http/server.js +6 -1
  9. package/dist/plugins/approval-ask-store-memory.d.ts +1 -1
  10. package/dist/plugins/approval-ask-store-memory.js +17 -17
  11. package/dist/plugins/approval-ask-store-sql.d.ts +12 -12
  12. package/dist/plugins/approval-ask-store-sql.js +58 -58
  13. package/dist/plugins/background-agent-store-sql.d.ts +1 -1
  14. package/dist/plugins/background-agent-store-sql.js +2 -2
  15. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  16. package/dist/plugins/checkpoint-store-sql.js +6 -6
  17. package/dist/plugins/mailbox-store-sql.d.ts +2 -2
  18. package/dist/plugins/mailbox-store-sql.js +2 -2
  19. package/dist/plugins/memory-engine-pg.d.ts +2 -2
  20. package/dist/plugins/memory-engine-pg.js +17 -17
  21. package/dist/plugins/memory-engine-tidb.d.ts +2 -2
  22. package/dist/plugins/memory-engine-tidb.js +16 -16
  23. package/dist/plugins/memory-sync-store-pg.d.ts +2 -2
  24. package/dist/plugins/memory-sync-store-pg.js +19 -19
  25. package/dist/plugins/memory-sync-store-tidb.d.ts +1 -1
  26. package/dist/plugins/memory-sync-store-tidb.js +21 -21
  27. package/dist/plugins/pg-pool.js +26 -26
  28. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  29. package/dist/plugins/task-list-store-sql.js +11 -11
  30. package/dist/plugins/tidb-pool.js +26 -26
  31. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  32. package/dist/plugins/workflow-journal-store-sql.js +4 -4
  33. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  34. package/dist/plugins/workflow-run-store-sql.js +9 -9
  35. package/dist/security.js +9 -1
  36. package/package.json +2 -2
@@ -59,8 +59,8 @@ async function withReadSignal(op, signal, where) {
59
59
  // ─────────────────────────────────────────────────────────────────────────────────────────────────
60
60
  // DDL
61
61
  // ─────────────────────────────────────────────────────────────────────────────────────────────────
62
- export const APPROVAL_ASKS_TABLE = "approval_asks";
63
- export const APPROVAL_BATCHES_TABLE = "approval_batches";
62
+ export const APPROVAL_ASK_TABLE = "approval_ask";
63
+ export const APPROVAL_BATCH_TABLE = "approval_batch";
64
64
  /**
65
65
  * TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
66
66
  * `SCHEMA_STATEMENTS` 用 `...TIDB_APPROVAL_ASK_STATEMENTS` 展开它们,于是这两张表随中央
@@ -106,18 +106,18 @@ export const APPROVAL_BATCHES_TABLE = "approval_batches";
106
106
  * 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
107
107
  */
108
108
  export const TIDB_APPROVAL_ASK_STATEMENTS = [
109
- `CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
109
+ `CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
110
110
  batch_id VARCHAR(64) NOT NULL,
111
111
  task_id VARCHAR(64) NOT NULL,
112
112
  state VARCHAR(32) NOT NULL,
113
113
  bound_ask_id VARCHAR(64) NULL,
114
- version INT NOT NULL DEFAULT 0,
114
+ rev INT NOT NULL DEFAULT 0,
115
115
  created_at_ms BIGINT NOT NULL,
116
116
  updated_at_ms BIGINT NOT NULL,
117
117
  PRIMARY KEY (batch_id),
118
118
  KEY idx_batch_task (task_id)
119
119
  ) COLLATE utf8mb4_bin`,
120
- `CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
120
+ `CREATE TABLE IF NOT EXISTS ${APPROVAL_ASK_TABLE} (
121
121
  ask_id VARCHAR(64) NOT NULL,
122
122
  task_id VARCHAR(64) NOT NULL,
123
123
  source_task_id VARCHAR(255) NOT NULL,
@@ -137,7 +137,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
137
137
  state VARCHAR(32) NOT NULL,
138
138
  -- clay 批 2026-07-26 TINYINT(1) 归一先例(image-bake-store-sql.ts 头注):布尔列 TiDB 侧 TINYINT(1).
139
139
  provisional TINYINT(1) NOT NULL DEFAULT 0,
140
- version INT NOT NULL DEFAULT 0,
140
+ rev INT NOT NULL DEFAULT 0,
141
141
  decision VARCHAR(16) NULL,
142
142
  decision_actor TEXT NULL,
143
143
  decision_note TEXT NULL,
@@ -173,18 +173,18 @@ export async function ensureTiDBApprovalAskSchema(pool) {
173
173
  await pool.query(stmt);
174
174
  }
175
175
  export async function ensurePgApprovalAskSchema(q) {
176
- await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
176
+ await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
177
177
  batch_id VARCHAR(64) COLLATE "C" NOT NULL,
178
178
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
179
179
  state VARCHAR(32) COLLATE "C" NOT NULL,
180
180
  bound_ask_id VARCHAR(64) COLLATE "C",
181
- version INT NOT NULL DEFAULT 0,
181
+ rev INT NOT NULL DEFAULT 0,
182
182
  created_at_ms BIGINT NOT NULL,
183
183
  updated_at_ms BIGINT NOT NULL,
184
184
  PRIMARY KEY (batch_id)
185
185
  )`);
186
- await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCHES_TABLE} (task_id)`);
187
- await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
186
+ await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCH_TABLE} (task_id)`);
187
+ await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASK_TABLE} (
188
188
  ask_id VARCHAR(64) COLLATE "C" NOT NULL,
189
189
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
190
190
  source_task_id VARCHAR(255) COLLATE "C" NOT NULL,
@@ -201,7 +201,7 @@ export async function ensurePgApprovalAskSchema(q) {
201
201
  state VARCHAR(32) COLLATE "C" NOT NULL,
202
202
  -- clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT 0/1(此前本家族用过 BOOLEAN,已裁弃).
203
203
  provisional SMALLINT NOT NULL DEFAULT 0,
204
- version INT NOT NULL DEFAULT 0,
204
+ rev INT NOT NULL DEFAULT 0,
205
205
  decision VARCHAR(16) COLLATE "C",
206
206
  decision_actor TEXT COLLATE "C",
207
207
  decision_note TEXT COLLATE "C",
@@ -220,18 +220,18 @@ export async function ensurePgApprovalAskSchema(q) {
220
220
  -- (task_id, idempotency_key) 复合唯一——MySQL twin 逐字同形(改动理由见那侧注)。
221
221
  CONSTRAINT uq_approval_ask_idem UNIQUE (task_id, idempotency_key)
222
222
  )`);
223
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASKS_TABLE} (task_id, state)`);
223
+ await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASK_TABLE} (task_id, state)`);
224
224
  // 车3 §5.1:sync 腿开流重放读口(listPendingBySession)的支撑索引 —— MySQL twin 的
225
225
  // `KEY idx_ask_session_state (session_id, state)` 同形。
226
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASKS_TABLE} (session_id, state)`);
227
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASKS_TABLE} (batch_id)`);
228
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASKS_TABLE} (state)`);
226
+ await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASK_TABLE} (session_id, state)`);
227
+ await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASK_TABLE} (batch_id)`);
228
+ await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASK_TABLE} (state)`);
229
229
  }
230
230
  const ASK_COLS = "ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, " +
231
- "provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, " +
231
+ "provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, " +
232
232
  "gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, " +
233
233
  "created_at_ms, updated_at_ms";
234
- const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, version, created_at_ms, updated_at_ms";
234
+ const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
235
235
  /** 唯一键冲突的方言判别(checkpoint-store-sql.ts 同一对常量)——`decideAsk` 用它把 `(task_id,
236
236
  * idempotency_key)` 撞键翻成 typed 的 `idempotency_conflict` 臂,不让驱动错误对象漏给消费层。 */
237
237
  const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
@@ -266,7 +266,7 @@ function mapAskRow(r) {
266
266
  boundInputHash: strOrNull(r.bound_input_hash),
267
267
  state: String(r.state),
268
268
  provisional: toBool(r.provisional),
269
- version: Number(r.version),
269
+ rev: Number(r.rev),
270
270
  decision: strOrNull(r.decision) ?? null,
271
271
  decisionActor: r.decision_actor == null ? null : parseJsonColumn(r.decision_actor),
272
272
  decisionNote: strOrNull(r.decision_note),
@@ -289,7 +289,7 @@ function mapBatchRow(r) {
289
289
  taskId: String(r.task_id),
290
290
  state: String(r.state),
291
291
  boundAskId: strOrNull(r.bound_ask_id),
292
- version: Number(r.version),
292
+ rev: Number(r.rev),
293
293
  createdAtMs: Number(r.created_at_ms),
294
294
  updatedAtMs: Number(r.updated_at_ms),
295
295
  };
@@ -353,12 +353,12 @@ export class SqlApprovalAskStore {
353
353
  * 在这一点上同义,不必再纠结 InnoDB 读视图与 TiDB start_ts 的差别。
354
354
  */
355
355
  async getAskOn(conn, askId) {
356
- const { rows } = await conn.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE ask_id = $1`), [askId]);
356
+ const { rows } = await conn.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [askId]);
357
357
  return rows[0] ? mapAskRow(rows[0]) : null;
358
358
  }
359
359
  /** {@link getAskOn} 的批侧同形(同一条纪律:失败臂回读走本连接)。 */
360
360
  async getBatchOn(conn, batchId) {
361
- const { rows } = await conn.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCHES_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCHES_TABLE} WHERE batch_id = $1`), [batchId]);
361
+ const { rows } = await conn.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = $1`), [batchId]);
362
362
  return rows[0] ? mapBatchRow(rows[0]) : null;
363
363
  }
364
364
  async ensureAsk(row) {
@@ -370,11 +370,11 @@ export class SqlApprovalAskStore {
370
370
  const conn = await this.db.connect();
371
371
  try {
372
372
  await conn.begin();
373
- await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
373
+ await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
374
374
  // 🔴 `idempotency_key` **不在 INSERT 列清单里**(车4 §12-E 双写者收口):该列已归回决专用,只有
375
375
  // `decideAsk` 赢 CAS 时写它,铸行恒 NULL。留在这里的 NULL 字面量就是这条纪律的落笔处。
376
- const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
377
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
376
+ const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
377
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
378
378
  "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15) ON CONFLICT (ask_id) DO NOTHING"), [
379
379
  row.askId,
380
380
  row.taskId,
@@ -427,8 +427,8 @@ export class SqlApprovalAskStore {
427
427
  const setSql = cols.map((c) => `${c} = ${ph(dialect, ++n)}`).join(", ");
428
428
  const askIdPh = ph(dialect, ++n);
429
429
  const fromPh = ph(dialect, ++n);
430
- // `version = version + 1`(恒变列,MySQL 同值 UPDATE affectedRows=0 坑——见文件头注 CAS 铁则②)。
431
- const res = await this.db.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET ${setSql}, version = version + 1 WHERE ask_id = ${askIdPh} AND state = ${fromPh}`, [...vals, askId, from]);
430
+ // `rev = rev + 1`(恒变列,MySQL 同值 UPDATE affectedRows=0 坑——见文件头注 CAS 铁则②)。
431
+ const res = await this.db.query(`UPDATE ${APPROVAL_ASK_TABLE} SET ${setSql}, rev = rev + 1 WHERE ask_id = ${askIdPh} AND state = ${fromPh}`, [...vals, askId, from]);
432
432
  return res.affected === 1;
433
433
  }
434
434
  async decideAsk(askId, batchId, decision) {
@@ -438,8 +438,8 @@ export class SqlApprovalAskStore {
438
438
  const conn = await this.db.connect();
439
439
  try {
440
440
  await conn.begin();
441
- // ① 批必须还 OPEN(自增写 version 保证 affected 语义在 CLIENT_FOUND_ROWS 关闭下仍=1;=0 ⇒ 批已封)。
442
- const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCHES_TABLE} SET version = version + 1, updated_at_ms = ? WHERE batch_id = ? AND state = 'OPEN'`, `UPDATE ${APPROVAL_BATCHES_TABLE} SET version = version + 1, updated_at_ms = $1 WHERE batch_id = $2 AND state = 'OPEN'`), [now, batchId]);
441
+ // ① 批必须还 OPEN(自增写 rev 保证 affected 语义在 CLIENT_FOUND_ROWS 关闭下仍=1;=0 ⇒ 批已封)。
442
+ const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = ? WHERE batch_id = ? AND state = 'OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = $1 WHERE batch_id = $2 AND state = 'OPEN'`), [now, batchId]);
443
443
  if (batchRes.affected !== 1) {
444
444
  await conn.rollback();
445
445
  return { ok: false, reason: "batch_closed", row: await this.getAskOn(conn, askId) };
@@ -449,7 +449,7 @@ export class SqlApprovalAskStore {
449
449
  // CAS(输)再报 409,把一次合法回放变成冲突。同一 UPDATE ⇒ 键与终态同生共死。
450
450
  let askRes; // 显式标注:`let x;` 是 evolving-any,本仓按强类型用
451
451
  try {
452
- askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='DECIDED', decision=?, decision_actor=?, decision_note=?, decided_at_ms=?, idempotency_key=?, version=version+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASKS_TABLE} SET state='DECIDED', decision=$1, decision_actor=$2, decision_note=$3, decided_at_ms=$4, idempotency_key=$5, version=version+1, updated_at_ms=$6 WHERE ask_id=$7 AND state='STREAM_PENDING'`), [decision.decision, decision.decisionActor == null ? null : this.json(decision.decisionActor), decision.decisionNote ?? null, now, decision.idempotencyKey ?? null, now, askId]);
452
+ askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=?, decision_actor=?, decision_note=?, decided_at_ms=?, idempotency_key=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=$1, decision_actor=$2, decision_note=$3, decided_at_ms=$4, idempotency_key=$5, rev=rev+1, updated_at_ms=$6 WHERE ask_id=$7 AND state='STREAM_PENDING'`), [decision.decision, decision.decisionActor == null ? null : this.json(decision.decisionActor), decision.decisionNote ?? null, now, decision.idempotencyKey ?? null, now, askId]);
453
453
  }
454
454
  catch (err) {
455
455
  // `(task_id, idempotency_key)` 撞键 = 同一 key 已被本 task 内**另一条** ask 的回决占用。这是调用方
@@ -490,27 +490,27 @@ export class SqlApprovalAskStore {
490
490
  await conn.begin();
491
491
  // ⓪ 锁序不变量(真双库 §6-4b 死锁实抓,2026-08-05):凡同一事务触碰批行+ask 行者,**一律批行先**
492
492
  // (decideAsk/bindBatch/abortBatch 皆批先;expireAsk 原是 ask 先 ⇒ 与 decideAsk 构成 AB-BA,
493
- // TiDB 悲观锁当场 `Deadlock found`)。这里用无谓词的 version 自增先取批行锁作序列化点——
493
+ // TiDB 悲观锁当场 `Deadlock found`)。这里用无谓词的 rev 自增先取批行锁作序列化点——
494
494
  // 带 state 谓词的转移放后面(谓词不符时部分引擎不留锁,序列化点会失效)。ask CAS 输时整个
495
495
  // 事务回滚,这次自增一并撤销,无痕。
496
- const lockRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCHES_TABLE} SET version=version+1 WHERE batch_id=?`, `UPDATE ${APPROVAL_BATCHES_TABLE} SET version=version+1 WHERE batch_id=$1`), [batchId]);
496
+ const lockRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=?`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=$1`), [batchId]);
497
497
  if (lockRes.affected !== 1) {
498
498
  await conn.rollback(); // 批行不存在 = ensureAsk 从未为它建批(编程错误形,如实不赢)
499
499
  return { won: false, voidedSiblings: [] };
500
500
  }
501
501
  // ① 中选:ask STREAM_PENDING → PARKING(单赢者;输 = 已被回决/取消,整体放弃)。
502
- const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='PARKING', version=version+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASKS_TABLE} SET state='PARKING', version=version+1, updated_at_ms=$1 WHERE ask_id=$2 AND state='STREAM_PENDING'`), [now, askId]);
502
+ const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=$1 WHERE ask_id=$2 AND state='STREAM_PENDING'`), [now, askId]);
503
503
  if (askRes.affected !== 1) {
504
504
  await conn.rollback();
505
505
  return { won: false, voidedSiblings: [] };
506
506
  }
507
507
  // ② 批 OPEN → ROUTING_UNBOUND(已是 ROUTING_UNBOUND = 兄弟先到,affected=0 也算成功续走③——不检查
508
508
  // affected,批状态无论如何这一刻都已经是 ROUTING_UNBOUND 或更前进)。
509
- await conn.query(this.q(`UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ROUTING_UNBOUND', version=version+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ROUTING_UNBOUND', version=version+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
509
+ await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
510
510
  // ③ 兄弟撤卡:PG 用 RETURNING 单语句拿名单;TiDB 无 RETURNING,同事务先 SELECT 后 UPDATE。
511
511
  let voidedSiblings;
512
512
  if (this.db.dialect === "pg") {
513
- const res = await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
513
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
514
514
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
515
515
  }
516
516
  else {
@@ -521,11 +521,11 @@ export class SqlApprovalAskStore {
521
521
  // 会在名单里却没被改(撤卡帧误发)。PG 的 RETURNING 与 InMemory 都返回「真正被改的那一组」,
522
522
  // 所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。锁序:本方法已先取批行锁(步⓪),
523
523
  // 此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
524
- const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASKS_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
524
+ const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
525
525
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
526
526
  if (voidedSiblings.length > 0) {
527
527
  const inList = voidedSiblings.map(() => "?").join(", ");
528
- await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
528
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
529
529
  now,
530
530
  ...voidedSiblings,
531
531
  ]);
@@ -554,7 +554,7 @@ export class SqlApprovalAskStore {
554
554
  await conn.begin();
555
555
  // bind-once:批 ROUTING_UNBOUND → ROUTING_BOUND + bound_ask_id(WHERE bound_ask_id IS NULL 是第二道
556
556
  // 保险——state 谓词已经把并发者挡在外面,这里双保险防「同一批被 bind 两次」哪怕未来状态机被误改)。
557
- const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ROUTING_BOUND', bound_ask_id=?, version=version+1, updated_at_ms=? WHERE batch_id=? AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`, `UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ROUTING_BOUND', bound_ask_id=$1, version=version+1, updated_at_ms=$2 WHERE batch_id=$3 AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`), [askId, now, batchId]);
557
+ const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=?, rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=$1, rev=rev+1, updated_at_ms=$2 WHERE batch_id=$3 AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`), [askId, now, batchId]);
558
558
  if (batchRes.affected !== 1) {
559
559
  // 判别式的信息来源 = 批行的真实态。🔴 回读**放在 rollback 之后、走池上的独立读**,不在本事务内读:
560
560
  // TiDB 的事务快照按 start_ts 取,同事务内的普通 SELECT 可能读到 BEGIN 时刻的旧值(InnoDB 的读视图
@@ -568,7 +568,7 @@ export class SqlApprovalAskStore {
568
568
  return { ok: false, batchState: cur.state, ...(cur.boundAskId !== null ? { boundAskId: cur.boundAskId } : {}) };
569
569
  }
570
570
  // 中选 ask PARKING → PARKED(带 gate 三件)。
571
- const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='PARKED', gate_token=?, gate_bound_call_id=?, gate_bound_input_hash=?, version=version+1, updated_at_ms=? WHERE ask_id=? AND state='PARKING'`, `UPDATE ${APPROVAL_ASKS_TABLE} SET state='PARKED', gate_token=$1, gate_bound_call_id=$2, gate_bound_input_hash=$3, version=version+1, updated_at_ms=$4 WHERE ask_id=$5 AND state='PARKING'`), [gate.gateToken, gate.gateBoundCallId ?? null, gate.gateBoundInputHash ?? null, now, askId]);
571
+ const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=?, gate_bound_call_id=?, gate_bound_input_hash=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='PARKING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=$1, gate_bound_call_id=$2, gate_bound_input_hash=$3, rev=rev+1, updated_at_ms=$4 WHERE ask_id=$5 AND state='PARKING'`), [gate.gateToken, gate.gateBoundCallId ?? null, gate.gateBoundInputHash ?? null, now, askId]);
572
572
  if (askRes.affected !== 1) {
573
573
  await conn.rollback();
574
574
  // ask 不在 PARKING(被别的竞争者收走)⇒ 整事务回滚,批**没有**被绑上。如实报批的回滚后真值:
@@ -579,7 +579,7 @@ export class SqlApprovalAskStore {
579
579
  // 读法与 expireAsk 逐字同形:PG `RETURNING` 单语句;TiDB 无 RETURNING ⇒ 同事务先 SELECT 后 UPDATE。
580
580
  let voidedSiblings;
581
581
  if (this.db.dialect === "pg") {
582
- const res = await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
582
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
583
583
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
584
584
  }
585
585
  else {
@@ -590,11 +590,11 @@ export class SqlApprovalAskStore {
590
590
  // 都返回「真正被改的那一组」,所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。
591
591
  // 修法:锁定读把行集钉死,再只更新这批 id ⇒ 返回名单恒等于被改集合,三形同义。
592
592
  // 锁序:本方法已先取批行锁(上面那条 CAS),此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
593
- const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASKS_TABLE} WHERE batch_id=? AND state='PARKING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
593
+ const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='PARKING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
594
594
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
595
595
  if (voidedSiblings.length > 0) {
596
596
  const inList = voidedSiblings.map(() => "?").join(", ");
597
- await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [now, ...voidedSiblings]);
597
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [now, ...voidedSiblings]);
598
598
  }
599
599
  }
600
600
  await conn.commit();
@@ -618,7 +618,7 @@ export class SqlApprovalAskStore {
618
618
  const conn = await this.db.connect();
619
619
  try {
620
620
  await conn.begin();
621
- const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ABORTED', version=version+1, updated_at_ms=? WHERE batch_id=? AND (state='OPEN' OR state='ROUTING_UNBOUND')`, `UPDATE ${APPROVAL_BATCHES_TABLE} SET state='ABORTED', version=version+1, updated_at_ms=$1 WHERE batch_id=$2 AND (state='OPEN' OR state='ROUTING_UNBOUND')`), [now, batchId]);
621
+ const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ABORTED', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND (state='OPEN' OR state='ROUTING_UNBOUND')`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ABORTED', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND (state='OPEN' OR state='ROUTING_UNBOUND')`), [now, batchId]);
622
622
  if (batchRes.affected !== 1) {
623
623
  await conn.rollback();
624
624
  return [];
@@ -627,18 +627,18 @@ export class SqlApprovalAskStore {
627
627
  // (§3.0 原话),PARKING/PARKED/DENIED 同样不在这条 UPDATE 的谓词里,保持不动。
628
628
  let voided;
629
629
  if (this.db.dialect === "pg") {
630
- const res = await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId]);
630
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId]);
631
631
  voided = res.rows.map((r) => String(r.ask_id));
632
632
  }
633
633
  else {
634
634
  // 🔴 同 `expireAsk`/`bindBatch` 的锁定读改造(车5 同病修):非锁定读的名单与宽谓词 UPDATE 的
635
635
  // 行集不是同一个 ⇒ 撤卡帧漏发/误发,且分歧只在 MySQL 方言上成立。锁序:批行锁(上一条 CAS)
636
636
  // 已取,此处再取 ask 行锁。
637
- const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASKS_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' FOR UPDATE`, [batchId]);
637
+ const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' FOR UPDATE`, [batchId]);
638
638
  voided = sel.rows.map((r) => String(r.ask_id));
639
639
  if (voided.length > 0) {
640
640
  const inList = voided.map(() => "?").join(", ");
641
- await conn.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET state='VOID', version=version+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [now, ...voided]);
641
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [now, ...voided]);
642
642
  }
643
643
  }
644
644
  await conn.commit();
@@ -669,13 +669,13 @@ export class SqlApprovalAskStore {
669
669
  * `updatedAtMs`**——它被队列轮转每轮刷新,量它的 TTL 永不到期。
670
670
  */
671
671
  async listByState(state, limit) {
672
- const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE state = ? ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE state = $1 ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT $2`), [state, limit]);
672
+ const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = ? ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = $1 ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT $2`), [state, limit]);
673
673
  return rows.map(mapAskRow);
674
674
  }
675
675
  async listPendingByTask(taskId, signal) {
676
676
  if (signal?.aborted === true)
677
677
  throw abortedReadError("listPendingByTask");
678
- const { rows } = await withReadSignal(this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = ? AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = $1 AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`), [taskId]), signal, "listPendingByTask");
678
+ const { rows } = await withReadSignal(this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`), [taskId]), signal, "listPendingByTask");
679
679
  return rows.map(mapAskRow);
680
680
  }
681
681
  async listPendingBySession(sessionId, owner, signal) {
@@ -684,12 +684,12 @@ export class SqlApprovalAskStore {
684
684
  // 租户门的两段谓词文本(见接口注):`owner = ?` 对 NULL 恒 UNKNOWN ⇒ 无租户身份的部署形必须走
685
685
  // `owner IS NULL`,否则这条读口在那些部署上恒回零行(一条静默失效的重放面,比报错更难发现)。
686
686
  const { rows } = await withReadSignal(this.db.query(owner === null
687
- ? this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`)
688
- : this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
687
+ ? this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`)
688
+ : this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
689
689
  return rows.map(mapAskRow);
690
690
  }
691
691
  async getAsk(askId) {
692
- const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE ask_id = $1`), [
692
+ const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
693
693
  askId,
694
694
  ]);
695
695
  return rows[0] ? mapAskRow(rows[0]) : null;
@@ -697,13 +697,13 @@ export class SqlApprovalAskStore {
697
697
  async getByIdempotencyKey(taskId, key) {
698
698
  assertIdempotencyKeyShape(key); // F2 卫生门:坏形键不做注定方言分歧的查询
699
699
  // 谓词跟 UNIQUE `(task_id, idempotency_key)` 逐字对齐 ⇒ 至多一行,且天然带租户门(见接口注)。
700
- const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = ? AND idempotency_key = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = $1 AND idempotency_key = $2`), [taskId, key]);
700
+ const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND idempotency_key = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND idempotency_key = $2`), [taskId, key]);
701
701
  return rows[0] ? mapAskRow(rows[0]) : null;
702
702
  }
703
- async deferReconcile(askId, expectedState, expectedVersion, nowMs) {
703
+ async deferReconcile(askId, expectedState, expectedRev, nowMs) {
704
704
  // 纯 touch:**不碰 state**(见接口注——同态转移在状态机表上无出边,这里刻意走另一扇门),只推
705
- // `updated_at_ms` 与恒变的 `version`。`AND state=? AND version=?` 双谓词 = 两副本同扫时的单赢者门。
706
- const res = await this.db.query(this.q(`UPDATE ${APPROVAL_ASKS_TABLE} SET updated_at_ms = ?, version = version + 1 WHERE ask_id = ? AND state = ? AND version = ?`, `UPDATE ${APPROVAL_ASKS_TABLE} SET updated_at_ms = $1, version = version + 1 WHERE ask_id = $2 AND state = $3 AND version = $4`), [nowMs, askId, expectedState, expectedVersion]);
705
+ // `updated_at_ms` 与恒变的 `rev`。`AND state=? AND rev=?` 双谓词 = 两副本同扫时的单赢者门。
706
+ const res = await this.db.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = ?, rev = rev + 1 WHERE ask_id = ? AND state = ? AND rev = ?`, `UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = $1, rev = rev + 1 WHERE ask_id = $2 AND state = $3 AND rev = $4`), [nowMs, askId, expectedState, expectedRev]);
707
707
  return res.affected === 1;
708
708
  }
709
709
  async resolveProvisional(askId, from, to, patch) {
@@ -718,15 +718,15 @@ export class SqlApprovalAskStore {
718
718
  const askIdPh = ph(dialect, ++n);
719
719
  const provPh = ph(dialect, ++n);
720
720
  const fromPh = ph(dialect, ++n);
721
- const res = await this.db.query(`UPDATE ${APPROVAL_ASKS_TABLE} SET ${setSql}, version = version + 1 WHERE ask_id = ${askIdPh} AND provisional = ${provPh} AND state = ${fromPh}`, [...vals, askId, 1, from]);
721
+ const res = await this.db.query(`UPDATE ${APPROVAL_ASK_TABLE} SET ${setSql}, rev = rev + 1 WHERE ask_id = ${askIdPh} AND provisional = ${provPh} AND state = ${fromPh}`, [...vals, askId, 1, from]);
722
722
  return res.affected === 1;
723
723
  }
724
724
  async deleteByTask(taskId) {
725
- await this.db.query(this.q(`DELETE FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = ?`, `DELETE FROM ${APPROVAL_ASKS_TABLE} WHERE task_id = $1`), [taskId]);
726
- await this.db.query(this.q(`DELETE FROM ${APPROVAL_BATCHES_TABLE} WHERE task_id = ?`, `DELETE FROM ${APPROVAL_BATCHES_TABLE} WHERE task_id = $1`), [taskId]);
725
+ await this.db.query(this.q(`DELETE FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ?`, `DELETE FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1`), [taskId]);
726
+ await this.db.query(this.q(`DELETE FROM ${APPROVAL_BATCH_TABLE} WHERE task_id = ?`, `DELETE FROM ${APPROVAL_BATCH_TABLE} WHERE task_id = $1`), [taskId]);
727
727
  }
728
728
  async getBatch(batchId) {
729
- const { rows } = await this.db.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCHES_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCHES_TABLE} WHERE batch_id = $1`), [batchId]);
729
+ const { rows } = await this.db.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = $1`), [batchId]);
730
730
  return rows[0] ? mapBatchRow(rows[0]) : null;
731
731
  }
732
732
  }
@@ -32,7 +32,7 @@
32
32
  import type { Pool } from "mysql2/promise";
33
33
  import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "@sema-agent/core";
34
34
  import type { PgQueryFn } from "./pg-query.js";
35
- export declare const BACKGROUND_AGENT_TABLE = "background_agents";
35
+ export declare const BACKGROUND_AGENT_TABLE = "background_agent";
36
36
  type ReapOpts = {
37
37
  maxAgeMs?: number;
38
38
  keep?: number;
@@ -1,6 +1,6 @@
1
1
  import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "@sema-agent/core";
2
2
  import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
3
- export const BACKGROUND_AGENT_TABLE = "background_agents";
3
+ export const BACKGROUND_AGENT_TABLE = "background_agent";
4
4
  const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json";
5
5
  /** put/update 共用的投影列(scope_key/handle 键列另拼;record_json 恒最后)。core 1.383([1565] 提货单)
6
6
  * `updateIf` 的 CAS 守卫需要 `parked_checkpoint_token`/`park_claim_id` 是**专用列**——守卫条件必须在
@@ -133,7 +133,7 @@ function limitClause(limit) {
133
133
  return ` LIMIT ${Number.isFinite(n) ? Math.max(0, n) : 0}`;
134
134
  }
135
135
  /* ┌─ 历史:曾经的运行期 ADD COLUMN 迁移腿(已折进 CREATE,2026-07-26 clay 裁「删库重建」)──────────┐
136
- * 两个 ensure 曾在每次 boot 给 `background_agents` 补三列;现已全部折叠为 CREATE TABLE 里的列定义。
136
+ * 两个 ensure 曾在每次 boot 给 `background_agent` 补三列;现已全部折叠为 CREATE TABLE 里的列定义。
137
137
  * 保留由来,因为它解释了这些列**为什么存在**,以及当年为什么 TiDB 腿用探针而不是 `IF NOT EXISTS`:
138
138
  *
139
139
  * ① root_session_id(core 1.367 δ,additive):1.248 建的存量表没有这列,靠迁移补。
@@ -89,7 +89,7 @@ export interface CheckpointAskCandidate {
89
89
  sourceTaskId: string | null;
90
90
  /** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
91
91
  boundCallId: string | null;
92
- /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_asks.bound_input_hash` 相等才算命中)。 */
92
+ /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_ask.bound_input_hash` 相等才算命中)。 */
93
93
  boundInputHash: string | null;
94
94
  /** 在场 ⇒ 这行读不出(blob 版本超前 / JSON 坏)。收敛器视同不匹配;**从不是** false,缺席即可读。 */
95
95
  unparseable?: true;
@@ -348,7 +348,7 @@ export declare class SqlCheckpointStore implements CheckpointStore {
348
348
  *
349
349
  * The extra `task_active` guard closes a multi-gate window: between a resume's CAS (pending→resolved) and
350
350
  * the task hitting its NEXT gate (writing a new pending checkpoint), the session has no pending checkpoint —
351
- * yet `putCtx` only stamps `updated_at` at submit, so the ctx could be older than `cutoff` and was being
351
+ * yet `putCtx` only stamps `updated_at_ms` at submit, so the ctx could be older than `cutoff` and was being
352
352
  * reaped, making the next resume 409 ("resume context missing"). `task_active` holds the session lock for
353
353
  * the WHOLE submit→last-resume-complete span (running OR suspended), so excluding sessions with an active
354
354
  * claim keeps the ctx exactly as long as a run still needs it, while still GC'ing it once the run is terminal.
@@ -736,9 +736,9 @@ export class SqlCheckpointStore {
736
736
  // ─────────────────────── checkpoint_ctx (service rebuild inputs; sessionId-keyed) ───────────────────────
737
737
  /** Upsert the resume rebuild inputs for a session (written at SUBMIT when durable mode is on). */
738
738
  async putCtx(sessionId, ctx) {
739
- await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES (?,?,?) " +
740
- "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at = VALUES(updated_at)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES ($1,$2::jsonb,$3) " +
741
- "ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at = EXCLUDED.updated_at"), [sessionId, this.json(ctx, "checkpoint ctx"), Date.now()]);
739
+ await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES (?,?,?) " +
740
+ "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at_ms = VALUES(updated_at_ms)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES ($1,$2::jsonb,$3) " +
741
+ "ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at_ms = EXCLUDED.updated_at_ms"), [sessionId, this.json(ctx, "checkpoint ctx"), Date.now()]);
742
742
  }
743
743
  async getCtx(sessionId) {
744
744
  const { rows } = await this.db.query(this.q("SELECT ctx FROM checkpoint_ctx WHERE session_id=?", "SELECT ctx FROM checkpoint_ctx WHERE session_id=$1"), [sessionId]);
@@ -750,15 +750,15 @@ export class SqlCheckpointStore {
750
750
  *
751
751
  * The extra `task_active` guard closes a multi-gate window: between a resume's CAS (pending→resolved) and
752
752
  * the task hitting its NEXT gate (writing a new pending checkpoint), the session has no pending checkpoint —
753
- * yet `putCtx` only stamps `updated_at` at submit, so the ctx could be older than `cutoff` and was being
753
+ * yet `putCtx` only stamps `updated_at_ms` at submit, so the ctx could be older than `cutoff` and was being
754
754
  * reaped, making the next resume 409 ("resume context missing"). `task_active` holds the session lock for
755
755
  * the WHOLE submit→last-resume-complete span (running OR suspended), so excluding sessions with an active
756
756
  * claim keeps the ctx exactly as long as a run still needs it, while still GC'ing it once the run is terminal.
757
757
  */
758
758
  async reapCtx(cutoff) {
759
- const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at < ? " +
759
+ const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at_ms < ? " +
760
760
  "AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
761
- "AND session_id NOT IN (SELECT session_id FROM task_active)", "DELETE FROM checkpoint_ctx WHERE updated_at < $1 " +
761
+ "AND session_id NOT IN (SELECT session_id FROM task_active)", "DELETE FROM checkpoint_ctx WHERE updated_at_ms < $1 " +
762
762
  "AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
763
763
  "AND session_id NOT IN (SELECT session_id FROM task_active)"), [cutoff]);
764
764
  return res.affected;
@@ -39,8 +39,8 @@ import type { Pool as MySqlPool } from "mysql2/promise";
39
39
  import type { Pool as PgPool } from "pg";
40
40
  import type { MailboxLease, MailboxStore } from "@sema-agent/core";
41
41
  type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
42
- export declare const MAILBOX_TABLE = "mailboxes";
43
- export declare const MAILBOX_MSG_TABLE = "mailbox_messages";
42
+ export declare const MAILBOX_TABLE = "mailbox";
43
+ export declare const MAILBOX_MSG_TABLE = "mailbox_message";
44
44
  export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
45
45
  export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
46
46
  export declare class TiDBMailboxStore implements MailboxStore {
@@ -1,6 +1,6 @@
1
1
  import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
2
- export const MAILBOX_TABLE = "mailboxes";
3
- export const MAILBOX_MSG_TABLE = "mailbox_messages";
2
+ export const MAILBOX_TABLE = "mailbox";
3
+ export const MAILBOX_MSG_TABLE = "mailbox_message";
4
4
  /** InMemory append 同文案形(default-deny 谓词精神:无 scope 的盒=路由不明,写入即拒)。 */
5
5
  function assertScope(scope) {
6
6
  if (scope === undefined || scope === "") {
@@ -4,8 +4,8 @@ import type { MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchRep
4
4
  /** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
5
5
  * the retired MemoryStore plane and this entry plane must never cross-write. */
6
6
  export declare const PG_MEMORY_ENGINE_TABLES: {
7
- readonly entries: "agent_memory_engine_entries";
8
- readonly cursors: "agent_memory_engine_cursors";
7
+ readonly entry: "agent_memory_engine_entry";
8
+ readonly cursor: "agent_memory_engine_cursor";
9
9
  };
10
10
  /** Options for {@link ensurePgMemoryEngineSchema} — same `memoryVector` contract as PgAgentSchemaOptions. */
11
11
  export interface PgMemoryEngineSchemaOptions {