@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.
Files changed (121) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +2 -2
  3. package/USAGE.md +101 -9
  4. package/dist/approval-ask-audit-store.d.ts +100 -1
  5. package/dist/approval-ask-audit-store.js +103 -2
  6. package/dist/approval-card.d.ts +148 -13
  7. package/dist/approval-card.js +82 -13
  8. package/dist/approval.d.ts +31 -0
  9. package/dist/approval.js +18 -0
  10. package/dist/auto-mode-face.d.ts +111 -0
  11. package/dist/auto-mode-face.js +99 -0
  12. package/dist/bench/s1/arms.js +5 -5
  13. package/dist/bench/s1/live-deps.js +11 -11
  14. package/dist/bench/s1/run-firm.js +3 -0
  15. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  16. package/dist/bench/s1/runner-ctx.js +7 -0
  17. package/dist/boot/config-center.js +23 -1
  18. package/dist/boot/coordinators.js +13 -1
  19. package/dist/boot/parked-revive-gate.d.ts +7 -2
  20. package/dist/boot/parked-revive-gate.js +12 -1
  21. package/dist/boot/resolve-spec.d.ts +3 -0
  22. package/dist/boot/resolve-spec.js +3 -1
  23. package/dist/boot/runner-deps.d.ts +12 -0
  24. package/dist/boot/runner-deps.js +28 -3
  25. package/dist/boot/runtime-caps.d.ts +19 -9
  26. package/dist/boot/runtime-caps.js +49 -21
  27. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  28. package/dist/boot/session-shell-gate-registry.js +21 -0
  29. package/dist/boot/stores.js +12 -5
  30. package/dist/config-catalog.js +14 -5
  31. package/dist/config-center/http-client.js +7 -11
  32. package/dist/config-center/read-warnings.d.ts +20 -0
  33. package/dist/config-center/read-warnings.js +36 -0
  34. package/dist/config-provider.d.ts +3 -0
  35. package/dist/config-provider.js +4 -8
  36. package/dist/config-types.d.ts +41 -10
  37. package/dist/config.d.ts +21 -0
  38. package/dist/config.js +40 -5
  39. package/dist/hooks/hook-runner.js +8 -8
  40. package/dist/http/route-ctx.d.ts +75 -0
  41. package/dist/http/routes/a2a-serve.js +5 -3
  42. package/dist/http/routes/approvals-assistant.js +19 -6
  43. package/dist/http/routes/capabilities.js +21 -4
  44. package/dist/http/routes/memory-origin.d.ts +2 -2
  45. package/dist/http/routes/rules.js +3 -2
  46. package/dist/http/routes/runs.d.ts +0 -14
  47. package/dist/http/routes/runs.js +20 -8
  48. package/dist/http/routes/tasks.js +34 -8
  49. package/dist/http/routes/workflows.js +19 -4
  50. package/dist/http/server.d.ts +11 -1
  51. package/dist/http/server.js +215 -43
  52. package/dist/http/wire-types.d.ts +9 -4
  53. package/dist/leader/diffout.js +2 -1
  54. package/dist/leader/endpoint.js +45 -12
  55. package/dist/leader/fanout.js +6 -5
  56. package/dist/leader/leader.js +17 -14
  57. package/dist/leader/merge.js +17 -12
  58. package/dist/leader/planner.js +3 -2
  59. package/dist/leader/repair-oracle.js +6 -4
  60. package/dist/leader/repair-wire.js +5 -3
  61. package/dist/leader/wire.d.ts +30 -1
  62. package/dist/leader/wire.js +36 -19
  63. package/dist/main.js +43 -7
  64. package/dist/observability/err-text.d.ts +6 -0
  65. package/dist/observability/err-text.js +10 -0
  66. package/dist/observability/fail-open.d.ts +40 -0
  67. package/dist/observability/fail-open.js +19 -0
  68. package/dist/observability/metrics.js +2 -2
  69. package/dist/observability/run-terminal-log.d.ts +120 -0
  70. package/dist/observability/run-terminal-log.js +360 -0
  71. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  72. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  73. package/dist/permission-rule-vocab.d.ts +40 -0
  74. package/dist/permission-rule-vocab.js +17 -0
  75. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  76. package/dist/plugins/checkpoint-store-sql.js +19 -3
  77. package/dist/plugins/file-run-store.d.ts +3 -34
  78. package/dist/plugins/file-run-store.js +19 -0
  79. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  80. package/dist/plugins/local-checkpoint-store.js +3 -0
  81. package/dist/plugins/mailbox-store-sql.d.ts +28 -9
  82. package/dist/plugins/mailbox-store-sql.js +72 -10
  83. package/dist/plugins/memory-run-store.d.ts +3 -15
  84. package/dist/plugins/memory-run-store.js +19 -0
  85. package/dist/plugins/permission-rule-store-file.js +8 -3
  86. package/dist/plugins/permission-rule-store-sql.js +16 -10
  87. package/dist/plugins/remote-scratchpad.js +3 -2
  88. package/dist/plugins/run-store-sql.d.ts +3 -42
  89. package/dist/plugins/run-store-sql.js +30 -1
  90. package/dist/plugins/store-backend.d.ts +1 -1
  91. package/dist/plugins/store-contracts.d.ts +66 -1
  92. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  93. package/dist/plugins/workflow-run-store-sql.js +10 -2
  94. package/dist/rules-consent.js +20 -13
  95. package/dist/run-cancel-context.d.ts +13 -0
  96. package/dist/run-cancel-context.js +15 -0
  97. package/dist/run-local.js +2 -2
  98. package/dist/runs.d.ts +4 -1
  99. package/dist/runs.js +41 -10
  100. package/dist/runtime-caps-resolver.d.ts +133 -17
  101. package/dist/runtime-caps-resolver.js +41 -3
  102. package/dist/task-settings.d.ts +57 -3
  103. package/dist/task-settings.js +78 -5
  104. package/dist/task-workflow.d.ts +32 -2
  105. package/dist/task-workflow.js +8 -3
  106. package/dist/tool-approval.d.ts +26 -3
  107. package/dist/tool-approval.js +96 -13
  108. package/dist/trace/core-keyset-guard.d.ts +7 -8
  109. package/dist/trace/engine-notice-wire.d.ts +1 -1
  110. package/dist/trace/engine-notice-wire.js +2 -0
  111. package/dist/trace/ledger-sink.d.ts +13 -4
  112. package/dist/trace/ledger-sink.js +14 -6
  113. package/dist/trace/project.d.ts +58 -3
  114. package/dist/trace/project.js +33 -1
  115. package/dist/trace/redact.d.ts +22 -11
  116. package/dist/trace/redact.js +660 -21
  117. package/dist/trace/sema-provenance.d.ts +35 -0
  118. package/dist/trace/sema-provenance.js +14 -0
  119. package/dist/turn-activity.d.ts +32 -2
  120. package/dist/turn-activity.js +29 -4
  121. 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`/`reap` 删整盒
11
- * (含 box 行)= 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)
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 行无消息)不删(InMemory 同形——`newest === undefined` 不删)。
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
- constructor(pool: MySqlPool);
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
- constructor(pool: PgPool);
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
- constructor(pool) {
161
+ logger;
162
+ crossProcessSafe = true;
163
+ constructor(pool, logger) {
108
164
  this.pool = pool;
165
+ this.logger = logger;
109
166
  }
110
167
  async tx(fn) {
111
168
  const c = await this.pool.getConnection();
@@ -130,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.map(rowToMessage), maxSeq };
220
+ return { messages: rowsToMessages(msgs, scope, handle, this.logger), maxSeq };
163
221
  });
164
222
  }
165
223
  async ack(scope, handle, owner, upToSeq) {
@@ -203,8 +261,8 @@ export class TiDBMailboxStore {
203
261
  const [newest] = (await c.query(`SELECT sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY sent_at_ms DESC LIMIT 1 FOR UPDATE`, [scope, h]));
204
262
  if (newest.length === 0 || Number(newest[0].sent_at_ms) >= cutoff)
205
263
  continue;
206
- await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, h]);
207
264
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, h]);
265
+ await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL WHERE scope_key = ? AND handle = ?`, [scope, h]);
208
266
  dropped++;
