@sema-agent/server 7.57.0 → 7.59.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 (61) hide show
  1. package/USAGE.md +2 -0
  2. package/dist/approval-card.d.ts +47 -0
  3. package/dist/approval-card.js +18 -0
  4. package/dist/boot/parked-revive-gate.d.ts +3 -0
  5. package/dist/boot/parked-revive-gate.js +2 -2
  6. package/dist/boot/resolve-spec.js +1 -1
  7. package/dist/boot/runner-deps.d.ts +2 -2
  8. package/dist/boot/runner-deps.js +4 -0
  9. package/dist/boot/stores.js +15 -8
  10. package/dist/config-catalog.d.ts +0 -29
  11. package/dist/config-catalog.js +3 -0
  12. package/dist/config-center/http-client.js +7 -11
  13. package/dist/config-center/read-warnings.d.ts +20 -0
  14. package/dist/config-center/read-warnings.js +36 -0
  15. package/dist/config-provider.d.ts +3 -0
  16. package/dist/config-provider.js +4 -8
  17. package/dist/config-types.d.ts +12 -2
  18. package/dist/config.js +13 -2
  19. package/dist/cross-session-settings.d.ts +70 -0
  20. package/dist/cross-session-settings.js +37 -0
  21. package/dist/deployment-governance.d.ts +30 -1
  22. package/dist/deployment-governance.js +11 -5
  23. package/dist/hooks/hook-runner.js +8 -8
  24. package/dist/main.js +1 -0
  25. package/dist/memory-scope.d.ts +20 -0
  26. package/dist/memory-scope.js +9 -1
  27. package/dist/observability/metrics.js +1 -1
  28. package/dist/observability/run-terminal-log.d.ts +4 -0
  29. package/dist/observability/run-terminal-log.js +15 -1
  30. package/dist/plugins/device-store-sql.d.ts +30 -6
  31. package/dist/plugins/device-store-sql.js +23 -4
  32. package/dist/plugins/file-history-store-sql.d.ts +5 -0
  33. package/dist/plugins/file-history-store-sql.js +7 -7
  34. package/dist/plugins/image-bake-store-sql.js +1 -1
  35. package/dist/plugins/mailbox-store-sql.d.ts +33 -11
  36. package/dist/plugins/mailbox-store-sql.js +74 -12
  37. package/dist/plugins/retention-lane-store-sql.d.ts +8 -4
  38. package/dist/plugins/retention-lane-store-sql.js +1 -1
  39. package/dist/plugins/retention-store-sql.d.ts +7 -3
  40. package/dist/plugins/retention-store-sql.js +2 -2
  41. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  42. package/dist/plugins/task-list-store-sql.js +1 -1
  43. package/dist/plugins/usage-window-store-sql.d.ts +5 -3
  44. package/dist/plugins/usage-window-store-sql.js +1 -5
  45. package/dist/run-local.d.ts +2 -1
  46. package/dist/run-local.js +3 -3
  47. package/dist/runs.d.ts +13 -0
  48. package/dist/runs.js +2 -0
  49. package/dist/task-settings.d.ts +13 -3
  50. package/dist/task-settings.js +1 -1
  51. package/dist/tool-approval.d.ts +23 -1
  52. package/dist/tool-approval.js +6 -1
  53. package/dist/trace/core-keyset-guard.d.ts +7 -8
  54. package/dist/trace/engine-notice-wire.d.ts +1 -1
  55. package/dist/trace/engine-notice-wire.js +5 -0
  56. package/dist/trace/project.d.ts +45 -3
  57. package/dist/trace/project.js +23 -0
  58. package/dist/trace/redact.js +7 -3
  59. package/dist/trace/sema-provenance.d.ts +15 -0
  60. package/dist/trace/sema-provenance.js +3 -0
  61. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { MAILBOX_INVALID_PEER_META_CODE, MailboxStoreError, readMailboxPeerMeta } from "@sema-agent/core";
1
2
  import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
2
3
  export const MAILBOX_TABLE = "mailbox";
3
4
  export const MAILBOX_MSG_TABLE = "mailbox_message";
@@ -43,14 +44,61 @@ function decodeHopChain(raw) {
43
44
  return undefined;
44
45
  return parsed;
45
46
  }
47
+ const PEER_META_MAX_BYTES = 60_000;
48
+ function encodePeerMeta(raw) {
49
+ const meta = readMailboxPeerMeta(raw);
50
+ if (meta === undefined)
51
+ return null;
52
+ const text = JSON.stringify(meta);
53
+ if (Buffer.byteLength(text, "utf8") > PEER_META_MAX_BYTES) {
54
+ throw new Error(`MailboxStore: peerMeta exceeds the ${PEER_META_MAX_BYTES}-byte column (refusing — a truncated record would silently read back as ABSENT, which the drain treats as a foreign record)`);
55
+ }
56
+ return text;
57
+ }
58
+ function decodePeerMeta(raw) {
59
+ if (raw === null || raw === undefined)
60
+ return undefined;
61
+ const text = bufToStr(raw);
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(text);
65
+ }
66
+ catch (err) {
67
+ throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore: stored peer_meta is not JSON (refusing the lease — a corrupt typed record must not be read as "no metadata"): ${err instanceof Error ? err.message : String(err)}`);
68
+ }
69
+ try {
70
+ return readMailboxPeerMeta(parsed);
71
+ }
72
+ catch (err) {
73
+ if (err instanceof MailboxStoreError)
74
+ throw err;
75
+ throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore: stored peer_meta failed validation (refusing the lease): ${err instanceof Error ? err.message : String(err)}`);
76
+ }
77
+ }
78
+ function rowsToMessages(rows, scope, handle, logger) {
79
+ const out = [];
80
+ for (const r of rows) {
81
+ try {
82
+ out.push(rowToMessage(r));
83
+ }
84
+ catch (err) {
85
+ if (err instanceof MailboxStoreError)
86
+ logger?.warn("mailbox_peer_meta_corrupt", { scope, handle, seq: Number(r.seq), code: err.code, note: "claimLease refused for the whole box (fail-closed); repair or NULL the peer_meta column of this row" });
87
+ throw err;
88
+ }
89
+ }
90
+ return out;
91
+ }
46
92
  function rowToMessage(r) {
47
93
  const hopChain = decodeHopChain(r.hop_chain);
94
+ const peerMeta = decodePeerMeta(r.peer_meta);
48
95
  return {
49
96
  seq: Number(r.seq),
50
97
  ...(r.from_id !== null && r.from_id !== undefined ? { from: bufToStr(r.from_id) } : {}),
51
98
  content: String(r.content),
52
99
  sentAt: Number(r.sent_at_ms),
53
100
  ...(hopChain !== undefined ? { hopChain } : {}),
101
+ ...(peerMeta !== undefined ? { peerMeta } : {}),
54
102
  };
55
103
  }
