@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
package/MIGRATION.md CHANGED
@@ -7,6 +7,14 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.111.0(车TA,S-961 / S-972)—— `wiring_manifest` 帧 +`instructions` 段(additive,零动作)
11
+
12
+ - **wire(additive,零动作)**:`wiring_manifest` 帧(四条腿:流式 SSE / 账本行 / 非流式 `wiringManifest` 回执 / 续跑腿)多一段本服务自铸的 `instructions: { selected: string[] }` = 这条腿选中的项目指令文件(相对 cwd 的链成员);`[]` = 没有,缺席 = 无陈述(契约附录 G.21)。按封闭 schema 校验该帧的消费方加这一可选键;不要它的部署 `PROJECT_MEMORY_ENABLED=false`(连同项目记忆注入一起关)。`task.user_steer_undrained{cause:"crashed"}` 的 `message` 改句序(行动句在前、< 400 字符),`detail` 不变 —— 按文本匹配的脚本改读 `detail.cause`(S-972)。
13
+ - **会话删除 / 托管留存级联接上用户输入受理账本(车TC,S-965;零 DDL、零动作)**:`DELETE /v1/sessions/:id` 与 SQL 后端的托管留存清扫自本版起连带删掉该会话的受理行(SQL `user_input_acceptance` 按 `session_id` 删;local `ledger.jsonl` 追加一行会话级墓碑 `{"kind":"erased","sessionId":…,"at":…}`);`DELETE` 回执形不变。**回滚**(local 车道):≤7.110.x 不认 `erased` 行 —— 启动 warn `user_input_ledger_corrupt_read`,被删会话的行在旧进程里复现;要干净回滚,停服后删 `<localDataRoot>/user-input-ledger/ledger.jsonl`(代价 = 丢掉还没告知的崩溃丢失提醒)。SQL 车道回滚无影响。**嵌入方(类型面硬破坏)**:`UserInputLedgerStore`(包根导出型)新增必选 `deleteBySession(sessionId): Promise<number>`;自建 `StoreBackend.userInputLedger()` 的嵌入方 tsc 红即通知。
14
+ - **同步子代的 steer 入用户输入受理账本(车TF,S-966;零 DDL、零动作)**:`POST /v1/runs/:id/subagents/:target/steer` 自本版起与 run 自己的 steer 同一本账(先账后引擎;行记在回执上的那条 run 名下)——进程在子代收场前崩了 ⇒ 同会话下一条 run 首拍的 `task.user_steer_undrained{cause:"crashed"}` 把它并进那条 run 的条数;账本写不进 ⇒ 该路由回 **503 `steering.ledger_unavailable`**(此前 200);200 回执 additive 多一个 `messageId`(按封闭 schema 校验回执的消费方加这一可选键)。workflow agent 的 steer 不变(不入账,崩溃丢失仍零告知,另票)。**回滚**:无数据动作(行形不变;≤7.110.x 读得懂这些行,照常收敛 / 告知)。
15
+ - **用户输入受理账本收敛判词合一 / SQL 坏行逐行跳过(车TD,S-969;零 DDL、零动作)**:local 车道 boot 收敛自本版起读 run 行 —— 前世进程没结清、而承载它的 run 已正常收场 / 已挂起的受理补结清、不再告知(7.110.x 一律按崩溃告知);SQL 表里读不出的行逐行跳过 + 计数 `server.user-input-ledger.corrupt-record-skipped`,其余照常(7.110.x 整页读失败)。**嵌入方(类型面硬破坏)**:`UserInputLedgerStore.listOpen` 返回 `Promise<OpenScanPage>`(`{ open, next? }`,`next` = 这一页原始末行的键、缺席 = 扫到底;包根新导出 `OpenScanPage`)—— 自建店改交页对象、读不出的行自己跳过并照原始行给游标;`WriterGoneEvidence` 改为 `{ kind, runs }`(两臂都带 run 店读口)。tsc 红即通知。**回滚**:回到 7.110.x 无数据动作(账本行形不变)。
16
+ - **`@sema-agent/core` 7.38.0 → 7.38.2(车TG;两只 patch,core 硬破坏 0、wire 键 / 码 / 闭集零变化,零动作)**:① 内建读拒表的家锚定行(`~/.sema/settings.json` 等)另落**账户家**(core 7.38.1 #1244,S-941)—— 引擎进程的 `HOME` 被改指时,账户真实家下的同名文件由「读得到」变为 `read_path_denied`;真要读的部署用 `READ_DENY_BUILTIN_EXCLUDE` 删那一行。S-941 时期按 DEPLOY-PREREQS 加的 `READ_DENY_PATTERNS` 不锚家同名行可撤(不撤无害)。账户查询无答的主机上这些行在任何位置判(项目自己的 `.sema/settings.json` 也拒,一次 `console.warn`,下一条 run 重问)。② local 车道 File 店族的 JSONL 末段整条记录缺换行 ⇒ 认下并在下次打开补换行(core 7.38.2 #1278,S-975);末段非对象 ⇒ 截 + 多一条 `corrupt-record-skipped` 计数。**回滚**到 ≤7.110.x:回退版本的 core 会把「只缺换行」的末行截掉(只在崩溃留下的那一形上;盘上格式不变,零数据动作)。
17
+
10
18
  ## 7.110.0(车ST,S-955;小车 SV,S-956;车SX,S-958 / S-959;小车 SA,S-962 / S-963;主会话 S-964;车SW,S-928)—— `@sema-agent/core` 7.38.0(minor,core 硬破坏 0):通告码受众 / 铸者改读 core 表 · `terminal.failed.detail` 过境 · hook 载荷 `permission_mode` = 腿实时模式 · 类对判据走 core 屏 · run-local 无 TTY 拒记部署政策名下 · 复合体读不出词的 PreToolUse 结果交引擎屏 · 🔴 `MEMORY_SYNC_SCOPE` 缺省不再从非 v2 `MEMORY_SCOPE` 派生(拒启) · 崩溃丢掉的 200-applied steer 下一条 run 告知(用户输入受理账本)
11
19
 
12
20
  - **🔴 记忆同步 env(S-964,配置面硬破坏)**:单用户 file 引擎上设了 `MEMORY_SYNC_URL` / `MEMORY_SYNC_TOKEN`、**没设** `MEMORY_SYNC_SCOPE`、且 `MEMORY_SCOPE` 不是 typed 键(`user:` / `org:` / `proj:` / `userproj:`;含缺省 `local`)的部署**升级后拒启**。此前这一形派生同步层 `user:<MEMORY_SCOPE>` —— 一个本部署从不写的层(写家是裸 `MEMORY_SCOPE`,core 同步按同一个名字取条目)⇒ 同步腿自 142-S2.5-W1 起每轮推 0 条。出路二选一(错误句原文点名):`MEMORY_SYNC_SCOPE=<要复制的层>`,或把 `MEMORY_SCOPE` 设成中心期待的 typed 键(如 `user:<tenant>`,缺省同步层随之同名)。显式 `MEMORY_SYNC_SCOPE` 的部署零变化。
@@ -3,8 +3,7 @@
3
3
  * `server/designs/2026-08-31-s38-local-stream-approval-v1.md`,v1 三臂 + v1.1 §6 两件增益)——
