@sema-agent/server 7.56.0 → 7.58.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/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +101 -9
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -9
- package/dist/boot/runtime-caps.js +49 -21
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/boot/stores.js +12 -5
- package/dist/config-catalog.js +14 -5
- 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 +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/hooks/hook-runner.js +8 -8
- package/dist/http/route-ctx.d.ts +75 -0
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +19 -6
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +19 -4
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +215 -43
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +45 -12
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +30 -1
- package/dist/leader/wire.js +36 -19
- package/dist/main.js +43 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +19 -3
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +19 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +3 -0
- package/dist/plugins/mailbox-store-sql.d.ts +28 -9
- package/dist/plugins/mailbox-store-sql.js +72 -10
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +19 -0
- package/dist/plugins/permission-rule-store-file.js +8 -3
- package/dist/plugins/permission-rule-store-sql.js +16 -10
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +30 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +66 -1
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.js +20 -13
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +41 -3
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +26 -3
- package/dist/tool-approval.js +96 -13
- 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 +2 -0
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +58 -3
- package/dist/trace/project.js +33 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +660 -21
- package/dist/trace/sema-provenance.d.ts +35 -0
- package/dist/trace/sema-provenance.js +14 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +3 -3
|
@@ -231,6 +231,9 @@ export class LocalCheckpointStore {
|
|
|
231
231
|
}
|
|
232
232
|
return out;
|
|
233
233
|
}
|
|
234
|
+
async peekScopeByToken(token) {
|
|
235
|
+
return (await this.inner.get(token))?.scope;
|
|
236
|
+
}
|
|
234
237
|
async peekPendingScope(sessionId, binding) {
|
|
235
238
|
return this.pickPendingRow(await this.pendings(), sessionId, binding)?.cp.scope;
|
|
236
239
|
}
|
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
* 终版文本,conformance 无两可):
|
|
8
8
|
* 1. **同主重 claim = 续期 + 纳新**(crash-retry 正形):活 lease 只挡**别的** owner;同 owner 任意
|
|
9
9
|
* 时刻重 claim 重设 TTL 并拿到当前全部未 ack 消息(maxSeq 随最新行走)。
|
|
10
|
-
* 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop
|
|
11
|
-
*
|
|
10
|
+
* 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop` 删整盒(含 box 行)
|
|
11
|
+
* = 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)。⚠️ **`reap` 自 [ref](core
|
|
12
|
+
* 2026-07-25)起不再与 drop 同句**:reap 是收件人活着时的龄扫,**清箱保高水位**(删消息、清租约、盒行与
|
|
13
|
+
* next_seq 留着)——本文件 2026-09-03(车BF,codex r1 [high])才跟上;此前 reap 删盒行 ⇒ 下一条 append 从 1
|
|
14
|
+
* 重铸,陈旧 ack 可删掉一条没人见过的新消息。跨进程试剂盒明钉此条。
|
|
12
15
|
* 3. **ack 无 owner 守卫**(信任 claim 赢者链):`seq <= upToSeq` 删行;lease 的 `maxSeq <= upToSeq`
|
|
13
16
|
* 时顺带清 lease(全量 ack = 释放)。
|
|
14
17
|
* 4. **lease 过期判 = 严格 `expiresAt > now` 才挡**;`releaseLease` 只在 owner 匹配时清。
|
|
15
18
|
* 5. `peekCount` 含已 lease 消息(可见性 ≠ lease 态);空盒 claim = null(box 行在而消息 0 也 null)。
|
|
16
19
|
* 6. **reap = SQL 全量真扫**(core File 实现只扫已加载盒 = 进程内 backstop,[ref] 确认 SQL 是全量
|
|
17
|
-
* 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs`
|
|
18
|
-
* box 行无消息)
|
|
20
|
+
* 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs` **清箱**(删消息 + 清租约,
|
|
21
|
+
* 盒行/高水位保留,见定谳 2 的 [ref] 半句);空盒(有 box 行无消息)不清(InMemory 同形——`newest ===
|
|
22
|
+
* undefined` 不动)。
|
|
19
23
|
*
|
|
20
24
|
* 键列 = 字节等价(TiDB VARBINARY / PG COLLATE "C",roster F2 案):scope/handle/lease_owner 的
|
|
21
25
|
* `=` 必须纯字节——PAD SPACE 近撞不得穿隔离/抢别人 lease。PG unstorable bytes([ref] 三层定谳):
|
|
@@ -37,15 +41,28 @@
|
|
|
37
41
|
*/
|
|
38
42
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
39
43
|
import type { Pool as PgPool } from "pg";
|
|
40
|
-
import type { MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
41
|
-
type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
|
|
44
|
+
import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
42
45
|
export declare const MAILBOX_TABLE = "mailbox";
|
|
43
46
|
export declare const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
47
|
+
/** 店侧日志座(可选;boot 注入 `Logger`)。7.58.0 合并重扫 [medium]:坏 `peer_meta` 行的 fail-closed 抛必须**响亮**——core 唯一的
|
|
48
|
+
* claimLease 调用方(peer-session-drain)吞掉该错,店不记这一笔就成了「静默永久卡箱」。抛之前记 `mailbox_peer_meta_corrupt`
|
|
49
|
+
* (scope/handle/seq/code),运维按 seq 修行。 */
|
|
50
|
+
export interface MailboxStoreLogger {
|
|
51
|
+
warn(event: string, fields: Record<string, unknown>): void;
|
|
52
|
+
}
|
|
44
53
|
export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
|
|
45
54
|
export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
46
55
|
export declare class TiDBMailboxStore implements MailboxStore {
|
|
47
56
|
private readonly pool;
|
|
48
|
-
|
|
57
|
+
private readonly logger?;
|
|
58
|
+
/** [ref]([ref] §1.2⑥):跨进程安全**显式**声明 —— core 的 `mailboxCrossProcessMountVerdict` 只对字面 `true` 放行
|
|
59
|
+
* peer lane(缺席/false/畸形一律响亮拒挂 `config.peer_lane_unmounted`)。真值前提 = 本文件头注「并发正确性」段:seq
|
|
60
|
+
* 在事务内经盒行 FOR UPDATE 单铸、lease/ack/drop/reap 统一盒行锁序、reap 事务内逐盒当前读重验 —— 多副本(=多 OS
|
|
61
|
+
* 进程)共享一个盒本就是 SQL 双生的设计象限。**终验** = core 跨进程试剂盒 `mailboxCrossProcessContract` 跑在真第二个
|
|
62
|
+
* OS 进程上:`test/mailbox-cross-process-kit.test.ts`(file 腿恒跑验 harness;tidb/pg 腿 ENV-GATED,发车前双库门必跑,
|
|
63
|
+
* 红即摘本声明——core 契约逐字「declares true AFTER passing」,声明不许先于试剂盒长期悬空)。 */
|
|
64
|
+
readonly crossProcessSafe = true;
|
|
65
|
+
constructor(pool: MySqlPool, logger?: MailboxStoreLogger | undefined);
|
|
49
66
|
private tx;
|
|
50
67
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
51
68
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
@@ -59,7 +76,10 @@ export declare class TiDBMailboxStore implements MailboxStore {
|
|
|
59
76
|
}
|
|
60
77
|
export declare class PgMailboxStore implements MailboxStore {
|
|
61
78
|
private readonly pool;
|
|
62
|
-
|
|
79
|
+
private readonly logger?;
|
|
80
|
+
/** [ref] 跨进程安全显式声明(TiDB 孪生同注:事务 + 盒行 FOR UPDATE 锁序是真值前提;终验 = test/mailbox-cross-process-kit.test.ts pg 腿)。 */
|
|
81
|
+
readonly crossProcessSafe = true;
|
|
82
|
+
constructor(pool: PgPool, logger?: MailboxStoreLogger | undefined);
|
|
63
83
|
private tx;
|
|
64
84
|
/** 身份键拒绝式([ref] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
|
|
65
85
|
private assertIdentity;
|
|
@@ -73,5 +93,4 @@ export declare class PgMailboxStore implements MailboxStore {
|
|
|
73
93
|
maxAgeMs?: number;
|
|
74
94
|
}): Promise<number>;
|
|
75
95
|
}
|
|
76
|
-
export {};
|
|
77
96
|
//# sourceMappingURL=mailbox-store-sql.d.ts.map
|
|
@@ -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,6 +187,7 @@ 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
192
|
await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [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]));
|
|
@@ -139,7 +197,7 @@ export class TiDBMailboxStore {
|
|
|
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;
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-memory async run registry + replayable event log — the LOCAL (DB-less) twin of {@link TiDBRunStore}/
|
|
3
|
-
* {@link PgRunStore} (clay 2026-06-25: the seamless local↔cloud switch — a locally-run HTTP service must serve the
|
|
4
|
-
* SAME contract a cloud worker does, incl. `/v1/runs` async runs + the trace event log). Behaviourally identical to
|
|
5
|
-
* the SQL twins (the shared `storeBehaviorSuite` keeps them in lock-step), but process-local + ephemeral: a restart
|
|
6
|
-
* loses run history (file-backed durability is the immediate follow-on; the conversation HISTORY + checkpoints DO
|
|
7
|
-
* persist via core's FileStorageBackend). Single-process, so the cross-replica/durable reapers degrade — see below.
|
|
8
|
-
*
|
|
9
|
-
* Mirrors the SQL semantics exactly: the `task_active` single-active-run claim (a Map keyed by sessionId), the
|
|
10
|
-
* NON-terminal suspend/needs_review parks that KEEP the claim, the markResuming CAS that resets cancel/preempt, the
|
|
11
|
-
* owner null-safe guard on the flag/heartbeat paths (`<=>` → JS `(a??null)===(b??null)`), and the keyset list order.
|
|
12
|
-
*/
|
|
13
1
|
import { type UsageRow } from "../usage-analytics.js";
|
|
14
|
-
import type {
|
|
15
|
-
import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
|
|
2
|
+
import type { TaskStatus } from "@sema-agent/core";
|
|
3
|
+
import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult } from "./store-contracts.js";
|
|
16
4
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
17
5
|
/**
|
|
18
6
|
* [ref] P0 — the local lane's stand-in for the SQL twins' checkpoint-table JOIN. The file/memory run stores
|
|
@@ -66,7 +54,7 @@ export declare class MemoryRunStore {
|
|
|
66
54
|
getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
|
|
67
55
|
/** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
|
|
68
56
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
69
|
-
setTerminal(taskId: string, status: TaskStatus, result:
|
|
57
|
+
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
70
58
|
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
|
|
71
59
|
setSuspended(taskId: string): Promise<void>;
|
|
72
60
|
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
|
|
1
2
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
3
|
+
import { notifyRunTerminal } from "../observability/run-terminal-log.js";
|
|
2
4
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
3
5
|
export class MemoryRunStore {
|
|
4
6
|
runs = new Map();
|
|
@@ -78,6 +80,7 @@ export class MemoryRunStore {
|
|
|
78
80
|
r.updatedAt = new Date();
|
|
79
81
|
}
|
|
80
82
|
async appendEvent(taskId, seq, type, data) {
|
|
83
|
+
data = redactLedgerEventData(type, data);
|
|
81
84
|
const list = this.events.get(taskId) ?? [];
|
|
82
85
|
if (list.some((e) => e.seq === seq))
|
|
83
86
|
return;
|
|
@@ -100,6 +103,8 @@ export class MemoryRunStore {
|
|
|
100
103
|
.map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
|
|
101
104
|
}
|
|
102
105
|
async setTerminal(taskId, status, result, error) {
|
|
106
|
+
result = redactTerminalResult(result);
|
|
107
|
+
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
103
108
|
const r = this.runs.get(taskId);
|
|
104
109
|
if (!r) {
|
|
105
110
|
for (const [sessionId, tid] of this.active)
|
|
@@ -109,15 +114,29 @@ export class MemoryRunStore {
|
|
|
109
114
|
}
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
117
|
+
let pending;
|
|
112
118
|
if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
|
|
113
119
|
r.status = status;
|
|
114
120
|
r.result = result;
|
|
115
121
|
r.error = error;
|
|
116
122
|
r.errorCode = result?.errorCode ?? null;
|
|
117
123
|
r.updatedAt = new Date();
|
|
124
|
+
pending = {
|
|
125
|
+
runId: taskId,
|
|
126
|
+
sessionId: r.sessionId,
|
|
127
|
+
owner: r.owner,
|
|
128
|
+
status,
|
|
129
|
+
resultTaskId: result?.taskId,
|
|
130
|
+
model: result?.model,
|
|
131
|
+
errorCode: r.errorCode ?? undefined,
|
|
132
|
+
errorMessage: result?.errorMessage ?? error ?? undefined,
|
|
133
|
+
elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
|
|
134
|
+
};
|
|
118
135
|
}
|
|
119
136
|
if (this.active.get(r.sessionId) === taskId)
|
|
120
137
|
this.active.delete(r.sessionId);
|
|
138
|
+
if (pending)
|
|
139
|
+
notifyRunTerminal(pending);
|
|
121
140
|
}
|
|
122
141
|
async setSuspended(taskId) {
|
|
123
142
|
const r = this.runs.get(taskId);
|
|
@@ -5,6 +5,7 @@ import { performance } from "node:perf_hooks";
|
|
|
5
5
|
import { FilePermissionRuleStoreProvider, AppendLog, ensureDir, readJsonlRecords, } from "@sema-agent/core";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
|
+
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
8
9
|
const RuleScopeSchema = z.union([
|
|
9
10
|
z.object({ kind: z.literal("global") }).strict(),
|
|
10
11
|
z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
|
|
@@ -19,6 +20,7 @@ const RuleOffer2Schema = z.union([
|
|
|
19
20
|
candidates: z.array(z.number().int().nonnegative().safe()),
|
|
20
21
|
segments: z.array(z.string()).optional(),
|
|
21
22
|
uncoveredSegments: z.number().int().nonnegative().safe().optional(),
|
|
23
|
+
uncoveredDetail: z.array(z.object({ segment: z.string(), reason: z.enum(UNCOVERED_SEGMENT_REASONS) }).strict()).optional(),
|
|
22
24
|
})
|
|
23
25
|
.strict(),
|
|
24
26
|
]);
|
|
@@ -27,7 +29,7 @@ const ApprovalRecordSchema = z
|
|
|
27
29
|
id: z.string().min(1),
|
|
28
30
|
principal: z.string().optional(),
|
|
29
31
|
owner: z.union([z.object({ kind: z.literal("principal"), principal: z.string() }).strict(), z.object({ kind: z.literal("local-owner") }).strict()]).optional(),
|
|
30
|
-
schema: z.literal(
|
|
32
|
+
schema: z.literal(RULE_APPROVAL_RECORD_SCHEMA),
|
|
31
33
|
kind: z.enum(["card", "import", "starter"]),
|
|
32
34
|
state: z.enum(["pending", "approved", "redeemed"]),
|
|
33
35
|
candidates: z.array(RuleCandidateSchema),
|
|
@@ -36,7 +38,10 @@ const ApprovalRecordSchema = z
|
|
|
36
38
|
toolCallId: z.string().optional(),
|
|
37
39
|
boundInputHash: z.string().optional(),
|
|
38
40
|
command: z.string().optional(),
|
|
39
|
-
edited: z
|
|
41
|
+
edited: z
|
|
42
|
+
.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
|
|
43
|
+
.strict()
|
|
44
|
+
.optional(),
|
|
40
45
|
rev: z.number().int().nonnegative().safe(),
|
|
41
46
|
selectedOffer: z.number().int().nonnegative().safe().optional(),
|
|
42
47
|
redeemedDots: z.record(z.string(), RuleDotSchema).optional(),
|
|
@@ -138,7 +143,7 @@ export class FileRuleApprovalRecordStore {
|
|
|
138
143
|
const parsed = ApprovalLineSchema.safeParse(raw);
|
|
139
144
|
if (!parsed.success) {
|
|
140
145
|
const stale = StaleApprovalLineSchema.safeParse(raw);
|
|
141
|
-
if (stale.success && stale.data.record.schema !==
|
|
146
|
+
if (stale.success && stale.data.record.schema !== RULE_APPROVAL_RECORD_SCHEMA) {
|
|
142
147
|
this.rows.set(stale.data.record.id, toStaleEnvelope(stale.data.record));
|
|
143
148
|
continue;
|
|
144
149
|
}
|
|
@@ -4,6 +4,7 @@ import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuar
|
|
|
4
4
|
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
6
|
import { canonicalJson } from "../canonical-json.js";
|
|
7
|
+
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
7
8
|
export function writerOfSqlRuleStore(store) {
|
|
8
9
|
const w = Reflect.get(store, PERMISSION_RULE_WRITER);
|
|
9
10
|
if (w === null || typeof w !== "object")
|
|
@@ -26,8 +27,8 @@ const RuleAddSchema = z
|
|
|
26
27
|
const PersistedAllowRuleSchema = z
|
|
27
28
|
.object({
|
|
28
29
|
rule: z.string(),
|
|
29
|
-
tool: z.
|
|
30
|
-
match: z.enum(
|
|
30
|
+
tool: z.enum(PERSISTED_RULE_TOOLS),
|
|
31
|
+
match: z.enum(PERSISTED_RULE_MATCHES),
|
|
31
32
|
command: z.string(),
|
|
32
33
|
scope: DurableRuleScopeSchema,
|
|
33
34
|
adds: z.array(RuleAddSchema),
|
|
@@ -52,7 +53,9 @@ const QuarantinedRuleAddSchema = z
|
|
|
52
53
|
const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
|
|
53
54
|
const RuleCandidateSchema = z.object({ rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
54
55
|
const RedeemedDotsSchema = z.record(z.string(), RuleDotSchema);
|
|
55
|
-
const RuleEditedSchema = z
|
|
56
|
+
const RuleEditedSchema = z
|
|
57
|
+
.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
|
|
58
|
+
.strict();
|
|
56
59
|
const RuleOffer2Schema = z.union([
|
|
57
60
|
z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
|
|
58
61
|
z
|
|
@@ -61,6 +64,7 @@ const RuleOffer2Schema = z.union([
|
|
|
61
64
|
candidates: z.array(z.number().int().nonnegative().safe()),
|
|
62
65
|
segments: z.array(z.string()).optional(),
|
|
63
66
|
uncoveredSegments: z.number().int().nonnegative().safe().optional(),
|
|
67
|
+
uncoveredDetail: z.array(z.object({ segment: z.string(), reason: z.enum(UNCOVERED_SEGMENT_REASONS) }).strict()).optional(),
|
|
64
68
|
})
|
|
65
69
|
.strict(),
|
|
66
70
|
]);
|
|
@@ -104,8 +108,9 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
|
|
|
104
108
|
state VARCHAR(32) NOT NULL,
|
|
105
109
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
106
110
|
candidates_json LONGTEXT NOT NULL,
|
|
107
|
-
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 2
|
|
108
|
-
--
|
|
111
|
+
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 3 —— design/382 §3.5 由 2 升,
|
|
112
|
+
-- 代码里的单点是 RULE_APPROVAL_RECORD_SCHEMA;OCC 守卫另在 rev,两轴禁混,schema-naming 门③ 白名单
|
|
113
|
+
-- 登记)。读侧对非 3 的行(含上一形的 2)交还 design/375 §4.5 的
|
|
109
114
|
-- StaleRuleApprovalRecord 信封(identity 键 only)—— 本仓 SCHEMA POLICY 是删表重建,重建后的表
|
|
110
115
|
-- 不该有这种行,这一臂是「未来再换形」的防御位,绝不静默 widen 成一条本形担保不了的记录。
|
|
111
116
|
version INT NOT NULL,
|
|
@@ -258,7 +263,8 @@ export async function ensurePgPermissionRuleSchema(q) {
|
|
|
258
263
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
259
264
|
candidates_json TEXT COLLATE "C" NOT NULL,
|
|
260
265
|
-- version / offers_json / selected_offer:design/375 换形三列(MySQL 孪生的行内注写了理由:
|
|
261
|
-
-- version=数据格式版本恒
|
|
266
|
+
-- version=数据格式版本恒 3(design/382 §3.5 由 2 升;单点 RULE_APPROVAL_RECORD_SCHEMA),非 3 的行(含 2)
|
|
267
|
+
-- 读成 Stale 信封;offers_json=按 index 引用的 OPTION 结构,
|
|
262
268
|
-- 禁存文本副本;selected_offer=确认的 OFFER index,与 ticket/redeemed_dots 的 CANDIDATE index
|
|
263
269
|
-- 两空间分离)。
|
|
264
270
|
version INT NOT NULL,
|
|
@@ -525,7 +531,7 @@ export class SqlRuleApprovalRecordStore {
|
|
|
525
531
|
const ownerKind = String(row.owner_kind);
|
|
526
532
|
const principal = row.principal === null || row.principal === undefined ? undefined : String(row.principal);
|
|
527
533
|
const identity = ownerKind === "principal" ? { principal: principal ?? "" } : { owner: { kind: "local-owner" } };
|
|
528
|
-
if (Number(row.version) !==
|
|
534
|
+
if (Number(row.version) !== RULE_APPROVAL_RECORD_SCHEMA) {
|
|
529
535
|
return { staleSchema: true, id: String(row.record_id), ...identity };
|
|
530
536
|
}
|
|
531
537
|
const candidates = parseColumn(z.array(RuleCandidateSchema), row.candidates_json, "candidates_json", id);
|
|
@@ -541,7 +547,7 @@ export class SqlRuleApprovalRecordStore {
|
|
|
541
547
|
return {
|
|
542
548
|
id: String(row.record_id),
|
|
543
549
|
...identity,
|
|
544
|
-
schema:
|
|
550
|
+
schema: RULE_APPROVAL_RECORD_SCHEMA,
|
|
545
551
|
kind: kind.data,
|
|
546
552
|
state: state.data,
|
|
547
553
|
candidates,
|
|
@@ -584,14 +590,14 @@ export class SqlRuleApprovalRecordStore {
|
|
|
584
590
|
]);
|
|
585
591
|
}
|
|
586
592
|
async discardPendingRecord(recordId) {
|
|
587
|
-
const { affected } = await this.db.query(this.q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = ? AND state = 'pending' AND version =
|
|
593
|
+
const { affected } = await this.db.query(this.q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = ? AND state = 'pending' AND version = ${RULE_APPROVAL_RECORD_SCHEMA}`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = $1 AND state = 'pending' AND version = ${RULE_APPROVAL_RECORD_SCHEMA}`), [recordId]);
|
|
588
594
|
return affected === 1;
|
|
589
595
|
}
|
|
590
596
|
async cas(id, expectRev, next) {
|
|
591
597
|
if (next.rev !== expectRev + 1) {
|
|
592
598
|
throw new Error(`rule-approval CAS must advance rev by exactly one (expectRev=${expectRev}, next.rev=${next.rev})`);
|
|
593
599
|
}
|
|
594
|
-
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_APPROVAL_TABLE} SET state = ?, rev = ?, candidates_json = ?, offers_json = ?, selected_offer = ?, redeemed_dots_json = ?, edited_json = ?, updated_at_ms = ? WHERE record_id = ? AND rev = ? AND version =
|
|
600
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_APPROVAL_TABLE} SET state = ?, rev = ?, candidates_json = ?, offers_json = ?, selected_offer = ?, redeemed_dots_json = ?, edited_json = ?, updated_at_ms = ? WHERE record_id = ? AND rev = ? AND version = ${RULE_APPROVAL_RECORD_SCHEMA}`, `UPDATE ${PERMISSION_RULE_APPROVAL_TABLE} SET state = $1, rev = $2, candidates_json = $3, offers_json = $4, selected_offer = $5, redeemed_dots_json = $6, edited_json = $7, updated_at_ms = $8 WHERE record_id = $9 AND rev = $10 AND version = ${RULE_APPROVAL_RECORD_SCHEMA}`), [
|
|
595
601
|
next.state,
|
|
596
602
|
next.rev,
|
|
597
603
|
this.enc(next.candidates, "permission_rule_approval.candidates_json"),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactHead } from "../observability/run-terminal-log.js";
|
|
1
2
|
const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh", "device"]);
|
|
2
3
|
export function isRemoteScratchpadLane(provider) {
|
|
3
4
|
return provider !== undefined && REMOTE_SCRATCHPAD_LANES.has(provider);
|
|
@@ -28,12 +29,12 @@ export function withRemoteScratchpad(factory, logger) {
|
|
|
28
29
|
if (!r.ok || r.value.exitCode !== 0) {
|
|
29
30
|
logger?.warn?.("remote_scratchpad_mkdir_failed", {
|
|
30
31
|
dir,
|
|
31
|
-
...(r.ok ? { exitCode: r.value.exitCode, stderr: r.value.stderr
|
|
32
|
+
...(r.ok ? { exitCode: r.value.exitCode, stderr: redactHead(r.value.stderr, 256) } : { error: redactHead(r.error.message, 512) }),
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
catch (err) {
|
|
36
|
-
logger?.warn?.("remote_scratchpad_mkdir_failed", { dir, error: err instanceof Error ? err.message : String(err) });
|
|
37
|
+
logger?.warn?.("remote_scratchpad_mkdir_failed", { dir, error: redactHead(err instanceof Error ? err.message : String(err), 512) });
|
|
37
38
|
}
|
|
38
39
|
})());
|
|
39
40
|
e.exec = async (command, options) => {
|
|
@@ -1,47 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* Async run registry (S1) + replayable event log (S2) — SINGLE-FILE DUAL-DIALECT ([ref] A12 定型半场).
|
|
3
|
-
* ONE implementation, TWO dialects; the historical `TiDBRunStore` / `PgRunStore` class names survive as thin
|
|
4
|
-
* ctor subclasses so every consumer (store-backend.ts, the run/reaper/session/usage suites) is untouched.
|
|
5
|
-
*
|
|
6
|
-
* A run is the background execution of one task. `POST /v1/runs` records it `running` and returns
|
|
7
|
-
* immediately; the background driver (`src/runs.ts`) appends events here as they happen and marks the
|
|
8
|
-
* run terminal when done. Because runs + events are durable and shared, any stateless instance can
|
|
9
|
-
* report status or replay the event stream — a client can reconnect (even to a different instance)
|
|
10
|
-
* with `Last-Event-ID` and resume. The two dialects are kept in lock-step by ONE shared integration suite
|
|
11
|
-
* (test/run-store-reaper-integration.test.ts) that runs the SAME park→resume→claim scenario against BOTH
|
|
12
|
-
* real engines — the anti-drift guard.
|
|
13
|
-
*
|
|
14
|
-
* ── Dialect deltas, kept EXPLICIT (never hidden behind an abstraction) ────────────────────────────────
|
|
15
|
-
* - `?` placeholders vs `$n` (transaction control itself is normalized by `SqlDriver`/`SqlTxConn` —
|
|
16
|
-
* mysql2's native begin/commit/rollback vs PG's statement form; see sql-driver.ts)
|
|
17
|
-
* - dup-key classification (createRun's task_active claim race) — the PREDICATE has a single owner
|
|
18
|
-
* (`sql-errors.ts`, [ref] P1-①); only the dialect DISPATCH is visible here
|
|
19
|
-
*
|
|
20
|
-
* ⚠️ The run ACTIVE-status word list written out in the `status IN (…)` / `NOT IN (…)` fragments below
|
|
21
|
-
* is the closed set owned by `store-contracts.ts` (`isTerminalRunStatus` / `isParkedRunStatus`). The SQL
|
|
22
|
-
* text stays literal ON PURPOSE (A12: both dialects readable side by side), so the tie is a PARITY GATE:
|
|
23
|
-
* `test/source-hygiene-gate.test.ts` → "[ref] P1-② run 活状态 SQL 字面 ↔ store-contracts 闭集属主 parity".
|
|
24
|
-
* core adding a TaskStatus member turns that gate red on every fragment here — update them in the same batch.
|
|
25
|
-
* - null-safe owner compare: `<=>` (TiDB) vs `IS NOT DISTINCT FROM` (PG) — a null-owner run still matches
|
|
26
|
-
* `owner=null` on both, single-DB-fleet defense-in-depth (see {@link requestCancel})
|
|
27
|
-
* - multi-table DELETE: `DELETE t FROM t JOIN j …` (TiDB) vs `DELETE FROM t USING j …` (PG)
|
|
28
|
-
* - `UPDATE t AS a SET col=…` — PG's SET columns are NOT alias-qualified (PG rejects `SET a.col`), while
|
|
29
|
-
* TiDB's SET clause DOES qualify (`SET tr.col = …`) — see reapSuspended / failSuspendedWithExpiredCheckpoint
|
|
30
|
-
* - JSON binding: TiDB `JSON.stringify` verbatim vs PG `pgSafeJsonStringify` + `$n::jsonb` casts
|
|
31
|
-
* - PG-only protocol-byte defenses on plain-TEXT columns: `pgSanitizeText` on objective_preview (createRun)
|
|
32
|
-
* and error/error_code (setTerminal) — a raw NUL would throw 22P05 and roll back the whole terminal write,
|
|
33
|
-
* stranding the run in a live status forever (codex R12); TiDB stores these verbatim
|
|
34
|
-
* - dynamic multi-clause builders (listRuns/listSessions): TiDB's `?` is position-agnostic so the WHERE
|
|
35
|
-
* array is built with plain string pushes; PG's `$n` needs numbered placeholders, so those two methods
|
|
36
|
-
* keep an EXPLICIT per-dialect branch (a shared positional-counter abstraction would hide the very
|
|
37
|
-
* placeholder-numbering divergence this file exists to keep visible)
|
|
38
|
-
* - schema ownership: TiDB DDL in tidb-pool.ts, PG DDL centrally in pg-pool.ts (neither store creates tables).
|
|
39
|
-
*/
|
|
40
|
-
import type { TaskResult, TaskStatus } from "@sema-agent/core";
|
|
1
|
+
import type { TaskStatus } from "@sema-agent/core";
|
|
41
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
42
3
|
import type { Pool as PgPool } from "pg";
|
|
43
4
|
import { type UsageRow } from "../usage-analytics.js";
|
|
44
|
-
import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
|
|
5
|
+
import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult } from "./store-contracts.js";
|
|
45
6
|
import { type SqlDriver } from "./sql-driver.js";
|
|
46
7
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
47
8
|
export type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
|
|
@@ -119,7 +80,7 @@ export declare class SqlRunStore {
|
|
|
119
80
|
* abort text, or vice versa). Positive form ([1.207 codex M1]): the negative NOT IN missed the
|
|
120
81
|
* blocked/timeout terminals — a first write of those could still be overwritten. The claim DELETE stays
|
|
121
82
|
* unconditional (idempotent; the first writer already released it). */
|
|
122
|
-
setTerminal(taskId: string, status: TaskStatus, result:
|
|
83
|
+
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
123
84
|
/**
|
|
124
85
|
* Durable F4 ([ref]): a task suspended on a policy `ask`. Sets the run NON-terminal `suspended` and
|
|
125
86
|
* KEEPS the `task_active` claim — the session stays locked (no new run) until an operator resumes it to a
|