56
104
  export async function ensureTiDBMailboxSchema(pool) {
@@ -75,6 +123,11 @@ export async function ensureTiDBMailboxSchema(pool) {
75
123
  -- 见 encodeHopChain 头注:缺席 vs 空数组绝不互折)。TEXT 而非 LONGTEXT——链上界在 core(28 跳,
76
124
  -- 短 token),写入面另有 HOP_CHAIN_MAX_BYTES fail-loud 守卫关掉静默截断类。
77
125
  hop_chain TEXT NULL,
126
+ -- peer_meta(S-90;core fc52b4d4 design/385 片2b):drain 点权威判断读的 typed peer 记录,JSON 文本;
127
+ -- NULL = 缺席(**有语义**:pre-385/foreign 记录,drain 永不回溯准入;与空对象绝不互折)。写入面先过 core
128
+ -- readMailboxPeerMeta(坏值带码拒于任何副作用前)+ PEER_META_MAX_BYTES 守卫关掉静默截断类。
129
+ -- 新列按 2026-07-26 schema policy 直接折进 CREATE(不开 ALTER seam,存量库删库重建)。
130
+ peer_meta TEXT NULL,
78
131
  PRIMARY KEY (scope_key, handle, seq),
79
132
  KEY idx_mbm_sent (scope_key, handle, sent_at_ms)
80
133
  ) COLLATE utf8mb4_bin`);
@@ -98,14 +151,18 @@ export async function ensurePgMailboxSchema(q) {
98
151
  content TEXT COLLATE "C" NOT NULL,
99
152
  sent_at_ms BIGINT NOT NULL,
100
153
  hop_chain TEXT COLLATE "C", -- core 5.17.0 design/176(TiDB twin 同注:NULL = 缺席且有语义)
154
+ peer_meta TEXT COLLATE "C", -- S-90 core fc52b4d4 design/385 片2b(TiDB twin 同注:typed peer 记录 JSON;NULL = 缺席且有语义)
101
155
  PRIMARY KEY (scope_key, handle, seq)
102
156
  )`);
103
157
  await q(`CREATE INDEX IF NOT EXISTS idx_mbm_sent ON ${MAILBOX_MSG_TABLE} (scope_key, handle, sent_at_ms)`);
104
158
  }
105
159
  export class TiDBMailboxStore {
106
160
  pool;
107
- constructor(pool) {
161
+ logger;
162
+ crossProcessSafe = true;
163
+ constructor(pool, logger) {
108
164
  this.pool = pool;
165
+ this.logger = logger;
109
166
  }
110
167
  async tx(fn) {
111
168
  const c = await this.pool.getConnection();
@@ -130,16 +187,17 @@ export class TiDBMailboxStore {
130
187
  assertKeyBytes("scope", scope);
131
188
  assertKeyBytes("handle", handle);
132
189
  const hopChain = encodeHopChain(msg.hopChain);
190
+ const peerMeta = encodePeerMeta(msg.peerMeta);
133
191
  return this.tx(async (c) => {
134
- await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
192
+ await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE scope_key = scope_key`, [scope, handle, scope]);
135
193
  let [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
136
194
  if (rows.length === 0) {
137
- await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
195
+ await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE scope_key = scope_key`, [scope, handle, scope]);
138
196
  [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
139
197
  }
140
198
  const seq = Number(rows[0].next_seq);
141
199
  await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = ? WHERE scope_key = ? AND handle = ?`, [seq + 1, scope, handle]);
142
- await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain) VALUES (?, ?, ?, ?, ?, ?, ?)`, [scope, handle, seq, msg.from ?? null, msg.content, msg.sentAt, hopChain]);
200
+ await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [scope, handle, seq, msg.from ?? null, msg.content, msg.sentAt, hopChain, peerMeta]);
143
201
  return seq;
144
202
  });
145
203
  }
@@ -154,12 +212,12 @@ export class TiDBMailboxStore {
154
212
  bufToStr(box.lease_owner) !== owner) {
155
213
  return null;
156
214
  }
157
- const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
215
+ const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain, peer_meta FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
158
216
  if (msgs.length === 0)
159
217
  return null;
160
218
  const maxSeq = Number(msgs[msgs.length - 1].seq);
