@sema-agent/server 7.20.0 → 7.22.0-rc.1

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 (73) hide show
  1. package/USAGE.md +31 -3
  2. package/dist/adoption/runner.js +4 -5
  3. package/dist/approval-card.d.ts +52 -0
  4. package/dist/approval-card.js +108 -0
  5. package/dist/boot/governance-seams.d.ts +112 -0
  6. package/dist/boot/governance-seams.js +151 -0
  7. package/dist/boot/leader.d.ts +3 -0
  8. package/dist/boot/leader.js +7 -1
  9. package/dist/boot/resolve-spec.d.ts +3 -0
  10. package/dist/boot/resolve-spec.js +10 -2
  11. package/dist/boot/runner-deps.d.ts +6 -2
  12. package/dist/boot/runner-deps.js +13 -0
  13. package/dist/boot/stores.d.ts +1 -0
  14. package/dist/boot/stores.js +24 -5
  15. package/dist/config-types.d.ts +50 -4
  16. package/dist/config.js +129 -3
  17. package/dist/fleet/fleet-bus.d.ts +23 -2
  18. package/dist/fleet/fleet-bus.js +41 -2
  19. package/dist/fleet/subagent-tail-bus.d.ts +59 -0
  20. package/dist/fleet/subagent-tail-bus.js +60 -0
  21. package/dist/http/routes/capabilities.js +27 -0
  22. package/dist/http/routes/diagnostics.js +6 -0
  23. package/dist/http/routes/runs.js +10 -1
  24. package/dist/http/routes/tasks.js +15 -4
  25. package/dist/http/server.d.ts +5 -0
  26. package/dist/http/server.js +130 -7
  27. package/dist/leader/wire.d.ts +7 -1
  28. package/dist/leader/wire.js +12 -5
  29. package/dist/main.js +33 -8
  30. package/dist/memory-posture.d.ts +80 -0
  31. package/dist/memory-posture.js +40 -0
  32. package/dist/observability/fail-open.d.ts +4 -0
  33. package/dist/observability/fail-open.js +4 -0
  34. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -1
  35. package/dist/parked-decide.js +9 -0
  36. package/dist/plugins/approval-ask-store-sql.d.ts +3 -1
  37. package/dist/plugins/approval-ask-store-sql.js +5 -6
  38. package/dist/plugins/background-agent-store-sql.js +4 -1
  39. package/dist/plugins/checkpoint-store-sql.d.ts +2 -0
  40. package/dist/plugins/checkpoint-store-sql.js +4 -5
  41. package/dist/plugins/file-run-store.d.ts +2 -1
  42. package/dist/plugins/image-bake-store-sql.js +5 -5
  43. package/dist/plugins/memory-embedder-fingerprint.d.ts +23 -0
  44. package/dist/plugins/memory-embedder-fingerprint.js +28 -1
  45. package/dist/plugins/memory-engine-tidb.js +3 -5
  46. package/dist/plugins/memory-engine-vector-util.d.ts +0 -5
  47. package/dist/plugins/memory-engine-vector-util.js +10 -4
  48. package/dist/plugins/memory-run-store.d.ts +2 -1
  49. package/dist/plugins/pg-session-storage.js +3 -3
  50. package/dist/plugins/run-store-sql.d.ts +12 -3
  51. package/dist/plugins/run-store-sql.js +4 -3
  52. package/dist/plugins/session-policy-store-sql.d.ts +1 -0
  53. package/dist/plugins/session-policy-store-sql.js +3 -3
  54. package/dist/plugins/shared-memory-store-sql.js +4 -10
  55. package/dist/plugins/sql-driver.d.ts +7 -5
  56. package/dist/plugins/sql-errors.d.ts +41 -0
  57. package/dist/plugins/sql-errors.js +25 -0
  58. package/dist/plugins/tidb-session-storage.js +3 -3
  59. package/dist/plugins/workflow-run-store-sql.js +4 -4
  60. package/dist/run-local.js +15 -0
  61. package/dist/runs.js +106 -38
  62. package/dist/task-mcp.d.ts +22 -1
  63. package/dist/task-mcp.js +71 -1
  64. package/dist/tool-approval.d.ts +35 -0
  65. package/dist/tool-approval.js +4 -1
  66. package/dist/trace/core-keyset-guard.d.ts +2 -2
  67. package/dist/trace/ledger-events.d.ts +43 -0
  68. package/dist/trace/ledger-events.js +2 -0
  69. package/dist/trace/ledger-sink.d.ts +21 -2
  70. package/dist/trace/ledger-sink.js +5 -1
  71. package/dist/trace/project.d.ts +4 -1
  72. package/dist/trace/project.js +15 -0
  73. package/package.json +2 -2
@@ -26,9 +26,9 @@ import { toIso } from "./sql-row-helpers.js";
26
26
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
27
27
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, // 两孪生店同源(理由见 kernel 处顶注)
28
28
  } from "../session-sync-kernel.js";
29
- function isDupKey(err) {
30
- return Boolean(err) && err.code === "23505"; // PG unique_violation
31
- }
29
+ import { isPgUniqueViolation } from "./sql-errors.js";
30
+ /** 判据属主 = `sql-errors.ts`(A-032 P1-①)。 */
31
+ const isDupKey = isPgUniqueViolation;
32
32
  /** node-pg parses jsonb → object already; only strings need JSON.parse, everything else passes through. */
