@sema-agent/server 7.110.0 → 7.111.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 (56) hide show
  1. package/MIGRATION.md +8 -0
  2. package/dist/approval-ask-audit-store.d.ts +38 -25
  3. package/dist/approval-ask-audit-store.js +26 -59
  4. package/dist/bench/s1/run-firm.d.ts +11 -0
  5. package/dist/bench/s1/run-firm.js +47 -32
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/session-faces.d.ts +4 -0
  8. package/dist/boot/session-faces.js +3 -1
  9. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  10. package/dist/boot/stage-07-capability-layer.js +4 -3
  11. package/dist/boot/stage-08-reapers.d.ts +1 -0
  12. package/dist/boot/stage-09-leader.d.ts +1 -0
  13. package/dist/boot/stage-09-leader.js +2 -1
  14. package/dist/boot/stage-10-http-server.d.ts +1 -0
  15. package/dist/boot/workflow-orchestration.js +1 -1
  16. package/dist/http/resume-legs.js +6 -3
  17. package/dist/http/routes/runs.js +14 -1
  18. package/dist/http/routes/tasks.js +8 -6
  19. package/dist/http/server.d.ts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/instruction-selection.d.ts +34 -0
  22. package/dist/instruction-selection.js +23 -0
  23. package/dist/observability/corrupt-read-seat.d.ts +18 -0
  24. package/dist/observability/corrupt-read-seat.js +18 -0
  25. package/dist/observability/fail-open.d.ts +15 -3
  26. package/dist/observability/fail-open.js +15 -3
  27. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
  28. package/dist/orchestration/workflow-agent-session-index.js +11 -50
  29. package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
  30. package/dist/orchestration/workflow-notify-journal.js +20 -36
  31. package/dist/plugins/file-outcome-sink.d.ts +4 -1
  32. package/dist/plugins/file-outcome-sink.js +9 -3
  33. package/dist/plugins/file-run-store.js +3 -3
  34. package/dist/plugins/retention-store-sql.d.ts +1 -1
  35. package/dist/plugins/retention-store-sql.js +2 -0
  36. package/dist/plugins/store-backend.js +1 -0
  37. package/dist/plugins/store-contracts.d.ts +19 -2
  38. package/dist/plugins/store-contracts.js +9 -0
  39. package/dist/plugins/user-input-ledger-file.d.ts +4 -2
  40. package/dist/plugins/user-input-ledger-file.js +11 -1
  41. package/dist/plugins/user-input-ledger-sql.d.ts +14 -4
  42. package/dist/plugins/user-input-ledger-sql.js +46 -18
  43. package/dist/project-memory.d.ts +10 -2
  44. package/dist/project-memory.js +5 -2
  45. package/dist/runs.js +5 -3
  46. package/dist/trace/ledger-sink.d.ts +4 -0
  47. package/dist/trace/ledger-sink.js +1 -1
  48. package/dist/trace/project.d.ts +11 -3
  49. package/dist/trace/project.js +6 -5
  50. package/dist/trace/self-minted-wire.d.ts +26 -0
  51. package/dist/trace/self-minted-wire.js +9 -0
  52. package/dist/trace/wire-projection-faces.d.ts +3 -3
  53. package/dist/trace/wire-projection-faces.js +4 -2
  54. package/dist/user-input-ledger.d.ts +101 -48
  55. package/dist/user-input-ledger.js +52 -35
  56. package/package.json +2 -2
@@ -1,14 +1,20 @@
1
- import { mkdirSync, appendFileSync } from "node:fs";
2
1
  import { join } from "node:path";
2
+ import { AppendLog, ensureDir } from "@sema-agent/core";
3
3
  export class FileOutcomeSink {
4
4
  path;
5
+ log;
5
6
  constructor(root) {
6
7
  const dir = join(root, "outcomes");
7
- mkdirSync(dir, { recursive: true, mode: 0o700 });
8
+ ensureDir(dir);
8
9
  this.path = join(dir, "outcomes.jsonl");
9
10
  }
10
11
  async recordCore(o) {
11
- appendFileSync(this.path, `${JSON.stringify({ at: Date.now(), ...o })}\n`, { mode: 0o600 });
12
+ this.log ??= new AppendLog(this.path);
13
+ this.log.append({ at: Date.now(), ...o }, false);
14
+ }
15
+ dispose() {
16
+ this.log?.close();
17
+ this.log = undefined;
12
18
  }
13
19
  }
14
20
  //# sourceMappingURL=file-outcome-sink.js.map
@@ -7,7 +7,7 @@ import { randomBytes } from "node:crypto";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
- import { RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
10
+ import { RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError } from "./store-contracts.js";
11
11
  import { notifyRunTerminal } from "../observability/run-terminal.js";
12
12
  import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
13
13
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
@@ -603,7 +603,7 @@ export class FileRunStore {
603
603
  for (const r of orphans) {
604
604
  r.status = "failed";
605
605
  r.result = null;
606
- r.error = "run orphaned by engine restart";
606
+ r.error = RUN_ORPHANED_ERROR;
607
607
  r.updatedAt = new Date();
608
608
  this.persist(r);
609
609
  }
@@ -808,7 +808,7 @@ export class FileRunStore {
808
808
  pidStillRunning: owner !== undefined && isProcessLive({ pid: owner.pid }),
809
809
  note: "the claim's owner process is provably not the one that wrote it (fingerprint falsified, or the pid is gone) and the row has nothing left to resume — taking the claim over",
810
810
  });
811
- return { kind: "stale", reason: `run orphaned by engine restart (claim held by ${who}, which is gone)`, snapshot };
811
+ return { kind: "stale", reason: buildRunOrphanedError(who), snapshot };
812
812
  }