161
219
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = ?, lease_expires_at_ms = ?, lease_max_seq = ? WHERE scope_key = ? AND handle = ?`, [owner, now + ttlMs, maxSeq, scope, handle]);
162
- return { messages: msgs.map(rowToMessage), maxSeq };
220
+ return { messages: rowsToMessages(msgs, scope, handle, this.logger), maxSeq };
163
221
  });
164
222
  }
165
223
  async ack(scope, handle, owner, upToSeq) {
@@ -203,8 +261,8 @@ export class TiDBMailboxStore {
203
261
  const [newest] = (await c.query(`SELECT sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY sent_at_ms DESC LIMIT 1 FOR UPDATE`, [scope, h]));
204
262
  if (newest.length === 0 || Number(newest[0].sent_at_ms) >= cutoff)
205
263
  continue;
206
- await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, h]);
207
264
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, h]);
265
+ await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL WHERE scope_key = ? AND handle = ?`, [scope, h]);
208
266
  dropped++;
209
267
  }
210
268
  return dropped;
@@ -213,8 +271,11 @@ export class TiDBMailboxStore {
213
271
  }
214
272
  export class PgMailboxStore {
215
273
  pool;
216
- constructor(pool) {
274
+ logger;
275
+ crossProcessSafe = true;
276
+ constructor(pool, logger) {
217
277
  this.pool = pool;
278
+ this.logger = logger;
218
279
  }
219
280
  async tx(fn) {
220
281
  const c = await this.pool.connect();
@@ -245,6 +306,7 @@ export class PgMailboxStore {
245
306
  assertKeyBytes("scope", scope);
246
307
  assertKeyBytes("handle", handle);
247
308
  const hopChain = encodeHopChain(msg.hopChain);
309
+ const peerMeta = encodePeerMeta(msg.peerMeta);
248
310
  return this.tx(async (c) => {
249
311
  await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
250
312
  scope,
@@ -262,7 +324,7 @@ export class PgMailboxStore {
262
324
  }
263
325
  const seq = Number(rows[0].next_seq);
264
326
  await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = $1 WHERE scope_key = $2 AND handle = $3`, [seq + 1, scope, handle]);
265
- await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt, hopChain]);
327
+ await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt, hopChain, peerMeta]);
266
328
  return seq;
267
329
  });
268
330
  }
@@ -275,12 +337,12 @@ export class PgMailboxStore {
275
337
  const box = boxes[0];
276
338
  if (box.lease_owner !== null && Number(box.lease_expires_at_ms) > now && String(box.lease_owner) !== owner)
277
339
  return null;
278
- const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
340
+ const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain, peer_meta FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
279
341
  if (msgs.length === 0)
280
342
  return null;
281
343
  const maxSeq = Number(msgs[msgs.length - 1].seq);
282
344
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = $1, lease_expires_at_ms = $2, lease_max_seq = $3 WHERE scope_key = $4 AND handle = $5`, [owner, now + ttlMs, maxSeq, scope, handle]);
283
- return { messages: msgs.map((r) => rowToMessage(r)), maxSeq };
345
+ return { messages: rowsToMessages(msgs, scope, handle, this.logger), maxSeq };
284
346
  });
285
347
  }
286
348
  async ack(scope, handle, owner, upToSeq) {
@@ -327,8 +389,8 @@ export class PgMailboxStore {
327
389
  const { rows: newest } = await c.query(`SELECT sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY sent_at_ms DESC LIMIT 1 FOR UPDATE`, [scope, h]);
328
390
  if (newest.length === 0 || Number(newest[0].sent_at_ms) >= cutoff)
329
391
  continue;
330
- await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, h]);
331
392
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, h]);
393
+ await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL WHERE scope_key = $1 AND handle = $2`, [scope, h]);
332
394
  dropped++;
333
395
  }
334
396
  return dropped;
@@ -15,7 +15,9 @@
15
15
  *
16
16
  * ── 方言差异台账(显式,永不藏进抽象;A12 判据)───────────────────────────────────────────────────
17
17
  * - `?` 占位符 vs `$n`;
18
- * - 幂等插入:`INSERT IGNORE` vs `ON CONFLICT (…) DO NOTHING`;
18
+ * - 幂等建行(哨兵形 A,S-127):`ON DUPLICATE KEY UPDATE <pk> = <pk>` vs `ON CONFLICT (…) DO NOTHING`
19
+ * —— MySQL 侧刻意**不用** `INSERT IGNORE`:重复键的 IGNORE 取 S 锁,与随后的 `FOR UPDATE` 之间是
20
+ * S→X 升级,InnoDB 上两并发事务互等即 ER_LOCK_DEADLOCK(B-013);ON DUPLICATE 的重复键取的就是 X;
19
21
  * - upsert:`ON DUPLICATE KEY UPDATE col = VALUES(col)` vs `ON CONFLICT (…) DO UPDATE SET col = EXCLUDED.col`;
20
22
  * - 条件自增:`IF(cond, 0, 1)` vs `CASE WHEN cond THEN 0 ELSE 1 END`(本文件不用 —— 见 `acquire` 顶注:
21
23
  * 自增判据在**应用层**算,因为它同时要产出返回给调用方的 token);
@@ -122,8 +124,10 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
122
124
  * **本 tick 零写静默跳过**。
123
125
  *
124
126
  * 事务三步,一步不能省:
125
- * ① `INSERT IGNORE`(PG:`ON CONFLICT DO NOTHING`)一条 `expires_at_ms = 0` 的**哨兵行** —— 它永远
126
- * 不会改写一条既存租约(两方言的这两个动词都只在缺行时写);
127
+ * ① 幂等建一条 `expires_at_ms = 0` 的**哨兵行**(形 A,S-127:MySQL `ON DUPLICATE KEY UPDATE
128
+ * singleton = singleton` / PG `ON CONFLICT DO NOTHING`)—— 它永远不会改写一条既存租约(两方言的这
129
+ * 两个动词都只在缺行时写),且重复键取的是 **X** 锁,与 ② 的 `FOR UPDATE` 同锁模式(B-013:
130
+ * `INSERT IGNORE` 在这里取 S 再升 X,InnoDB 上两副本抢租互等直接死锁);
127
131
  * ② `SELECT … FOR UPDATE` —— 行现在必定存在,锁真的拿得到;
128
132
  * ③ 判 + `UPDATE`,同事务提交。
129
133
  *
@@ -170,7 +174,7 @@ export declare class SqlRetentionLaneStore implements RetentionLaneStore {
170
174
  * (车1 交接件②,逐字)。
171
175
  *
172
176
  * 🔴 为什么删行是**拆锁**而不是"解除":`retention_hold` 的行同时是**域级互斥哨兵** —— 车1 的三条破坏性
173
- * 事务首步就是锁读这一行(`INSERT IGNORE` 补哨兵 → `SELECT … FOR UPDATE`),而 `FOR UPDATE`
177
+ * 事务首步就是锁读这一行(ON DUPLICATE 补哨兵 → `SELECT … FOR UPDATE`),而 `FOR UPDATE`
174
178
  * **锁不住一条不存在的行**(TiDB 无 gap lock;PG READ COMMITTED 同病)。删掉行 = 下一个 PUT 与一条
175
179
  * 在飞的删除事务又可以同时读到"没有 hold",于是 operator 拿到成功回执之后数据仍被删 —— 那正是 §5 F2
176
180
  * 要消灭的那件事。upsert 让两者抢**同一把行锁**,「PUT 提交完成 ⇒ 其后每个破坏性事务的首读必见
@@ -14,7 +14,7 @@ export class SqlRetentionLaneStore {
14
14
  const conn = await this.db.connect();
15
15
  try {
16
16
  await conn.begin();
17
- await conn.query(this.q(`INSERT IGNORE INTO ${RETENTION_LEASE_TABLE} (singleton, holder, fencing_token, expires_at_ms) VALUES ('x','',0,0)`, `INSERT INTO ${RETENTION_LEASE_TABLE} (singleton, holder, fencing_token, expires_at_ms) VALUES ('x','',0,0) ON CONFLICT (singleton) DO NOTHING`));
17
+ await conn.query(this.q(`INSERT INTO ${RETENTION_LEASE_TABLE} (singleton, holder, fencing_token, expires_at_ms) VALUES ('x','',0,0) ON DUPLICATE KEY UPDATE singleton = singleton`, `INSERT INTO ${RETENTION_LEASE_TABLE} (singleton, holder, fencing_token, expires_at_ms) VALUES ('x','',0,0) ON CONFLICT (singleton) DO NOTHING`));
18
18
  const { rows } = await conn.query(this.q(`SELECT holder, fencing_token, expires_at_ms FROM ${RETENTION_LEASE_TABLE} WHERE singleton = 'x' FOR UPDATE`, `SELECT holder, fencing_token, expires_at_ms FROM ${RETENTION_LEASE_TABLE} WHERE singleton = 'x' FOR UPDATE`));
19
19
  const cur = rows[0];
20
20
  const curHolder = String(cur?.holder ?? "");
@@ -28,7 +28,9 @@
28
28
  * ── 方言差异台账(显式,永不藏进抽象;A12 判据)───────────────────────────────────────────────────
29
29
  * - `?` 占位符 vs `$n`;多值集合 MySQL 用 `IN (?,?,…)`、PG 用 `= ANY($n::text[])`(先例:
30
30
  * workflow-run-store-sql 的 reap)
31
- * - 幂等插入:`INSERT IGNORE` vs `ON CONFLICT (…) DO NOTHING`
31
+ * - 幂等建行:哨兵形(形 A,S-127)`ON DUPLICATE KEY UPDATE <pk> = <pk>` vs `ON CONFLICT (…) DO NOTHING`
32
+ * —— 随后有同行 `FOR UPDATE` 的建行**必须**用 ON DUPLICATE(重复键取 X,同锁模式);墓碑那种
33
+ * 「只读 affected 不加锁读」的写点(形 B)才保留 `INSERT IGNORE`
32
34
  * - 级联 DELETE 的 JOIN 形:`DELETE te FROM a te JOIN b …` vs `DELETE FROM a te USING b …`
33
35
  * - null-safe 比较:`<=>` vs `IS NOT DISTINCT FROM`(及其**占位符数量**差:`?` 不可复用)
34
36
  * - JSON 提取(workflow_run 的 originatingSessionId):`JSON_UNQUOTE(JSON_EXTRACT(run,'$.x'))` vs
@@ -213,8 +215,10 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
213
215
  * 事务**首步**:拿本域的互斥行锁,并读出「这个域是不是冻结中」(§5 F2 承重)。
214
216
  *
215
217
  * 两步,缺一不可:
216
- * ① `INSERT IGNORE`(PG:`ON CONFLICT DO NOTHING`)一条 `held = 0` 的**哨兵行** —— 它永远不会改写
217
- * 一条既存的 hold(两方言的这两个动词都只在缺行时写),只保证**接下来那把行锁有东西可锁**;
218
+ * ① 幂等建一条 `held = 0` 的**哨兵行**(形 A,S-127:MySQL `ON DUPLICATE KEY UPDATE domain = domain` /
219
+ * PG `ON CONFLICT DO NOTHING`)—— 它永远不会改写一条既存的 hold(两方言的这两个动词都只在缺行时
220
+ * 写),只保证**接下来那把行锁有东西可锁**;重复键取的是 **X** 锁,与 ② 的 `FOR UPDATE` 同锁模式
221
+ * (`INSERT IGNORE` 在这里取 S 再升 X = InnoDB 上两个并发 sweep 互等的死锁形,B-013);
218
222
  * ② `SELECT held … FOR UPDATE` —— 现在这行必定存在,于是锁真的拿到了。
219
223
  *
220
224
  * 🔴 为什么必须先造行(codex 交叉复审 R1-[high],验真后改):`FOR UPDATE` 锁不住缺席行(TiDB 无 gap
@@ -38,7 +38,7 @@ export const TIDB_RETENTION_STATEMENTS = [
38
38
  -- 而 SELECT … FOR UPDATE **锁不住一条不存在的行**(TiDB 无 gap lock;PG 的 READ COMMITTED 同样
39
39
  -- 不锁缺席行)。presence 形下两边都读到「没有 hold」是完全可能的,于是 operator 拿到成功回执之后
40
40
  -- 数据仍被删 —— 那正是 F2 要消灭的那件事,只是换了个位置复发。
41
- -- ⇒ 行**恒存在**(破坏性事务首步 INSERT IGNORE 一条 held=0 的哨兵),冻结与否写在这一列上。
41
+ -- ⇒ 行**恒存在**(破坏性事务首步幂等建一条 held=0 的哨兵),冻结与否写在这一列上。
42
42
  -- 于是 hold 的放置(同一 PK 的 upsert)与三只破坏性事务抢的是**同一把行锁**,串行化由引擎给出:
43
43
  -- PUT 提交完成 ⇒ 其后每一个破坏性事务的首读必见 held=1。放置/解除都必须走这一列(车2 交接件)。
44
44
  held TINYINT(1) NOT NULL DEFAULT 0,
@@ -186,7 +186,7 @@ export class SqlRetentionStore {
186
186
  }
187
187
  async holdInForce(conn, domain) {
188
188
  const key = domain;
189
- await conn.query(this.q(`INSERT IGNORE INTO ${RETENTION_HOLD_TABLE} (domain, held, placed_by, placed_at_ms, note) VALUES (?,0,NULL,NULL,NULL)`, `INSERT INTO ${RETENTION_HOLD_TABLE} (domain, held, placed_by, placed_at_ms, note) VALUES ($1,0,NULL,NULL,NULL) ON CONFLICT (domain) DO NOTHING`), [key]);
189
+ await conn.query(this.q(`INSERT INTO ${RETENTION_HOLD_TABLE} (domain, held, placed_by, placed_at_ms, note) VALUES (?,0,NULL,NULL,NULL) ON DUPLICATE KEY UPDATE domain = domain`, `INSERT INTO ${RETENTION_HOLD_TABLE} (domain, held, placed_by, placed_at_ms, note) VALUES ($1,0,NULL,NULL,NULL) ON CONFLICT (domain) DO NOTHING`), [key]);
190
190
  const { rows } = await conn.query(this.q(`SELECT held FROM ${RETENTION_HOLD_TABLE} WHERE domain = ? FOR UPDATE`, `SELECT held FROM ${RETENTION_HOLD_TABLE} WHERE domain = $1 FOR UPDATE`), [key]);
191
191
  return Number(rows[0]?.held ?? 0) === 1;
192
192
  }
@@ -8,8 +8,8 @@ export declare const TASK_LIST_ITEM_TABLE = "task_list_item";
8
8
  *
9
9
  * 🔴 为什么不直接把 sessionId 当 listKey:sessionId 是**调用方可自报**的不透明串(提交门
10
10
  * `http/server.ts` 只卡 ≤64 **字符** —— [#15] 刻意的宽松提交契约,不卡字节数也不卡字符集)。
11
- * 而本文件的身份键是字节面严格的:{@link assertListKeyBytes} 对 >190 字节当场拒(TiDB
12
- * `INSERT IGNORE` 静默截断案),PG twin 对孤代理/NUL 走 {@link PgUnstorableError} 拒。直接透传
11
+ * 而本文件的身份键是字节面严格的:{@link assertListKeyBytes} 对 >190 字节当场拒(超长键截断案),
12
+ * PG twin 对孤代理/NUL 走 {@link PgUnstorableError} 拒。直接透传
13
13
  * 的话,一个 64 字符全四字节 emoji 的会话(256 字节)会让**整条请求**在场景装配处 500 —— 而那条腿
14
14
  * 只是待办清单。派生一个恒可存的键把这一整类失败模式消掉(既不是 fail-open 也不是 fail-loud,是让
15
15
  * 那条臂不存在)。
@@ -56,7 +56,7 @@ export async function ensurePgTaskListSchema(q) {
56
56
  }
57
57
  async function ensureMeta(exec, dialect, listKey) {
58
58
  await exec(dialect === "tidb"
59
- ? `INSERT IGNORE INTO ${TASK_LIST_META_TABLE} (list_key, next_id, next_sort) VALUES (?, 1, 1)`
59
+ ? `INSERT INTO ${TASK_LIST_META_TABLE} (list_key, next_id, next_sort) VALUES (?, 1, 1) ON DUPLICATE KEY UPDATE list_key = list_key`
60
60
  : `INSERT INTO ${TASK_LIST_META_TABLE} (list_key, next_id, next_sort) VALUES ($1, 1, 1) ON CONFLICT (list_key) DO NOTHING`, [listKey]);
61
61
  }
62
62
  async function allocateOn(exec, dialect, listKey) {
@@ -7,9 +7,11 @@
7
7
  * first-use bucket lapsed 即弃)完全由 core 纯函数执行——SQL 层零窗语义。
8
8
  *
9
9
  * 并发正确性(本 store 的靶心;两副本同 key 并发 charge 绝不能丢账——丢账方向=治理窗漏放):
10
- * - **ensure-first + 行锁**(mailbox twin 正字先例,[ref] 复审靶1):事务内先幂等
11
- * INSERT IGNORE / ON CONFLICT DO NOTHING 建空行 SELECT ... FOR UPDATE 时行恒在,
12
- * 「无行 FOR UPDATE + INSERT」的并发死锁(InnoDB gap-lock)/dup 撞车形整个消掉。
10
+ * - **ensure-first + 行锁**(mailbox twin 正字先例,[ref] 复审靶1):事务内先幂等建空行
11
+ * (形 A,S-127:MySQL `ON DUPLICATE KEY UPDATE <pk> = <pk>` / PG `ON CONFLICT DO NOTHING`)⇒
12
+ * `SELECT FOR UPDATE` 时行恒在,「无行 FOR UPDATE + INSERT」的并发死锁/dup 撞车形整个消掉。
13
+ * MySQL 侧动词**不能**是 `INSERT IGNORE`:InnoDB 对重复键的 IGNORE 取 S 锁,随后的 FOR UPDATE 要升 X,
14
+ * 两个并发 charge 各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK(B-013);ON DUPLICATE 的重复键取的就是 X。
13
15
  * - charge 频率低(core 只在任务进场/turn 边界/收尾 delta 提交),悲观锁简单正确,不用乐观 CAS。
14
16
  * - `read` 无锁单 SELECT(readUsageRecord 纯函数);行缺席 = EMPTY_USAGE_WINDOW_RECORD。
15
17
  * - record JSON 坏形(手工污染)= **fail-loud throw**:治理窗算错=计费面撒谎,不 fail-open。
@@ -69,11 +69,7 @@ export class TiDBUsageWindowStore {
69
69
  const c = await this.pool.getConnection();
70
70
  try {
71
71
  await c.beginTransaction();
72
- await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?)`, [
73
- key,
74
- JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
75
- at,
76
- ]);
72
+ await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE usage_key = usage_key`, [key, JSON.stringify(EMPTY_USAGE_WINDOW_RECORD), at]);
77
73
  const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
78
74
  if (rows.length === 0) {
79
75
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
@@ -82,7 +82,8 @@ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigVie
82
82
  * test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
83
83
  * · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
84
84
  */
85
- export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string): TaskSpec;
85
+ /** @param engineDataRoot 本腿已铸的引擎记忆根(`memoryEngineBackendFor(config, root)?.root`,S-133 单一写者);`undefined` = 引擎未接。 */
86
+ export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string, engineDataRoot: string | undefined): TaskSpec;
86
87
  /** {@link createLocalApprover} 的 io 面(全部可注入,所以两条臂都测得动)。 */
87
88
  export interface LocalApproverIo {
88
89
  /** 这次调用是不是接在一个人面前(生产里 = `process.stdin.isTTY`)。 */
package/dist/run-local.js CHANGED
@@ -111,8 +111,8 @@ export function missingGatewayRequirements(config) {
111
111
  }
112
112
  return missing;
113
113
  }
114
- export function applyLocalGovernance(base, config, workspaceDir) {
115
- return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }));
114
+ export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot) {
115
+ return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
116
116
  }
117
117
  export function createLocalApprover(io) {
118
118
  return async (req, signal) => {
@@ -472,7 +472,7 @@ export async function runLocal(argv, deps = {}) {
472
472
  ...(mcp ? { mcp } : {}),
473
473
  ...(a2a ? { a2a } : {}),
474
474
  ...(Object.keys(limits).length > 0 ? { limits } : {}),
475
- }, config, workspaceDir);
475
+ }, config, workspaceDir, memoryEngine?.root);
476
476
  logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
477
477
  const settleSessionBackground = async () => {
478
478
  const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
package/dist/runs.d.ts CHANGED
@@ -136,6 +136,19 @@ export declare function isSessionConflictResult(result: {
136
136
  * would not survive to a resume replica) → 409 — the request is well-formed, the deployment's state
137
137
  * conflicts with it, the `config.refresh_unavailable` family shape ("change the deployment, not the request").
138
138
  * Status choices are pinned by the `config.`/`memory.` family status sets in error-code-catalog-live.test.ts.
139
+ *
140
+ * S-125 件⑥(core 7.4.0 [ref]):`rewind.child_scope_unsupported` —— 这条腿是一个**委派子代**,它的文件编辑
141
+ * 记在**根 session** 的文件历史里,所以「把文件还原到某个 entry」是根 session 的动作、不是子代的;core 在
142
+ * prepare 期响亮拒(零计费,文件一个字节没动)。映射 **422**:请求形式合法、语义上找错了执行者 —— 与
143
+ * `resume_at.*` 的「调用方指错了锚」同一族同一档,而不是 404(目标在,只是不归你)也不是 409(部署状态没冲突)。
144
+ * ⚠️ **前缀不吞**:本码前缀是 `rewind.`,与下面那条 `resume_at.` **开集前缀臂**不同族 —— 它不会被那一臂顺手
145
+ * 兜走(兜走会把一条「找错执行者」的拒答成「锚解析不了」),也不是 `rewind_snapshot.`(那是「快照找不到」)。
146
+ * 三个前缀各自成族,逐码对表见 `test/resume-anchor.test.ts` 的 S-125 `rewind.` 族格。
147
+ * 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
148
+ * `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
149
+ * lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
150
+ * 委派入口接线那天零改动即生效。同族其余五码今天**仍落 200-with-failed-body**,那是本批**之前**就存在的
151
+ * 缺口(不是本批回归),已在上述对表格里逐码显式登记为债务,并由那道门看住不许陈腐。
139
152
  */
