@sema-agent/server 7.58.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 +3 -3
- package/dist/config-catalog.d.ts +0 -29
- package/dist/config-catalog.js +3 -0
- 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/main.js +1 -0
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +9 -1
- package/dist/observability/run-terminal-log.d.ts +4 -0
- package/dist/observability/run-terminal-log.js +14 -0
- 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 +5 -2
- package/dist/plugins/mailbox-store-sql.js +2 -2
- 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 +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +3 -0
- package/dist/trace/sema-provenance.d.ts +7 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/package.json +3 -3
package/dist/memory-scope.js
CHANGED
|
@@ -60,12 +60,20 @@ export function buildMemoryRemoteLaneWarn(config, engineWired) {
|
|
|
60
60
|
"WORKER fs; verify the lane really shares it.",
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
export function memoryEngineRootFor(config, fallbackRoot) {
|
|
64
|
+
if (config.memoryEngineBackend !== "file")
|
|
65
|
+
return join(config.localDataRoot ?? fallbackRoot ?? resolveMemoryEngineRoot(undefined), "memory-work");
|
|
66
|
+
return fileMemoryEngineRoot(config, fallbackRoot);
|
|
67
|
+
}
|
|
68
|
+
export function fileMemoryEngineRoot(config, fallbackRoot) {
|
|
69
|
+
return resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
|
|
70
|
+
}
|
|
63
71
|
export function memoryEngineBackendFor(config, fallbackRoot) {
|
|
64
72
|
if (config.requirePrincipal === true || !config.memoryEngineEnabled)
|
|
65
73
|
return undefined;
|
|
66
74
|
if (memoryEngineRemoteLanePosture(config)?.posture === "dark")
|
|
67
75
|
return undefined;
|
|
68
|
-
const root =
|
|
76
|
+
const root = fileMemoryEngineRoot(config, fallbackRoot);
|
|
69
77
|
const dir = join(root, "memory");
|
|
70
78
|
return { backend: new FileMemoryEngineBackend(dir, { controlDir: deriveControlPlaneDir(root, dir) }), root };
|
|
71
79
|
}
|
|
@@ -60,6 +60,10 @@ export declare function redactTerminalResult<T extends {
|
|
|
60
60
|
remoteEnvFailures?: readonly {
|
|
61
61
|
message?: string | undefined;
|
|
62
62
|
}[] | undefined;
|
|
63
|
+
/** 🔴 形上**故意**是 `unknown`:本函数也是**读面**(旧持久 blob / 非 core Runner 的形不可信),
|
|
64
|
+
* 声明成 `{path,edits}[]` 会逼每一个喂坏形的调用点铸一次宽松断言 —— 而「坏形从哪来」正是本座要
|
|
65
|
+
* 处理的事(同一份不信任在 `remoteEnvFailures` 的实现里也已成文)。运行期逐项判形,不抛。 */
|
|
66
|
+
editedFiles?: unknown;
|
|
63
67
|
} | null | undefined>(result: T): T;
|
|
64
68
|
/** S-107(第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行
|
|
65
69
|
* (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。
|
|
@@ -74,6 +74,7 @@ export function redactErrorMessage(raw) {
|
|
|
74
74
|
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
const MAX_EDITED_FILES = 1000;
|
|
77
78
|
export function redactTerminalResult(result) {
|
|
78
79
|
if (result === undefined || result === null)
|
|
79
80
|
return result;
|
|
@@ -94,6 +95,19 @@ export function redactTerminalResult(result) {
|
|
|
94
95
|
: r.remoteEnvFailures;
|
|
95
96
|
changed = true;
|
|
96
97
|
}
|
|
98
|
+
if (r.editedFiles !== undefined) {
|
|
99
|
+
out.editedFiles = Array.isArray(r.editedFiles)
|
|
100
|
+
? r.editedFiles
|
|
101
|
+
.slice(0, MAX_EDITED_FILES)
|
|
102
|
+
.map((f) => {
|
|
103
|
+
if (f === null || typeof f !== "object")
|
|
104
|
+
return f;
|
|
105
|
+
const path = f.path;
|
|
106
|
+
return typeof path === "string" ? { ...f, path: redactSecrets(path) } : f;
|
|
107
|
+
})
|
|
108
|
+
: r.editedFiles;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
97
111
|
return changed ? out : result;
|
|
98
112
|
}
|
|
99
113
|
export function redactLedgerEventData(type, data) {
|
|
@@ -129,15 +129,39 @@ export declare class SqlDeviceStore implements DeviceStore {
|
|
|
129
129
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
130
130
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
131
131
|
/**
|
|
132
|
-
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)
|
|
132
|
+
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与首绑 CAS 插入在一个事务里。
|
|
133
133
|
*
|
|
134
|
-
* 🔴 `beginPessimistic` + `FOR UPDATE
|
|
135
|
-
*
|
|
136
|
-
* double-admit 形)
|
|
137
|
-
*
|
|
138
|
-
*
|
|
134
|
+
* 🔴 `beginPessimistic` + `FOR UPDATE`(设备行):设备行必须是**当前读**。快照读下,一次与吊销并发的
|
|
135
|
+
* 绑定会读到「还 active」的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(sql-driver.ts 头注点名
|
|
136
|
+
* 的 double-admit 形)。这把锁在 `devices` 表上,与下面被插入的 `device_session` 行不是同一行。
|
|
137
|
+
*
|
|
138
|
+
* 🔴 形 B · cas-insert(S-127 / B-013)—— 本站点是全仓 `INSERT IGNORE` 的**正当用法**:
|
|
139
|
+
* `INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected(1/0)是引擎对「谁赢」的**权威**回答,而
|
|
140
|
+
* `ON DUPLICATE KEY UPDATE pk = pk` 的 affected 新行/既有行都是 1、分不出赢家 ⇒ 这里换不得动词。
|
|
141
|
+
* 代价必须付清:**同一事务此后不得对被插入的那一行做加锁读**。InnoDB 下撞重复键的 IGNORE 在既有行
|
|
142
|
+
* 取 **S 锁**,随后的 `SELECT … FOR UPDATE` 要升 X;两个并发首绑各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK
|
|
143
|
+
* (TiDB 悲观事务无共享锁,这一形在它上面恒绿 —— 双库门看不见)。于是两条路径分家:
|
|
144
|
+
* · **赢家**(affected === 1):行是自己刚插的,回执用**入参 + 插入时刻**直接构造,零回读。插入时刻
|
|
145
|
+
* 仍由 **DB 时钟**给出(插入前一句 `SELECT NOW(3)` / `SELECT now()`,再把它当参数写进 INSERT)——
|
|
146
|
+
* 时间权威留在 DB,回执里的 `boundAtMs` 与落库值**逐字节同一个**,不是应用侧的近似。
|
|
147
|
+
* · **输家**(affected === 0):**结束当前事务**,在**新事务**里 `SELECT … FOR UPDATE` 观察。那一刻
|
|
148
|
+
* 赢家已提交,读到的是一把干净的 X 锁(无升级);判 同 deviceId = 幂等成功,异 deviceId =
|
|
149
|
+
* `conflict`(无未定义态)。设备状态门在输家路径上已经过完,不需要重来。
|
|
139
150
|
*/
|
|
140
151
|
bindSession(input: BindSessionInput): Promise<BindSessionResult>;
|
|
152
|
+
/**
|
|
153
|
+
* 首绑 CAS 的**输家观察**(形 B 的后半,S-127)—— 独立事务,与那次 `INSERT IGNORE` 无锁关系。
|
|
154
|
+
*
|
|
155
|
+
* 🔴 `FOR UPDATE`(**真库红修**,2026-08-28 双库跑当场抓到,mock 抓不到):TiDB 的悲观事务里**普通
|
|
156
|
+
* SELECT 仍是快照读**(读的是本事务 start_ts 的视图),只有加锁读才是当前读。放在新事务里读同样要
|
|
157
|
+
* 这把锁 —— 新事务的 start_ts 虽在赢家提交之后,但「读的是不是当前」这条判据不该靠时序巧合来满足。
|
|
158
|
+
* 与旧形的**唯一**差别:此刻我们手上没有那一行的 S 锁,于是这把 X 是**直接取**的,不是升级 ⇒ 两个
|
|
159
|
+
* 并发输家不会互等(B-013 的死锁形正是那次升级)。
|
|
160
|
+
*
|
|
161
|
+
* 行不在 = 赢家的绑定在这一瞬被 `unbindSession` 删了(真实可能,不是不变量破)。这仍然是**响亮**的:
|
|
162
|
+
* 首绑既没成、也说不出冲突对象,折成任何一种业务回执都是撒谎([ref]:未知失败不伪装成正常拒绝)。
|
|
163
|
+
*/
|
|
164
|
+
private observeLostBind;
|
|
141
165
|
/**
|
|
142
166
|
* O4 显式换绑(2026-08-30 v2 稿 §2)—— **单事务**:目标设备门(当前读)+ 幂等短路 + 前态 CAS +
|
|
143
167
|
* `session_rebound` 审计行一起提交。三形语义 oracle = `InMemoryDeviceStore.rebindSession`。
|
|
@@ -533,14 +533,33 @@ export class SqlDeviceStore {
|
|
|
533
533
|
await conn.rollback();
|
|
534
534
|
return { outcome: "device_revoked" };
|
|
535
535
|
}
|
|
536
|
-
const
|
|
536
|
+
const clock = await conn.query(this.q(`SELECT ${ms("tidb", NOW("tidb"), "bound_at_ms")}`, `SELECT ${ms("pg", NOW("pg"), "bound_at_ms")}`));
|
|
537
|
+
const boundAtMs = Number(clock.rows[0].bound_at_ms);
|
|
538
|
+
const ins = await conn.query(this.q(`INSERT IGNORE INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES (?, ?, ?, ?, FROM_UNIXTIME(? / 1000), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, to_timestamp($5::double precision / 1000.0), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject, boundAtMs]);
|
|
539
|
+
if (ins.affected === 1) {
|
|
540
|
+
await conn.commit();
|
|
541
|
+
return { outcome: "bound", row: { rootSessionId: input.rootSessionId, deviceId: input.deviceId, owner: input.owner, boundAtMs, rev: 0 } };
|
|
542
|
+
}
|
|
543
|
+
await conn.rollback();
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
await conn.rollback().catch(() => undefined);
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
conn.release();
|
|
551
|
+
}
|
|
552
|
+
return await this.observeLostBind(input);
|
|
553
|
+
}
|
|
554
|
+
async observeLostBind(input) {
|
|
555
|
+
const conn = await this.db.connect();
|
|
556
|
+
try {
|
|
557
|
+
await conn.beginPessimistic();
|
|
537
558
|
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ? FOR UPDATE`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1 FOR UPDATE`), [input.rootSessionId]);
|
|
538
559
|
await conn.commit();
|
|
539
560
|
const row = rows[0] ? mapSessionRow(rows[0]) : null;
|
|
540
561
|
if (!row)
|
|
541
|
-
throw new Error("device-store: session binding vanished
|
|
542
|
-
if (ins.affected === 1)
|
|
543
|
-
return { outcome: "bound", row };
|
|
562
|
+
throw new Error("device-store: the winning session binding vanished before the losing binder could observe it");
|
|
544
563
|
const same = row.deviceId === input.deviceId && row.owner.tenant === input.owner.tenant && row.owner.subject === input.owner.subject;
|
|
545
564
|
return { outcome: same ? "idempotent" : "conflict", row };
|
|
546
565
|
}
|
|
@@ -61,6 +61,11 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
61
61
|
* row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
|
|
62
62
|
* longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
|
|
63
63
|
* the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
|
|
64
|
+
*
|
|
65
|
+
* Shape A (S-127): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
|
|
66
|
+
* On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
|
|
67
|
+
* below has to upgrade it to X — two concurrent scope writers each holding an S deadlock (B-013). ON
|
|
68
|
+
* DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
|
|
64
69
|
*/
|
|
65
70
|
private lockScope;
|
|
66
71
|
/** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
|
|
@@ -49,7 +49,7 @@ export class SqlFileHistoryStore {
|
|
|
49
49
|
for (const scope of [...new Set(scopes)].sort()) {
|
|
50
50
|
let locked = false;
|
|
51
51
|
for (let i = 0; i < 3 && !locked; i++) {
|
|
52
|
-
await conn.query(this.q("INSERT
|
|
52
|
+
await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
|
|
53
53
|
const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
|
|
54
54
|
locked = Boolean(r.rows[0]);
|
|
55
55
|
}
|
|
@@ -358,15 +358,15 @@ export class SqlFileHistoryStore {
|
|
|
358
358
|
}
|
|
359
359
|
const seqRow = await conn.query(this.q("SELECT publish_seq FROM file_history_boundary WHERE scope = ? ORDER BY publish_seq DESC LIMIT 1 FOR UPDATE", "SELECT publish_seq FROM file_history_boundary WHERE scope = $1 ORDER BY publish_seq DESC LIMIT 1"), [scope]);
|
|
360
360
|
const publishSeq = (seqRow.rows[0] !== undefined ? Number(seqRow.rows[0].publish_seq) : 0) + 1;
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
const won = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
|
|
364
|
-
if (!won.rows[0]) {
|
|
365
|
-
throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
|
|
366
|
-
}
|
|
361
|
+
const priorWinner = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
|
|
362
|
+
if (priorWinner.rows[0]) {
|
|
367
363
|
await conn.rollback();
|
|
368
364
|
return { ok: true };
|
|
369
365
|
}
|
|
366
|
+
const ins = await conn.query(this.q("INSERT IGNORE INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES (?,?,CAST(UNIX_TIMESTAMP(NOW(3))*1000 AS SIGNED),?)", "INSERT INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES ($1,$2,(EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint,$3) ON CONFLICT (scope, entry_id) DO NOTHING"), [scope, entryId, publishSeq]);
|
|
367
|
+
if (ins.affected !== 1) {
|
|
368
|
+
throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
|
|
369
|
+
}
|
|
370
370
|
for (const [keyHash, cap] of staged) {
|
|
371
371
|
let version;
|
|
372
372
|
if (cap.kind === "unchanged") {
|
|
@@ -140,7 +140,7 @@ export class SqlImageBake {
|
|
|
140
140
|
try {
|
|
141
141
|
if (this.db.dialect === "tidb") {
|
|
142
142
|
await conn.beginPessimistic();
|
|
143
|
-
await conn.query("INSERT
|
|
143
|
+
await conn.query("INSERT INTO image_bake_admit (pool) VALUES (?) ON DUPLICATE KEY UPDATE pool = pool", [this.poolName]);
|
|
144
144
|
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
|
|
145
145
|
const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
|
|
146
146
|
if (busy.rows.length === 0) {
|
|
@@ -36,8 +36,11 @@
|
|
|
36
36
|
* - reap 候删 = 事务外粗筛 + 事务内**逐盒锁定重验**(当前读 newest):窗内新到的消息救活盒,
|
|
37
37
|
* 绝不连带删信;盒行已被并发 drop = 跳过不计。
|
|
38
38
|
* - append 的 ensure 与 FOR UPDATE 之间被并发 drop 提交 = 同事务内重建一次(语义 = drop 后重建,
|
|
39
|
-
* seq 从 1)
|
|
40
|
-
*
|
|
39
|
+
* seq 从 1)。建盒动词 = 形 A(S-127)`ON DUPLICATE KEY UPDATE <pk> = <pk>`:重复键取 X 锁,与随后的
|
|
40
|
+
* `FOR UPDATE` **同锁模式**(`INSERT IGNORE` 取 S 再升 X = InnoDB 上的并发死锁形,B-013)。
|
|
41
|
+
* - 键长 > 190 字节写入即拒(fail-loud):截断键与全长查询参数字节比对恒 miss(分配读空行崩/隔离键
|
|
42
|
+
* 错位)。守卫在**语句之前**,与建行动词无关 —— 换成 ON DUPLICATE 之后超长键由引擎直接报错(严格
|
|
43
|
+
* 模式下 IGNORE 会把它降级成静默截断),方向=更响亮,守卫依旧是第一道。
|
|
41
44
|
*/
|
|
42
45
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
43
46
|
import type { Pool as PgPool } from "pg";
|
|
@@ -189,10 +189,10 @@ export class TiDBMailboxStore {
|
|
|
189
189
|
const hopChain = encodeHopChain(msg.hopChain);
|
|
190
190
|
const peerMeta = encodePeerMeta(msg.peerMeta);
|
|
191
191
|
return this.tx(async (c) => {
|
|
192
|
-
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]);
|
|
193
193
|
let [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
194
194
|
if (rows.length === 0) {
|
|
195
|
-
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]);
|
|
196
196
|
[rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
197
197
|
}
|
|
198
198
|
const seq = Number(rows[0].next_seq);
|
|
@@ -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;
|
|
@@ -21,14 +21,14 @@ import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskReq
|
|
|
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" | "crossSessionMessage";
|
|
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";
|
|
@@ -52,7 +52,7 @@ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requi
|
|
|
52
52
|
type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
|
|
53
53
|
type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
|
|
54
54
|
type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
|
|
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";
|
|
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";
|
|
56
56
|
type TaskResultStripped = "checkpointToken";
|
|
57
57
|
type _GuardTaskResult = AssertAllKeysHandled<Exclude<keyof TaskResult, TaskResultForwarded | TaskResultStripped>>;
|
|
58
58
|
type _GuardTaskResultReverse = AssertAllKeysHandled<Exclude<TaskResultForwarded | TaskResultStripped, keyof TaskResult>>;
|