@sema-agent/server 7.57.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.
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore } from "@sema-agent/core";
2
+ import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore, mailboxCrossProcessMountVerdict } from "@sema-agent/core";
3
3
  import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
4
4
  import { memoryEmbedderFor } from "../plugins/memory-embedder.js";
5
5
  import { ensurePgMemoryEmbedderMetaSchema, reconcileEmbedderFingerprint } from "../plugins/memory-embedder-fingerprint.js";
@@ -299,19 +299,26 @@ export async function openStores(ctx) {
299
299
  const pgPool = backend?.pgPool?.();
300
300
  if (pgPool) {
301
301
  await ensurePgMailboxSchema(async (text, params) => pgPool.query(text, params));
302
- mailboxStore = new PgMailboxStore(pgPool);
302
+ mailboxStore = new PgMailboxStore(pgPool, logger);
303
303
  }
304
304
  else if (mysqlPool) {
305
305
  await ensureTiDBMailboxSchema(mysqlPool);
306
- mailboxStore = new TiDBMailboxStore(mysqlPool);
306
+ mailboxStore = new TiDBMailboxStore(mysqlPool, logger);
307
307
  }
308
308
  else if (backend?.kind === "local") {
309
309
  mailboxStore = new FileMailboxStore(config.localDataRoot ?? localRoot, {
310
310
  onCorruptRead: (info) => logger.warn("mailbox_corrupt_read", { path: info.path, reason: info.reason }),
311
311
  });
312
312
  }
313
- if (mailboxStore)
314
- logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
313
+ if (mailboxStore) {
314
+ const laneVerdict = mailboxCrossProcessMountVerdict(mailboxStore);
315
+ logger.info("mailbox_store_enabled", {
316
+ backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file",
317
+ crossProcessSafe: mailboxStore.crossProcessSafe,
318
+ peerLaneMountable: laneVerdict.ok,
319
+ ...(laneVerdict.ok ? {} : { peerLaneRefusal: laneVerdict.code }),
320
+ });
321
+ }
315
322
  }
316
323
  let usageWindowStore;
