@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -1,8 +1,10 @@
1
1
  import { type MemoryBackend, type MemoryEntry, type PatchReport } from "@sema-agent/core";
2
2
  import type { MemorySyncStore } from "./plugins/memory-sync-store-pg.js";
3
+ import { type MemorySyncIdentityFlaw } from "./memory-sync-identity.js";
3
4
  /** 一轮同步请求(验型后的干净形状——`parseMemorySyncRequest` 是唯一铸造点)。 */
4
5
  export interface MemorySyncRequest {
5
- /** 对端标识(`CentralSyncCursor.peer`,per-(scope,peer) 游标分区键)= 客户端 transport 的身份(本仓的客户端腿:规范化的中心基址,`memory-sync-client.ts` `memorySyncPeerOf`)。 */
6
+ /** 对端标识(`CentralSyncCursor.peer`,per-(scope,peer) 游标分区键)= 客户端到达本中心用的同步身份(规范化基址)。验型时已过同步身份的唯一铸点
7
+ * (`memory-sync-identity.ts`,「声明的身份」座位:必须已是规范形、不带凭据);路由随后与本中心声明的身份按串比(不等 ⇒ 409,零写)。 */
6
8
  peer: string;
7
9
  /** 客户端游标的共同基线:id → rev at last completed sync round(空 map = 首轮)。 */
8
10
  baseRevs: Record<string, string>;
@@ -48,12 +50,16 @@ export interface MemorySyncResponse {
48
50
  * 同步一轮打满全集也在此内)。deletes 上限同基数放宽(全集删除也要能一轮表达)。 */
49
51
  export declare const MEMORY_SYNC_MAX_ENTRIES = 500;
50
52
  export declare const MEMORY_SYNC_MAX_DELETES = 2000;
53
+ /** 验型的答:干净形,或体的形错(HTTP 422),或 `peer` 不是声明的同步身份(HTTP 400 `request.field_invalid`,`field: "peer"`,拒因词 = 铸点的闭集,不回显值)。 */
51
54
  type ParseResult = {
52
55
  ok: true;
53
56
  value: MemorySyncRequest;
54
57
  } | {
55
58
  ok: false;
56
59
  error: string;
60
+ } | {
61
+ ok: false;
62
+ peerFlaw: MemorySyncIdentityFlaw;
57
63
  };
58
64
  /** Shape-validate + NORMALIZE the request body (picked-not-spread:只挑认识的字段重建,body 上的
59
65
  * 多余键绝不透传进数据面)。`scope` = 路径上已过 owner 门的 scope——每个条目的 scope 必须逐字
@@ -1,6 +1,6 @@
1
1
  import { computeEntryRev, MEMORY_ORIGIN_CAUSES, nextSyncBaseline, reconcileMemoryEntries, screenInboundEntries, } from "@sema-agent/core";
2
2
  import { exportMemoryScope } from "./memory-export.js";
3
- import { MEMORY_SYNC_PEER_COLUMN_CHARS } from "./plugins/memory-key-guards.js";
3
+ import { declaredMemorySyncIdentityFlaw } from "./memory-sync-identity.js";
4
4
  export const MEMORY_SYNC_MAX_ENTRIES = 500;
5
5
  export const MEMORY_SYNC_MAX_DELETES = 2000;
6
6
  export function parseMemorySyncRequest(body, scope) {
@@ -8,8 +8,11 @@ export function parseMemorySyncRequest(body, scope) {
8
8
  return { ok: false, error: "body must be a JSON object { peer, baseRevs, entries, deletes }" };
9
9
  const b = body;
10
10
  const peer = b["peer"];
11
- if (typeof peer !== "string" || peer.length === 0 || peer.length > MEMORY_SYNC_PEER_COLUMN_CHARS)
12
- return { ok: false, error: `peer must be a non-empty string of at most ${MEMORY_SYNC_PEER_COLUMN_CHARS} characters` };
11
+ if (typeof peer !== "string")
12
+ return { ok: false, error: "peer must be a string — the sync identity the client reaches this center at" };
13
+ const peerFlaw = declaredMemorySyncIdentityFlaw(peer);
14
+ if (peerFlaw !== undefined)
15
+ return { ok: false, peerFlaw };
13
16
  const rawBase = b["baseRevs"];
14
17
  if (rawBase === null || typeof rawBase !== "object" || Array.isArray(rawBase))
15
18
  return { ok: false, error: "baseRevs must be an object mapping entry id → rev (may be empty on the first round)" };
@@ -1,4 +1,4 @@
1
- import type { Logger } from "./logger.js";
1
+ import { type Logger } from "./logger.js";
2
2
  import type { Metrics } from "./metrics.js";
3
3
  /**
4
4
  * 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。
@@ -31,13 +31,17 @@ export declare const FAIL_OPEN_TAGS: {
31
31
  readonly cls: "F";
32
32
  readonly note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。";
33
33
  };
34
+ readonly "server.terminal.stored-result-code-folded": {
35
+ readonly cls: "F";
36
+ readonly note: "`terminal.ts` 的 `sealStoredResult`(出口封口的**读入口**,读座必填;调用点:`GET /v1/runs/:id` 终态臂的读面记忆腿与非终态臂 `sealStoredRunRowResult` = 读座 `run.result`,账本 `done` 行回放 = 读座 `ev.done.result`,`GET /v1/runs/:id/events` / `GET /v1/tasks/:id/turns` / trace SSE 三条耐久读腿共用):一条更早落盘的存量结果,失败码不合本服务的对外码形(小写、`.` 分族;例:驱动原码 `ER_*` / SQLSTATE `42703`)⇒ 读面上照写口同一只判官折成 `internal.store_error`(原码进 `message` 尾注,随结果过汇点脱敏)。每读一次记一次;detail = 定形字段 `original=<码 | <not-a-code>> outward=<对外词> messageBytes=<失败句字节数> at=<读座>`(失败句只报字节数;码只在它是码时报原值 —— 短 token 且秘密脱敏器原样放过它)。存量行的读面改写(与 `server.approval-card.retired-card-word-read` 同类):放行的最坏后果 = 无 —— 这一折与结果第一次离开引擎时的那一折同判同形;登记在这里是为了**响亮**且不随读次放大 —— 读面每次轮询 / 每个观看者 / 每次重连都重放同一批行,计数数的就是「旧失败被读了多少次」,warn 每进程一次;结果第一次离开引擎(写入口 `sealResultForExit`)那一折不走本 tag,每折一行 `[terminal] outward_failure_code_folded … at=exit`(每次都是一件新的失败)。计数持续非零 = 有升级前落盘的、码不合码形的失败行还在被读(随行收割 / 留存期满而归零)。";
37
+ };
34
38
  readonly "server.mcp-panel.last-leg-unreadable": {
35
39
  readonly cls: "F";
36
40
  readonly note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。";
37
41
  };
38
42
  readonly "server.ledger.forward-append-failed": {
39
43
  readonly cls: "F";
40
- readonly note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。";
44
+ readonly note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。同一出口还收 sync 腿 `file_link` 的账本孪生写失败(`type=file_link`:回放面缺这张文件卡,工具面不 fault;流还活着时 live 帧已发,流已断时这张卡就是整张没送达 —— 该腿有账本口,死直写不另记丢帧,丢与不丢只由这一笔判;修前裸吞)。";
41
45
  };
42
46
  readonly "server.tool-end-media.live-backlog-degraded": {
43
47
  readonly cls: "F";
@@ -105,7 +109,7 @@ export declare const FAIL_OPEN_TAGS: {
105
109
  };
106
110
  readonly "server.stream.frame-dropped-stream-closed": {
107
111
  readonly cls: "F";
108
- readonly note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。";
112
+ readonly note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。⚠️ `frame=engine_notice` 与 `frame=file_link` **只在本腿没有账本口的部署上计**(无 durable run 行):配了账本口的腿,这一帧在账本里,账本孪生即送达,死直写不再另记 —— 丢没丢由账本那一笔判(通告 `server.engine-notice.durable-append-failed`、文件卡 `server.ledger.forward-append-failed`),所以本计数里的这两型就是真丢。读遥测时别拿本 tag 当「壳在通告到达前断流」的旁证:有账本口的腿上那件事要从账本行时间与接入日志的 499 对出来。";
109
113
  };
110
114
  readonly "server.question.open-frame-undelivered": {
111
115
  readonly cls: "P-DEBT";
@@ -277,7 +281,7 @@ export declare const FAIL_OPEN_TAGS: {
277
281
  };
278
282
  readonly "server.engine-notice.durable-append-failed": {
279
283
  readonly cls: "F";
280
- readonly note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。";
284
+ readonly note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看;写失败时 live 可以多于账本 —— 用户看见了、回放里没有,本 tag 计一次);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。";
281
285
  };
282
286
  readonly "server.memory.locked-layer-probe-partial": {
283
287
  readonly cls: "F";
@@ -357,7 +361,7 @@ export declare const FAIL_OPEN_TAGS: {
357
361
  };
358
362
  readonly "server.approval-ask-audit.append-failed": {
359
363
  readonly cls: "F";
360
- readonly note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。";
364
+ readonly note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/有序关停/迟到结算各类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。起服收敛器写的收敛行**不在其列**:它是那一组的幂等锚与成因上界、是 boot 期写,写不进即拒启(S-2480 起)。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。";
361
365
  };
362
366
  readonly "server.run-terminal.redact-threw": {
363
367
  readonly cls: "F";
@@ -481,7 +485,7 @@ export declare const FAIL_OPEN_TAGS: {
481
485
  };
482
486
  readonly "server.approval-crash-convergence.cause-unreadable": {
483
487
  readonly cls: "F";
484
- readonly note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `deniedReason` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。";
488
+ readonly note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `cause` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。";
485
489
  };
486
490
  readonly "server.outbound.hop-member-unreadable": {
487
491
  readonly cls: "F";
@@ -599,8 +603,8 @@ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
599
603
  * 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
600
604
  * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
601
605
  *
602
- * ⚠️ **两条 question 臂在生产上已不可达**([ref] 之后,2026-08-07):本函数唯一的生产调用点是
603
- * `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧的直写口
606
+ * ⚠️ **两条 question 臂在生产上已不可达**([ref] 之后,2026-08-07):本函数的生产调用点有两处,都在 `routes/tasks.ts` ——
607
+ * `emitAsk` 断流臂与通告(`engine_notice`)的 live 口(后者只在本腿没有账本口时调,default 臂);question 与 question_complete 两型帧的直写口
604
608
  * (同文件 `writeQuestionLive`)先过断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;
605
609
  * 另一条 question 路(`runs.ts` 的 durable append)根本不经 emitAsk。elicitation 族自账本孪生起也只在**无 durable
606
610
  * run 行**的部署上抵达这里(有孪生的腿直写面已死时孪生即送达,不碰死连接)。在产的只剩那一形与 default。
@@ -608,6 +612,16 @@ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
608
612
  * 留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
609
613
  */
610
614
  export declare function failOpenTagForDroppedFrame(frameType: string): FailOpenTag;
615
+ /**
616
+ * 一次性 warn 的契约文字 `[sema] fail-open: <tag>` —— 跨仓黑盒锚(`audits/failopen-governance-176.md` §3 的 stderr 外部面),**唯一铸点**。
617
+ * 它是 JSON 日志行的 `msg`(默认落点)与注入口收到的裸文本行(测试落点)共用的那一句,两处不各拼一遍。
618
+ */
619
+ export declare function buildFailOpenAnchor(tag: FailOpenTag): string;
620
+ /** 一次性 warn 发声那一拍的事实:哪个 tag、此刻为止的累计次数。日志行字段闭集就是这两个。 */
621
+ export interface FailOpenAnnouncement {
622
+ readonly tag: FailOpenTag;
623
+ readonly count: number;
624
+ }
611
625
  /** {@link createFailOpenRecorder} 返回的活对象(有行为、有状态 ⇒ `create*` 而非 `build*`)。 */
612
626
  export interface FailOpenRecorder {
613
627
  /** 走到一条登记过的兜底臂。总不抛——观测本身绝不能变成故障(见 reapers 同族判据)。 */
@@ -643,8 +657,11 @@ export interface FailOpenRecorderDeps {
643
657
  * 用注入而不是直接 import:脱敏器自己 import 了本模块(它有一条 fail-open 留痕臂),直接 import 会成环。
644
658
  */
645
659
  redactDetail?: (detail: string) => string;
646
- /** 测试注入:替掉 stderr 契约行的落点。缺省直写 `process.stderr`。 */
647
- writeStderr?: (line: string) => void;
660
+ /**
661
+ * 测试注入:替掉一次性 warn 的落点;收到旧的裸文本行(`[sema] fail-open: <tag>\n`,契约文字 + 换行)与发声那一拍的事实。缺省 = {@link announceAsJsonLine}(stderr 上的一行 JSON 日志;
662
+ * 注入口名字沿用「stderr 契约行」时代,落点已是日志行 —— 一百多处测试用它静音这一件,不为改名牵动)。
663
+ */
664
+ writeStderr?: (line: string, facts: FailOpenAnnouncement) => void;
648
665
  /** 测试注入:替掉探针文件追加。缺省 `appendFileSync`(同步=进程猝死也不丢已记的行)。 */
649
666
  appendProbe?: (path: string, line: string) => void;
650
667
  }
@@ -1,4 +1,5 @@
1
1
  import { appendFileSync } from "node:fs";
2
+ import { createLogger } from "./logger.js";
2
3
  export const FAIL_OPEN_TAGS = {
3
4
  "server.approval-ask-journal.compaction-failed": {
4
5
  cls: "F",
@@ -12,13 +13,17 @@ export const FAIL_OPEN_TAGS = {
12
13
  cls: "F",
13
14
  note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。",
14
15
  },
16
+ "server.terminal.stored-result-code-folded": {
17
+ cls: "F",
18
+ note: "`terminal.ts` 的 `sealStoredResult`(出口封口的**读入口**,读座必填;调用点:`GET /v1/runs/:id` 终态臂的读面记忆腿与非终态臂 `sealStoredRunRowResult` = 读座 `run.result`,账本 `done` 行回放 = 读座 `ev.done.result`,`GET /v1/runs/:id/events` / `GET /v1/tasks/:id/turns` / trace SSE 三条耐久读腿共用):一条更早落盘的存量结果,失败码不合本服务的对外码形(小写、`.` 分族;例:驱动原码 `ER_*` / SQLSTATE `42703`)⇒ 读面上照写口同一只判官折成 `internal.store_error`(原码进 `message` 尾注,随结果过汇点脱敏)。每读一次记一次;detail = 定形字段 `original=<码 | <not-a-code>> outward=<对外词> messageBytes=<失败句字节数> at=<读座>`(失败句只报字节数;码只在它是码时报原值 —— 短 token 且秘密脱敏器原样放过它)。存量行的读面改写(与 `server.approval-card.retired-card-word-read` 同类):放行的最坏后果 = 无 —— 这一折与结果第一次离开引擎时的那一折同判同形;登记在这里是为了**响亮**且不随读次放大 —— 读面每次轮询 / 每个观看者 / 每次重连都重放同一批行,计数数的就是「旧失败被读了多少次」,warn 每进程一次;结果第一次离开引擎(写入口 `sealResultForExit`)那一折不走本 tag,每折一行 `[terminal] outward_failure_code_folded … at=exit`(每次都是一件新的失败)。计数持续非零 = 有升级前落盘的、码不合码形的失败行还在被读(随行收割 / 留存期满而归零)。",
19
+ },
15
20
  "server.mcp-panel.last-leg-unreadable": {
16
21
  cls: "F",
17
22
  note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。",
18
23
  },
19
24
  "server.ledger.forward-append-failed": {
20
25
  cls: "F",
21
- note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。",
26
+ note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。同一出口还收 sync 腿 `file_link` 的账本孪生写失败(`type=file_link`:回放面缺这张文件卡,工具面不 fault;流还活着时 live 帧已发,流已断时这张卡就是整张没送达 —— 该腿有账本口,死直写不另记丢帧,丢与不丢只由这一笔判;修前裸吞)。",
22
27
  },
23
28
  "server.tool-end-media.live-backlog-degraded": {
24
29
  cls: "F",
@@ -86,7 +91,7 @@ export const FAIL_OPEN_TAGS = {
86
91
  },
87
92
  "server.stream.frame-dropped-stream-closed": {
88
93
  cls: "F",
89
- note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。",
94
+ note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。⚠️ `frame=engine_notice` 与 `frame=file_link` **只在本腿没有账本口的部署上计**(无 durable run 行):配了账本口的腿,这一帧在账本里,账本孪生即送达,死直写不再另记 —— 丢没丢由账本那一笔判(通告 `server.engine-notice.durable-append-failed`、文件卡 `server.ledger.forward-append-failed`),所以本计数里的这两型就是真丢。读遥测时别拿本 tag 当「壳在通告到达前断流」的旁证:有账本口的腿上那件事要从账本行时间与接入日志的 499 对出来。",
90
95
  },
91
96
  "server.question.open-frame-undelivered": {
92
97
  cls: "P-DEBT",
@@ -258,7 +263,7 @@ export const FAIL_OPEN_TAGS = {
258
263
  },
259
264
  "server.engine-notice.durable-append-failed": {
260
265
  cls: "F",
261
- note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。",
266
+ note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看;写失败时 live 可以多于账本 —— 用户看见了、回放里没有,本 tag 计一次);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。",
262
267
  },
263
268
  "server.memory.locked-layer-probe-partial": {
264
269
  cls: "F",
@@ -338,7 +343,7 @@ export const FAIL_OPEN_TAGS = {
338
343
  },
339
344
  "server.approval-ask-audit.append-failed": {
340
345
  cls: "F",
341
- note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。",
346
+ note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/有序关停/迟到结算各类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。起服收敛器写的收敛行**不在其列**:它是那一组的幂等锚与成因上界、是 boot 期写,写不进即拒启(S-2480 起)。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。",
342
347
  },
343
348
  "server.run-terminal.redact-threw": {
344
349
  cls: "F",
@@ -441,7 +446,7 @@ export const FAIL_OPEN_TAGS = {
441
446
  },
442
447
  "server.approval-crash-convergence.cause-unreadable": {
443
448
  cls: "F",
444
- note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `deniedReason` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。",
449
+ note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `cause` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。",
445
450
  },
446
451
  "server.outbound.hop-member-unreadable": {
447
452
  cls: "F",
@@ -563,6 +568,14 @@ export function failOpenTagForDroppedFrame(frameType) {
563
568
  return "server.stream.frame-dropped-stream-closed";
564
569
  }
565
570
  }
571
+ export function buildFailOpenAnchor(tag) {
572
+ return `[sema] fail-open: ${tag}`;
573
+ }
574
+ let announceEmitter;
575
+ function announceAsJsonLine(facts) {
576
+ announceEmitter ??= createLogger("warn");
577
+ announceEmitter.warn(buildFailOpenAnchor(facts.tag), { tag: facts.tag, count: facts.count });
578
+ }
566
579
  export const FAIL_OPEN_PROBE_ENV = "SEMA_FAILOPEN_PROBE";
567
580
  const FAIL_OPEN_COUNTER = "fail_open_total";
568
581
  export function createFailOpenRecorder(deps = {}) {
@@ -571,7 +584,7 @@ export function createFailOpenRecorder(deps = {}) {
571
584
  const logged = new Set(deps.seedLogged ?? []);
572
585
  const backlog = new Map(deps.seedBacklog ?? []);
573
586
  const probePath = deps.probePath ?? process.env[FAIL_OPEN_PROBE_ENV];
574
- const writeStderr = deps.writeStderr ?? ((line) => void process.stderr.write(line));
587
+ const announce = deps.writeStderr ?? ((_line, facts) => announceAsJsonLine(facts));
575
588
  const appendProbe = deps.appendProbe ?? ((path, line) => appendFileSync(path, line));
576
589
  deps.metrics?.counter(FAIL_OPEN_COUNTER, "fallback (fail-open) arms traversed, by registered tag — see docs/FAIL-OPEN-CENSUS.md");
577
590
  if (deps.metrics !== undefined && backlog.size > 0) {
@@ -630,7 +643,7 @@ export function createFailOpenRecorder(deps = {}) {
630
643
  if (warned.has(tag))
631
644
  return;
632
645
  try {
633
- writeStderr(`[sema] fail-open: ${tag}\n`);
646
+ announce(`${buildFailOpenAnchor(tag)}\n`, { tag, count: counts.get(tag) ?? 1 });
634
647
  warned.add(tag);
635
648
  }
636
649
  catch {
@@ -1,5 +1,6 @@
1
- import { type PersistedTaskResult, type RunRecord } from "../plugins/store-contracts.js";
2
- import { type StoredResultReadSeat } from "../terminal.js";
1
+ import type { CollapseUnder, ExitResult, SeatKeyPaths, TextLeafPaths } from "../trace/core-keyset-guard.js";
2
+ import { type PersistedResultSideKeys, type PersistedTaskResult, type RunRecord } from "../plugins/store-contracts.js";
3
+ import { type PauseSeat, type StoredResultReadSeat } from "../terminal.js";
3
4
  /** 一次终局写口交给观测面的原料(store 侧装配,**未脱敏** —— 脱敏在本模块内做,见 {@link notifyRunTerminal})。 */
4
5
  export interface RunTerminalRecord {
5
6
  /** 这条 run 的账本 id = `GET /v1/runs/:id` 的 id(本仓 run 行主键名为 taskId,同一个值)。 */
@@ -165,35 +166,87 @@ export declare function redactModelText(raw: string): string;
165
166
  /** 回声座上的值留不留:形内 ⇒ `true`(原样留);`undefined` / 空串 ⇒ `false`(缺席本身,不计数);其余越界 ⇒ `false` + 计数。
166
167
  * 调用方按 `false` 删键 / 不铸键 —— 本函数是全部回声座(结果面 / 账本孪生 / 子代通告 / side-query 200 体)的**唯一**判决口。 */
167
168
  export declare function admitProviderEcho(value: unknown, seat: string): value is string;
168
- /** {@link redactTerminalResult} 会碰的全部顶层键 = 现行形里**不是** `"verbatim"` 的行(`"redacted"` ∪ [ref] 的
169
- * `"provider-echo"`)∪ 旧形平面键;`memoRedactTerminalResult` 的记忆键由它派生(两处同源,见 `RESULT_KEY_TREATMENT` 顶注)。
170
- * 🔴 [ref] 起改名(原 `REDACTABLE_RESULT_LEAVES`):回声键不脱敏但**会被改**(越界 ⇒ 删键),漏进记忆键集 = 读面记忆腿对存量行
171
- * O(1) 原样直返,读面纵深对那几片恒不生效([ref] 立案时 `editedFiles` 那一格的同一个病)。 */
169
+ /**
170
+ * 🔴 **[ref]:一条结果上的文本叶一张座表** —— 结果汇点({@link redactTerminalResult})判「哪片叶要脱、怎么脱」的**唯一**一处。
171
+ *
172
+ * 键 = 一条结果上**每一片自由文本叶**的路径(装得下任意文字的串 / 模板串 / 品牌串 / `unknown`;点分;数组元素记 `[]`;索引签名记 `*`),值 = 闭集处置词。结果形 = 本仓结算点交给封口的三种出口结果
173
+ * (普通腿 `TaskResult` / verify 腿 `VerificationResult` / cascade 腿 `CascadeRunResult`,与停驻座门 ⑩f 同一组)∪ server 侧记键(`PersistedResultSideKeys`)。
174
+ * 键集**由类型派生**(`trace/core-keyset-guard.ts` ⑩g 的 `TextLeafPaths`),表的型就是派生键集上的映射 ⇒ core 在任何一种出口结果的任何一层加一片
175
+ * 自由文本叶(或把数字座 / 闭集词座放宽成串、加一个 `unknown` 座)⇒ 少一行 **TS2741**;表里留了 core 没有的残条目 ⇒ **TS2353**。数字 / 布尔 /
176
+ * 闭集字面量联合装不下文字,不进表(闭集词逐字过境是它的型担保的,不是一行表上的判断)。
177
+ *
178
+ * 存在理由(两次「按层记键」的病,第二次是本表的来历):
179
+ * · [ref] U2:读面记忆手抄过自己的叶集 ⇒ 收成「`TaskResult` 顶层键 × 处置」一张表(`RESULT_KEY_TREATMENT`,本表取代,零别名);
180
+ * · [ref]:那张表按 `keyof TaskResult` 记,组合腿结果的**超形**(verify 的 `verification`、cascade 的五键)不在键集里 ⇒
181
+ * `verification.findings[]` / `evidence`(验证器模型写的自由文本)、`attempts[].gateError` / `diagnostics`(门的异常文案 / 门附的理由)原样落 run 行 /
182
+ * 账本 done 行 / 同步 200 体,读面整只回放;而且表只到顶层,叶在哪一层由六只手写臂各自去够(`terminal.message` / `reason`、`remoteEnvFailures[]`、
183
+ * `editedFiles[].path`、`result` / `salvagedOutput`、`rewindNotes[]`),回声键另有一张实现表外加一段加载期对账。
184
+ * 现在:一张表 + 一只走表函数({@link applyResultTextLeaves}),写入口(店 / 账本)与读出口(同步体 / 读面)同调。
185
+ *
186
+ * **处置词(闭集,穷尽 `switch` 消费 —— 加词不加臂 = 编译红)**:
187
+ * · `"model-text"` —— 模型自由文本 / 模型可控字节 ⇒ {@link redactModelText}([ref]:与 CoT 同一只 `redactSecrets`、同一张替换串表;不设体量上限,抛则占位);
188
+ * · `"error-text"` —— 有界的披露散文(异常文案 / 引擎披露行)⇒ {@link redactErrorMessage};
189
+ * · `"provider-echo"` —— 服务商回声、引擎逐字转交的不透明 id ⇒ 只做形状校验({@link admitProviderEcho}:形内原样、越界删键 + 计数),不过脱敏器;
190
+ * · `"provider-join-key"` —— 出身 provider 的跨面 join 键(工具调用 id)⇒ 本面逐字(形状在 core brain 入口一次施加,见 {@link admitProviderEcho} 顶注);
191
+ * · `"verbatim"` —— 本栈内部铸造的 id / 部署配置 / 引擎观测面 ⇒ 不碰。**唯一的模型可控逐字座**是 `structuredOutput`(见该行注)。
192
+ * · `"outward-code"` —— 机器码座(因由的 `terminal.code`、cascade 每一级的 `attempts[].errorCode`:同一类码)⇒ 本汇点不碰 —— 折码的属主是出口封口
193
+ * (`terminal.ts` 的写封 / 读封,{@link sealResultForExit}):出了本服务码形的码在汇点之前已折成 `internal.store_error` 并留运维线索,汇点再折一次就是第二个属主。
194
+ * · 归属词 —— 该路径下是一棵**另有派生表**的子树,本表只登记属主,不在两张表里各判一次:`"stats-tree"`(stats 五表)/ `"terminal-detail"`(detail 三臂表)/
195
+ * `"api-failure"`({@link API_FAILURE_TREATMENT};结果面与账本子代通告面两张脸共用)/ `"stranded-answer"`({@link STRANDED_ANSWER_TREATMENT},逐行)。
196
+ * 属主表的型是 ⑩g 的 `OwnerTable`:每个成员只许盖得住它之下全部自由文本叶的词 ⇒ 子树里多一片叶而属主那一行盖不住(数字袋 `stats.nested` 里加一个串)
197
+ * 同样 tsc 红 —— 折叠只换了「在哪张表上红」,不留盲区。
198
+ * 停驻座的凭据与门(`PAUSE_SEATS` 的六条路径)**不在本表**:凭据归出口封口摘、门归 {@link redactCheckpointGate};键集从座表派生后减掉,不手抄。
199
+ *
200
+ * [ref] / [ref](7.75.1)那条规则在这里成文:**结果上的模型自由文本一律过脱敏器**,没有「别的 sink 自判」这条退路;留 `"verbatim"` 的是本栈铸的
201
+ * id / 目录 id / 部署配置(`structuredOutput` 一座除外,见行内注),逐叶理由见行内注。
202
+ */
203
+ export type TextLeafTreatment = "model-text" | "error-text" | "provider-echo" | "provider-join-key" | "verbatim" | "outward-code" | "stats-tree" | "terminal-detail" | "api-failure" | "stranded-answer";
204
+ /**
205
+ * 子树另有属主的路径前缀 —— 派生出的叶路径落在它之下就折成它本身(⑩g 的 `CollapseUnder`:按**路径**折,不按类型停 —— 结构类型的「停在某型」
206
+ * 会把恰好同形的别处子树一起吞掉)。前四只各对应一只归属词;`attempts.[].model` 是 core 的 `ModelRef`(`string | Model`):本服务的梯子只给目录 id 串
207
+ * (`http/run-meta.ts` 的 `cascadeConfig`,后台腿与同步腿同一只),对象臂不可达 ⇒ 整座按串处置,不展开 `Model` 的二十余格(对象臂真到达那天,它的凭据座
208
+ * —— `headers` —— 是出口封口的事,与 `PAUSE_SEATS` 同类,不是文本表的事)。
209
+ * ⚠️ 五只前缀里**只有这一只没有属主表接着判**(前四只的属主表是 `OwnerTable`,子树里多一片叶盖不住即红):`Model` 加一个串成员不会红 ——
210
+ * 它与「对象臂不可达」是同一个前提,前提翻了(梯子交对象)就得给它一张属主表或展开成行。
211
+ */
212
+ type TextLeafOwnedPrefix = "stats" | "terminal.detail" | "apiFailure" | "strandedHumanAnswers.[]" | "attempts.[].model";
213
+ /** 停驻座的凭据 / 门路径(从 `PAUSE_SEATS` 派生):先折叠(门是一棵子树),再从键集里减掉。 */
214
+ type PauseSeatPath = SeatKeyPaths<PauseSeat, "token" | "gate">;
215
+ /** 本表的键集(派生)。 */
216
+ export type ResultTextLeafPath = Exclude<CollapseUnder<TextLeafPaths<ExitResult | PersistedResultSideKeys>, TextLeafOwnedPrefix | PauseSeatPath>, PauseSeatPath>;
217
+ export declare const RESULT_TEXT_LEAVES: {
218
+ readonly [P in ResultTextLeafPath]: TextLeafTreatment;
219
+ };
220
+ /** {@link redactTerminalResult} 会碰的全部顶层键 = 文本叶表上**会动字节**的行的根键 ∪ 旧形平面键 ∪ 停驻座的根键 ∪ `editedFiles`(条目上界);
221
+ * `memoRedactTerminalResult` 的记忆键由它派生(两处同源,不手抄)。漏进记忆键集 = 读面记忆腿对存量行那几片 O(1) 原样直返,读面纵深恒不生效
222
+ * ([ref] 立案时 `editedFiles` 那一格、[ref] 立案时 `attempts` 那一格的同一个病 —— 后者修前不在任何键表里)。
223
+ * 回声行不脱敏但**会被改**(越界 ⇒ 删键),所以同样进集([ref] 起名 `TREATED_…`,原 `REDACTABLE_…`)。 */
172
224
  export declare const TREATED_RESULT_LEAVES: readonly string[];
173
- export declare function redactTerminalResult<T extends {
174
- terminal?: unknown;
175
- /** 旧形(≤7.63.0)的三个顶层座;形上取 `unknown` —— 读盘面对盘上字节不作假设。 */
176
- errorMessage?: unknown;
177
- blockedReason?: unknown;
178
- checkpointGate?: unknown;
179
- remoteEnvFailures?: readonly {
180
- message?: string | undefined;
181
- }[] | undefined;
182
- /** [ref]:模型自由文本两叶 + 引擎披露散文一族。形上同样取 `unknown`(读面纪律,见下)。 */
183
- result?: unknown;
184
- salvagedOutput?: unknown;
185
- rewindNotes?: unknown;
186
- /** 🔴 形上**故意**是 `unknown`:本函数也是**读面**(旧持久 blob / 非 core Runner 的形不可信),
187
- * 声明成 `{path,edits}[]` 会逼每一个喂坏形的调用点铸一次宽松断言 —— 而「坏形从哪来」正是本座要
188
- * 处理的事(同一份不信任在 `remoteEnvFailures` 的实现里也已成文)。运行期逐项判形,不抛。 */
189
- editedFiles?: unknown;
190
- /** [ref]:形上同样 `unknown` —— 本函数是读盘面,`stats` 的树形逐层现判(见 {@link redactStats})。 */
191
- stats?: unknown;
192
- /** [ref]:provider 回声三键(形上 `unknown`,读盘面纪律同上;处置见 {@link PROVIDER_ECHO_SHAPERS})。 */
193
- apiFailure?: unknown;
194
- strandedHumanAnswers?: unknown;
195
- responseModel?: unknown;
196
- } | null | undefined>(result: T): T;
225
+ /**
226
+ * 同口:终态 `result` 整块也持久化(`GET /v1/runs/:id` 直出),写入前把里面的**自由文本座**脱敏,其余键
227
+ * 逐字不动;`undefined`/`null` 透传。
228
+ *
229
+ * 🔴 [ref](core 7.6.0 [ref] D-8):自由文本座**搬家了** —— 失败文案与阻塞理由从顶层平面键
230
+ * (`errorMessage` / `blockedReason`)搬进因由 `terminal` 的两条臂(`failed.message` / `blocked.reason`)。
231
+ * 本函数**两种形都认**,而这不是「兼容读臂」(codex 对抗复审 r1 [high],亲读验真后**改判**;首版删掉了旧键
232
+ * 臂,那是错的):本函数是**读盘面**,而 `task_run.result` / `task_event(done).result` 里躺着 7.63.0 及以前
233
+ * 写下的**真字节**,它们本来就是平面形。写口只剩因由一条(没有任何东西会再写出旧形),所以这里不存在
234
+ * 「两个写者」;而不认旧形的代价是**可复现的凭据回放**:上线前落盘的原文经 `GET /v1/runs/:id/events` 重新
235
+ * 明文外发(读面这一层纵深正是为那批行存在的)。⇒ 规则仍只有一条:**读盘认盘上真有的形,写盘只写一种形**。
236
+ *
237
+ * 读面也走本函数,所以形上一律取宽:旧持久 blob 与非 core 的 Runner 实现给的形不可信,运行期逐层判形、不抛(走表函数与各属主同款不信任)。
238
+ */
239
+ /**
240
+ * **结果整块的唯一汇点** —— 三孪生 store 的 `setTerminal` / park 两口(经 {@link sealRunRowResult})、`redactLedgerEventData` 的 `done.result`、
241
+ * `POST /v1/tasks` 200 体(`http/routes/tasks.ts` 的 `mintSubmitExitResult`)、读面(`GET /v1/runs/:id` / `/events`,经 {@link memoRedactTerminalResult})
242
+ * 全调它,于是新铸与**存量回放**同时被覆盖(core 铸点修不回溯已写行)。四段,各有一个属主:
243
+ * ① `editedFiles` 条目上界(容器界,不是文本处置;先截后走表,不为越界条目付扫描);
244
+ * ② **文本叶表** {@link RESULT_TEXT_LEAVES} 一张走完({@link applyResultTextLeaves})—— 自由文本 / 回声 / 两棵另有属主的子树都在这一步;
245
+ * ③ 停驻座的门({@link PAUSE_SEATS} × {@link redactCheckpointGate});
246
+ * ④ 旧形(≤7.63.0)的三个顶层平面键 —— 盘上冻结形,不在任何活类型上(见 {@link LEGACY_RESULT_LEAVES})。
247
+ * 形上取宽:本函数也是**读盘面**(旧持久 blob / 非 core Runner 的形不可信),每一段逐层判形、不抛;字节没变 ⇒ **同一引用**。
248
+ */
249
+ export declare function redactTerminalResult<T extends object | null | undefined>(result: T): T;
197
250
  /**
198
251
  * 🔴 **[ref] —— run 行 `result` 列的唯一入口**:先过出口封口({@link sealResultForExit}:摘恢复凭据 + 折域外码),
199
252
  * 再过 {@link redactTerminalResult}。三家 run 店(SQL 两方言 / file / memory)的**三个**写口 —— `setTerminal` 与
@@ -207,10 +260,14 @@ export declare function redactTerminalResult<T extends {
207
260
  *
208
261
  * `null` 原样(= 这条腿没有结果可落:引擎没给终局就收流 / 断连竞态 park / 重开 re-park)。
209
262
  *
210
- * `readSeat`:读面借这同一只组合读存量行时(`GET /v1/runs/:id` 的非终态臂)给出读座 —— 只决定折码运维行是「一件新的失败」还是「同一行的再观看」
211
- * (见 `sealResultForExit`);店的写口不给。
263
+ * 本函数是**写入口**(结果第一次落盘 ⇒ 折码运维线索 `at=exit`);读面读存量行用 {@link sealStoredRunRowResult}(读座必填)。
264
+ */
265
+ export declare function sealRunRowResult(result: PersistedTaskResult | null): PersistedTaskResult | null;
266
+ /**
267
+ * {@link sealRunRowResult} 的**读入口**(`GET /v1/runs/:id` 的非终态臂读停驻行):同一套封口 + 同一只汇点,只差折码运维线索的观看点 ——
268
+ * 读座必填({@link StoredResultReadSeat}),再观看走 fail-open 纪律(见 `terminal.ts` 的 `sealStoredResult`)。
212
269
  */
213
- export declare function sealRunRowResult(result: PersistedTaskResult | null, readSeat?: StoredResultReadSeat): PersistedTaskResult | null;
270
+ export declare function sealStoredRunRowResult(result: PersistedTaskResult | null, seat: StoredResultReadSeat): PersistedTaskResult | null;
214
271
  /** [ref](第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行
215
272
  * (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。
216
273
  * 三孪生 `appendEvent` 入口 + 事件读面(旧行纵深)同调本函数:只认 `done.result`(整块走 `redactTerminalResult`)与
@@ -255,7 +312,7 @@ export declare function redactLeaderResultWithFlag(result: unknown): {
255
312
  };
256
313
  export declare function memoRedactedRead<T>(seat: string, raw: T, redact: (v: T) => T): T;
257
314
  /** run 终态 result 的读面记忆只按**本函数会改字节的那几叶**({@link TREATED_RESULT_LEAVES}:脱敏叶 ∪ [ref] 回声叶 ∪ 旧形平面键,属主是
258
- * `RESULT_KEY_TREATMENT`)做键与留体——
315
+ * 文本叶表 {@link RESULT_TEXT_LEAVES})做键与留体——
259
316
  * 第十六轮重扫:按整块 result 摘要每次轮询要 stringify 一份可能数 MB 的 result,且大 result 的脏行永远存不进去。叶全缺席
260
317
  * ⇒ O(1) 直返(`redactTerminalResult` 本就同引用)。
261
318
  * 🔴 [ref](core 7.6.0 D-8):新形把前两片搬进了 `terminal`(同属一个键,按它整只做叶更省);旧形的两片
@@ -294,4 +351,5 @@ export declare function redactLedgerEventForRead(type: string, data: unknown): u
294
351
  export declare function diffScanShadowCountForTest(): number;
295
352
  export declare function resetDiffScanShadowCountForTest(): void;
296
353
  export declare function resetReadRedactionMemoForTest(): void;
354
+ export {};
297
355
  //# sourceMappingURL=run-terminal.d.ts.map