@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.
- package/USAGE.md +2 -0
- package/dist/approval-card.d.ts +47 -0
- package/dist/approval-card.js +18 -0
- package/dist/boot/parked-revive-gate.d.ts +3 -0
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.js +1 -1
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/stores.js +15 -8
- package/dist/config-catalog.d.ts +0 -29
- package/dist/config-catalog.js +3 -0
- package/dist/config-center/http-client.js +7 -11
- package/dist/config-center/read-warnings.d.ts +20 -0
- package/dist/config-center/read-warnings.js +36 -0
- package/dist/config-provider.d.ts +3 -0
- package/dist/config-provider.js +4 -8
- package/dist/config-types.d.ts +12 -2
- package/dist/config.js +13 -2
- package/dist/cross-session-settings.d.ts +70 -0
- package/dist/cross-session-settings.js +37 -0
- package/dist/deployment-governance.d.ts +30 -1
- package/dist/deployment-governance.js +11 -5
- package/dist/hooks/hook-runner.js +8 -8
- package/dist/main.js +1 -0
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +9 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +4 -0
- package/dist/observability/run-terminal-log.js +15 -1
- package/dist/plugins/device-store-sql.d.ts +30 -6
- package/dist/plugins/device-store-sql.js +23 -4
- package/dist/plugins/file-history-store-sql.d.ts +5 -0
- package/dist/plugins/file-history-store-sql.js +7 -7
- package/dist/plugins/image-bake-store-sql.js +1 -1
- package/dist/plugins/mailbox-store-sql.d.ts +33 -11
- package/dist/plugins/mailbox-store-sql.js +74 -12
- package/dist/plugins/retention-lane-store-sql.d.ts +8 -4
- package/dist/plugins/retention-lane-store-sql.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +7 -3
- package/dist/plugins/retention-store-sql.js +2 -2
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/usage-window-store-sql.d.ts +5 -3
- package/dist/plugins/usage-window-store-sql.js +1 -5
- package/dist/run-local.d.ts +2 -1
- package/dist/run-local.js +3 -3
- package/dist/runs.d.ts +13 -0
- package/dist/runs.js +2 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +23 -1
- package/dist/tool-approval.js +6 -1
- package/dist/trace/core-keyset-guard.d.ts +7 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +5 -0
- package/dist/trace/project.d.ts +45 -3
- package/dist/trace/project.js +23 -0
- package/dist/trace/redact.js +7 -3
- package/dist/trace/sema-provenance.d.ts +15 -0
- package/dist/trace/sema-provenance.js +3 -0
- 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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
* -
|
|
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
|
-
* ①
|
|
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
|
-
* 事务首步就是锁读这一行(
|
|
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
|
|
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
|
-
* -
|
|
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
|
-
* ① `
|
|
217
|
-
*
|
|
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
|
-
-- ⇒ 行**恒存在**(
|
|
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
|
|
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 字节当场拒(
|
|
12
|
-
*
|
|
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
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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`);
|
package/dist/run-local.d.ts
CHANGED
|
@@ -82,7 +82,8 @@ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigVie
|
|
|
82
82
|
* test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
|
|
83
83
|
* · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
|
|
84
84
|
*/
|
|
85
|
-
|
|
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")
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -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
|
|
190
|
-
*
|
|
191
|
-
|
|
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"];
|
package/dist/task-settings.js
CHANGED
|
@@ -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;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -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`
|
package/dist/tool-approval.js
CHANGED
|
@@ -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,
|
|
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
|
|
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) {
|