4
4
  * local 车道(`DB_BACKEND=local`)的 **File ask 审计店**。SINGLE-FILE 店([ref] A12 单文件店纪律,
5
5
  * 形照 `plugins/memory-optout-grant-store-sql.ts`:一个实现文件、数据形/偏离/坏值姿态全部成文在头注;
6
- * 崩溃安全机制照 `orchestration/workflow-notify-journal.ts` 的 File 形:append-only JSONL、fsync 逐行、
7
- * 重放容忍撕尾行)。
6
+ * 崩溃安全机制 = append-only JSONL、fsync 逐行、重放容忍撕尾行 —— 追加与重放都走本仓 JSONL 店族的**共用口**(S-967,见下「追加 / 重放口」段))。
8
7
  *
9
8
  * ## 它治的病([ref] / [ref]① 车定界钉;⚠️ **病因描述是 [ref] 当时的事实,S-384 起已变**)
10
9
  * 当时:local 车道 ask 账进程内易失(`InMemoryApprovalAskStore`),活卡窗内崩溃 = **零痕迹**:重启后
@@ -135,11 +134,24 @@
135
134
  * ## `crashed_before_park` 与 FAIL-OPEN census 的关系(稿未闭环②,施工时裁定,成文于此)
136
135
  * **不入词表**:census 词表登记的是「走了兜底而外面看不出」的**臂**;`crashed_before_park` 是一条
137
136
  * **终态成因注**(收敛器 fail-closed 地把孤儿收成 DENIED 同码并在读面显形),它的存在恰恰是响亮化
138
- * 本身,不是静默降级。本店唯一的静默降级臂 = 运行期 append 失败(审批可用性不押在审计盘上),已按
139
- * [ref] 律走 `recordFailOpen("server.approval-ask-audit.append-failed")`(census 登记行同批)。
140
- * boot 期(mkdir/重放)失败则**拒启**(与 File 店族同律:`FileStorageBackend` ctor 同 root 同姿态)——
137
+ * 本身,不是静默降级。本店的静默降级臂有两条:① 运行期 append 失败(审批可用性不押在审计盘上),已按
138
+ * [ref] 律走 `recordFailOpen("server.approval-ask-audit.append-failed")`(census 第 53 行);② boot 重放跳过中间坏行
139
+ * (S-967 起由重放属主逐条披露 + `server.approval-ask-audit.corrupt-record-skipped`,census 第 106 行;此前只计 `corruptLines`)。
140
+ * boot 期(mkdir/重放的读错)失败则**拒启**(与 File 店族同律:`FileStorageBackend` ctor 同 root 同姿态)——
141
141
  * 一台「审计写不进却继续批」的机器 = [ref] 换个皮回来。
142
142
  *