209
267
  }
210
268
  return dropped;
@@ -213,8 +271,11 @@ export class TiDBMailboxStore {
213
271
  }
214
272
  export class PgMailboxStore {
215
273
  pool;
216
- constructor(pool) {
274
+ logger;
275
+ crossProcessSafe = true;
276
+ constructor(pool, logger) {
217
277
  this.pool = pool;
278
+ this.logger = logger;
218
279
  }
219
280
  async tx(fn) {
220
281
  const c = await this.pool.connect();
@@ -245,6 +306,7 @@ export class PgMailboxStore {
245
306
  assertKeyBytes("scope", scope);
246
307
  assertKeyBytes("handle", handle);
247
308
  const hopChain = encodeHopChain(msg.hopChain);
309
+ const peerMeta = encodePeerMeta(msg.peerMeta);
248
310
  return this.tx(async (c) => {
249
311
  await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
250
312
  scope,
@@ -262,7 +324,7 @@ export class PgMailboxStore {
262
324
  }
263
325
  const seq = Number(rows[0].next_seq);
264
326
  await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = $1 WHERE scope_key = $2 AND handle = $3`, [seq + 1, scope, handle]);
265
- await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt, hopChain]);
327
+ await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt, hopChain, peerMeta]);
266
328
  return seq;
267
329
  });
268
330
  }
@@ -275,12 +337,12 @@ export class PgMailboxStore {
275
337
  const box = boxes[0];
276
338
  if (box.lease_owner !== null && Number(box.lease_expires_at_ms) > now && String(box.lease_owner) !== owner)
277
339
  return null;
278
- const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
340
+ const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain, peer_meta FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
279
341
  if (msgs.length === 0)
280
342
  return null;
281
343
  const maxSeq = Number(msgs[msgs.length - 1].seq);
282
344
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = $1, lease_expires_at_ms = $2, lease_max_seq = $3 WHERE scope_key = $4 AND handle = $5`, [owner, now + ttlMs, maxSeq, scope, handle]);
283
- return { messages: msgs.map((r) => rowToMessage(r)), maxSeq };
345
+ return { messages: rowsToMessages(msgs, scope, handle, this.logger), maxSeq };
284
346
  });
285
347
  }
286
348
  async ack(scope, handle, owner, upToSeq) {
@@ -327,8 +389,8 @@ export class PgMailboxStore {
327
389
  const { rows: newest } = await c.query(`SELECT sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY sent_at_ms DESC LIMIT 1 FOR UPDATE`, [scope, h]);
328
390
  if (newest.length === 0 || Number(newest[0].sent_at_ms) >= cutoff)
329
391
  continue;
330
- await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, h]);
331
392
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, h]);
393
+ await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL WHERE scope_key = $1 AND handle = $2`, [scope, h]);
332
394
  dropped++;
333
395
  }
334
396
  return dropped;
@@ -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 { TaskResult, TaskStatus } from "@sema-agent/core";
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: TaskResult | null, error: string | null): Promise<void>;
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(2),
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.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string() }).strict().optional(),
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 !== 2) {
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.literal("Bash"),
30
- match: z.enum(["exact", "prefix"]),
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.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string() }).strict();
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;OCC 守卫另在 rev,
108
- -- 两轴禁混,schema-naming 门③ 白名单登记)。读侧对非 2 的行交还 design/375 §4.5 的
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=数据格式版本恒 2,非 2 的行读成 Stale 信封;offers_json=按 index 引用的 OPTION 结构,
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) !== 2) {
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: 2,
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 = 2`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = $1 AND state = 'pending' AND version = 2`), [recordId]);
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 = 2`, `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 = 2`), [
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.slice(0, 256) } : { error: r.error.message }),
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: TaskResult | null, error: string | null): Promise<void>;
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