140
153
  export declare function resumeAtHttpStatus(result: {
141
154
  status: string;
package/dist/runs.js CHANGED
@@ -72,6 +72,8 @@ export function resumeAtHttpStatus(result) {
72
72
  return 404;
73
73
  if (result.errorCode === "rewind_snapshot.unresolvable")
74
74
  return 422;
75
+ if (result.errorCode === "rewind.child_scope_unsupported")
76
+ return 422;
75
77
  if (result.errorCode === "memory.admission_denied")
76
78
  return 403;
77
79
  if (result.errorCode === "memory.admission_required")
@@ -186,9 +186,19 @@ export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
186
186
  export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
187
187
  /** 本 service 消费的 `settings.permissions` 子键 —— 闭集(值形校验另有既有门,本门只判键名)。 */
188
188
  export declare const TASK_SETTINGS_PERMISSION_KEYS: readonly ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
189
- /** CC/SDK 合约里合法、本 service **不接线**的顶层键(出处 = `@sema-agent/sdk` `SemaSettings` 尾注逐字点名的
190
- * 三个 deferred CC 键)。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响文案,不影响判决。 */
191
- export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "sandbox", "statusLine"];
189
+ /** CC/SDK 合约里合法、本 service **不接线**的顶层键。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响
190
+ * 文案,不影响判决。前三个的出处 = `@sema-agent/sdk` `SemaSettings` 尾注逐字点名的 deferred CC 键。
191
+ *
192
+ * 🔴 `crossSessionInbound` / `dialogExpiry`(S-111)为什么在**这一列**而不是受理列:它们是**每会话**的
193
+ * 保护型设置(`refuse` = 本会话退出入站跨会话车道),而引擎只给了**部署级**座位(一个 Runner 单例,
194
+ * getter 无会话上下文)。受理它们只能做到「解析 + 记一行运维日志」——调用方拿到 202、以为自己的
195
+ * opt-out 生效了,而实际没有任何 per-session 生效点。**在保护型配置上,「受理但不执行」比「响亮拒」
196
+ * 危险得多**:前者是静默的假安全,后者是调用方当场看得见的事实(400 体的 `unsupportedKeys` 逐字列名,
197
+ * 机器可读)。所以本版按 `statusLine` 族同姿势明拒。
198
+ * 部署层的同名治理**照常可用**,走部署旋钮(`cross-session-settings.ts` 的 managed 层);等引擎开出
199
+ * 每任务座、或本仓 per-run 上下文覆盖到每一条 run 腿,这两个键**移到上面的受理列**即可(一行),
200
+ * 那一刻它们才真的有生效点。 */
201
+ export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "crossSessionInbound", "dialogExpiry", "sandbox", "statusLine"];
192
202
  /** 同上,`settings.permissions` 子层(出处 = SDK `SettingsPermissions` 的三个声明键)。
193
203
  * `additionalDirectories` 有**同请求替代面**:顶层 `body.additionalDirectories`([ref],resolve-spec 真消费)。 */
194
204
  export declare const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS: readonly ["additionalDirectories", "disableBypassPermissionsMode"];
@@ -74,7 +74,7 @@ function cleanStringArray(v) {
74
74
  }
75
75
  export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
76
76
  export const TASK_SETTINGS_PERMISSION_KEYS = ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
77
- export const TASK_SETTINGS_UNSUPPORTED_KEYS = ["apiKeyHelper", "sandbox", "statusLine"];
77
+ export const TASK_SETTINGS_UNSUPPORTED_KEYS = ["apiKeyHelper", "crossSessionInbound", "dialogExpiry", "sandbox", "statusLine"];
78
78
  export const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS = ["additionalDirectories", "disableBypassPermissionsMode"];
79
79
  export const MAX_SETTINGS_KEY_ECHO = 20;
80
80
  const MAX_SETTINGS_KEY_ECHO_CHARS = 64;
@@ -1,7 +1,7 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  import { type AskRequest, type AskOutcome, type RuleOffer } from "@sema-agent/core";
3
3
  import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
4
- import { type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
4
+ import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
5
5
  import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
6
6
  import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
7
7
  import { type GovernanceAskMarks } from "./governance-ask-marks.js";
@@ -211,6 +211,28 @@ export interface ToolApprovalFrame {
211
211
  * **同一个**函数 ⇒ 两面同值),词表外的值=形不合=不铸键([ref] 闭集纪律,绝不透传未知词)。
212
212
  */
213
213
  ruleOffersAbsence?: RuleOffersAbsence;
214
+ /**
215
+ * S-114(core 7.4.0 [ref];**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 是 auto 模式分类器的**限额
216
+ * 回落卡**:分类器连续(缺省 3)或累计(缺省 20)拒到限额的**那一次**调用不再静默 deny,而是落成一张
217
+ * 必须真人批的卡(同一次铸造同时盖 `requiresRealApproval`,两键是孪生)。
218
+ * `{consecutive, total, limit:"consecutive"|"total", autoDenyAfterMs}` —— 触限时的两个计数、哪一道界
219
+ * 触了、以及**这张卡自己**的自动拒窗(ms;`0` = 不武装)。
220
+ *
221
+ * 🔴 **additive echo-only,server 零消费**(`ruleOffers` 先例逐字):窗的执行全在 core 的 `resolveAsk`
222
+ * ([ref] 的引擎自有 deadline —— 窗到即 deny 且盖 `settledBy:"timeout"` / `resolution:"window_expired"` /
223
+ * `autoDenied:true`)。本仓**不得**据 `autoDenyAfterMs` 自铸第二只定时器:那会与上游的窗构成同一语义面
224
+ * 的两个写者,而两只窗的重叠比原缺陷更坏且静默(源头修复纪律)。
225
+ *
226
+ * 值经 `approval-card.ts` 的 `readDenialLimitFallback` 窄读(`.strict()` 形校验;与 `card_json`
227
+ * **同一个**函数 ⇒ 两面同值),形不合/闭集外 ⇒ 不铸整键。四成员皆机器数/闭词、非内容族 ⇒ 零 redact。
228
+ * **缺席不是断言**:绝大多数 ask 根本不是回落卡,消费端只读在场。
229
+ *
230
+ * ⚠️ 耐久腿(park 行)今天**不携**本键 —— core 7.4.0 的 `PendingAction` 上没有这一位(亲读
231
+ * `dist/core/checkpoint-store.d.ts`,零命中);core [ref] 预告在 7.4.x,本仓**只登记不预铸**。
232
+ * 卡面那一份(`ApprovalCardSchema.denialLimitFallback`)走的是 `card_json`,与 park 行的 `pendingAction`
233
+ * 是两条腿,别混。
234
+ */
235
+ denialLimitFallback?: DenialLimitFallback;
214
236
  /**
215
237
  * [ref] 件 G1(core 5.33.0 backlog [ref],判据帖 [ref] G1;**ADDITIVE**,`"tool_approval"` only)——
216
238
  * 这次 ask **为什么**被收紧的**结构化**因由(`AskRequest.probeCause`:工具的 `reversibilityProbe`
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
8
8
  import { createLogger } from "./observability/logger.js";
9
9
  import { recordFailOpen } from "./observability/fail-open.js";
10
10
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
11
- import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, readRuleOffersAbsence, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
11
+ import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readRuleEvidence, readRuleOffersAbsence, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
12
12
  import { projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
13
13
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
14
14
  const defaultLogger = createLogger();
@@ -842,6 +842,7 @@ export class ToolApprovalCoordinator {
842
842
  : {}),
843
843
  ...(ruleLaneMaterial !== undefined ? { ruleOffers: ruleLaneMaterial.offers } : {}),
844
844
  ...((a) => (a !== undefined ? { ruleOffersAbsence: a } : {}))(readRuleOffersAbsence(req)),
845
+ ...((f) => (f !== undefined ? { denialLimitFallback: f } : {}))(readDenialLimitFallback(req)),
845
846
  ...((c) => (c !== undefined ? { probeCause: c } : {}))(readProbeCause(req)),
846
847
  ...((c) => (c !== undefined ? { ruleEvidence: c } : {}))(readRuleEvidence(req)),
847
848
  ...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
@@ -925,6 +926,10 @@ export class ToolApprovalCoordinator {
925
926
  frame.inputHasBidi = true;
926
927
  else
927
928
  delete frame.inputHasBidi;
929
+ if (cardForFrame.denialLimitFallback !== undefined)
930
+ frame.denialLimitFallback = cardForFrame.denialLimitFallback;
931
+ else
932
+ delete frame.denialLimitFallback;
928
933
  if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
929
934
  effectiveGovernanceForced = true;
930
935
  ruleLaneMaterial = undefined;
@@ -17,26 +17,25 @@
17
17
  * ⑨⑩([ref],2026-08-31)把两个最大的 wire 面也纳入:TaskSpec 按「server 有无写点」记 Wired/Unwired,
18
18
  * TaskResult 按「转发/摘除」记 Forwarded/Stripped —— 谁改 resolve-spec.ts / runs.ts 的发射点谁同步本文件。
19
19
  */
20
- import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxStore, TaskSpec, TaskResult } from "@sema-agent/core";
20
+ import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult } from "@sema-agent/core";
21
21
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
22
22
  type AssertAllKeysHandled<T extends never> = T;