143
+ * ## 追加 / 重放口(S-967:一只追加日志实现,不留两份)
144
+ * 追加 = core `AppendLog`(惰性打开,首次 append 才铸文件;一行 = 单次 `writeSync` 的 `JSON.stringify(rec) + "\n"`、`fsync=true` 逐行;文件 0o600、目录 0o700;
145
+ * 末段无换行的字节在打开与每次追加前由它按 core 的同一只判词修 —— 解析成对象 = 丢了换行的整条记录,**补换行、字节保留**;不解析 = 崩溃半写(撕尾),
146
+ * **截断**回最后一个换行;解析成非对象 = 截断并披露(core 7.38.2 #1278))。重放 = `replayJsonlSkipping`(登记名 `approval-ask-audit`,fail-open + F 类 tag
147
+ * `server.approval-ask-audit.corrupt-record-skipped`:中间坏行逐条披露 + 计数;撕尾不计,无换行的完整末行照常进重放)。本店只剩两样自己的东西:**形判据**({@link LEDGER_SHAPE},逐 `kind` 的最小必填键
148
+ * + 词表外成因行「计坏行、结清照认」)与**失败方向**(运行期 append 失败 ⇒ `recordFailOpen("server.approval-ask-audit.append-failed")`,审批照常)。
149
+ * **一行「在账」的判据 = 它的字节解析成一只对象**(与共用口同一条,core 7.38.2 #1278:读面、打开修尾、追加门修尾三站问同一只判词;换行只是边界的拼法,
150
+ * 不是记录存在的条件 —— 没有一只 JSON 对象的真前缀能被解析,所以解析得出的末行就是整条记录;失败的 append 由 core 截回它开写处,不留自己的字节)。
151
+ * 修前本店手写了第二份追加 / 修尾(私有的脏尾标 + 写字节注入席),撕尾用**孤立换行隔离**保留残字节而不是截断;与共用口的形差(撕尾隔离 ⇄ 截断、撕尾计数、
152
+ * 字节全落却报错后多落的孤立换行、空白行计数、坏行披露)逐条记在 CHANGELOG 7.111.0 S-967 段,读数钉 = `test/approval-ask-audit.test.ts` 的「S-967」两组;
153
+ * 「完整 JSON 却缺换行的末行」一条(R2)在 core 7.38.2 下与修前同形(认下 + 补换行,C2 正向钉),不再是形差。
154
+ *
143
155
  * ## 装配(为什么不在 `plugins/store-backend.ts`)
144
156
  * 本店是协调器侧的审计 sink,不是跨后端 store 契约的一员(SQL 车道有真 ask 店,永不装本店)——
145
157
  * 装配点在 `boot/coordinators.ts`(判据:`toolApprovalEnabled` ∧ `backend.kind === "local"` ∧
@@ -152,10 +164,14 @@
152
164
  * journal 同款成文)。读面列表按收敛时刻新前旧后、上限 {@link CRASH_CONVERGED_LIST_MAX}(有界 wire)。
153
165
  */
154
166
  import type { HostDecision } from "./host-decision.js";
167
+ import { AppendLog } from "@sema-agent/core";
168
+ import type { Logger } from "./observability/logger.js";
155
169
  /** 数据子目录名(挂在 local 数据根下,与 `workflow-notify/` 同层同形)。 */
156
170
  export declare const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
157
171
  /** 崩溃收敛行的读面上限(新前旧后截断;有界 wire —— 读面不许随月份线性长)。 */
158
172
  export declare const CRASH_CONVERGED_LIST_MAX = 200;
173
+ /** 追加口的窄形(core `AppendLog` 的 `append` / `close`)—— 店的测试注入席({@link FileApprovalAskAuditStore} 构造器 `internals.appendLog`)按它造替身。 */
174
+ export type AuditAppendPort = Pick<AppendLog, "append" | "close">;
159
175
  /** 铸造点入参(协调器 askBroadcast 的 register-before-emit 之后挂线)。 */
160
176
  export interface AuditAskInput {
161
177
  /** wire 身份 = 呈卡帧上那把 `approvalId`(respond/queue 同源可达;不是本地 pending 键)。 */
@@ -302,14 +318,10 @@ export type LateSettleUnresolved = "ambiguous_anchor";
302
318
  export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink, ApprovalAskAuditReadFace {
303
319
  /** 账本绝对路径(测试/取证按文件字节对账 —— 崩溃语义的真源是文件,不是内存投影)。 */
304
320
  readonly ledgerPath: string;
305
- private fd;
306
- /** codex R2-F1(采):上一次 append 抛出时可能已落**部分**字节(半写)—— 置位后,下一次成功写必须
307
- * 先落一个孤立换行把残尾隔离并 fsync(与 boot 修尾同一形,只是发生在同进程内),否则新记录会与残尾
308
- * 并成一条坏行、重放双双丢失(丢 settle+leg_closed 会让 ask-only 组冒充 pending 孤儿)。定界自己
309
- * 失败 ⇒ 脏标保持,本条记录走留痕臂 —— 绝不往可能的残尾上直接续字节。 */
310
- private tailDirty;
311
- /** 写字节口(测试注入席:半写故障的确定性构造;生产恒缺省 = `fs.writeSync`)。 */
312
- private readonly writeBytes;
321
+ /** 追加口(core `AppendLog`,惰性打开 —— 首次 append 才铸文件;`close()` 后下一次 append 重开)。撕尾修复 / 短写补齐 / 逐行 fsync 都在它里面,本店不再自写一份。 */
322
+ private log;
323
+ /** 追加口的铸造座(测试注入席:单条写失败 / 半写的确定性构造;生产恒缺省 = `new AppendLog(path)`)。 */
324
+ private readonly openLog;
313
325
  /** boot 重放快照(冻结于构造时刻 = 前世账本;本进程新写的行不进它 —— 收敛只对前世行)。 */
314
326
  private readonly replayed;
315
327
  /** 前世账本里的腿闭事件(taskId + 行序;decided 臂按「决议行之后」判)。 */
@@ -339,24 +351,25 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
339
351
  /** [ref] 反查索引 ③:approvalId → 铸造坐标(命中后回填 late 行的 `taskId`)。 */
340
352
  private readonly askTaskId;
341
353
  /**
342
- * boot 期 fail-stop:mkdir/重放失败**拒启**(File 店族同律)。root = local 数据根
354
+ * boot 期 fail-stop:建目录 / 重放的**读错**(非 ENOENT)⇒ 抛 ⇒ **拒启**(File 店族同律)。root = local 数据根
343
355
  * (`config.localDataRoot`;与 `LocalBackend` 同源,见头注装配段)。
344
- * 0o700 目录 + 0o600 账本:行携租户身份(owner/sessionId),不得组/他人可读
345
- * (`workflow-notify-journal.ts` 同姿态)。
356
+ * 0o700 目录 + 0o600 账本:行携租户身份(owner/sessionId),不得组/他人可读(目录由 core `ensureDir`、账本由 `AppendLog` 铸)。
357
+ * 追加口惰性打开(S-967,与 `FileUserInputLedgerStore` 同形):末段的修(撕尾截断 / 完整末行补换行)随首次 append 的打开发生 —— 重放读数不依赖它
358
+ * (撕尾不进重放、完整末行进重放,修前修后读数同);
359
+ * 目录已在而盘后来满 / 变只读 ⇒ boot 照常,append 走 `recordFailOpen`。
360
+ * @param internals.appendLog 测试注入席(见 {@link AuditAppendPort})。
361
+ * @param internals.warn 腐读披露座(缺席 ⇒ 重放属主退回 `console.warn`,不退回沉默)。
346
362
  */
347
363
  constructor(root: string, internals?: {
348
- writeBytes?: (fd: number, buf: Buffer, off: number, len: number) => number;
364
+ appendLog?: (path: string) => AuditAppendPort;
365
+ warn?: Logger["warn"];
349
366
  });
350
- /** 重放前世账本。撕尾/坏行 = 未提交态,跳过(计数,一条 warn 级 stderr 契约由调用方日志承担 ——
351
- * 这里不持 logger 席,与 journal 同姿态;坏行数暴露给收敛日志)。 */
367
+ /** 重放的坏值计数(`convergeCrashedAsks().corruptLines`):重放属主跳过的坏行(撕尾不计)+ 成因词表外的行(行在、结清照认,坏值照样计数)。 */
352
368
  private corruptLines;
353
- /** 尾部无终止换行(崩在 append 半途)—— ctor 在首次 append 前补孤立换行隔离之(codex R1-F3)。
354
- * ⚠️ 判据是**换行缺席**不是「解析失败」:崩在「JSON 全落、换行未落」的字节窗时撕尾能 parse 成功,
355
- * 但不修尾照样会与下一条记录并行。 */
356
- private tornTail;
369
+ /** 重放前世账本(共用口 `replayJsonlSkipping`):撕尾 = 未落完的写,静默跳过;坏行跳过 + 逐条披露;坏行数另暴露给收敛日志。 */
357
370
  private replay;
358
371
  private group;
359
- /** append + fsync(逐行落盘:进程猝死不丢已记的行 —— 本店的全部价值就在崩溃时刻)。
372
+ /** append + fsync(逐行落盘:进程猝死不丢已记的行 —— 本店的全部价值就在崩溃时刻;写口 = 共用的 `AppendLog`:单次 `writeSync` 一行、末段在打开与每次追加前修(撕尾截、完整末行补换行)、失败的写截回开写处)。
360
373
  * 运行期失败**响亮不阻断**:审批可用性不押在审计盘上(盘满/只读时人还得能批),但丢行必须在遥测
361
374
  * 显形([ref] 三件套,census 登记行同批)。
362
375
  * 🔴 返回**这条记录是否真的落盘并 fsync 成功**([ref] codex R1-F2 采):迟到结算的 late 判据只认
@@ -432,7 +445,7 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
432
445
  * `owner === scope` 的行(null-owner 行对 principal 不可见,fail-closed 宁少列不超范围;`"__none__"`
433
446
  * 未鉴权哨兵自然恒空)。 */
434
447
  listCrashConverged(scope: string | undefined): CrashConvergedRow[];
435
- /** 收 append fd(优雅关闭腿 best-effort;SIGKILL 下不跑也不丢 —— 每行已 fsync)。 */
448
+ /** 收追加口(优雅关闭腿;SIGKILL 下不跑也不丢 —— 每行已 fsync)。core `AppendLog.close()` 自己吞 `closeSync` 的错、不抛(全函数),本店不再包 catch。 */
436
449
  close(): void;
437
450
  }
438
451
  //# sourceMappingURL=approval-ask-audit-store.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
3
2
  import { join } from "node:path";
3
+ import { AppendLog, ensureDir } from "@sema-agent/core";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
+ import { replayJsonlSkipping } from "./observability/corrupt-read-seat.js";
5
6
  export const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
6
7
  export const CRASH_CONVERGED_LIST_MAX = 200;
7
8
  export const AUDIT_SHUTDOWN_CAUSE_WORDS = { shutdown_before_park: true };
@@ -160,11 +161,20 @@ function readLedgerRecord(u, noteOutOfSetSeat) {
160
161
  return undefined;
161
162
  }
162
163
  }
164
+ const LEDGER_SHAPE = {
165
+ what: "approval-ask audit record of an unknown kind or with a missing / out-of-vocabulary member",
166
+ read(record) {
167
+ let outOfSetCause = false;
168
+ const rec = readLedgerRecord(record, () => {
169
+ outOfSetCause = true;
170
+ });
171
+ return rec === undefined ? undefined : { rec, outOfSetCause };
172
+ },
173
+ };
163
174
  export class FileApprovalAskAuditStore {
164
175
  ledgerPath;
165
- fd;
166
- tailDirty = false;
167
- writeBytes;
176
+ log;
177
+ openLog;
168
178
  replayed = new Map();
169
179
  replayedLegClosed = [];
170
180
  replayedShutdownMarks = [];
@@ -177,44 +187,21 @@ export class FileApprovalAskAuditStore {
177
187
  parkExpired = new Set();
178
188
  askTaskId = new Map();
179
189
  constructor(root, internals) {
180
- this.writeBytes = internals?.writeBytes ?? ((fd, buf, off, len) => writeSync(fd, buf, off, len));
190
+ this.openLog = internals?.appendLog ?? ((p) => new AppendLog(p));
181
191
  const dir = join(root, APPROVAL_ASK_AUDIT_DIR);
182
- mkdirSync(dir, { recursive: true, mode: 0o700 });
192
+ ensureDir(dir);
183
193
  this.ledgerPath = join(dir, "ledger.jsonl");
184
- this.replay();
185
- if (this.tornTail) {
186
- this.fd = openSync(this.ledgerPath, "a", 0o600);
187
- writeSync(this.fd, "\n");
188
- fsyncSync(this.fd);
189
- }
194
+ this.replay(internals?.warn);
190
195
  }
191
196
  corruptLines = 0;
192
- tornTail = false;
193
- replay() {
194
- if (!existsSync(this.ledgerPath))
195
- return;
196
- const text = readFileSync(this.ledgerPath, "utf8");
197
- this.tornTail = text.length > 0 && !text.endsWith("\n");
197
+ replay(warn) {
198
+ const { records, skipped } = replayJsonlSkipping(this.ledgerPath, "approval-ask-audit", LEDGER_SHAPE, warn);
199
+ this.corruptLines = skipped;
198
200
  let line = 0;
199
- for (const raw of text.split("\n")) {
200
- if (raw.trim() === "")
201
- continue;
201
+ for (const { rec, outOfSetCause } of records) {
202
202
  line += 1;
203
- let parsed;
204
- try {
205
- parsed = JSON.parse(raw);
206
- }
207
- catch {
208
- this.corruptLines += 1;
209
- continue;
210
- }
211
- const rec = readLedgerRecord(parsed, () => {
203
+ if (outOfSetCause)
212
204
  this.corruptLines += 1;
213
- });
214
- if (rec === undefined) {
215
- this.corruptLines += 1;
216
- continue;
217
- }
218
205
  switch (rec.kind) {
219
206
  case "ask": {
220
207
  const g = this.group(rec.approvalId, line);
@@ -265,25 +252,11 @@ export class FileApprovalAskAuditStore {
265
252
  }
266
253
  append(rec) {
267
254
  try {
268
- if (this.fd === undefined)
269
- this.fd = openSync(this.ledgerPath, "a", 0o600);
270
- if (this.tailDirty) {
271
- const nl = Buffer.from("\n", "utf8");
272
- let noff = 0;
273
- while (noff < nl.length)
274
- noff += this.writeBytes(this.fd, nl, noff, nl.length - noff);
275
- fsyncSync(this.fd);
276
- this.tailDirty = false;
277
- }
278
- const buf = Buffer.from(`${JSON.stringify(rec)}\n`, "utf8");
279
- let off = 0;
280
- while (off < buf.length)
281
- off += this.writeBytes(this.fd, buf, off, buf.length - off);
282
- fsyncSync(this.fd);
255
+ this.log ??= this.openLog(this.ledgerPath);
256
+ this.log.append(rec, true);
283
257
  return true;
284
258
  }
285
259
  catch (err) {
286
- this.tailDirty = true;
287
260
  recordFailOpen("server.approval-ask-audit.append-failed", `kind=${rec.kind} err=${err instanceof Error ? err.message : String(err)}`);
288
261
  return false;
289
262
  }
@@ -440,14 +413,8 @@ export class FileApprovalAskAuditStore {
440
413
  return rows;
441
414
  }
442
415
  close() {
443
- if (this.fd !== undefined) {
444
- try {
445
- closeSync(this.fd);
446
- }
447
- catch {
448
- }
449
- this.fd = undefined;
450
- }
416
+ this.log?.close();
417
+ this.log = undefined;
451
418
  }
452
419
  }
453
420
  //# sourceMappingURL=approval-ask-audit-store.js.map
@@ -15,6 +15,17 @@
15
15
  *
16
16
  * 🔴 ENV GATING: the live path requires E2B_API_KEY + DEEPSEEK_API_KEY; absent → the driver refuses the live path
17
17
  * with a clear message (never a silent fake run). The deterministic --dry-run never needs keys.
18
+ *
19
+ * 🔴 LEDGER I/O (S-977): the WRITE side is the repo's shared append port — core `AppendLog` (lazy open, one `writeSync` loop per
20
+ * row, a failed write rolled back, a torn tail repaired on open AND before every append) — so a crash that tore the last
21
+ * row no longer glues the resume's first row onto the broken suffix (which lost that cell's row ⇒ one cell re-run at real
22
+ * E2B / DeepSeek cost). No fsync (a crash loses at most the tail row ⇒ that cell stays re-runnable — the same posture as before).
23
+ * The READ side ({@link readLedger}) stays PRIVATE — **registered exception, "bench CLI"**: this is a single-process offline
24
+ * experiment driver, not a serving store; its ledger path comes from `--ledger`, its readers are this driver and its tests, and a
25
+ * skipped (unparseable) ledger row costs one re-run of that cell — never a gate, never a tenant fact. The serving-layer replay owner
26
+ * (`replayJsonlSkipping`, `observability/corrupt-read-seat.ts`) is a closed registry of census-registered seats (each one a row in
27
+ * `docs/FAIL-OPEN-CENSUS.md` with an F tag in the serving telemetry); a bench seat there would register a non-serving face in the
28
+ * serving census. Direction note: a parseable row with an unknown arm tag still THROWS ({@link armUpper}) — loud, not skipped.
18
29
  */
19
30
  import { Runner } from "@sema-agent/core";
20
31
  import { type Arm } from "./arms.js";
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { Runner, InMemoryCheckpointStore } from "@sema-agent/core";
3
+ import { AppendLog, Runner, InMemoryCheckpointStore } from "@sema-agent/core";
4
4
  import { runArm } from "./arms.js";
5
5
  import { FIRM_TRAPS, ALL_TRAPS } from "./tasks.js";
6
6
  import { makeBenchClock } from "./runner-ctx.js";
@@ -71,9 +71,18 @@ function armUpper(tag) {
71
71
  throw new Error(`s1 run-firm: unknown arm tag ${JSON.stringify(tag)} in ledger row (expected solo|sup|team)`);
72
72
  }
73
73
  }
74
- async function appendLedger(ledgerPath, rec) {
75
- await fs.mkdir(path.dirname(ledgerPath), { recursive: true });
76
- await fs.appendFile(ledgerPath, JSON.stringify(rec) + "\n", "utf8");
74
+ function createLedgerWriter(ledgerPath) {
75
+ let log;
76
+ return {
77
+ append(rec) {
78
+ log ??= new AppendLog(ledgerPath);
79
+ log.append(rec, false);
80
+ },
81
+ close() {
82
+ log?.close();
83
+ log = undefined;
84
+ },
85
+ };
77
86
  }
78
87
  function withTimeout(p, ms, label) {
79
88
  return new Promise((resolve, reject) => {
@@ -121,35 +130,41 @@ export async function runFirm(opts) {
121
130
  const { done } = await readLedger(opts.ledgerPath);
122
131
  const ctx = { budget: opts.budget, clock: makeBenchClock(1_000_000), runId: opts.runId, gitSha: opts.gitSha };
123
132
  let ranThisPass = 0;
124
- for (const cell of cells) {
125
- const key = cellKeyStr(cell);
126
- if (done.has(key)) {
127
- log("cell_skip_done", { key });
128
- continue;
129
- }
130
- log("cell_start", { key });
131
- const cellId = `${opts.runId}-${cell.taskId}-${armTag(cell.arm)}-${String(cell.seed)}`;
132
- let built;
133
- let row;
134
- try {
135
- built = opts.mockDepsFactory
136
- ? opts.mockDepsFactory(cell.trap, cell.seed, cellId)
137
- : buildLiveCellOrThrow(opts, cell.trap, cell.seed, cellId);
138
- const supStore = cell.arm === "SUP" ? new InMemoryCheckpointStore() : undefined;
139
- row = await withTimeout(runArm(cell.arm, cell.trap, cell.seed, ctx, built.deps, supStore), cellTimeoutMs, key);
140
- }
141
- catch (e) {
142
- log("cell_infra_failed", { key, error: e instanceof Error ? e.message : String(e) });
143
- row = infraFailedRow(cell, ctx);
144
- }
145
- finally {
146
- await built?.dispose().catch(() => { });
133
+ const ledger = createLedgerWriter(opts.ledgerPath);
134
+ try {
135
+ for (const cell of cells) {
136
+ const key = cellKeyStr(cell);
137
+ if (done.has(key)) {
138
+ log("cell_skip_done", { key });
139
+ continue;
140
+ }
141
+ log("cell_start", { key });
142
+ const cellId = `${opts.runId}-${cell.taskId}-${armTag(cell.arm)}-${String(cell.seed)}`;
143
+ let built;
144
+ let row;
145
+ try {
146
+ built = opts.mockDepsFactory
147
+ ? opts.mockDepsFactory(cell.trap, cell.seed, cellId)
148
+ : buildLiveCellOrThrow(opts, cell.trap, cell.seed, cellId);
149
+ const supStore = cell.arm === "SUP" ? new InMemoryCheckpointStore() : undefined;
150
+ row = await withTimeout(runArm(cell.arm, cell.trap, cell.seed, ctx, built.deps, supStore), cellTimeoutMs, key);
151
+ }
152
+ catch (e) {
153
+ log("cell_infra_failed", { key, error: e instanceof Error ? e.message : String(e) });
154
+ row = infraFailedRow(cell, ctx);
155
+ }
156
+ finally {
157
+ await built?.dispose().catch(() => { });
158
+ }
159
+ ledger.append(row);
160
+ ranThisPass++;
161
+ if (row.runStatus !== "infra-failed")
162
+ done.add(key);
163
+ log("cell_done", { key, runStatus: row.runStatus });
147
164
  }
148
- await appendLedger(opts.ledgerPath, row);
149
- ranThisPass++;
150
- if (row.runStatus !== "infra-failed")
151
- done.add(key);
152
- log("cell_done", { key, runStatus: row.runStatus });
165
+ }
166
+ finally {
167
+ ledger.close();
153
168
  }
154
169
  const { records } = await readLedger(opts.ledgerPath);
155
170
  const lastByKey = new Map();
@@ -41,7 +41,7 @@ export function createLiveCoordinators(ctx) {
41
41
  });
42
42
  }
43
43
  const approvalAskAudit = config.toolApprovalEnabled && backend?.kind === "local" && config.localDataRoot !== undefined
44
- ? new FileApprovalAskAuditStore(config.localDataRoot)
44
+ ? new FileApprovalAskAuditStore(config.localDataRoot, { warn: (event, fields) => logger.warn(event, fields) })
45
45
  : undefined;
46
46
  if (approvalAskAudit) {
47
47
  const converged = approvalAskAudit.convergeCrashedAsks();
@@ -12,6 +12,7 @@ import { SessionWatchRegistry } from "../session-watch.js";
12
12
  import type { TaskListLane } from "./task-list-lane.js";
13
13
  import { type RunnerDeps } from "@sema-agent/core";
14
14
  import type { MailboxRecipientLifecycle } from "../plugins/mailbox-recipient-lifecycle.js";
15
+ import type { UserInputLedgerStore } from "../user-input-ledger.js";
15
16
  export interface SessionFacesCtx {
16
17
  config: ServiceConfig;
17
18
  logger: Logger;
@@ -43,6 +44,9 @@ export interface SessionFacesCtx {
43
44
  /** S-790 件B:后台子代的耐久店(与 `RunnerDeps.backgroundAgentStore` 同实例)—— 删除协调器据它枚举以本会话为根的子代并结束
44
45
  * 它们(E21 级联的一条腿,做完才删元数据)。缺席 = 本部署没有耐久后台子代(纯内存 dev);那一形只剩本进程登记表里的活行,照样停。 */
45
46
  backgroundAgentStore: RunnerDeps["backgroundAgentStore"];
47
+ /** S-965:用户输入受理账本的**店**(E21 级联的一条腿:该会话的受理行随会话删)。与受理 / 收敛 / 告知读的是同一只(stage-07 取一次)。
48
+ * 缺席 = 本部署没装账本(env-only:没有 backend / run 店)⇒ 无行可删。 */
49
+ userInputLedgerStore: UserInputLedgerStore | undefined;
46
50
  }
47
51
  export declare function createSessionFaces(ctx: SessionFacesCtx): {
48
52
  ownerAware: OwnerAwareSessionStore;
@@ -8,7 +8,7 @@ import { defaultTaskRegistry, peerSessionBoxHandle } from "@sema-agent/core";
8
8
  import { reapSessionBackgroundHard } from "../background-session-reap.js";
9
9
  import { probeSessionChildren, retireSessionChildren, SessionChildrenStopIncompleteError } from "../session-children-retire.js";
10
10
  export function createSessionFaces(ctx) {
11
- const { config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore, resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryErasure, workflowCompletionInbox, taskListLane, deviceStore, dropSessionScoped, mailboxStore, mailboxRecipientLifecycle, backgroundAgentStore, } = ctx;
11
+ const { config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore, resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryErasure, workflowCompletionInbox, taskListLane, deviceStore, dropSessionScoped, mailboxStore, mailboxRecipientLifecycle, backgroundAgentStore, userInputLedgerStore, } = ctx;
12
12
  const mysqlPool = backend?.mysqlPool();
13
13
  const auditPgPool = backend?.pgPool();
14
14
  const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
@@ -78,6 +78,8 @@ export function createSessionFaces(ctx) {
78
78
  await sessionPolicyStore.deleteBySession(sessionId);
79
79
  if (taskAttachmentStore)
80
80
  await taskAttachmentStore.deleteBySession(sessionId);
81
+ if (userInputLedgerStore)
82
+ await userInputLedgerStore.deleteBySession(sessionId);
81
83
  if (taskListLane)
82
84
  await taskListLane.deleteBySession(sessionId);
83
85
  if (deviceStore)
@@ -24,6 +24,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
24
24
  };
25
25
  taskTimeoutSec: number;
26
26
  userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
27
+ userInputLedgerStore: import("../user-input-ledger.js").UserInputLedgerStore | undefined;
27
28
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
28
29
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
29
30
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -51,10 +51,11 @@ export async function runStage07(ctx) {
51
51
  if (gateRefusal !== undefined)
52
52
  throw new Error(gateRefusal);
53
53
  await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
54
- const userInputLedger = backend && runStore
54
+ const userInputLedgerStore = backend && runStore ? backend.userInputLedger() : undefined;
55
+ const userInputLedger = backend && runStore && userInputLedgerStore
55
56
  ? await (async () => {
56
57
  const { ledger, converged } = await createUserInputLedgerAtBoot({
57
- store: backend.userInputLedger(),
58
+ store: userInputLedgerStore,
58
59
  writer: instanceId,
59
60
  evidence: writerGoneEvidenceFor(backend.kind, runStore),
60
61
  newId: uuidv7,
@@ -170,6 +171,6 @@ export async function runStage07(ctx) {
170
171
  const builtinScenarioNames = Object.keys(scenarioDetails);
171
172
  configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
172
173
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
173
- return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, userInputLedger, webSearchProvider: webSearchCfg?.provider };
174
+ return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, userInputLedger, userInputLedgerStore, webSearchProvider: webSearchCfg?.provider };
174
175
  }
175
176
  //# sourceMappingURL=stage-07-capability-layer.js.map
@@ -37,6 +37,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
37
37
  };
38
38
  taskTimeoutSec: number;
39
39
  userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
40
+ userInputLedgerStore: import("../user-input-ledger.js").UserInputLedgerStore | undefined;
40
41
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
41
42
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
42
43
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -73,6 +73,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
73
73
  };
74
74
  taskTimeoutSec: number;
75
75
  userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
76
+ userInputLedgerStore: import("../user-input-ledger.js").UserInputLedgerStore | undefined;
76
77
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
77
78
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
78
79
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -6,7 +6,7 @@ import { createLeaderFace } from "./leader.js";
6
6
  import { assertStaticWiringConsistent } from "../http/routes/diagnostics.js";
7
7
  import { buildDisplaySafeUrl } from "../trace/redact.js";
8
8
  export function runStage09(ctx) {
9
- const { approvalExemptionStore, backend, backgroundAgentStore, brain, checkpointStore, config, configCenter, deviceLane, dropSessionScoped, durableEnabled, executionEnvFactory, fileHistoryErasure, governanceSeams, localRoot, logger, mailboxRecipientLifecycle, mailboxStore, metrics, pricing, resumeAnchorStore, runStore, runnerDeps, sessionPolicyStore, sessionStore, streamApprovalGate, taskAttachmentStore, taskListLane, toolResultStore, tracer, usageWindowStore, workflowCompletionInbox } = ctx;
9
+ const { approvalExemptionStore, backend, backgroundAgentStore, brain, checkpointStore, config, configCenter, deviceLane, dropSessionScoped, durableEnabled, executionEnvFactory, fileHistoryErasure, governanceSeams, localRoot, logger, mailboxRecipientLifecycle, mailboxStore, metrics, pricing, resumeAnchorStore, runStore, runnerDeps, sessionPolicyStore, sessionStore, streamApprovalGate, taskAttachmentStore, taskListLane, toolResultStore, tracer, usageWindowStore, userInputLedgerStore, workflowCompletionInbox } = ctx;
10
10
  const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
11
11
  deviceStore: deviceLane?.store,
12
12
  dropSessionScoped,
@@ -14,6 +14,7 @@ export function runStage09(ctx) {
14
14
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryErasure,
15
15
  workflowCompletionInbox, taskListLane, mailboxStore, mailboxRecipientLifecycle,
16
16
  backgroundAgentStore,
17
+ userInputLedgerStore,
17
18
  });
18
19
  const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
19
20
  const drainState = { draining: false };
@@ -73,6 +73,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
73
73
  };
74
74
  taskTimeoutSec: number;
75
75
  userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
76
+ userInputLedgerStore: import("../user-input-ledger.js").UserInputLedgerStore | undefined;
76
77
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
77
78
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
78
79
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -20,7 +20,7 @@ export function createWorkflowOrchestration(ctx) {
20
20
  }))
21
21
  : undefined;
22
22
  const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
23
- ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
23
+ ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows"), undefined, (msg, meta) => logger.warn(msg, meta)))
24
24
  : undefined;
25
25
  const workflowCompletionInbox = config.selfOrchestrationEnabled
26
26
  ? config.workflowRunStoreBackend === "memory"
@@ -20,6 +20,7 @@ import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
20
20
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
21
21
  import { withPrincipal } from "../observability/principal-context.js";
22
22
  import { withModelReachScope } from "../model-reach-scope.js";
23
+ import { createInstructionSelectionLeg } from "../instruction-selection.js";
23
24
  import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
24
25
  import { redactSecrets } from "../trace/redact.js";
25
26
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
@@ -503,7 +504,8 @@ export function createResumeLegs(ctx) {
503
504
  await args.onResumeCommitted();
504
505
  let unregisterEngineNotice;
505
506
  try {
506
- result = await withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
507
+ const instructionLeg = createInstructionSelectionLeg();
508
+ result = await instructionLeg.run(sessionId, () => withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
507
509
  const resumeNotifiedKeys = new NotifiedKeys();
508
510
  if (deps.runStore && taskId) {
509
511
  const rs0 = deps.runStore;
@@ -526,6 +528,7 @@ export function createResumeLegs(ctx) {
526
528
  persistThinking: deps.config.traceThinking,
527
529
  notifiedKeys: resumeNotifiedKeys,
528
530
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
531
+ instructionsSegment: instructionLeg.segment,
529
532
  });
530
533
  const vAppend = (type, data) => verifySink.append(type, data);
531
534
  const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
@@ -771,7 +774,7 @@ export function createResumeLegs(ctx) {
771
774
  break;
772
775
  case "wiring_manifest":
773
776
  await flush();
774
- await append("wiring_manifest", wiringManifestEventData(ev));
777
+ await append("wiring_manifest", wiringManifestEventData(ev, instructionLeg.segment()));
775
778
  break;
776
779
  case "tool_roster_delta": {
777
780
  await flush();
@@ -875,7 +878,7 @@ export function createResumeLegs(ctx) {
875
878
  return await (deps.sendUserFile && taskId
876
879
  ? deps.sendUserFile.runWithContext({ taskId, emit: async (frame) => { const { type, ...rest } = frame; await append(type, rest); } }, withElicitation)
877
880
  : withElicitation());
878
- }));
881
+ })));
879
882
  }
880
883
  catch (e) {
881
884
  if (e instanceof CheckpointError) {
@@ -1155,6 +1155,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1155
1155
  }
1156
1156
  const handle = resolution?.handle;
1157
1157
  if (handle) {
1158
+ let acceptance;
1158
1159
  try {
1159
1160
  if (verb === "resume") {
1160
1161
  if (!handle.resume) {
@@ -1170,11 +1171,23 @@ export async function handleRunVerbs(req, res, match, ctx) {
1170
1171
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
1171
1172
  return;
1172
1173
  }
1174
+ const messageId = uuidv7();
1175
+ const accepted = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1176
+ if (accepted === "refused")
1177
+ return;
1178
+ acceptance = accepted;
1173
1179
  const marker = await handle.steer(redacted);
1174
- sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1180
+ if (acceptance !== undefined) {
1181
+ const ledger = deps.userInputLedger;
1182
+ const settle = () => ledger.settleLeg(handle);
1183
+ void handle.settled.then(settle, settle);
1184
+ }
1185
+ sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", messageId, marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
1175
1186
  return;
1176
1187
  }
1177
1188
  catch (e) {
1189
+ if (acceptance !== undefined)
1190
+ await deps.userInputLedger.withdraw(acceptance);
1178
1191
  const code = e.code;
1179
1192
  if (code === "steering.not_running") {
1180
1193
  send409(code, "sub-agent just finished — no longer accepting steers");