813
813
  takeOverClaim(sessionId, reason, snapshot) {
814
814
  const nowClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
@@ -8,7 +8,7 @@
8
8
  * core 把破坏性级联整个下沉成 store 方法(`expireCheckpoints` / `deleteExpiredSessions` /
9
9
  * `deleteOrphanToolResults`),并点名 server 的 session-purge 顺序契约(E21,`boot/session-faces.ts` 的
10
10
  * `purgeSession` 协调器)为参考实现。那条顺序契约横跨**十余张表**(run 账本 / checkpoint / tool_result /
11
- * anchor / exemption / policy / attachment / snapshot / workflow inbox / session_meta+event),而设计稿
11
+ * anchor / exemption / policy / attachment / 用户输入受理账本 / snapshot / workflow inbox / session_meta+event),而设计稿
12
12
  * §6 的 F5 又要求「审计行与删除同一个 DB 事务」—— 一个事务 ⇒ 一条连接 ⇒ **一个属主**。
13
13
  *
14
14
  * ⇒ 布局裁定:本能力**不能**挂在 session / checkpoint / tool-result 任何一只店上(每只店只拥有自己的表,
@@ -1,5 +1,6 @@
1
1
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
2
  import { TASK_LIST_ITEM_TABLE, TASK_LIST_META_TABLE, taskListKeyFor } from "./task-list-store-sql.js";
3
+ import { USER_INPUT_ACCEPTANCE_TABLE } from "./user-input-ledger-sql.js";
3
4
  import { ensureIndexes } from "./ensure-index.js";
4
5
  export const MANAGED_RETENTION = "managed";
5
6
  export const UNMANAGED_RETENTION = "none";
@@ -382,6 +383,7 @@ export class SqlRetentionStore {
382
383
  ["session_policy", "session_id"],
383
384
  ["session_policy_rev_mark", "session_id"],
384
385
  ...(this.opts.taskAttachmentTable ? [["task_attachment", "session_id"]] : []),
386
+ [USER_INPUT_ACCEPTANCE_TABLE, "session_id"],
385
387
  ["file_history_tracked", "scope"],
386
388
  ["file_history_version", "scope"],
387
389
  ["file_history_boundary", "scope"],
@@ -277,6 +277,7 @@ class LocalBackend {
277
277
  this.approvalAskStore.dispose();
278
278
  this.sendFileLedgerStore.dispose();
279
279
  this.userInputLedgerStore.dispose();
280
+ this.outcomeSinkInst.dispose();
280
281
  this.workflowJournalStore.dispose();
281
282
  this.checkpointStoreInst?.close();
282
283
  this.permissionRuleStoresInst?.dispose();
@@ -106,11 +106,28 @@ export type PersistedTaskResult = TaskResult & {
106
106
  };
107
107
  /**
108
108
  * 🔴 run 店「持有它的实例丢了」那一判的**行上落款**(S-928 codex r1-F1 提为常量):三只 run 店的 `reapStale` 把心跳过期的 running 行翻成
109
- * `failed` 并写下这一句。它是 run 店死亡判决留在行上的唯一证据 —— 用户输入受理账本的 SQL 收敛臂据它在 reap **之后**仍能判出「写者死了」
110
- * (`user-input-ledger.ts` 的判词)。改这一句 = 改那条判据;所以三个写者与那一个读者都只引用本常量。读时折叠成同一句话的 wire 展示站点
109
+ * `failed` 并写下这一句。它是 run 店死亡判决留在行上的证据之一(另一只 = {@link RUN_ORPHANED_ERROR})—— 用户输入受理账本的收敛判词
110
+ * (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」。改这一句 = 改那条判据;所以三个写者与那一个读者都只引用本常量。读时折叠成同一句话的 wire 展示站点
111
111
  * (`routes/runs.ts` 的 stale 折叠、`routes/trace-usage.ts` 的 WORKER_DOWN 帧)刻意留字面量:它们由文案冻结门逐字钉着,且不参与任何判据。
112
112
  */
113
113
  export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
114
+ /**
115
+ * 🔴 File 车道「持有它的进程随重启没了」那一判的行上落款(S-969 L8 提为常量):boot 孤儿回收(`FileRunStore.reclaimOrphanedAtBoot`,
116
+ * 数据根 LOCK 作证前世进程已死 ⇒ 仍 running 的行无条件判孤儿)与运行期 claim 接管的 running 臂(`judgeClaim`,外来 + owner 已死 + 无可续)
117
+ * 都写这一句;接管臂在括号里附上前任持有者的进程指纹(取证用,形 = {@link buildRunOrphanedError})。与 {@link RUN_STALLED_ERROR} 同一种身份:
118
+ * run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}。
119
+ */
120
+ export declare const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
121
+ /** File 车道两个孤儿落款写口共用的那一句(`heldBy` = 接管臂的前任持有者描述;boot 回收臂不带)。 */
122
+ export declare function buildRunOrphanedError(heldBy?: string): string;
123
+ /**
124
+ * run 行上是不是一份**持有者死亡判决**:`failed`,且 `error` 是 run 店的两只死亡落款之一 —— {@link RUN_STALLED_ERROR}(三只 run 店的
125
+ * `reapStale`:心跳过期 ⇒ 实例丢了)或 {@link RUN_ORPHANED_ERROR}(File 车道:前世进程随重启没了;接管臂带括注)。判决只说「持有它的那个进程
126
+ * 死在这条腿收场之前」,不说是谁 —— 持有者是谁读行上的 `instanceId`(两只落款都不改它)。唯一读者 = 用户输入受理账本的收敛判词
127
+ * (`src/user-input-ledger.ts` `judgeOpen`);改任一句落款 = 改那条判据,所以写者与读者都只引用本模块的常量。
128
+ * 正常失败(别的 error)/ park 过期收割(`approval expired before decision`:腿 park 过)都**不是**死亡判决。
129
+ */
130
+ export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "error">): boolean;
114
131
  export interface RunRecord {
115
132
  taskId: string;
116
133
  sessionId: string;
@@ -1,6 +1,15 @@
1
1
  export class StoreBootRefusalError extends Error {
2
2
  }
3
3
  export const RUN_STALLED_ERROR = "run stalled (instance lost?)";
4
+ export const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
5
+ export function buildRunOrphanedError(heldBy) {
6
+ return heldBy === undefined ? RUN_ORPHANED_ERROR : `${RUN_ORPHANED_ERROR} (claim held by ${heldBy}, which is gone)`;
7
+ }
8
+ export function isHolderDeathVerdict(run) {
9
+ if (run.status !== "failed" || run.error === null)
10
+ return false;
11
+ return run.error === RUN_STALLED_ERROR || run.error === RUN_ORPHANED_ERROR || run.error.startsWith(`${RUN_ORPHANED_ERROR} (`);
12
+ }
4
13
  export const RUN_STATUS_CLASS = {
5
14
  running: "live",
6
15
  completed: "succeeded",
@@ -1,5 +1,5 @@
1
1
  import type { Logger } from "../observability/logger.js";
2
- import { UserInputLedgerFold, type NewUserInputAcceptance, type OpenScanCursor, type SettleHow, type UserInputAcceptance, type UserInputLedgerRecord, type UserInputLedgerStore } from "../user-input-ledger.js";
2
+ import { UserInputLedgerFold, type NewUserInputAcceptance, type OpenScanCursor, type OpenScanPage, type SettleHow, type UserInputAcceptance, type UserInputLedgerRecord, type UserInputLedgerStore } from "../user-input-ledger.js";
3
3
  /** 账本文件名(取证按文件字节对账 —— 崩溃语义的真源是文件)。 */
4
4
  export declare const USER_INPUT_LEDGER_FILE = "ledger.jsonl";
5
5
  /** 纯内存孪生(测试 / 夹具)。 */
@@ -13,9 +13,11 @@ export declare class MemoryUserInputLedgerStore implements UserInputLedgerStore
13
13
  settle(acceptId: string, how: SettleHow, atMs: number): Promise<boolean>;
14
14
  converge(acceptId: string, atMs: number): Promise<boolean>;
15
15
  announce(acceptId: string, taskId: string, atMs: number): Promise<boolean>;
16
- listOpen(after?: OpenScanCursor, limit?: number): Promise<UserInputAcceptance[]>;
16
+ listOpen(after?: OpenScanCursor, limit?: number): Promise<OpenScanPage>;
17
17
  listCrashed(sessionId: string): Promise<UserInputAcceptance[]>;
18
18
  listUnannounced(sessionId: string, limit: number): Promise<UserInputAcceptance[]>;
19
+ /** S-965:会话树删除 = 一行会话级墓碑(守卫:该会话此刻有行 ⇒ 落;否则零写、回 0)。写抛 ⇒ 原样抛(协调器据此不删会话本体)。 */
20
+ deleteBySession(sessionId: string): Promise<number>;
19
21
  }
20
22
  /** local 车道(File)孪生。 */
21
23
  export declare class FileUserInputLedgerStore extends MemoryUserInputLedgerStore {
@@ -39,6 +39,10 @@ export class MemoryUserInputLedgerStore {
39
39
  async listUnannounced(sessionId, limit) {
40
40
  return this.fold.listUnannounced(sessionId, limit);
41
41
  }
42
+ async deleteBySession(sessionId) {
43
+ const n = this.fold.sessionSize(sessionId);
44
+ return (await this.write({ kind: "erased", sessionId, at: Date.now() })) ? n : 0;
45
+ }
42
46
  }
43
47
  const RECORD_SHAPE = {
44
48
  what: "user-input ledger record of an unknown kind or with a missing / out-of-vocabulary member",
@@ -46,8 +50,14 @@ const RECORD_SHAPE = {
46
50
  const o = record;
47
51
  const str = (k) => (typeof o[k] === "string" && o[k].length > 0 ? o[k] : undefined);
48
52
  const at = typeof o.at === "number" && Number.isFinite(o.at) ? o.at : undefined;
53
+ if (at === undefined)
54
+ return undefined;
55
+ if (o.kind === "erased") {
56
+ const sessionId = str("sessionId");
57
+ return sessionId === undefined ? undefined : { kind: "erased", sessionId, at };
58
+ }
49
59
  const acceptId = str("acceptId");
50
- if (acceptId === undefined || at === undefined)
60
+ if (acceptId === undefined)
51
61
  return undefined;
52
62
  switch (o.kind) {
53
63
  case "accepted": {
@@ -13,13 +13,22 @@
13
13
  * 无 owner / principal / scope 列(行随它挂靠的会话走 —— 收编形Ⅲ);`writer` 是受理进程的 `instanceId`(随进程消亡,不是租户轴)。
14
14
  * 行里不存输入正文(只有身份与时刻)。
15
15
  *
16
- * ## 不在本表上的东西
17
- * 会话删除 / 托管留存的级联**未接**(行至多是几条身份 + 时刻;S-928 收车档登记的欠账)。
16
+ * ## 会话树删除(S-965)
17
+ * `deleteBySession` = `DELETE … WHERE session_id = ?`(真删行;File 孪生是一行会话级墓碑,同义)。两条删会话树的级联各有一臂:在线删除协调器
18
+ * 经本动词;托管留存的事务化孪生(`retention-store-sql.ts` `purgeSessionTrees` ④)在**自己的事务**里对本表发同一只谓词(`session_id` 单键,
19
+ * 多值集合按方言)—— 它不调店,理由见该文件顶注的布局裁定(一个事务 ⇒ 一条连接 ⇒ 一个属主)。谓词走 `(session_id, state, accepted_at_ms)` 索引的前缀。
20
+ *
21
+ * ## 读不出的行(S-969 L6):逐行跳过,游标按原始行推进
22
+ * 三只读口都按 `state` 列取行(`= 'open'` / `= 'converged'`),所以 `state` 词表外的行任何读口都取不到;被取到的行上读不出的列 —— `input_kind`
23
+ * 词表外(新版本写的输入族:滚动发布 / 回滚的混版窗)、收敛行缺 `state_at_ms` —— 由 {@link readRows} 逐行跳过 + 计 F 类 tag
24
+ * `server.user-input-ledger.corrupt-record-skipped`(File 孪生重放坏行的同一只登记座),其余行照交。修前一行读不出 ⇒ 整页抛:收敛断点不前进
25
+ * (其后的 open 行永不收敛)、同会话的通告与读面一并失效。`listOpen` 的续翻游标取**原始**末行的键(`accept_id` / `accepted_at_ms` 是 NOT NULL
26
+ * 键列,坏行也读得出),跳过的行照样推进;页里少了几行不等于扫到底(那由原始行数判)。SQL 文本 / DDL 不变。
18
27
  */
19
28
  import type { Pool as MysqlPool } from "mysql2/promise";
20
29
  import type { Pool as PgPool } from "pg";
21
30
  import { type SqlDriver } from "./sql-driver.js";
22
- import { type NewUserInputAcceptance, type OpenScanCursor, type SettleHow, type UserInputAcceptance, type UserInputLedgerStore } from "../user-input-ledger.js";
31
+ import { type NewUserInputAcceptance, type OpenScanCursor, type OpenScanPage, type SettleHow, type UserInputAcceptance, type UserInputLedgerStore } from "../user-input-ledger.js";
23
32
  export declare const USER_INPUT_ACCEPTANCE_TABLE = "user_input_acceptance";
24
33
  export declare class SqlUserInputLedgerStore implements UserInputLedgerStore {
25
34
  private readonly db;
@@ -29,9 +38,10 @@ export declare class SqlUserInputLedgerStore implements UserInputLedgerStore {
29
38
  settle(acceptId: string, how: SettleHow, atMs: number): Promise<boolean>;
30
39
  converge(acceptId: string, atMs: number): Promise<boolean>;
31
40
  announce(acceptId: string, taskId: string, atMs: number): Promise<boolean>;
32
- listOpen(after?: OpenScanCursor, limit?: number): Promise<UserInputAcceptance[]>;
41
+ listOpen(after?: OpenScanCursor, limit?: number): Promise<OpenScanPage>;
33
42
  listCrashed(sessionId: string): Promise<UserInputAcceptance[]>;
34
43
  listUnannounced(sessionId: string, limit: number): Promise<UserInputAcceptance[]>;
44
+ deleteBySession(sessionId: string): Promise<number>;
35
45
  }
36
46
  /** MySQL 协议腿的构造绑定。 */
37
47
  export declare class TiDBUserInputLedgerStore extends SqlUserInputLedgerStore {
@@ -1,3 +1,4 @@
1
+ import { recordFailOpen } from "../observability/fail-open.js";
1
2
  import { isDupKeyError } from "./sql-errors.js";
2
3
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
3
4
  import { assertPageLimit, CRASHED_CAUSE, SETTLE_HOW_WORDS, USER_INPUT_KIND_WORDS, USER_INPUT_OPEN_SCAN_MAX, USER_INPUT_SESSION_READ_MAX, } from "../user-input-ledger.js";
@@ -12,35 +13,57 @@ function rowState(r) {
12
13
  case "settled": {
13
14
  const how = String(r["settle_how"]);
14
15
  if (!Object.hasOwn(SETTLE_HOW_WORDS, how) || !Number.isFinite(at))
15
- break;
16
+ return undefined;
16
17
  return { kind: "settled", how: how, atMs: at };
17
18
  }
18
19
  case "converged":
19
20
  if (!Number.isFinite(at))
20
- break;
21
+ return undefined;
21
22
  return { kind: "converged", cause: CRASHED_CAUSE, atMs: at };
22
23
  default:
23
- break;
24
+ return undefined;
24
25
  }
25
- throw new Error(`${USER_INPUT_ACCEPTANCE_TABLE} row ${String(r["accept_id"])}: unreadable state (${String(r["state"])} / ${String(r["settle_how"])} / ${String(r["state_at_ms"])})`);
26
26
  }
27
- function rowToAcceptance(r) {
27
+ function readRow(r) {
28
28
  const input = String(r["input_kind"]);
29
29
  if (!Object.hasOwn(USER_INPUT_KIND_WORDS, input))
30
- throw new Error(`${USER_INPUT_ACCEPTANCE_TABLE} row ${String(r["accept_id"])}: input_kind ${input} is not a ledger word`);
30
+ return { ok: false, reason: `input_kind ${input} is not a ledger word` };
31
+ const state = rowState(r);
32
+ if (state === undefined)
33
+ return { ok: false, reason: `unreadable state (${String(r["state"])} / ${String(r["settle_how"])} / ${String(r["state_at_ms"])})` };
31
34
  const announcedAt = r["announced_at_ms"] == null ? undefined : Number(r["announced_at_ms"]);
32
35
  return {
33
- acceptId: String(r["accept_id"]),
34
- input: input,
35
- messageId: String(r["message_id"]),
36
- sessionId: String(r["session_id"]),
37
- taskId: String(r["task_id"]),
38
- writer: String(r["writer"]),
39
- acceptedAtMs: Number(r["accepted_at_ms"]),
40
- state: rowState(r),
41
- ...(announcedAt !== undefined ? { announced: { atMs: announcedAt, taskId: String(r["announced_task_id"]) } } : {}),
36
+ ok: true,
37
+ acceptance: {
38
+ acceptId: String(r["accept_id"]),
39
+ input: input,
40
+ messageId: String(r["message_id"]),
41
+ sessionId: String(r["session_id"]),
42
+ taskId: String(r["task_id"]),
43
+ writer: String(r["writer"]),
44
+ acceptedAtMs: Number(r["accepted_at_ms"]),
45
+ state,
46
+ ...(announcedAt !== undefined ? { announced: { atMs: announcedAt, taskId: String(r["announced_task_id"]) } } : {}),
47
+ },
42
48
  };
43
49
  }
50
+ function readRows(rows, op) {
51
+ const out = [];
52
+ for (const r of rows) {
53
+ const read = readRow(r);
54
+ if (read.ok)
55
+ out.push(read.acceptance);
56
+ else
57
+ recordFailOpen("server.user-input-ledger.corrupt-record-skipped", `table=${USER_INPUT_ACCEPTANCE_TABLE} op=${op} accept_id=${String(r["accept_id"])} ${read.reason}`);
58
+ }
59
+ return out;
60
+ }
61
+ function rawCursor(r) {
62
+ const acceptedAtMs = Number(r["accepted_at_ms"]);
63
+ if (!Number.isFinite(acceptedAtMs))
64
+ throw new Error(`${USER_INPUT_ACCEPTANCE_TABLE} row ${String(r["accept_id"])}: accepted_at_ms ${String(r["accepted_at_ms"])} is not a number — cannot page past it`);
65
+ return { acceptedAtMs, acceptId: String(r["accept_id"]) };
66
+ }
44
67
  export class SqlUserInputLedgerStore {
45
68
  db;
46
69
  constructor(db) {
@@ -76,16 +99,21 @@ export class SqlUserInputLedgerStore {
76
99
  const { rows } = after === undefined
77
100
  ? await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`), [])
78
101
  : await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' AND (accepted_at_ms > ? OR (accepted_at_ms = ? AND accept_id > ?)) ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' AND (accepted_at_ms > $1 OR (accepted_at_ms = $2 AND accept_id > $3)) ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`), [after.acceptedAtMs, after.acceptedAtMs, after.acceptId]);
79
- return rows.map(rowToAcceptance);
102
+ const open = readRows(rows, "listOpen");
103
+ return rows.length === limit ? { open, next: rawCursor(rows[rows.length - 1]) } : { open };
80
104
  }
81
105
  async listCrashed(sessionId) {
82
106
  const { rows } = await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ? AND state = 'converged' ORDER BY accepted_at_ms DESC, accept_id DESC LIMIT ${USER_INPUT_SESSION_READ_MAX}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1 AND state = 'converged' ORDER BY accepted_at_ms DESC, accept_id DESC LIMIT ${USER_INPUT_SESSION_READ_MAX}`), [sessionId]);
83
- return rows.map(rowToAcceptance).reverse();
107
+ return readRows(rows, "listCrashed").reverse();
84
108
  }
85
109
  async listUnannounced(sessionId, limit) {
86
110
  const n = assertPageLimit("listUnannounced", limit);
87
111
  const { rows } = await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ? AND state = 'converged' AND announced_at_ms IS NULL ORDER BY accepted_at_ms, accept_id LIMIT ${n}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1 AND state = 'converged' AND announced_at_ms IS NULL ORDER BY accepted_at_ms, accept_id LIMIT ${n}`), [sessionId]);
88
- return rows.map(rowToAcceptance);
112
+ return readRows(rows, "listUnannounced");
113
+ }
114
+ async deleteBySession(sessionId) {
115
+ const res = await this.db.query(this.q(`DELETE FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ?`, `DELETE FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1`), [sessionId]);
116
+ return res.affected;
89
117
  }
90
118
  }
91
119
  export class TiDBUserInputLedgerStore extends SqlUserInputLedgerStore {
@@ -25,7 +25,8 @@ interface ProjectMemoryCtx {
25
25
  * every CLAUDE file it finds, per directory level up the ancestor chain, whereas this loader reads one root and
26
26
  * takes the single first hit. */
27
27
  declare const INSTRUCTION_FILE_CHAIN: readonly ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
28
- type ChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
28
+ /** 一个链成员(相对项目根的拼写)。导出给 S-961 的 wire 段与它的值闭集登记(`trace/self-minted-wire.ts`)—— 链长一员,两处 tsc 跟着红。 */
29
+ export type InstructionChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
29
30
  /** Recover (root, rel) for a path the LOADER declared — every declared path is `root + one chain member`.
30
31
  * The probe needs this to re-run the loader's parent-directory symlink gate each round (self-review: the probe
31
32
  * checked only the final component, so swapping `.claude` for a symlink out of the repo MID-RUN made the probe
@@ -33,7 +34,7 @@ type ChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
33
34
  * calls it with the host `node:path`. */
34
35
  export declare function instructionChainCoordinates(absPath: string, p?: PlatformPath): {
35
36
  root: string;
36
- rel: ChainMember;
37
+ rel: InstructionChainMember;
37
38
  } | null;
38
39
  /**
39
40
  * Build a `loadProjectMemory` for the host lane. Returns null (skip) unless hands are enabled AND cwd is a local
@@ -54,6 +55,13 @@ export declare function instructionChainCoordinates(absPath: string, p?: Platfor
54
55
  * an absolute local directory) is "not applicable", not "none" — it never reaches the mint (O6, see `LoadOutcome`). The member is the one the snapshot carries — a cache hit answers the cached pick, so
55
56
  * the notice follows what the model actually reads, not the disk (a same-process edit changes nothing until the
56
57
  * snapshot is re-read: restart, post-compact, or a new session).
58
+ *
59
+ * S-961 — the same statement (same predicate, same member) also settles the calling LEG's `wiring_manifest.instructions`
60
+ * segment through {@link settleLegInstructionStatement} (`instruction-selection.ts`): the leg scope is the run identity
61
+ * the ctx does not carry, and the first call inside a leg's scope whose `ctx.sessionId` equals the leg's session is that
62
+ * leg's own prepare (a session-less adhoc leg falls back to plain first-call; [ref] X1). The segment projects
63
+ * `instructionFile`, not `instructionSources`: an empty selected file with no git narrative returns no answer and so no
64
+ * `instructionSources`, yet it IS the pick (it shadows every member below it) — and the notice reads the same value.
57
65
  */
58
66
  export declare function makeLoadProjectMemory(opts: {
59
67
  logger?: {
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
3
3
  import { createHmac, randomBytes } from "node:crypto";
4
4
  import hostPath, { isAbsolute, join } from "node:path";
5
5
  import { announceInstructionSource } from "./instruction-source-notice.js";
6
+ import { settleLegInstructionStatement } from "./instruction-selection.js";
6
7
  import { gitSubprocessEnv } from "./server-secret-env.js";
7
8
  import { redactSecrets } from "./trace/redact.js";
8
9
  const INSTRUCTION_FILE_CHAIN = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
@@ -92,8 +93,10 @@ export function makeLoadProjectMemory(opts) {
92
93
  const seat = opts.instructionSource;
93
94
  return async (ctx) => {
94
95
  const outcome = await loadOutcome(ctx);
95
- if (seat && ctx.sessionId && !ctx.isSubagent && outcome.walked)
96
- await announceInstructionSource(seat, ctx.sessionId, outcome.instructionFile);
96
+ const statement = outcome.walked && !ctx.isSubagent ? { stated: true, file: outcome.instructionFile } : { stated: false };
97
+ settleLegInstructionStatement(statement, ctx.sessionId);
98
+ if (seat && ctx.sessionId && statement.stated)
99
+ await announceInstructionSource(seat, ctx.sessionId, statement.file);
97
100
  return outcome.answer;
98
101
  };
99
102
  async function loadOutcome(ctx) {
package/dist/runs.js CHANGED
@@ -6,6 +6,7 @@ import { isSessionErrorCode, sessionErrorHttp } from "./session-error-wire.js";
6
6
  import { redactErrorMessage } from "./observability/run-terminal.js";
7
7
  import { withPrincipal } from "./observability/principal-context.js";
8
8
  import { withModelReachScope } from "./model-reach-scope.js";
9
+ import { createInstructionSelectionLeg } from "./instruction-selection.js";
9
10
  import { redactSecrets } from "./trace/redact.js";
10
11
  import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
11
12
  import { createLedgerSink } from "./trace/ledger-sink.js";
@@ -247,7 +248,8 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
247
248
  metrics?.addGauge("runs_active", -1);
248
249
  metrics?.inc("tasks_total", { status: "needs_review" });
249
250
  };
250
- const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData) });
251
+ const instructionLeg = createInstructionSelectionLeg();
252
+ const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData), instructionsSegment: instructionLeg.segment });
251
253
  const append = (type, data) => sink.append(type, data);
252
254
  const flush = () => sink.flush();
253
255
  const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
@@ -422,7 +424,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
422
424
  await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
423
425
  }
424
426
  };
425
- await withModelReachScope(spec.sessionId, () => withPrincipal(principal, () => {
427
+ await instructionLeg.run(spec.sessionId, () => withModelReachScope(spec.sessionId, () => withPrincipal(principal, () => {
426
428
  const withElicit = elicitation
427
429
  ? () => elicitation.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: elicitEmit, abortSignal: cancelCtrl.signal }, runBody)
428
430
  : runBody;
@@ -445,7 +447,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
445
447
  return sendUserFile
446
448
  ? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } }, withApproval)
447
449
  : withApproval();
448
- }));
450
+ })));
449
451
  if (!reached && cancelCtrl.signal.aborted) {
450
452
  try {
451
453
  await flush();
@@ -2,6 +2,7 @@ import { type TerminalCause } from "@sema-agent/core";
2
2
  import { brainStatusEventData } from "./project.js";
3
3
  import { NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
4
4
  import { type LedgerEventType, type ParkLedgerEventType } from "./ledger-events.js";
5
+ import type { InstructionsSegment } from "../instruction-selection.js";
5
6
  /**
6
7
  * LedgerSink — the SINGLE "stream event → durable runs-ledger" projection, shared by the background leg
7
8
  * (runs.ts `runInBackground`) and the sync `/v1/tasks/stream` detach lane (server.ts, `x-detach-on-disconnect`).
@@ -115,5 +116,8 @@ export declare function createLedgerSink(opts: {
115
116
  * 各传自己看得见的那个词(位①=流事件 `ev.type`,位②=账本行 `type`);拿不到类型的打点方
116
117
  * (core 的 onActivity 第五席、resume 腿手动打点)省参,`kind` 诚实缺席(见 turn-activity.ts)。 */
117
118
  onActivity?: (kind?: string, statusData?: Record<string, unknown>) => void;
119
+ /** S-961:这条腿的 `wiring_manifest.instructions` 段(腿的 `InstructionSelectionLeg.segment`)。**必填**:账本行与 live 帧是
120
+ * 同一条腿的两个载体,段必须来自同一只腿;在投帧那一刻读(core 推帧之前已 await 过 loader)。返回 `undefined` ⇒ 段缺席。 */
121
+ instructionsSegment: () => InstructionsSegment | undefined;
118
122
  }): LedgerSink;
119
123
  //# sourceMappingURL=ledger-sink.d.ts.map
@@ -97,7 +97,7 @@ export function createLedgerSink(opts) {
97
97
  await append("workspace_changed", workspaceChangedEventData(ev));
98
98
  break;
99
99
  case "wiring_manifest":
100
- await append("wiring_manifest", wiringManifestEventData(ev));
100
+ await append("wiring_manifest", wiringManifestEventData(ev, opts.instructionsSegment()));
101
101
  break;
102
102
  case "tool_roster_delta": {
103
103
  const rosterDelta = toolRosterDeltaEventData(ev);
@@ -28,6 +28,7 @@ import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
28
28
  import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
29
29
  import { type SemaProvenanceInput } from "./sema-provenance.js";
30
30
  import type { LedgerEventType } from "./ledger-events.js";
31
+ import type { InstructionsSegment } from "../instruction-selection.js";
31
32
  /** 账本写口的统一签名 —— `type` 是**闭集**(`ledger-events.ts`):新型必须先登记,否则调用点编译红
32
33
  * (codex 交叉复审 F2:别名门 `ap("x")` 与动态首参让纯词法门结构性抓不全)。 */
33
34
  type AppendFn = (type: LedgerEventType, data: unknown) => Promise<void>;
@@ -719,11 +720,18 @@ export declare function toolRosterDeltaEventData(ev: {
719
720
  eventId?: unknown;
720
721
  parentToolCallId?: unknown;
721
722
  }): Record<string, unknown> | undefined;
723
+ /**
724
+ * 🔴 S-961(7.111.0)**第二参 `instructions` 必传**(值可以是 `undefined`):`wiring_manifest` 上唯一一段**本仓自铸**的段
725
+ * —— 这条腿的 project-memory loader 选中的指令文件链成员,`{ selected: string[] }`。它不在 core 的 manifest 里(core 不持有
726
+ * 选择规则),所以不从 `ev` 读:每条投帧的腿把**自己那只** `InstructionSelectionLeg.segment()` 递进来。必传而不是可选 ——
727
+ * 漏递的腿当场 tsc 红,而不是静默投出一个「缺席」(缺席在 wire 上是一句话:「这条腿没有关于指令文件的陈述」)。
728
+ * `undefined` ⇒ 段缺席;`{ selected: [] }` ⇒ 显式空(走了链、没选中)。登记:`wire-projection-faces.ts` 租户行 `minted`。
729
+ */
722
730
  export declare function wiringManifestEventData(ev: {
723
731
  manifest?: unknown;
724
732
  eventId?: unknown;
725
733
  parentToolCallId?: unknown;
726
- }): Record<string, unknown>;
734
+ }, instructions: InstructionsSegment | undefined): Record<string, unknown>;
727
735
  /**
728
736
  * `wiring_manifest` 的 **operator 投影**([ref] 件一)—— {@link wiringManifestEventData} 的租户形加上
729
737
  * `governance` 段与 core 的真 `configFingerprint`。
@@ -759,7 +767,7 @@ export declare function wiringManifestOperatorEventData(ev: {
759
767
  manifest?: unknown;
760
768
  eventId?: unknown;
761
769
  parentToolCallId?: unknown;
762
- }): Record<string, unknown>;
770
+ }, instructions: InstructionsSegment | undefined): Record<string, unknown>;
763
771
  /**
764
772
  * `wiring_manifest` 的**唯一分叉点**(S-528)—— 两条知道调用方身份的 live 消费腿共用它:
765
773
  * `/v1/tasks/stream` 的 SSE 帧,与 `POST /v1/tasks`(非流式)200 体里的 `wiringManifest` 起手回执。
@@ -780,7 +788,7 @@ export declare function wiringManifestFaceEventData(ev: {
780
788
  manifest?: unknown;
781
789
  eventId?: unknown;
782
790
  parentToolCallId?: unknown;
783
- }, operatorFace: boolean): Record<string, unknown>;
791
+ }, operatorFace: boolean, instructions: InstructionsSegment | undefined): Record<string, unknown>;
784
792
  /**
785
793
  * `human_input`(core 5.14.0 [ref] 新 `TaskEvent` 臂)—— 「谁把什么喂进了这条 run」的生命周期账本
786
794
  * 帧(objective / 实时 steer / nextTurn / parked-steer 续跑 / wake 五个载体共用一个投影点)。
@@ -464,7 +464,7 @@ export function toolRosterDeltaEventData(ev) {
464
464
  }
465
465
  return { delta: ev.delta, ...identityFields(ev) };
466
466
  }
467
- export function wiringManifestEventData(ev) {
467
+ export function wiringManifestEventData(ev, instructions) {
468
468
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
469
469
  const bool = (v) => (typeof v === "boolean" ? v : undefined);
470
470
  const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
@@ -630,11 +630,12 @@ export function wiringManifestEventData(ev) {
630
630
  ...(lsp !== undefined ? { lsp } : {}),
631
631
  ...(hands !== undefined ? { hands } : {}),
632
632
  ...(writeProtection !== undefined ? { writeProtection } : {}),
633
+ ...(instructions !== undefined ? { instructions: { selected: [...instructions.selected] } } : {}),
633
634
  ...identityFields(ev),
634
635
  };
635
636
  }
636
- export function wiringManifestOperatorEventData(ev) {
637
- const tenant = wiringManifestEventData(ev);
637
+ export function wiringManifestOperatorEventData(ev, instructions) {
638
+ const tenant = wiringManifestEventData(ev, instructions);
638
639
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
639
640
  const bool = (v) => (typeof v === "boolean" ? v : undefined);
640
641
  const sec = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? { ...v } : undefined);
@@ -678,8 +679,8 @@ export function wiringManifestOperatorEventData(ev) {
678
679
  ...(str(m.configFingerprint) !== undefined ? { configFingerprint: m.configFingerprint } : {}),
679
680
  };
680
681
  }
681
- export function wiringManifestFaceEventData(ev, operatorFace) {
682
- return operatorFace ? wiringManifestOperatorEventData(ev) : wiringManifestEventData(ev);
682
+ export function wiringManifestFaceEventData(ev, operatorFace, instructions) {
683
+ return operatorFace ? wiringManifestOperatorEventData(ev, instructions) : wiringManifestEventData(ev, instructions);
683
684
  }
684
685
  export function humanInputEventData(ev) {
685
686
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
@@ -32,6 +32,32 @@ export declare const SELF_MINTED_WIRE: {
32
32
  };
33
33
  };
34
34
  };
35
+ /**
36
+ * S-961(7.111.0):`wiring_manifest.instructions.selected[]` 的成员值 = 指令文件链的成员(相对 cwd 的拼写)。属主是 `project-memory.ts`
37
+ * 的 `INSTRUCTION_FILE_CHAIN`([ref] 序:CLAUDE 族先、AGENTS 族后,首中即选)—— 链加 / 删一员 ⇒ 本行 `satisfies` 编译红,登记跟着动。
38
+ * 段本身(键)登记在 `wire-projection-faces.ts` 的 `wiring_manifest_tenant.minted`。
39
+ */
40
+ readonly wiringInstructionsSelected: {
41
+ readonly at: "wiring_manifest instructions.selected[](本腿 loader 选中的链成员;段缺席 = 本腿无陈述)";
42
+ readonly members: {
43
+ "CLAUDE.md": {
44
+ attr: string;
45
+ since: string;
46
+ };
47
+ ".claude/CLAUDE.md": {
48
+ attr: string;
49
+ since: string;
50
+ };
51
+ "AGENTS.md": {
52
+ attr: string;
53
+ since: string;
54
+ };
55
+ ".claude/AGENTS.md": {
56
+ attr: string;
57
+ since: string;
58
+ };
59
+ };
60
+ };
35
61
  /**
36
62
  * 会话所有者自助抹除面的**已分类拒** → HTTP 状态(S-930 起与 operator 面 `POST /v1/memory/erase` 同一只映射;`since` 记的是**本面**
37
63
  * 自哪一版起这样答 —— 7.107.0 及以前本面只认前两只,其余落 500 `internal.error`)。
@@ -12,6 +12,15 @@ export const SELF_MINTED_WIRE = {
12
12
  crashed: { attr: "-", since: "7.110.0" },
13
13
  },
14
14
  },
15
+ wiringInstructionsSelected: {
16
+ at: "wiring_manifest instructions.selected[](本腿 loader 选中的链成员;段缺席 = 本腿无陈述)",
17
+ members: {
18
+ "CLAUDE.md": { attr: "-", since: "7.111.0" },
19
+ ".claude/CLAUDE.md": { attr: "-", since: "7.111.0" },
20
+ "AGENTS.md": { attr: "-", since: "7.111.0" },
21
+ ".claude/AGENTS.md": { attr: "-", since: "7.111.0" },
22
+ },
23
+ },
15
24
  sessionMemoryEraseStatus: {
16
25
  at: "POST /v1/sessions/:id/memory/erase(已分类拒 errorCode → HTTP 状态)",
17
26
  members: {
@@ -16,7 +16,7 @@
16
16
  * `NotificationExcluded` 直接读成本表这一行的 `excluded`,于是「哪几个键有意不上 wire」全仓**只有一个写者**。
17
17
  * 旧形是两个:守卫文件一簿、`test/wire-whitelist-exhaustiveness.test.ts` 的 `KnownExcluded` 一簿,那份门注
18
18
  * 自己写着「两处必须同时改」—— 而「必须同时改」正是 [ref] 要下线的东西(对账门不算替代,单写者才算)。
19
- * 本表是纯数据、无行为(唯一的派生是 `wiring_manifest_operator` 行的 `excluded` 取值器 = 租户行减具名常量,S-724),
19
+ * 本表是纯数据、无行为(唯一的派生是 `wiring_manifest_operator` 行的两只取值器:`excluded` = 租户行减具名常量,S-724;`minted` = 租户行,S-961),
20
20
  * 被守卫文件以 `typeof import(...)` 的**类型查询**读取(编译期擦除,零运行期边)。
21
21
  *
22
22
  * ## 门在哪里
@@ -170,14 +170,14 @@ export declare const WIRE_PROJECTION_FACES: {
170
170
  readonly coreType: `WiringManifest & ${string}`;
171
171
  readonly excluded: readonly ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence", "permissionMode"];
172
172
  readonly unwrapped: readonly ["manifest"];
173
- readonly minted: readonly [];
173
+ readonly minted: readonly ["instructions"];
174
174
  };
175
175
  readonly wiring_manifest_operator: {
176
176
  readonly projector: "wiringManifestOperatorEventData";
177
177
  readonly coreType: `WiringManifest & ${string}`;
178
178
  readonly excluded: readonly string[];
179
179
  readonly unwrapped: readonly ["manifest"];
180
- readonly minted: readonly [];
180
+ readonly minted: readonly string[];
181
181
  };
182
182
  readonly engine_notice: {
183
183
  readonly projector: "engineNoticeEventData";