23
23
  type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure" | "agentMessage" | "_sema_provenance";
24
- type NotificationExcluded = "peer";
24
+ type NotificationExcluded = "peer" | "crossSessionMessage" | "crossSessionNotice";
25
25
  type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
26
26
  type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
27
27
  type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "description" | "agentType" | "name" | "currentAction" | "currentTool" | "parentSessionId" | "startedAt" | "workflowRunId" | "progressTaskId" | "progressParentTaskId" | "model";
28
28
  type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
29
29
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
30
30
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
31
- type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval";
31
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback";
32
32
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
33
33
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
34
34
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end";
35
35
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
36
- type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain";
36
+ type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
37
37
  type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
38
- type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
39
- type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain";
38
+ type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
40
39
  type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
41
40
  type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
42
41
  type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable";
@@ -49,11 +48,11 @@ type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<Permiss
49
48
  type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls";
50
49
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
51
50
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
52
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends";
51
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested";
53
52
  type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
54
53
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
55
54
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
56
- type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "status" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "blockedReason" | "checkpointId" | "checkpointGate" | "workspaceRestoreMode" | "remoteEnvFailures" | "errorMessage" | "errorCode" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveMemoryScopes" | "stats";
55
+ type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "status" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "blockedReason" | "checkpointId" | "checkpointGate" | "workspaceRestoreMode" | "remoteEnvFailures" | "errorMessage" | "errorCode" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveMemoryScopes" | "stats" | "editedFiles";
57
56
  type TaskResultStripped = "checkpointToken";
58
57
  type _GuardTaskResult = AssertAllKeysHandled<Exclude<keyof TaskResult, TaskResultForwarded | TaskResultStripped>>;
59
58
  type _GuardTaskResultReverse = AssertAllKeysHandled<Exclude<TaskResultForwarded | TaskResultStripped, keyof TaskResult>>;
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -15,6 +15,11 @@ export const ENGINE_NOTICE_WIRE_CODES = [
15
15
  "memory.capture_optout_unpersisted",
16
16
  "task.halt_unconsumed",
17
17
  "task.late_approval",
18
+ "config.durable_gate_unavailable",
19
+ "peer.inbound_disposition",
20
+ "peer.held_settled",
21
+ "peer.idle_subscription",
22
+ "classifier.denial_limit",
18
23
  ];
19
24
  const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
20
25
  export function isEngineNoticeWireCode(code) {