33
33
  function parseEntry(p) {
34
34
  return (typeof p === "string" ? JSON.parse(p) : p);
@@ -14,7 +14,14 @@
14
14
  * ── Dialect deltas, kept EXPLICIT (never hidden behind an abstraction) ────────────────────────────────
15
15
  * - `?` placeholders vs `$n` (transaction control itself is normalized by `SqlDriver`/`SqlTxConn` —
16
16
  * mysql2's native begin/commit/rollback vs PG's statement form; see sql-driver.ts)
17
- * - dup-key `ER_DUP_ENTRY` vs SQLSTATE `23505` (createRun's task_active claim race)
17
+ * - dup-key classification (createRun's task_active claim race) — the PREDICATE has a single owner
18
+ * (`sql-errors.ts`, A-032 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` → "A-032 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.
18
25
  * - null-safe owner compare: `<=>` (TiDB) vs `IS NOT DISTINCT FROM` (PG) — a null-owner run still matches
19
26
  * `owner=null` on both, single-DB-fleet defense-in-depth (see {@link requestCancel})
20
27
  * - multi-table DELETE: `DELETE t FROM t JOIN j …` (TiDB) vs `DELETE FROM t USING j …` (PG)
@@ -36,6 +43,7 @@ import type { Pool as PgPool } from "pg";
36
43
  import { type UsageRow } from "../usage-analytics.js";
37
44
  import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
38
45
  import { type SqlDriver } from "./sql-driver.js";
46
+ import type { LedgerEventType } from "../trace/ledger-events.js";
39
47
  export type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
40
48
  /** Dual-dialect async run registry + event log. See the file header for the dialect-delta ledger. */
41
49
  export declare class SqlRunStore {
@@ -43,7 +51,8 @@ export declare class SqlRunStore {
43
51
  constructor(db: SqlDriver);
44
52
  /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
45
53
  private q;
46
- /** mysql2 names the dup-key `ER_DUP_ENTRY`; PG raises SQLSTATE `23505`. */
54
+ /** Dup-key classification single owner in `sql-errors.ts` (A-032 P1-①; the old local form read
55
+ * only mysql2's string `code`, so an error carrying just `errno` fell through the claim-race arm). */
47
56
  private isDupKey;
48
57
  /** JSON column binding: TiDB stores the string verbatim; PG goes through the protocol-safe envelope
49
58
  * (a raw NUL byte in an event payload would otherwise throw 22P05 mid-write). */
@@ -97,7 +106,7 @@ export declare class SqlRunStore {
97
106
  heartbeat(taskId: string, owner: string | null): Promise<void>;
98
107
  /** Append one event. A single INSERT — `updated_at` (liveness) is owned by `heartbeat`, so there
99
108
  * is no second statement to keep atomic here. */
100
- appendEvent(taskId: string, seq: number, type: string, data: unknown): Promise<void>;
109
+ appendEvent(taskId: string, seq: number, type: LedgerEventType, data: unknown): Promise<void>;
101
110
  /** Highest event seq for a task (0 if none). Lets a resumed leg CONTINUE the durable event log past the
102
111
  * suspend (core 1.70 `resumeStream`) — the suspend wrote events 1..K, the resume appends from K+1 without
103
112
  * colliding on the (task_id, seq) PK. */
@@ -3,6 +3,7 @@ import { escapeLike } from "./sql-escape.js";
3
3
  import { pgSanitizeText } from "./pg-safe-json.js";
4
4
  import { parseJsonLenient as parseJson, toIso as iso } from "./sql-row-helpers.js";
5
5
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
6
+ import { isDupKeyError } from "./sql-errors.js";
6
7
  /** Dual-dialect async run registry + event log. See the file header for the dialect-delta ledger. */
7
8
  export class SqlRunStore {
8
9
  db;
@@ -13,10 +14,10 @@ export class SqlRunStore {
13
14
  q(tidb, pg) {
14
15
  return this.db.dialect === "tidb" ? tidb : pg;
15
16
  }
16
- /** mysql2 names the dup-key `ER_DUP_ENTRY`; PG raises SQLSTATE `23505`. */
17
+ /** Dup-key classification single owner in `sql-errors.ts` (A-032 P1-①; the old local form read
18
+ * only mysql2's string `code`, so an error carrying just `errno` fell through the claim-race arm). */
17
19
  isDupKey(err) {
18
- const code = err?.code;
19
- return Boolean(err) && (this.db.dialect === "tidb" ? code === "ER_DUP_ENTRY" : code === "23505");
20
+ return isDupKeyError(this.db.dialect, err);
20
21
  }
21
22
  /** JSON column binding: TiDB stores the string verbatim; PG goes through the protocol-safe envelope
22
23
  * (a raw NUL byte in an event payload would otherwise throw 22P05 mid-write). */
@@ -14,6 +14,7 @@ export declare class SqlSessionPolicyStore implements SessionPolicyStore {
14
14
  constructor(db: SqlDriver);
15
15
  /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
16
16
  private q;
17
+ /** 判据属主 = `sql-errors.ts`(A-032 P1-①;本站点旧形已是并集形,收编后语义不变)。 */
17
18
  private isDupKey;
18
19
  /** Read the stored rules for (session, principal-owner); null when none (parity with InMemory). */
19
20
  getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
@@ -43,6 +43,7 @@ import { createHash } from "node:crypto";
43
43
  import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
44
44
  import { pgHasUnstorable } from "./pg-safe-json.js";
45
45
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
46
+ import { isDupKeyError } from "./sql-errors.js";
46
47
  /** `session_policy.policy_key` 的**唯一铸造点**(core 复合键的 sha256)。
47
48
  *
48
49
  * 🔴 导出而不是让收编腿再抄一份(clay 宪法「schema 单一属主禁复制」):design/183 form b 的身份重绑要
@@ -61,10 +62,9 @@ export class SqlSessionPolicyStore {
61
62
  q(tidb, pg) {
62
63
  return this.db.dialect === "tidb" ? tidb : pg;
63
64
  }
65
+ /** 判据属主 = `sql-errors.ts`(A-032 P1-①;本站点旧形已是并集形,收编后语义不变)。 */
64
66
  isDupKey(e) {
65
- return this.db.dialect === "tidb"
66
- ? e?.code === "ER_DUP_ENTRY" || e?.errno === 1062
67
- : e?.code === "23505";
67
+ return isDupKeyError(this.db.dialect, e);
68
68
  }
69
69
  /** Read the stored rules for (session, principal-owner); null when none (parity with InMemory). */
70
70
  async getRules(sessionId, principal) {
@@ -1,16 +1,10 @@
1
1
  import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError, } from "@sema-agent/core";
2
2
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
3
- /** 唯一键冲突的方言判别(approval-ask/checkpoint 同一对常量)——mysql2 `errno`,node-pg 挂 SQLSTATE。
3
+ import { isDupKeyError } from "./sql-errors.js";
4
+ /** 唯一键冲突的方言判别 —— 判据属主 = `sql-errors.ts`(A-032 P1-①:本站点原是全仓唯一的硬化形,
5
+ * 收编时把它**升格成属主**,其余六处按它对齐;`Reflect.get` 走原型链的理由见属主头注)。
4
6
  * 只用来把并发登记撞 PK 翻成一句人话,不让驱动错误对象漏给调用方。 */
5
- const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
6
- const PG_UNIQUE_VIOLATION = "23505";
7
- function isDupKey(dialect, err) {
8
- if (err === null || typeof err !== "object")
9
- return false;
10
- // Reflect.get (not a spread): driver error classes carry these on the PROTOTYPE chain in some
11
- // versions, and `{...err}` would silently miss them — a missed dup-key leaks the raw driver object.
12
- return dialect === "tidb" ? Number(Reflect.get(err, "errno")) === DUP_ENTRY : String(Reflect.get(err, "code")) === PG_UNIQUE_VIOLATION;
13
- }
7
+ const isDupKey = isDupKeyError;
14
8
  /** Per-org plane state (`connecting` / `unavailable` / `connected`). One row per org scope. */
15
9
  export const SHARED_MEMORY_SCOPE_TABLE = "shared_memory_scope";
16
10
  /** The model-facing store registry: one row per browsable library. */
@@ -16,9 +16,11 @@
16
16
  * `CAST(? AS JSON)` vs `$n::jsonb`, `INSERT IGNORE` vs `ON CONFLICT DO NOTHING`, `<=>` vs
17
17
  * `IS NOT DISTINCT FROM`, VARBINARY vs `COLLATE "C"` — every one of these stays written out at
18
18
  * the call site so a reader sees BOTH statements side by side (the real-DB suites are the oracle).
19
- * - error classification: `ER_DUP_ENTRY`/errno 1062 vs SQLSTATE `23505`, and WHICH unique key
20
- * collided (mysql2 `sqlMessage` text vs pg `err.constraint`) those live with the store that
21
- * needs them, because the discrimination rule is store-specific, not driver-generic.
19
+ * - WHICH unique key collided (mysql2 `sqlMessage` text vs pg `err.constraint`) that lives with
20
+ * the store that needs it, because the attribution rule is store-specific, not driver-generic.
21
+ * ⚠️ The dup-key predicate ITSELF is NOT store-specific and no longer lives at the call sites:
22
+ * `sql-errors.ts` owns it (A-032 P1-①; 7+ hand-rolled copies had drifted into three different
23
+ * recognition sets, so the same driver error got opposite verdicts in different stores).
22
24
  * - per-query timeouts: mysql2 takes `{sql, timeout}` while node-pg takes a `QueryConfig` with
23
25
  * `query_timeout`. The counter family (write-behind-counter.ts) needs those and keeps its own
24
26
  * exec; this seam stays on the plain (sql, params) form.
@@ -58,8 +60,8 @@
58
60
  * WHAT MUST STAY EXPLICIT (never parameterized away)
59
61
  * placeholders (?/$n) · upsert form (ON DUPLICATE vs ON CONFLICT) · JSON casts (CAST(? AS JSON) /
60
62
  * $n::jsonb) · null-safe compare (<=> vs IS NOT DISTINCT FROM) AND its placeholder-arity fallout ·
61
- * key-column byte semantics (VARBINARY vs COLLATE "C") · isolation/txn verbs · error classification
62
- * (ER_DUP_ENTRY/errno vs SQLSTATE, sqlMessage vs err.constraint) · PG-only protocol-byte handling
63
+ * key-column byte semantics (VARBINARY vs COLLATE "C") · isolation/txn verbs · dup-key ATTRIBUTION
64
+ * (sqlMessage vs err.constraint; the dup-key predicate itself = `sql-errors.ts`) · PG-only protocol-byte handling
63
65
  * (pgSafeJsonStringify / pgProtocolJsonStringify / pgSanitizeText / pgHasUnstorable) · DDL. Each arm's
64
66
  * BEHAVIOR (what pgSafeJsonStringify/pgProtocolJsonStringify actually do to the bytes) stays written out
65
67
  * in `pg-safe-json.ts`, untouched — `dialectJsonEncoder`/`dialectProtocolJsonEncoder` below (design/158
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SQL 驱动错误的**方言判别**——「这是不是唯一键冲突」的单一属主(A-032 P1-①,病族
3
+ * single-semantic-multi-site-drift)。`sql-escape.ts` / `sql-row-helpers.ts` / `store-contracts.ts`
4
+ * 的同级叶子:零 store 逻辑、零驱动依赖(不 import mysql2/pg),纯谓词。
5
+ *
6
+ * ## 为什么必须只有一份
7
+ * 收编前这条谓词在 src/ 手铸 ≥7 处,而且**识别集互相漂移**:一半站点只认 `code === "ER_DUP_ENTRY"`,
8
+ * 一半只认 `errno === 1062`,少数两者都认。同一个驱动错误因此在不同店里得到相反判决——漏判的代价
9
+ * 不是"少一句人话",而是 typed 契约错误(`checkpoint.already_exists` / `agent_record.already_exists` /
10
+ * `idempotency_conflict`)退化成**裸驱动错误对象**上抛,调用方按错误码分流的那条腿整条静默失效。
11
+ *
12
+ * ## 识别集 = 两个归因键的**并集**(刻意)
13
+ * mysql2 对同一条 `ER_DUP_ENTRY` 同时挂 `code`(errno 的字符串名)与 `errno`(数字),两者是**同一
14
+ * 件事的两种写法**,任一在场即可判定 —— 认并集不会造出假阳性,而认单键会漏掉任何只保留另一个键的
15
+ * 形(驱动版本差异、代理/包装层、序列化往返)。这正是收编前两个真漏判的成因,门钉在
16
+ * `test/sql-dup-key-single-source.test.ts`。
17
+ *
18
+ * ## 为什么是 `Reflect.get` 而不是属性访问/展开
19
+ * 驱动错误类在某些版本把这两个键挂在**原型链**上:`{...err}` 只拷自有可枚举键,会**静默**丢掉它们,
20
+ * 于是一次 dup 变成一个裸驱动对象漏给调用方。`Reflect.get` 与属性访问一样走原型链,写成 `Reflect.get`
21
+ * 是把"必须走原型链"这件事写在码面上(收编前唯一的硬化形 `shared-memory-store-sql.ts` 即此形)。
22
+ *
23
+ * ## 本模块**不**管的事(与 `sql-driver.ts` 头注的分工一致)
24
+ * 「撞的是**哪一个** UNIQUE 键」留在店内:mysql2 把键名写进 `sqlMessage` 文本、node-pg 写进
25
+ * `err.constraint`,而怎么按键名分诊(良性重发 vs 必须重试的竞态)是 store-specific 的判别规则,
26
+ * 不是驱动通用的。先例:`image-bake-store-sql.ts` 的 `dupKeyName`。
27
+ */
28
+ import type { SqlDialect } from "./sql-driver.js";
29
+ /** MySQL/TiDB `ER_DUP_ENTRY` 的数字 errno。 */
30
+ export declare const MYSQL_ER_DUP_ENTRY_ERRNO = 1062;
31
+ /** MySQL/TiDB `ER_DUP_ENTRY` 的字符串错误码(mysql2 的 `code`)。 */
32
+ export declare const MYSQL_ER_DUP_ENTRY_CODE = "ER_DUP_ENTRY";
33
+ /** PostgreSQL `unique_violation` 的 SQLSTATE。 */
34
+ export declare const PG_UNIQUE_VIOLATION_SQLSTATE = "23505";
35
+ /** MySQL/TiDB 的唯一键冲突:`code === "ER_DUP_ENTRY"` **或** `errno === 1062`(同一件事的两种写法)。 */
36
+ export declare function isMysqlDupKeyError(err: unknown): boolean;
37
+ /** PostgreSQL 的唯一键冲突:SQLSTATE `23505`(node-pg 挂在 `code` 上)。 */
38
+ export declare function isPgUniqueViolation(err: unknown): boolean;
39
+ /** 方言分派口——双方言店的调用形(`isDupKeyError(this.db.dialect, e)`)。 */
40
+ export declare function isDupKeyError(dialect: SqlDialect, err: unknown): boolean;
41
+ //# sourceMappingURL=sql-errors.d.ts.map
@@ -0,0 +1,25 @@
1
+ /** MySQL/TiDB `ER_DUP_ENTRY` 的数字 errno。 */
2
+ export const MYSQL_ER_DUP_ENTRY_ERRNO = 1062;
3
+ /** MySQL/TiDB `ER_DUP_ENTRY` 的字符串错误码(mysql2 的 `code`)。 */
4
+ export const MYSQL_ER_DUP_ENTRY_CODE = "ER_DUP_ENTRY";
5
+ /** PostgreSQL `unique_violation` 的 SQLSTATE。 */
6
+ export const PG_UNIQUE_VIOLATION_SQLSTATE = "23505";
7
+ /** 非对象(null / undefined / 字符串 / 数字)一律否——谓词自身绝不成为新的失败源。 */
8
+ function keyOf(err, key) {
9
+ if (err === null || typeof err !== "object")
10
+ return undefined;
11
+ return Reflect.get(err, key);
12
+ }
13
+ /** MySQL/TiDB 的唯一键冲突:`code === "ER_DUP_ENTRY"` **或** `errno === 1062`(同一件事的两种写法)。 */
14
+ export function isMysqlDupKeyError(err) {
15
+ return String(keyOf(err, "code")) === MYSQL_ER_DUP_ENTRY_CODE || Number(keyOf(err, "errno")) === MYSQL_ER_DUP_ENTRY_ERRNO;
16
+ }
17
+ /** PostgreSQL 的唯一键冲突:SQLSTATE `23505`(node-pg 挂在 `code` 上)。 */
18
+ export function isPgUniqueViolation(err) {
19
+ return String(keyOf(err, "code")) === PG_UNIQUE_VIOLATION_SQLSTATE;
20
+ }
21
+ /** 方言分派口——双方言店的调用形(`isDupKeyError(this.db.dialect, e)`)。 */
22
+ export function isDupKeyError(dialect, err) {
23
+ return dialect === "tidb" ? isMysqlDupKeyError(err) : isPgUniqueViolation(err);
24
+ }
25
+ //# sourceMappingURL=sql-errors.js.map
@@ -1,9 +1,9 @@
1
1
  import { emitLeafAdvance } from "../session-leaf-bus.js";
2
2
  import { BaseSessionStorage, SessionError, leafIdAfterEntry, normalizePromptEpoch, } from "@sema-agent/core";
3
3
  import { toIso } from "./sql-row-helpers.js";
4
- function isDupKey(err) {
5
- return Boolean(err) && err.code === "ER_DUP_ENTRY";
6
- }
4
+ import { isMysqlDupKeyError } from "./sql-errors.js";
5
+ /** 判据属主 = `sql-errors.ts`(A-032 P1-①;旧形只认 `code`,只带 errno 的驱动错误漏判)。 */
6
+ const isDupKey = isMysqlDupKeyError;
7
7
  /**
8
8
  * TiDB-backed {@link BaseSessionStorage} — the durable L1 event log for one session.
9
9
  *
@@ -1,6 +1,7 @@
1
1
  import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
2
2
  import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
3
3
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
+ import { isDupKeyError } from "./sql-errors.js";
4
5
  /** Row cap guard: a run blob beyond this is NOT stored on update (the prior revision stays) — TiDB's
5
6
  * ~6 MiB txn-entry limit would otherwise throw the (best-effort) progress write into the workflow loop.
6
7
  * `put` (create, small) still throws on a real failure. Mirrors the journal's oversize skip-degrade. */
@@ -49,10 +50,9 @@ export class SqlWorkflowRunStore {
49
50
  await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
50
51
  }
51
52
  catch (e) {
52
- // create-once error every backend throws (contract) — dup-key classification is dialect-specific:
53
- // TiDB's errno-string ER_DUP_ENTRY vs PG's SQLSTATE 23505.
54
- const isDupKey = this.db.dialect === "tidb" ? e.code === "ER_DUP_ENTRY" : e.code === "23505";
55
- if (isDupKey) {
53
+ // create-once error every backend throws (contract) — the dialect classification has a single
54
+ // owner (`sql-errors.ts`, A-032 P1-①); the old local form read only mysql2's string `code`.
55
+ if (isDupKeyError(this.db.dialect, e)) {
56
56
  throw new WorkflowRunStoreError("workflow_run.already_exists", "workflow run id already exists");
57
57
  }
58
58
  throw e;
package/dist/run-local.js CHANGED
@@ -72,6 +72,7 @@ import { memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js"
72
72
  import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
73
73
  import { buildPricing, cappedCeiling, createTracer } from "./budget.js";
74
74
  import { createSharedRunnerDeps } from "./boot/runner-deps.js";
75
+ import { createGovernanceSeams } from "./boot/governance-seams.js";
75
76
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
76
77
  import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
77
78
  import { createKeyResolver } from "./key-resolver.js";
@@ -580,8 +581,22 @@ export async function runLocal(argv, deps = {}) {
580
581
  * · `sharedMemoryStores`:**undefined** —— 供给面只有 SQL 形(`store-backend.ts` 明写:单机造 file 形
581
582
  * 等于给一个人的部署做「团队共享」,能力面会因此说谎)。
582
583
  */
584
+ // design/170 件B/C/D(#252):三个部署治理座席 —— 本地形与 server 形**同一只装配口**。为什么本地形也要
585
+ // 有:`COMPLIANCE_PROFILE` / `LOCKED_CONFIG_KEYS` / `RETENTION_MAX_AGE_DAYS` 是从同一份本地 `.env` 读出来
586
+ // 的,一个写下了档位的用户在 `sema` 与本机 HTTP 服务上必须得到同一个答案(两条腿对同一份配置两套行为
587
+ // 正是本仓在册的病族)。三键未配 = 三座席缺席 = 本腿逐字零变化。
588
+ const governanceSeams = createGovernanceSeams({
589
+ config,
590
+ logger,
591
+ retentionStores: [
592
+ { name: "sessionStore", store: sessionStore },
593
+ { name: "checkpointStore", store: fileBackend.checkpointStore },
594
+ { name: "toolResultStore", store: fileBackend.toolResultStore },
595
+ ],
596
+ });
583
597
  const sharedRunnerDeps = createSharedRunnerDeps({
584
598
  config,
599
+ governanceSeams,
585
600
  logger, // #240②:onNotice 席(共享基座键,本地形同席——结构化通告落同一 logger)
586
601
  // 交接件⑤:commit 尾注署名座进**共享基座** ⇒ 主 runner 与 subRunner 同源。
587
602
  // [931]① clay 拍:run-local = Sema 品牌本地形态,commit 尾注接 Sema 署名(core 1.300 缺省已翻转不署)。
package/dist/runs.js CHANGED
@@ -555,40 +555,6 @@ approval) {
555
555
  // Thread the owning principal so the cost tracer attributes this run's spend (incl. council/team
556
556
  // sub-tasks) to the principal's cumulative quota — mirrors the synchronous path.
557
557
  const runBody = async () => {
558
- if (verify !== undefined) {
559
- // Developer-mode verification gate (1.44): multi-round (impl → adversarial verify → fix loop), not
560
- // a stream — await the result and persist it terminal. The durable event log gets just the `done`
561
- // (the verifier subtask runs on its own session). `result.verification` carries the verdict.
562
- const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify);
563
- await flush();
564
- evictIfConflict(runner, spec.sessionId, result);
565
- // A nested suspend under the verify gate is mapped by core to status:"failed" carrying the capability
566
- // token — strip it before it touches the replayable log / run row (search [18] Q1b).
567
- await flushPromptManifest(); // [998]②: the verify leg has no stream — persist the manifest before the terminal
568
- const safe = await withModelUsage(stripCheckpointToken(result)); // E8: attach stats.modelUsage at terminal
569
- await append("done", { result: safe });
570
- reached = { kind: "done", result: safe };
571
- await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
572
- recordDone(safe, safe.status);
573
- return;
574
- }
575
- if (cascade !== undefined) {
576
- // Cascade (1.45): cheap→strong ladder; multi-attempt cold re-runs, not a stream. Persist terminal.
577
- const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade);
578
- await flush();
579
- evictIfConflict(runner, spec.sessionId, result);
580
- await flushPromptManifest(); // [998]②: same no-stream posture as the verify leg above
581
- const safe = await withModelUsage(stripCheckpointToken(result)); // see the verify branch; E8 modelUsage at terminal
582
- await append("done", { result: safe });
583
- reached = { kind: "done", result: safe };
584
- await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
585
- recordDone(safe, safe.status);
586
- return;
587
- }
588
- // design/47 (mid-task steering): hold the live stream handle so POST /v1/runs/:id/steer can inject a steer via
589
- // stream.steer() (drained at the next turn boundary). Only this plain-stream path is steerable — the verify/
590
- // cascade branches above return a result, not a stream (parity with the preemptable registration). Deregistered
591
- // in `finally`. Registering it AFTER the verify/cascade early-returns means those legs never enter the map.
592
558
  // core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress lives in the
593
559
  // child run's ISOLATED stream (core's only emitter is parentToolCallId-gated and pushes onto the CHILD queue;
594
560
  // the top stream NEVER carries it), so this sink is the ONLY path a tick reaches the top-run consumers. 1.78
@@ -596,21 +562,52 @@ approval) {
596
562
  // forget — `++seq` is a sync increment so sink-vs-loop appends get unique seqs), not just the fleet bus —
597
563
  // previously no fleet bus wired ⇒ ticks dropped entirely, and the stream-switch task_progress case below was
598
564
  // forward-compat then — [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。core swallow-guards a throwing sink.
565
+ // [3911] 定谳:per-agent tail 的**内容帧供给**由本腿 spec 的 forwardSubagentEvents 决定(core
566
+ // prepare-task 的 forward 门);缺席 ⇒ 那条流恒只有 task_progress。逐帧按**句柄**登记本腿的供给形,
567
+ // 让 tail 的 meta 帧如实声明(键为什么必须是句柄:SubagentTailBus.modeByHandle 顶注)。
568
+ //
569
+ // 🔴 **#253(core 5.33.0 #246 提货)起,这只 internals 在 verify/cascade 早返分支之前铸**:三条腿
570
+ // (verify 形 / cascade 形 / 普通流形)用**同一只**对象。5.32 及以前 core 的 `runWithVerification` /
571
+ // `runCascade` 签名里根本没有这一位,那两条腿因此对子代进度、活性打点、bg 终态通知**全盲**;5.33.0
572
+ // 开了尾可选 `internals` 座位并逐字转发给它驱动的每一条腿(实现腿/验证器腿/修复腿/每一级 rung),
573
+ // 于是这里只需要把既有供给链**接上去**,不新铸第二条链(黑盒判据见
574
+ // `test/issue190-status-wire-disclosure.test.ts` §4)。
575
+ // ⚠️ **`isolation` 一位永不传**:core 对 `internals.isolation` 在 verify/cascade 门上双相位响亮拒
576
+ // (`config.verify_internals_invalid` —— 这道门一次跑好几条腿,而 isolation 是 per-run 铸工作区),
577
+ // 本仓从不铸这一位,座位对象里也不许出现它。
578
+ const tailContentMode = spec.forwardSubagentEvents === true ? "on" : "progress_only";
599
579
  const fwdInternals = {
580
+ // #253:活性打点 —— 「只回结果」形没有事件循环,`recordTurnActivity` 此前只挂在 LedgerSink 的
581
+ // append 链上(那条链在 verify/cascade 腿上整段任务只响一次:终局 done)。core 的 onActivity 在
582
+ // 每次工具 start/end 同步回调,于是一条长跑的 verify 腿不再被 `GET /v1/runs/:id` 的
583
+ // `msSinceLastActivity` / 409 冲突体读成僵死。普通流形不经它(那条腿的打点在 sink 里,同一个函数,
584
+ // 重复打点无害但也无必要——本键对三腿一律在场,少一条判别分支)。
585
+ onActivity: () => recordTurnActivity(taskId),
600
586
  onForwardEvent: (e) => {
601
587
  fleetPublisher?.onForwardEvent(e);
602
588
  // C1 (core 1.219): a delegated child's tool lifecycle + task_progress ticks persist through the SHARED
603
589
  // sink (same whitelist+redact builders as the top stream — §E1, forwarded events are UNTRUSTED RAW;
604
590
  // fire-and-forget with sync seq allocation). Rationale + the deliberate per-chunk-delta omission live
605
591
  // on LedgerSink.onForwardEvent.
592
+ //
593
+ // 🔴 **别在这里加「腿结束就不落账」的围栏**(#253 施工期实测:加了当场打红 `runs-coverage` 的
594
+ // [1840]§三定性钉)。那条钉是**取证结论**,不是习惯:`run_in_background` 收养形子代的**整个**
595
+ // 生命周期都在父 run 终态之后,live events 流按设计 terminal break —— 帧照落账,消费端 done 后
596
+ // 重拉即全见。撕掉 sink = 对那一族子代**真丢帧**(cli [1839] 轮②「子代内层零转发」的病因分析里
597
+ // 写死了这一条)。codex 对抗复审 R2 [medium] 提的正是「终局后不该再落行」,**验真后驳回**:
598
+ // 本仓已有属主裁定 + 复现器 + 防回归钉,不能因为一次静态复审把它反过来。
606
599
  sink.onForwardEvent(e);
607
600
  // S2 live tail(core 1.370 bgAgentId,[1520]{core}):bg 子代帧按句柄分发给 per-agent 订阅者
608
601
  // (sync 腿 twin 在 http/server.ts)。hasSubscribers 快门=没人听不投影;投影一次 N 订阅者共享。
609
602
  const bg = e.bgAgentId;
610
- if (bg !== undefined && defaultSubagentTailBus.hasSubscribers(bg)) {
611
- const f = projectTailFrame(e);
612
- if (f)
613
- defaultSubagentTailBus.publish(bg, f);
603
+ if (bg !== undefined) {
604
+ // 供给形登记在 hasSubscribers 快门**之前**:没人在听时也要学,否则晚开的 tail 读不到。
605
+ defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
606
+ if (defaultSubagentTailBus.hasSubscribers(bg)) {
607
+ const f = projectTailFrame(e);
608
+ if (f)
609
+ defaultSubagentTailBus.publish(bg, f);
610
+ }
614
611
  }
615
612
  },
616
613
  // (durable half is the deployment's job): observe EVERY background
@@ -631,6 +628,9 @@ approval) {
631
628
  if (n.task_type === "workflow")
632
629
  return;
633
630
  // S2 复审 #3(sync 腿同注):中途终态信号。
631
+ // [3911] 终态即撤供给形登记:同一 a* 句柄可进入下一个**复活周期**,而新周期的宿主腿模式
632
+ // 可能相反——不撤会把上一周期的答案报给新周期的首帧前窗口(理由全文见 forgetHandleContentMode)。
633
+ defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
634
634
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
635
635
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
636
636
  }
@@ -678,6 +678,74 @@ approval) {
678
678
  }
679
679
  : {}),
680
680
  };
681
+ // 🔴 **#255(core 5.34.0 #253 / 判据帖 [3967] G1):status 第五席** —— 与上面四席**不同**,它只在
682
+ // 「只回结果」的两条腿上展开,**绝不**挂到 `runTaskStream` 那一位。理由是 core 的 `statusEmit` 是
683
+ // **双写**(`queue.push` 照旧 + 座位在场即喂**同一个** frozen 帧对象):普通流形的那条 queue 就是本
684
+ // 函数自己 `for await` 的流 —— 下面 loop 里 `sink.onEvent(ev)` 已落账、`metrics.inc` 已计数 ⇒ 两处
685
+ // 都写 = **同一帧两行账本 + 计数翻倍**。组合腿(verify/cascade)没有这条流(引擎把内部腿的 queue
686
+ // 自己排干丢弃),座位是它**唯一**的载体,这正是 5.34.0 开这一席的理由。
687
+ // (反向钉:`test/issue190-status-wire-disclosure.test.ts` §4⑤ —— 夹具照 core 的真姿势 dual-write,
688
+ // 普通流形上账本必须恰五行不是十行。)
689
+ //
690
+ // ⚠️ **帧是 frozen 的**:core 在双写前 `Object.freeze(frame)`,所以这条链上任何一环都不许写帧字段
691
+ // (改一下再往下传会当场 TypeError)。`brainStatusEventData` 是纯读投影,不改源帧。
692
+ // ⚠️ **同步回调不能 await**:失败留痕不断链(孪生姿势 = `onForwardEvent` 的 fire-and-forget 写)。
693
+ // ⚠️ 下面那个 `.catch` **买的不是进程安全**(别照抄别处的「逃逸 rejection 杀进程」说法):
694
+ // `sink.appendStatus` 返回的是**链步**promise,而 `LedgerSink.append` 内部已经给每一步挂了处理器
695
+ // (`chain = step.then(ok, err)`,「失败不断链」)⇒ 调用方 `void` 掉也不会有逃逸的 rejection
696
+ // (实测:摘掉这个 `.catch`,issue190 §4⑥/⑥b 仍绿)。它买的是**留痕** —— 披露行被静默丢掉时至少
697
+ // 有一行诊断可查;判别力由 §4⑥ 的「诊断行在场」断言看守。
698
+ const statusSeat = {
699
+ onStatusEvent: (e) => {
700
+ // S4 P0-a 计数腿:与普通流形 loop 的 `case "status"`、sync 腿、resume 腿同键同标签。
701
+ metrics?.inc("brain_retry_total", { phase: String(e.phase) });
702
+ // 账本腿:共享 LedgerSink 的 **`appendStatus` 同步入链门**(与 `onEvent` 的 `case "status"` 共用
703
+ // 同一个白名单 builder ⇒ 行的 type/data 与其余三腿逐字同形;判据 = issue190 §2/§3/§4 共用一份
704
+ // EXPECTED_ROW_DATA)。🔴 **别改回 `sink.onEvent(e)`**:那条路先 `await flush()` 才入链,同一拍
705
+ // 后到的 `onForwardEvent`(同步入链)会抢到更小的序号 ⇒ 账本时间线倒挂(codex 对抗复审 R2
706
+ // [medium],红先复现后修;反向钉 = issue190 §4⑤b)。
707
+ void sink.appendStatus(e).catch((err) => completionDiagLog?.("status_event_append_failed", { route: "bg-run-composite", taskId, err: err instanceof Error ? err.message : String(err) }));
708
+ },
709
+ };
710
+ if (verify !== undefined) {
711
+ // Developer-mode verification gate (1.44): multi-round (impl → adversarial verify → fix loop), not
712
+ // a stream — await the result and persist it terminal. The durable event log gets just the `done`
713
+ // (the verifier subtask runs on its own session). `result.verification` carries the verdict.
714
+ // #253(core 5.33.0 #246):第 4 位 = 供给座席,core 逐字转发给实现腿 + 验证器腿 + 每一轮修复腿。
715
+ // #255(core 5.34.0):`statusSeat` 只在这两条「只回结果」腿上展开(理由见其顶注)。
716
+ const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify, { ...fwdInternals, ...statusSeat });
717
+ await flush();
718
+ evictIfConflict(runner, spec.sessionId, result);
719
+ // A nested suspend under the verify gate is mapped by core to status:"failed" carrying the capability
720
+ // token — strip it before it touches the replayable log / run row (search [18] Q1b).
721
+ await flushPromptManifest(); // [998]②: the verify leg has no stream — persist the manifest before the terminal
722
+ const safe = await withModelUsage(stripCheckpointToken(result)); // E8: attach stats.modelUsage at terminal
723
+ await append("done", { result: safe });
724
+ reached = { kind: "done", result: safe };
725
+ await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
726
+ recordDone(safe, safe.status);
727
+ return;
728
+ }
729
+ if (cascade !== undefined) {
730
+ // Cascade (1.45): cheap→strong ladder; multi-attempt cold re-runs, not a stream. Persist terminal.
731
+ // #253(core 5.33.0 #246):第 4 位 = 供给座席,core 逐字转发给**每一级 rung**(顺序跑,一只 sink
732
+ // 按梯序拼接收到各级的帧 —— core 的 ATTRIBUTION 注写明 ToolActivity 自身不带 rung 身份)。
733
+ const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade, { ...fwdInternals, ...statusSeat });
734
+ await flush();
735
+ evictIfConflict(runner, spec.sessionId, result);
736
+ await flushPromptManifest(); // [998]②: same no-stream posture as the verify leg above
737
+ const safe = await withModelUsage(stripCheckpointToken(result)); // see the verify branch; E8 modelUsage at terminal
738
+ await append("done", { result: safe });
739
+ reached = { kind: "done", result: safe };
740
+ await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
741
+ recordDone(safe, safe.status);
742
+ return;
743
+ }
744
+ // design/47 (mid-task steering): hold the live stream handle so POST /v1/runs/:id/steer can inject a steer via
745
+ // stream.steer() (drained at the next turn boundary). Only this plain-stream path is steerable — the verify/
746
+ // cascade branches above return a result, not a stream (parity with the preemptable registration). Deregistered
747
+ // in `finally`. Registering it AFTER the verify/cascade early-returns means those legs never enter the map.
748
+ // (供给座席 `fwdInternals` 已在本函数顶部铸好并被三条腿共用 —— 见那里的顶注。)
681
749
  const stream = runner.runTaskStream({ ...spec, taskId, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, undefined, fwdInternals);
682
750
  steerableStreamRef = stream; // held for the finally's identity-guarded delete (1.78 review LOW)
683
751
  steerable?.set(taskId, stream);
@@ -22,7 +22,7 @@
22
22
  * `runtimeCapsResolver`) is a DEFERRED v2.
23
23
  * When the gate is closed, a sent `mcpServers` is IGNORED (advertised via `capabilities.mcpInjection`).
24
24
  */
25
- import type { McpServerSpec } from "@sema-agent/core";
25
+ import { type CompliancePosture, type LockedKey, type McpServerSpec } from "@sema-agent/core";
26
26
  /** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
27
27
  export declare const MAX_REQUEST_MCP_SERVERS = 32;
28
28
  /**
@@ -34,6 +34,8 @@ export declare const MAX_REQUEST_MCP_SERVERS = 32;
34
34
  */
35
35
  export declare function mcpInjectionHonored(config: {
36
36
  requirePrincipal?: boolean;
37
+ lockedConfigKeys?: readonly LockedKey[];
38
+ compliancePosture?: CompliancePosture;
37
39
  }): boolean;
38
40
  /**
39
41
  * Shape-check the raw `body.mcpServers`. `null` → a non-array (caller error; the route may 400). Otherwise the
@@ -43,6 +45,24 @@ export declare function validateRequestMcp(raw: unknown): {
43
45
  ok: McpServerSpec[];
44
46
  dropped: string[];
45
47
  } | null;
48
+ /**
49
+ * design/170 件C §4.3 —— **同步**拒面:部署锁了 `mcp` 而请求仍带 `mcpServers` ⇒ 400 `config.locked_key`。
50
+ *
51
+ * 为什么在这里也拒一次(与 core 的 prepare 门不是「两处裁决」):设计稿把裁决点定在 server 治理层,
52
+ * core 的 preflight 执行的是**同一条**判据的引擎侧终局形。二者语义等价、位置不同 —— 一个请求根本不该
53
+ * 进 core 才发现自己被锁,而消费端拿到一条同步 4xx 比拿到一个跑起来又终态失败的任务可诊断得多。
54
+ *
55
+ * 判据是「请求**占位**」而不是「请求的值合不合法」:锁是两态的,占了就整拒,不静默丢(静默丢正是
56
+ * design/170 §2.3 讲的那种把判决变成探针的形)。键缺席 / 显式 `null` 都不是占位。
57
+ *
58
+ * ⚠️ **不看车道**:多租户腿本来就会忽略 body MCP(`mcpInjectionHonored` 关),但锁在场时忽略是错的
59
+ * 失败方向 —— operator 声明了「本部署不收任务自带 MCP」,那就该说出来,而不是安静地当没看见。
60
+ *
61
+ * ⚠️ **resume 腿同拒,这是刻意的**(与本仓「4xx 会砖存量 task」的一般姿势有意分歧):锁上之后,一条存量
62
+ * 任务的 body 里带着 mcpServers 就再也过不了 core 的 preflight —— 放它进去只会换来一个跑起来又终态失败的
63
+ * 任务。同样是失败,同步 400 至少是**立刻可诊断**的那一种,而且拒得出理由(锁在哪、怎么改)。
64
+ */
65
+ export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: ReadonlySet<LockedKey>): void;
46
66
  /**
47
67
  * Merge GATED per-request MCP servers OVER the deployment baseline (center/config). The baseline WINS on a name
48
68
  * clash — a caller can ADD servers but can never SHADOW a configured one (the security baseline is authoritative,
@@ -60,6 +80,7 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
60
80
  provider?: string;
61
81
  };
62
82
  requirePrincipal?: boolean;
83
+ lockedConfigKeys?: readonly LockedKey[];
63
84
  }, logger?: {
64
85
  info?(m: string, meta?: unknown): void;
65
86
  warn(m: string, meta?: unknown): void;