317
324
  if (config.usageWindows) {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/settings-schema";
3
3
  import { centerPromptsFromEffective } from "../prompts-domain-validate.js";
4
+ import { foldEffectiveReadWarning } from "./read-warnings.js";
4
5
  export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, worker) {
5
6
  const scheme = new URL(baseUrl).protocol;
6
7
  if (scheme !== "http:" && scheme !== "https:")
@@ -21,17 +22,12 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
21
22
  delete wire.prompts;
22
23
  else
23
24
  wire.prompts = centerPrompts;
24
- const domainErrors = warnings
25
- .map((w) => w.kind === "domain-defaulted"
26
- ? { domain: w.domain, error: (w.error instanceof Error ? w.error.message : String(w.error)).slice(0, 600) }
27
- : w.kind === "unknown-keys-dropped"
28
- ? { domain: w.domain, error: `unknown keys dropped by domain schema: ${w.keys.join(", ")}`.slice(0, 600) }
29
- : w.kind === "unknown-keys-carried"
30
- ? { domain: w.domain, error: `unknown keys carried to consumer (spelling?): ${w.keys.join(", ")}`.slice(0, 600) }
31
- : w.kind === "unread-config-file"
32
- ? { domain: w.domain ?? "config.d", error: `unread config file ${w.file} (${w.why})`.slice(0, 600) }
33
- : undefined)
34
- .filter((r) => r !== undefined);
25
+ const domainErrors = [];
26
+ for (const w of warnings) {
27
+ const f = foldEffectiveReadWarning(w);
28
+ if (f !== null)
29
+ domainErrors.push(f);
30
+ }
35
31
  const effective = wire;
36
32
  return { effective, etag: res.headers.get("etag") ?? undefined, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
37
33
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `EffectiveReadWarning` → 候选门口径的**单点**折叠(本地腿 `config-provider.ts` 与远程腿 `http-client.ts` 共用;
3
+ * 两腿各抄一份映射正是漂移的成因——远程腿曾漏掉一次改动,版本超前的 center 多一个字段就整份远程 refresh 整拒)。
4
+ *
5
+ * **律只有一条:未知键 = 候选门错误,零特例。** 「别的写者按契约放的扩展键」(壳写在 models 条目上的 `sema*`)
6
+ * 不是本函数要分辨的事——那由 settings-schema 的 `EXTENSION_NAMESPACES` 在**声明处**说清(登记的键既不点名也不计数,
7
+ * 1.5.0 起);到了这里的每一个键都是「写了没生效」的拼错或越界,与 domain-defaulted 同级。此前本仓在下游按 kind/
8
+ * 按域/按键路径闭集/按 64 满额四层猜意图,每层都被下一轮复审打穿——意图只能在声明处说,消费方不猜。
9
+ *
10
+ * 截断:schema 的键列表有界(64),超界带 `truncatedFrom`(真实总数)——文案如实带上,消费方永远不把有界列表当穷举。
11
+ * 词表是闭集:`switch` 穷举 `kind`,schema 加新 kind 这里编译红,不会静默漏臂。
12
+ */
13
+ import type { EffectiveReadWarning } from "@sema-agent/settings-schema";
14
+ export type FoldedReadWarning = {
15
+ readonly domain: string;
16
+ readonly error: string;
17
+ };
18
+ /** null = 不入诊断面(`hosts-grandfathered`:值已被收编接受,与本地店同口径)。 */
19
+ export declare function foldEffectiveReadWarning(w: EffectiveReadWarning): FoldedReadWarning | null;
20
+ //# sourceMappingURL=read-warnings.d.ts.map
@@ -0,0 +1,36 @@
1
+ const MAX_ERROR_CHARS = 600;
2
+ const cap = (s) => s.slice(0, MAX_ERROR_CHARS);
3
+ const keyList = (prefix, keys, truncatedFrom) => {
4
+ const total = truncatedFrom ?? keys.length;
5
+ const budget = MAX_ERROR_CHARS - prefix.length;
6
+ const shown = [];
7
+ let used = 0;
8
+ const suffixFor = (n) => (n < total ? ` (+${total - n} more, list truncated)` : "");
9
+ for (const k of keys) {
10
+ const piece = (shown.length > 0 ? ", " : "") + k;
11
+ if (used + piece.length + suffixFor(shown.length + 1).length > budget)
12
+ break;
13
+ shown.push(k);
14
+ used += piece.length;
15
+ }
16
+ return `${prefix}${shown.join(", ")}${suffixFor(shown.length)}`;
17
+ };
18
+ export function foldEffectiveReadWarning(w) {
19
+ switch (w.kind) {
20
+ case "hosts-grandfathered":
21
+ return null;
22
+ case "domain-defaulted":
23
+ return { domain: w.domain, error: cap(w.error instanceof Error ? w.error.message : String(w.error)) };
24
+ case "unknown-keys-dropped":
25
+ return { domain: w.domain, error: keyList("unknown keys dropped by domain schema: ", w.keys, w.truncatedFrom) };
26
+ case "unknown-keys-carried":
27
+ return { domain: w.domain, error: keyList("unknown keys carried to consumer (spelling?): ", w.keys, w.truncatedFrom) };
28
+ case "unread-config-file":
29
+ return { domain: w.domain ?? "config.d", error: cap(`unread config file ${w.file} (${w.why})`) };
30
+ default: {
31
+ const never = w;
32
+ throw new Error(`unhandled EffectiveReadWarning kind: ${JSON.stringify(never)}`);
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=read-warnings.js.map
@@ -7,6 +7,9 @@ import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetc
7
7
  * 连坐全包回落 env——坏域按该域 schema default 落+错误单列,好域照常生效;caller(main.ts)对每条打
8
8
  * per-domain warn 点名坏域。gate 域(governance/rosters/…)坏文件 tolerant 仍 throw(READ 容错不放宽
9
9
  * gate fail-loud,registry-core 半场精化)——那条路走既有的整包 catch。 */
10
+ /** 壳写在 `config.d/models.json` 条目上的装饰键(`sema*`)自 settings-schema 1.5.0 起是**声明过的扩展命名空间**
11
+ * (`EXTENSION_NAMESPACES`):schema 照旧 strip、但不再报「未知键」,所以本函数收到的每一个未知键都是拼错/越界 ⇒ 候选门错误。
12
+ * 此前把这类键当「通知」放行的整套下游机制(域豁免/键路径闭集/满额特判/单独的通知面)已全部撤回——意图在声明处说,消费方不猜。 */
10
13
  export type FetchEffectiveResult = {
11
14
  effective: EffectiveConfig;
12
15
  etag?: string;
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes } from "node:crypto";
2
+ import { foldEffectiveReadWarning } from "./config-center/read-warnings.js";
2
3
  import { FileConfigStore } from "@sema-agent/settings-schema/node";
3
4
  import { findSkillContent, skillContentHash, refIntegrityIssues, siblingResolver, } from "@sema-agent/settings-schema";
4
5
  import { redactSecrets } from "./trace/redact.js";
@@ -62,14 +63,9 @@ export class LocalConfigProvider {
62
63
  const store = this.#injectedStore ??
63
64
  new FileConfigStore(this.root, {
64
65
  onWarning: (w) => {
65
- if (w.kind === "domain-defaulted")
66
- parseWarnings.push({ domain: w.domain, error: w.error instanceof Error ? w.error.message : String(w.error) });
67
- else if (w.kind === "unknown-keys-dropped")
68
- parseWarnings.push({ domain: w.domain, error: `unknown keys dropped by domain schema: ${w.keys.join(", ")}` });
69
- else if (w.kind === "unknown-keys-carried")
70
- parseWarnings.push({ domain: w.domain, error: `unknown keys carried to consumer (spelling?): ${w.keys.join(", ")}` });
71
- else if (w.kind === "unread-config-file")
72
- parseWarnings.push({ domain: w.domain ?? "config.d", error: `unread config file ${w.file} (${w.why})` });
66
+ const f = foldEffectiveReadWarning(w);
67
+ if (f !== null)
68
+ parseWarnings.push(f);
73
69
  },
74
70
  });
75
71
  const { effective: eff, domainErrors } = await store.getEffective({ tolerant: true });
@@ -673,7 +673,7 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
673
673
  if (!out)
674
674
  continue;
675
675
  if (typeof out.systemMessage === "string")
676
- ctx.logger.warn("hook_system_message", { event, message: clip(out.systemMessage, 500) });
676
+ ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
677
677
  if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
678
678
  ctx.logger.warn("hook_result_unsupported", { event });
679
679
  }
@@ -747,7 +747,7 @@ export function createTaskHooks(config, ctx) {
747
747
  if (!out)
748
748
  continue;
749
749
  if (typeof out.systemMessage === "string")
750
- ctx.logger.warn("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
750
+ ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
751
751
  if (out.continue === false) {
752
752
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
753
753
  return { action: "deny", message: reason, ...buildHookContextField(contexts) };
@@ -845,7 +845,7 @@ export function createTaskHooks(config, ctx) {
845
845
  if (!out)
846
846
  continue;
847
847
  if (typeof out.systemMessage === "string")
848
- ctx.logger.warn("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
848
+ ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
849
849
  if (out.continue === false) {
850
850
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
851
851
  }
@@ -895,7 +895,7 @@ export function createTaskHooks(config, ctx) {
895
895
  if (!out)
896
896
  continue;
897
897
  if (typeof out.systemMessage === "string")
898
- ctx.logger.warn("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
898
+ ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
899
899
  if (out.continue === false)
900
900
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
901
901
  if (out.decision === "block" && typeof out.reason === "string")
@@ -942,7 +942,7 @@ export function createTaskHooks(config, ctx) {
942
942
  if (!out)
943
943
  continue;
944
944
  if (typeof out.systemMessage === "string")
945
- ctx.logger.warn("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
945
+ ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
946
946
  if (out.continue === false)
947
947
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
948
948
  if (out.decision === "block" && typeof out.reason === "string")
@@ -974,7 +974,7 @@ export function createTaskHooks(config, ctx) {
974
974
  if (!out)
975
975
  continue;
976
976
  if (typeof out.systemMessage === "string")
977
- ctx.logger.warn("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
977
+ ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
978
978
  if (out.continue === false) {
979
979
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
980
980
  return { block: reason, ...buildHookContextField(contexts) };
@@ -1063,7 +1063,7 @@ export function createTaskHooks(config, ctx) {
1063
1063
  continue;
1064
1064
  }
1065
1065
  if (typeof out.systemMessage === "string")
1066
- ctx.logger.warn("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1066
+ ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1067
1067
  if (out.continue === false)
1068
1068
  sawContinueFalse = true;
1069
1069
  if (out.decision === "block" && block === undefined) {
@@ -1122,7 +1122,7 @@ export function createTaskHooks(config, ctx) {
1122
1122
  if (!out)
1123
1123
  continue;
1124
1124
  if (typeof out.systemMessage === "string")
1125
- ctx.logger.warn("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1125
+ ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1126
1126
  if (out.continue === false) {
1127
1127
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1128
1128
  }
@@ -235,7 +235,7 @@ export function createMetrics() {
235
235
  m.counter("remote_env_resume_total", "E2B remote-env resumeVM outcomes (v1.5, provider/result)");
236
236
  m.counter("remote_env_exec_stream_read_timeout_total", "E2B execStream idle read-timeouts (#1128, provider)");
237
237
  m.counter("resume_anchor_capture_failed", "E18 resume-at per-turn anchor captures that failed (best-effort, run unaffected)");
238
- m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open)");
238
+ m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open/waiting_first_token/recovered/gave_up)");
239
239
  m.gauge("store_backend_degraded", "1 when the auto DB probe failed at boot and this replica degraded to in-memory (S5)");
240
240
  m.counter("images_omitted_total", "Tasks whose images will be degraded to text placeholders (no-vision model), by model (S6)");
241
241
  m.counter("runs_reaped_total", "Run rows flipped by the background reapers (S7), by kind (stale/suspended/expired_checkpoint)");
@@ -177,7 +177,7 @@ export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlin
177
177
  function diffScanRefusal(patch, direct) {
178
178
  if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
179
179
  return "combined-diff";
180
- const fence = /-{4,5} ?BEGIN[^\n-]*PRIVATE KEY(?: BLOCK)?[^\n-]*-{4,5}/;
180
+ const fence = /-{4,5} ?BEGIN(?:[^\n-]|-(?!-))*PRIVATE KEY(?: BLOCK)?(?:[^\n-]|-(?!-))*-{4,5}/;
181
181
  if (fence.test(direct))
182
182
  return "pem-fence-survived";
183
183
  const ppkField = /Private-(?:MAC|Hash): [0-9a-fA-F]{32}/;
@@ -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;
@@ -17,11 +17,11 @@
17
17
  * ⑨⑩([ref],2026-08-31)把两个最大的 wire 面也纳入:TaskSpec 按「server 有无写点」记 Wired/Unwired,
18
18
  * TaskResult 按「转发/摘除」记 Forwarded/Stripped —— 谁改 resolve-spec.ts / runs.ts 的发射点谁同步本文件。
19
19
  */
20
- import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxStore, TaskSpec, TaskResult } from "@sema-agent/core";
20
+ import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult } from "@sema-agent/core";
21
21
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
22
22
  type AssertAllKeysHandled<T extends never> = T;
23
23
  type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure" | "agentMessage" | "_sema_provenance";
24
- type NotificationExcluded = "peer";
24
+ type NotificationExcluded = "peer" | "crossSessionMessage";
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";
@@ -33,10 +33,9 @@ type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "is
33
33
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
34
34
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end";
35
35
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
36
- type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain";
36
+ type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
37
37
  type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
38
- type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
39
- type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain";
38
+ type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
40
39
  type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
41
40
  type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
42
41
  type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable";
@@ -49,7 +48,7 @@ type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<Permiss
49
48
  type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls";
50
49
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
51
50
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
52
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends";
51
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested";
53
52
  type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
54
53
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
55
54
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -15,6 +15,8 @@ export const ENGINE_NOTICE_WIRE_CODES = [
15
15
  "memory.capture_optout_unpersisted",
16
16
  "task.halt_unconsumed",
17
17
  "task.late_approval",
18
+ "config.durable_gate_unavailable",
19
+ "peer.inbound_disposition",
18
20
  ];
19
21
  const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
20
22
  export function isEngineNoticeWireCode(code) {
@@ -343,8 +343,47 @@ export declare function workspaceChangedEventData(ev: {
343
343
  * ⚠️ 与 `storeWired` **不同**:本仓这两条至今未接(`RunnerDeps.permissionRuleSyncWired` /
344
344
  * `permissionRuleOrg` 全树零声明点,2026-08-10 [ref] 复扫亲验)⇒ 今天恒 `false`,这是**真值**不是
345
345
  * 缺席。哪天接上其中任一条,连本句一起改 —— 这一行是它的登记处。
346
+ * · `modelGate`(可选段 `{ class, removed[], restore }`;core fc52b4d4 [ref] / [ref] 片2b,[ref] 提货件,
347
+ * 黑板 [ref] @server①)—— **裁:租户可见**。它是 per-session 的**读面**:「我这条 session 因为工具-模型门卸掉了
348
+ * 哪些工具」——与 operator 通告 `config.tool_model_gate_removed` **同决策同源**(core 从同一张 trim 表铸两面),
349
+ * 但通告归 operator 车道(engine_notice 白名单,附录 D),本段归租户:消费端(cli L-70 [ref])要在**首帧一次读**
350
+ * 里知道自己少了哪些工具、怎么恢复,否则只能等到调用被拒才发现。三键都是引擎派生值:`class` = 合并 gate 表的 class 行键
351
+ * (prepare-task 对表外 class 响亮拒),`removed` = 工具 wire 名(core 表键,非用户内容),`restore` = core 铸的一句恢复指引(引擎文案)
352
+ * ⇒ 不脱敏、逐字。多 class 同卸 ⇒ core 报首 class + removed **并集**(core 合并律;本仓不按 class 过滤)。
353
+ * 仅本 run 有门卸时在场:**缺席 = 本 run 没卸**,不铸空段、不铸 `removed: []`。畸形(三键任一缺席/形不合)
354
+ * ⇒ 整段按缺席 —— 不铸半段,半段会被读成「只卸了这些」。它不构成 governance 四布尔的侧信道(门卸由
355
+ * 模型/门 class 决定,与 lockedConfig/compliance/memoryAdmission/retention 无函数关系)。
356
+ * · `autoMode`(`{ armed: boolean, reason: AutoModeArmReason }`;core 7.3.1 [ref],147③ 提货件,黑板 [ref])
357
+ * —— **裁:租户可见**。它是 per-leg 的**读面**:「我这条腿的 auto 模式武装了吗?没武装是卡在哪一臂?」
358
+ * ① **同主体同受众**:本仓早已把同一件事投在租户面上 —— `/v1/capabilities.permissionModeAuto.{armed,reason}`
359
+ * ([ref],7.57.0 已发)答的是「**若此刻提交**会不会武装」,本段答的是「**这条真跑起来的腿**武装了吗」;
360
+ * 同一个主体(调用方自己的 run)、同一条轴,只差事前/事后。剥掉它,壳就只能拿事前预测冒充事后事实;
361
+ * ② **两个值都是引擎派生的闭形**:`armed` 布尔,`reason` 是 core 的闭集词(`AUTO_MODE_ARM_REASONS`:
362
+ * armed / no_intent / no_face / denied / resolver_fault / latch_open)—— 非用户内容,不脱敏、逐字;
363
+ * ③ **不是 governance 侧信道**:它描述的是**本腿**的武装结论,与 lockedConfig/compliance/memoryAdmission/
364
+ * retention 四位无函数关系;而 `denied` 这一词所披露的「组织把这一位关了」在 [ref] 读面上**本就已经**
365
+ * 对同一租户可见(`org_denied`),本段没有扩大任何受众。
366
+ * 🔴 **逐字透传,不映射**:core 的六词与本仓 [ref] 读面的六词(`AUTO_MODE_UNARMED_REASONS`)**刻意不归一**
367
+ * —— 两张表回答的是两个问题、粒度也不同(core 的 `denied` 一词对应本仓的 `org_denied` / `local_denied`
368
+ * 两词:core 只看合成后的 caps deny 位,本仓分得出这个 false 是 center 下发的还是本地 env 写的)。
369
+ * ⚠️ **`settings_denied` 不折进 `denied`,它折进 `no_intent`**(codex 车BO r1 [medium] 验真后纠):
370
+ * settings 层的 kill-switch 折的是**模式**(`effectivePermissionMode`:auto → default),于是 resolve-spec
371
+ * 阶段④ 压根不写意图座 `TaskSpec.autoModeRequested` ⇒ core 的武装谓词第一项就落空;而本地 env 腿折的是
372
+ * **caps**(`applyLocalAutoModeDeny`),模式不动、座照写 ⇒ core 才报 `denied`。两条腿在 manifest 上
373
+ * **分不开**,要分辨得读 `/v1/capabilities`。对照表与保鲜钉在 `test/wiring-manifest-projection.test.ts`
374
+ * 的 147③ 节,分岔的行为面格在 `test/permission-mode-shell-gate-translation.test.ts`;
375
+ * 本函数**只透传**,一个字都不换。
376
+ * ⚠️ 缺席 ≠ 「不适用」:core 顶注逐字「absence here means an older mint or an external derivation, never
377
+ * "not applicable"」⇒ 缺席时**不铸** `armed:false`(那会把「老 mint 写的行」谎报成「这条腿确实没武装」)。
378
+ * ⚠️ **自相矛盾按缺席**:core 把 `armed ⇔ reason === "armed"` 写成段内不变量并在 derive 点拒(`readAutoModeFact`
379
+ * 抛)。账本回放/第三方 producer 送来的矛盾对若原样上 wire,按 `armed` 读与按 `reason` 读的消费端会得到
380
+ * **相反**的答案 —— 这条轴上(@cli `sema doctor permissions` 正读它)两个答案比没答案更坏 ⇒ 整段不铸。
381
+ * 这不是改语义:它与本函数「畸形值一律按缺席」是同一条规则,而这条不变量是 core 自己写的段内契约。
346
382
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
347
- * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
383
+ * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来 —— ⚠️ **但那条钉是夹具驱动的**:它只看
384
+ * `deriveWiringManifest(EFFECTIVE_FACTS)` 真铸出来的段,facts 不带的可选段(147③ 的 `autoMode` 就是)它一格
385
+ * 都红不了(2026-09-04 实测:该钉 26/26 全绿,红出 `autoMode` 的是编译期门)。段级 + 成员级的**编译期**门在
386
+ * `core-keyset-guard.ts` ⑥b —— 那才是「core 加了段/加了成员」的可靠红点,两道并用缺一不可。
348
387
  */
349
388
  export declare function wiringManifestEventData(ev: {
350
389
  manifest?: unknown;
@@ -410,8 +449,9 @@ export declare function humanInputEventData(ev: {
410
449
  }): Record<string, unknown>;
411
450
  /** S4 (SILENT-FALLBACK P0-a): the durable `brain_status` observation row, shared by the bg + resume append
412
451
  * sites. core marks the live `status` frame EPHEMERAL (never replayed by core on resume) — this row is the
413
- * SERVICE's own observation record (why a turn stalled: rate_limited/retrying/reconnecting/circuit_open),
414
- * not a core event replay. Whitelist: the closed phase union + neutral bounded detail (redacted) + retryInSec. */
452
+ * SERVICE's own observation record (why a turn stalled: rate_limited/retrying/reconnecting/circuit_open, and
453
+ * since core 7.3.1 / [ref] the pre-failure `waiting_first_token` wait), not a core event replay.
454
+ * Whitelist: the closed phase union verbatim + neutral bounded detail (redacted) + the bounded numeric seats. */
415
455
  export declare function brainStatusEventData(ev: {
416
456
  phase: string;
417
457
  detail?: string;
@@ -422,6 +462,8 @@ export declare function brainStatusEventData(ev: {
422
462
  errClass?: string;
423
463
  retryAtMs?: number;
424
464
  errorStatus?: number;
465
+ elapsedMs?: number;
466
+ timeoutMs?: number;
425
467
  eventId?: string;
426
468
  parentToolCallId?: string;
427
469
  }): Record<string, unknown>;
@@ -250,6 +250,25 @@ export function wiringManifestEventData(ev) {
250
250
  ...(bool(rulesIn.syncWired) !== undefined ? { syncWired: rulesIn.syncWired } : {}),
251
251
  ...(bool(rulesIn.orgGoverned) !== undefined ? { orgGoverned: rulesIn.orgGoverned } : {}),
252
252
  });
253
+ const gateIn = sec(m.modelGate);
254
+ const modelGate = (() => {
255
+ if (gateIn === undefined)
256
+ return undefined;
257
+ const cls = str(gateIn.class);
258
+ const restore = str(gateIn.restore);
259
+ const removed = Array.isArray(gateIn.removed) && gateIn.removed.every((r) => typeof r === "string") ? [...gateIn.removed] : undefined;
260
+ return cls !== undefined && restore !== undefined && removed !== undefined ? { class: cls, removed, restore } : undefined;
261
+ })();
262
+ const autoIn = sec(m.autoMode);
263
+ const autoMode = (() => {
264
+ if (autoIn === undefined)
265
+ return undefined;
266
+ const armed = bool(autoIn.armed);
267
+ const reason = str(autoIn.reason);
268
+ if (armed === undefined || reason === undefined)
269
+ return undefined;
270
+ return armed === (reason === "armed") ? { armed, reason } : undefined;
271
+ })();
253
272
  return {
254
273
  ...(num(m.schemaVersion) !== undefined ? { schemaVersion: m.schemaVersion } : {}),
255
274
  ...(str(legIn.kind) !== undefined ? { leg: { kind: legIn.kind } } : {}),
@@ -261,6 +280,8 @@ export function wiringManifestEventData(ev) {
261
280
  ...(str(sessionIn.store) !== undefined ? { session: { store: sessionIn.store } } : {}),
262
281
  ...(fleet !== undefined ? { fleet } : {}),
263
282
  ...(permissionRules !== undefined ? { permissionRules } : {}),
283
+ ...(modelGate !== undefined ? { modelGate } : {}),
284
+ ...(autoMode !== undefined ? { autoMode } : {}),
264
285
  ...identityFields(ev),
265
286
  };
266
287
  }
@@ -314,6 +335,8 @@ export function brainStatusEventData(ev) {
314
335
  ...(num(ev.maxRetries) ? { maxRetries: ev.maxRetries } : {}),
315
336
  ...(num(ev.retryInMs) ? { retryInMs: ev.retryInMs } : {}),
316
337
  ...(num(ev.retryAtMs) ? { retryAtMs: ev.retryAtMs } : {}),
338
+ ...(num(ev.elapsedMs) ? { elapsedMs: ev.elapsedMs } : {}),
339
+ ...(num(ev.timeoutMs) ? { timeoutMs: ev.timeoutMs } : {}),
317
340
  ...(num(ev.errorStatus) ? { errorStatus: ev.errorStatus } : {}),
318
341
  ...(typeof ev.errClass === "string" && ev.errClass.length > 0 ? { errClass: ev.errClass } : {}),
319
342
  ...identityFields(ev),
@@ -1,7 +1,7 @@
1
1
  import { recordFailOpen } from "../observability/fail-open.js";
2
2
  const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
3
3
  const FULL_SECRET_PATTERNS = [
4
- [/-----BEGIN[^-]+PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END[^-]+PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
4
+ [/-----BEGIN[^-](?:[^-](?:[^-]|-(?!-))*)?PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END[^-](?:[^-](?:[^-]|-(?!-))*)?PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
5
5
  [/---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----[\s\S]{0,8192}?---- END SSH2 ENCRYPTED PRIVATE KEY ----/g, "«redacted:private-key»"],
6
6
  [/\bsk-[A-Za-z0-9_-]{16,}\b/g, "«redacted:key»"],
7
7
  [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g, "«redacted:gh-token»"],
@@ -220,8 +220,12 @@ function isPemFenceLabel(s, from, to) {
220
220
  return false;
221
221
  if (!s.startsWith(PEM_KEY_WORD, end - PEM_KEY_WORD.length))
222
222
  return false;
223
- for (let i = from; i < to; i++)
224
- if (s.charCodeAt(i) === 45)
223
+ if (s.charCodeAt(from) === 45)
224
+ return false;
225
+ if (s.charCodeAt(from + 1) === 45)
226
+ return false;
227
+ for (let i = from + 1; i < to; i++)
228
+ if (s.charCodeAt(i) === 45 && s.charCodeAt(i + 1) === 45)
225
229
  return false;
226
230
  return true;
227
231
  }
@@ -11,7 +11,16 @@
11
11
  * (它是渲染判别,不是 lane 词)。
12
12
  */
13
13
  import type { SemaProvenance } from "@sema-agent/core";
14
+ /** wire 放行的 lane 词(PROJECTED)。 */
14
15
  export declare const SEMA_PROVENANCE_KINDS: readonly ["agent_message"];
16
+ /**
17
+ * 有意**不**放行的 lane 词(EXCLUDED,逐词记账;运行期落到「丢整键」臂 —— 与闭集外的未知词同臂,但这里的词是
18
+ * core 词表**内**的、经人裁定不上 wire 的):
19
+ * · `cross_session_message` —— core fc52b4d4 [ref] §4.1(session-box drain 铸;`from` 是对端**地址**、`peerMeta`
20
+ * 携 typed peer 记录)。[ref] 候:本仓不可达(`RunnerDeps.peerDirectory` 零声明点)+ 注入帧渲染形候 [ref] @server③
21
+ * 表态;与 core-keyset-guard.ts ① 的 `crossSessionMessage` / ①-nested 的 `peerMeta` 同批同判,接线时三处同翻。
22
+ */
23
+ export declare const SEMA_PROVENANCE_KINDS_EXCLUDED: readonly ["cross_session_message"];
15
24
  /** 本仓两条腿入参的**形**(不是 core 类型:入参是运行期载荷,`kind` 先当 string 收、经闭集才成 typed)。 */
16
25
  export interface SemaProvenanceInput {
17
26
  kind: string;
@@ -1,6 +1,9 @@
1
1
  export const SEMA_PROVENANCE_KINDS = ["agent_message"];
2
+ export const SEMA_PROVENANCE_KINDS_EXCLUDED = ["cross_session_message"];
2
3
  const _semaProvenanceExhaustive = true;
3
4
  void _semaProvenanceExhaustive;
5
+ const _semaProvenanceNoDoubleBook = true;
6
+ void _semaProvenanceNoDoubleBook;
4
7
  export function semaProvenanceOrAbsent(p) {
5
8
  if (!SEMA_PROVENANCE_KINDS.includes(p.kind))
6
9
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.57.0",
3
+ "version": "7.58.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,8 +54,8 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.2.0",
58
- "@sema-agent/settings-schema": "1.4.0",
57
+ "@sema-agent/core": "7.3.1",
58
+ "@sema-agent/settings-schema": "1.5.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
61
61
  "mysql2": "^3.22.4",