@sema-agent/sdk 6.8.0 → 6.10.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.
package/openapi.yaml CHANGED
@@ -56,8 +56,17 @@ openapi: 3.1.0
56
56
  # live — implemented & verified server-side; in the contract-test assertion set.
57
57
  # draft — verb exists but the response shape is not fully pinned; co-design pending (do not over-assert).
58
58
  # planned — NOT yet implemented server-side; documented for SDK/producer alignment; EXCLUDED from
59
- # contract-test assertions until it ships.
60
- # gated — implemented but off by a server deploy flag (e.g. leader); not on the default product path.
59
+ # contract-test assertions until it ships. ⚠️ It is a claim about the SERVER, and it EXPIRES:
60
+ # once the face ships, move it to `live`/`gated` the same day. Two gates now police that —
61
+ # `spec-path-drift-gate` (planned ∩ paths the SDK actually calls must be empty) and
62
+ # `test/live/route-existence.test.ts` (a planned path must have NO handler in the pinned
63
+ # server artifact). Both exist because `POST /v1/tasks/{taskId}/asks/{askId}/decision` kept
64
+ # this label for two server releases after it went live.
65
+ # gated — implemented but off by a server deploy flag (e.g. leader, STREAM_APPROVAL_ENABLED); answers
66
+ # 501 where the flag/deps are unmet. Says nothing about the flag's DEFAULT — that can differ
67
+ # per server version, so read the matching `capabilities` bit, never this label.
68
+ # retired — the face was DELETED server-side; the entry is a tombstone kept so consumers generated from
69
+ # an older spec can find out where it went. No deployment serves it.
61
70
  # ─────────────────────────────────────────────────────────────────────────────
62
71
 
63
72
  info:
@@ -907,7 +916,7 @@ paths:
907
916
  responses:
908
917
  '200':
909
918
  # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:这条 200 此前只有一句 description、**零 content
910
- # 声明**(机器视角=「无 body」),而 server 真发 JSON(`routes/runs.ts:1155`,与 output 面同一个
919
+ # 声明**(机器视角=「无 body」),而 server 真发 JSON(`routes/runs.ts`,与 output 面同一个
911
920
  # sendJson,含 g14 的条件 `cursorSemantics`)。补接同一个 schema。
912
921
  description: 'Stopped — same projection shape as the output verb.'
913
922
  content:
@@ -1077,8 +1086,8 @@ paths:
1077
1086
  schema:
1078
1087
  type: object
1079
1088
  # 封闭(census 批2 第二段):三个 200 铸造点都是单键字面量 —— `{deleted:false}` 两处
1080
- # (未知会话 / 外租户,`routes/sessions.ts:491,495`)+ purgeSession 的 `{deleted}` 返回
1081
- # (`http/server.ts:255` 的签名就是这一个键;它的另一臂 `{active}` 走 409)。
1089
+ # (未知会话 / 外租户,`routes/sessions.ts`)+ purgeSession 的 `{deleted}` 返回
1090
+ # (`http/server.ts` 的签名就是这一个键;它的另一臂 `{active}` 走 409)。
1082
1091
  additionalProperties: false
1083
1092
  required: [deleted]
1084
1093
  properties:
@@ -1115,7 +1124,7 @@ paths:
1115
1124
  application/json:
1116
1125
  schema:
1117
1126
  type: object
1118
- # 封闭:铸造点 `routes/sessions.ts:469` 是单键字面量 `{ sessionId: newId }`。
1127
+ # 封闭:铸造点 `routes/sessions.ts` 是单键字面量 `{ sessionId: newId }`。
1119
1128
  additionalProperties: false
1120
1129
  required: [sessionId]
1121
1130
  properties:
@@ -1157,7 +1166,7 @@ paths:
1157
1166
  application/json:
1158
1167
  schema:
1159
1168
  type: object
1160
- # 封闭:铸造点 `routes/sessions.ts:212` 是两键字面量;版本号本身走 ETag 响应头,不进体。
1169
+ # 封闭:铸造点 `routes/sessions.ts` 是两键字面量;版本号本身走 ETag 响应头,不进体。
1161
1170
  additionalProperties: false
1162
1171
  required: [sessionId, leafId]
1163
1172
  properties:
@@ -1295,7 +1304,7 @@ paths:
1295
1304
  description: Delivered onto the live stream.
1296
1305
  content:
1297
1306
  application/json:
1298
- # 封闭(census 批2 第三段,2026-07-30):live 臂铸造点 `routes/notify-wake.ts:98` 是两键字面量。
1307
+ # 封闭(census 批2 第三段,2026-07-30):live 臂铸造点 `routes/notify-wake.ts` 是两键字面量。
1299
1308
  # 4.3.0:形提成具名 `SessionNotifyResult` 的 live 臂(SDK 同批给了判别联合返回型)。
1300
1309
  schema:
1301
1310
  type: object
@@ -1308,7 +1317,7 @@ paths:
1308
1317
  description: No live stream — parked in the session inbox, drained on the next stream open.
1309
1318
  content:
1310
1319
  application/json:
1311
- # 封闭 + note 补 required:parked 臂铸造点 `routes/notify-wake.ts:116` 是三键字面量,note 无条件。
1320
+ # 封闭 + note 补 required:parked 臂铸造点 `routes/notify-wake.ts` 是三键字面量,note 无条件。
1312
1321
  schema:
1313
1322
  type: object
1314
1323
  additionalProperties: false
@@ -1352,7 +1361,7 @@ paths:
1352
1361
  responses:
1353
1362
  '200':
1354
1363
  # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:此前这里写的是 **202** + required [taskId,…]。
1355
- # 亲读真码:wake 的成功腿是 `driveResumeIntoRunLog` 的返回(`server.ts:2289` 与取消臂 `:2240`),
1364
+ # 亲读真码:wake 的成功腿是 `driveResumeIntoRunLog` 的返回(`server.ts`,成功腿与取消臂两处铸造点),
1356
1365
  # **恒为 200**(wake 是同步驱动到终局/再挂起,不是 accepted 回执),且 `taskId` 有条件
1357
1366
  # (无活跃行即省略——ApprovalDecisionResult 同一铸造点的同一注)。旧 202+required taskId 双谎。
1358
1367
  description: Woken — the resumed leg ran to its next settle (terminal, re-suspended, or needs_review).
@@ -1405,8 +1414,7 @@ paths:
1405
1414
  description: The snapshot listing.
1406
1415
  content:
1407
1416
  application/json:
1408
- # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts:624-629`(信封)与
1409
- # `:620`(行)。条件键=信封 `latest`(零快照即省略)、行 `bytes`(blobSizes 面缺席=诚实省略)。
1417
+ # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts`(信封与行两处)。条件键=信封 `latest`(零快照即省略)、行 `bytes`(blobSizes 面缺席=诚实省略)。
1410
1418
  # 4.3.0:形提成具名 `WorkspaceSnapshotPage`(SDK 同批把返回位的内联字面量也提成了 exported type,
1411
1419
  # 让 spec-resource-schema-gate 罩得住 —— CAPS-OPS-8)。
1412
1420
  schema: { $ref: '#/components/schemas/WorkspaceSnapshotPage' }
@@ -1450,8 +1458,7 @@ paths:
1450
1458
  description: One page of the tree.
1451
1459
  content:
1452
1460
  application/json:
1453
- # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts:651-657`(信封)与
1454
- # `:654`(行)。条件键=信封 `nextOffset`(尾页省略)、行 `size`(blobSizes 面缺席省略)。
1461
+ # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts`(信封与行两处)。条件键=信封 `nextOffset`(尾页省略)、行 `size`(blobSizes 面缺席省略)。
1455
1462
  # 4.3.0:形提成具名 `WorkspaceTreePage`(理由同 list 腿 —— CAPS-OPS-8)。
1456
1463
  schema: { $ref: '#/components/schemas/WorkspaceTreePage' }
1457
1464
  '400': { $ref: '#/components/responses/BadRequest' }
@@ -1571,7 +1578,7 @@ paths:
1571
1578
  application/json:
1572
1579
  schema:
1573
1580
  type: object
1574
- # 封闭:铸造点 `routes/sessions.ts:540` 是单键字面量。BC-3 保证 `rules` 恒非 null
1581
+ # 封闭:铸造点 `routes/sessions.ts` 是单键字面量。BC-3 保证 `rules` 恒非 null
1575
1582
  # (无策略的已属会话回 `{rev:0}` 空记录,绝不回 `{rules:null}`)。
1576
1583
  additionalProperties: false
1577
1584
  required: [rules]
@@ -1612,7 +1619,7 @@ paths:
1612
1619
  application/json:
1613
1620
  schema:
1614
1621
  type: object
1615
- # 封闭:铸造点 `routes/sessions.ts:558` 是单键字面量 `{ rules: stored }`。
1622
+ # 封闭:铸造点 `routes/sessions.ts` 是单键字面量 `{ rules: stored }`。
1616
1623
  additionalProperties: false
1617
1624
  required: [rules]
1618
1625
  properties:
@@ -1656,8 +1663,11 @@ paths:
1656
1663
 
1657
1664
  # ── 2c session-sync (P1d) — the cloud as a SYNC PEER ──────────────────────────────────────────────────────────
1658
1665
  # The local shell's local backend ⇄ this cloud service. PULL a session's whole state out, or PUSH the local
1659
- # peer's in. Gate the whole surface off `capabilities.sessionSync` (= durable backend + entry export + a
1660
- # file-snapshot store). Owner-gated like fork/delete (§9): a PULL 404s a non-owner; a PUSH is own-or-FRESH.
1666
+ # peer's in. Gate the whole surface off `capabilities.sessionSync` — a FOUR-way conjunction (durable backend
1667
+ # AND entry export AND import STAGING AND a file-snapshot store). The staging conjunct is not decorative: a
1668
+ # three-conjunct predicate was disproven in the field ([2024]) when a warm-cache wrapper store forwarded the
1669
+ # other three and dropped the staging family, so caps said true while PUSH Phase A 501'd forever.
1670
+ # Owner-gated like fork/delete (§9): a PULL 404s a non-owner; a PUSH is own-or-FRESH.
1661
1671
  /v1/sessions/{sessionId}/sync/manifest:
1662
1672
  parameters:
1663
1673
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -1675,7 +1685,7 @@ paths:
1675
1685
  `/sync/entries`) + per-snapshot relPath→blobHash + policy + anchors + leaf. The local peer feeds `entryIds`
1676
1686
  to the §7 classifier to decide fast-forward/fork BEFORE pulling the entry stream + only the blob hashes it
1677
1687
  lacks. Owner-scoped (non-owner → 404, no oracle); 400 on an id that is not uuid-SHAPED — 🔴 the sync family's
1678
- gate is `isUuidShape` (ANY uuid version), NOT `isUuidV7`: server `routes/session-sync.ts:135-138` deliberately
1688
+ gate is `isUuidShape` (ANY uuid version), NOT `isUuidV7`: server `routes/session-sync.ts` deliberately
1679
1689
  keeps a v4-keyed legacy shell session pushable (it was creatable via the lenient submit face), while
1680
1690
  crafted-id classes stay shut out. `fork`/`delete`/`policy` DO use `isUuidV7` — only this family is lenient;
1681
1691
  501 without a durable session store.
@@ -1686,7 +1696,7 @@ paths:
1686
1696
  application/json:
1687
1697
  schema:
1688
1698
  type: object
1689
- # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts:165` 是单键字面量。
1699
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts` 是单键字面量。
1690
1700
  additionalProperties: false
1691
1701
  required: [manifest]
1692
1702
  properties:
@@ -1889,7 +1899,7 @@ paths:
1889
1899
  description: >
1890
1900
  🔴 OPTIONAL content digest of the SOURCE's whole entry log (core `sessionLogDigest`, scheme
1891
1901
  `sema-log-v3`; this leg's core floor is ^1.416.0). Server producer/consumer:
1892
- `routes/session-sync.ts:295,310,357-366`. This is THE key that makes `ImportStaged.payloadVerified:true`
1902
+ `routes/session-sync.ts`. This is THE key that makes `ImportStaged.payloadVerified:true`
1893
1903
  reachable — until 4.3.0 the response schema promised a `payloadVerified`/`entry-ids+digest` pair
1894
1904
  that the request side had no way to trigger. Rationale: `identical` is classified from entry-id
1895
1905
  SETS only, and an entry id is a uuidv7 (NOT content-addressed), so "same ids, different payloads"
@@ -2086,7 +2096,7 @@ paths:
2086
2096
  Owner-gate / unknown session (no existence oracle), OR — [2400] CB-2, declared 2026-08-02 — the core
2087
2097
  code `checkpoint.not_found`: the pending checkpoint this decide addressed is gone. It is the ONE
2088
2098
  member of the mirrored `checkpoint.*` family the server sends as a 404 rather than a 409
2089
- (`src/http/server.ts:2199`: `e.code === "checkpoint.not_found" ? 404 : 409`), so the SDK maps it to
2099
+ (`src/http/server.ts`: `e.code === "checkpoint.not_found" ? 404 : 409`), so the SDK maps it to
2090
2100
  NotFoundError while the rest of the family maps to ConflictError. Client action: refetch the inbox —
2091
2101
  do not retry this decide.
2092
2102
  content:
@@ -2101,14 +2111,14 @@ paths:
2101
2111
  ConflictError vs ApprovalBindingMismatchError. binding_mismatch = SAFETY signal: refetch + re-present
2102
2112
  to the human, NEVER auto-retry.
2103
2113
  FOURTH member ([2400] CB-2, declared 2026-08-02): `gate_not_tool_approval` — the suspension this
2104
- sessionId is parked on is NOT a tool-approval gate (`src/http/server.ts:1581` rejects pre-CAS when the
2114
+ sessionId is parked on is NOT a tool-approval gate (`src/http/server.ts` rejects pre-CAS when the
2105
2115
  gate kind is neither `human` nor `irreversible_ask`: nothing is consumed, no model leg burns). It is
2106
2116
  the THIRD member of the wrong-door guard family alongside `gate_not_resumable` (§4b) and
2107
2117
  `gate_not_plan_review` (§4c) → SDK `GateNotToolApprovalError`. Client action: route by the row's
2108
2118
  `gateKind` — `plan_review` → `POST /v1/assistant/tasks/{taskId}/plan_review`, `resource_limit` →
2109
2119
  `POST /v1/assistant/tasks/{taskId}/resume`. Retrying the same body never succeeds.
2110
2120
  FIFTH group ([2400] CB-2): the core `checkpoint.*` family the server mirrors verbatim
2111
- (`src/http/server.ts:2199-2206`) — `checkpoint.gate_mismatch` / `checkpoint.reopen_failed` /
2121
+ (`src/http/server.ts`) — `checkpoint.gate_mismatch` / `checkpoint.reopen_failed` /
2112
2122
  `checkpoint.reopen_revote` / `checkpoint.already_exists` / `checkpoint.resume_aborted` /
2113
2123
  `checkpoint.unsupported_version` all arrive as 409 and fall on the SDK's `checkpoint.` PREFIX family
2114
2124
  (→ ConflictError, `.errorCode` carried verbatim; an OPEN set, so a future core code degrades with
@@ -2246,7 +2256,7 @@ paths:
2246
2256
  application/json:
2247
2257
  schema:
2248
2258
  type: object
2249
- # 封闭:铸造点 `routes/approvals-assistant.ts:146` 的 200 臂是单键字面量
2259
+ # 封闭:铸造点 `routes/approvals-assistant.ts` 的 200 臂是单键字面量
2250
2260
  # `{ revoked: true }`(404 臂是另一个字面量,走 NotFound 信封)。
2251
2261
  additionalProperties: false
2252
2262
  required: [revoked]
@@ -2475,7 +2485,7 @@ paths:
2475
2485
  application/json:
2476
2486
  schema:
2477
2487
  type: object
2478
- # 🔴 封闭 + 两处 spec 谎修正(census 批2 第三段,2026-07-30),铸造点 `routes/trace-usage.ts:61`:
2488
+ # 🔴 封闭 + 两处 spec 谎修正(census 批2 第三段,2026-07-30),铸造点 `routes/trace-usage.ts`:
2479
2489
  # ① 顶层信封 server 真发 `{sinceSec, rows}` —— `sinceSec`(生效的窗口秒数,默认 86400)
2480
2490
  # 此前整键不在 spec 里;
2481
2491
  # ② 行 `source` 三个 store 实现(sql/pg/local 的 sourceSummary)签名都是 `string | null`
@@ -2495,7 +2505,7 @@ paths:
2495
2505
  status: { type: string }
2496
2506
  count: { type: integer }
2497
2507
  '401': { $ref: '#/components/responses/Unauthorized' }
2498
- '501': { $ref: '#/components/responses/NotImplemented' } # 无 durable run store(capability.run_store_required)——`routes/trace-usage.ts:56`,此前漏登记
2508
+ '501': { $ref: '#/components/responses/NotImplemented' } # 无 durable run store(capability.run_store_required)——`routes/trace-usage.ts`,此前漏登记
2499
2509
 
2500
2510
  /v1/tasks/{taskId}/artifacts:
2501
2511
  parameters:
@@ -2536,7 +2546,7 @@ paths:
2536
2546
  # 🔴 修正(2026-07-25):此处原先 $ref 到 `AgentEvent`,是真错 —— 而且本文件自己就矛盾:专门为这条路写的
2537
2547
  # `TraceStreamEvent` schema 的描述里明写「A DISTINCT vocabulary from AgentEvent」,却没有任何地方引用它
2538
2548
  # (它是本文件里的一个孤儿 schema)。按 server 真码两侧取证后确定 TraceStreamEvent 才是对的:
2539
- # `streamTaskTrace` 每帧走 `mapTraceEvent`(`trace/project.ts:530`),发的是 SSE **event 名**
2549
+ # `streamTaskTrace` 每帧走 `mapTraceEvent`(`trace/project.ts`),发的是 SSE **event 名**
2540
2550
  # `block-thinking-delta` / `block-content-delta` / `tool-call` / `tool-result` / `turn` /
2541
2551
  # `prompt-assembled` / `done` / `error`,帧里**没有** `type` 字段;SDK 的 `trace.stream()` 注释逐字同款。
2542
2552
  # 照旧声明生成的客户端会去 switch 一个不存在的 `type` ⇒ 这条流路对它完全不可用。
@@ -2680,7 +2690,7 @@ paths:
2680
2690
  schema:
2681
2691
  type: object
2682
2692
  # 🔴 封闭 + `defaultModel` 补登记(census 批2 第三段,2026-07-30):铸造点
2683
- # `routes/capabilities.ts:322` 三键字面量。`defaultModel`([865]③ id/name 撕裂消解,server
2693
+ # `routes/capabilities.ts` 三键字面量。`defaultModel`([865]③ id/name 撕裂消解,server
2684
2694
  # 已发多版)SDK `models.list()` 的返回类型早就带,**spec 是唯一没登记的一侧** —— 生成式
2685
2695
  # 消费方对它全瞎。`name` 条件(目录无命中即省略),`id` 恒在。
2686
2696
  additionalProperties: false
@@ -2817,7 +2827,7 @@ paths:
2817
2827
  post:
2818
2828
  tags: [questions]
2819
2829
  operationId: askDecision
2820
- x-status: planned # design/172 §3.2 (#151 车4) — UNRELEASED; ships with server 7.3.0, gated OFF by STREAM_APPROVAL_ENABLED. SDK toolApprovals.decideAsk() 消费。
2830
+ x-status: gated # design/172 §3.2 (#151 车4) — SHIPPED in server 7.3.0 (7.3.0/7.4.0 both on npm); off-by-flag (STREAM_APPROVAL_ENABLED, default OFF ≤7.4.0 / ON ≥7.5.0) ⇒ 501 feature.approval_ask_disabled when the five-way gate is unmet. SDK toolApprovals.decideAsk() 消费。
2821
2831
  summary: Settle an IN-STREAM approval card (design/172 streaming approval protocol).
2822
2832
  description: >
2823
2833
  The in-stream sibling of POST /v1/tool-approvals/{id}/respond: the approver is live on the SSE stream, so
@@ -3123,7 +3133,7 @@ paths:
3123
3133
  POST-MERGE FULL row — replace by `row.id`, NOT a sparse patch) + `event: task_remove`/`event:
3124
3134
  workflow_remove` `{id, ts}` departures.
3125
3135
  🔴 Keep-alive is a REAL NAMED FRAME — `event: heartbeat` + `data: {}` every 15 s
3126
- (`src/http/routes/fleet.ts:223`), NOT an SSE `: hb` comment (the旁注 says why: a per-frame-parsing BFF
3136
+ (`src/http/routes/fleet.ts`), NOT an SSE `: hb` comment (the旁注 says why: a per-frame-parsing BFF
3127
3137
  drops comments, so downstream saw a zero-frame window). Do NOT implement the reader as "skip lines
3128
3138
  starting with `:`" — swallow the heartbeat BY NAME. (Corrected 2026-08-02; the `: hb` shape belongs to
3129
3139
  `GET /v1/sessions/{sessionId}/events` alone, and beats at a tunable ~25 s.)
@@ -3279,7 +3289,7 @@ paths:
3279
3289
  summary: 'Newest PUBLISHED entry for a profile → { image }.'
3280
3290
  responses:
3281
3291
  # 🔴 census 批2 四段:this arm was a bare open `{}` (no `image` key even declared) — the handler's real
3282
- # body is `{ image: entry }` (images.ts:478-484), entry = the SAME ImageIndexEntry as the list route.
3292
+ # body is `{ image: entry }` (images.ts), entry = the SAME ImageIndexEntry as the list route.
3283
3293
  '200': { description: '{ image: entry }.', content: { application/json: { schema: { type: object, required: [image], additionalProperties: false, properties: { image: { $ref: '#/components/schemas/ImageIndexEntry' } } } } } }
3284
3294
  '404': { $ref: '#/components/responses/NotFound' }
3285
3295
 
@@ -3293,7 +3303,7 @@ paths:
3293
3303
  x-status: live
3294
3304
  summary: 'Entry by immutable digest → { image }.'
3295
3305
  responses:
3296
- # 🔴 census 批2 四段:same open-`{}` gap as imagesByProfile — real body `{ image: entry }` (images.ts:466-472).
3306
+ # 🔴 census 批2 四段:same open-`{}` gap as imagesByProfile — real body `{ image: entry }` (images.ts).
3297
3307
  '200': { description: '{ image: entry }.', content: { application/json: { schema: { type: object, required: [image], additionalProperties: false, properties: { image: { $ref: '#/components/schemas/ImageIndexEntry' } } } } } }
3298
3308
  '404': { $ref: '#/components/responses/NotFound' }
3299
3309
 
@@ -3568,7 +3578,7 @@ paths:
3568
3578
  schema:
3569
3579
  type: object
3570
3580
  # 🔴 census 批2 五段:CLOSED — exact literal `{ scope, exportedAt, entries }`
3571
- # (memory-policy.ts:58), no fourth key.
3581
+ # (memory-policy.ts), no fourth key.
3572
3582
  required: [scope, exportedAt, entries]
3573
3583
  additionalProperties: false
3574
3584
  properties:
@@ -3649,7 +3659,7 @@ paths:
3649
3659
  schema:
3650
3660
  type: object
3651
3661
  # 🔴 census 批2 五段:path↔schema 脱钩修(same class as the earlier BakeClaim/usage-triplet
3652
- # finds) — `OutcomeRow` already existed named+correct (observability.ts:51's exact
3662
+ # finds) — `OutcomeRow` already existed named+correct (observability.ts's exact
3653
3663
  # `{ outcomes: rows }`) but this path never `$ref`'d it, declaring a bare open `{}` instead.
3654
3664
  required: [outcomes]
3655
3665
  additionalProperties: false
@@ -3819,7 +3829,7 @@ paths:
3819
3829
  description: 'Injected. Body `{ runId, label, status: "running", delivery: "applied", marker }`.'
3820
3830
  # 🔴 census 批2 五段:spec-谎修——this 200 declared a bare `description` with NO `content`/`schema`
3821
3831
  # at all (classify() reads it as "no-json", invisible to both the open- and closed-op ledgers)
3822
- # while the server genuinely sends a JSON body (workflows.ts:279). The named schema
3832
+ # while the server genuinely sends a JSON body (workflows.ts). The named schema
3823
3833
  # `WorkflowAgentSteerReceipt` already existed (path↔schema decoupling, same class as the earlier
3824
3834
  # BakeClaim/usage-triplet finds) — wiring it up here, not inventing a new one.
3825
3835
  content:
@@ -3863,7 +3873,7 @@ paths:
3863
3873
  content:
3864
3874
  application/json:
3865
3875
  # 🔴 census 批2 五段:CLOSED against the real emitter — `sendJson(res, 200, { model,
3866
- # ...deps.metrics.summarize() })` (server.ts:794) is a fixed key set (server observability/
3876
+ # ...deps.metrics.summarize() })` (server.ts) is a fixed key set (server observability/
3867
3877
  # metrics.ts `MetricsSummary`), not an ops free-for-all. Left `x-status: draft` / `x-sdk: none`
3868
3878
  # untouched (still deliberately outside the SDK's wrapped surface) — closing the SHAPE and
3869
3879
  # keeping it un-wrapped are independent axes.
@@ -4054,10 +4064,13 @@ components:
4054
4064
  `Capabilities.scenarios` instead. `source`/`builtin` = builtin vs center-config overlay; `toolset` names
4055
4065
  the tool bundle while `tools` are the resolved tool names; `promptSummary` is a human-readable prompt
4056
4066
  DIGEST (never the full prompt); `enabled` = a center scenario can be declared-but-disabled.
4057
- # 封闭(census 批2 第三段,2026-07-30):路由 `routes/capabilities.ts:38` 原样发 `deps.scenarioDetails[name]`,
4058
- # 其类型 `ScenarioDetail`(capabilities/scenarios.ts:353)恰是这 8 个必填键;builtin 工厂 `mk`(:379)与
4067
+ # 封闭(census 批2 第三段,2026-07-30):路由 `routes/capabilities.ts` 原样发 `deps.scenarioDetails[name]`,
4068
+ # 其类型 `ScenarioDetail`(capabilities/scenarios.ts)恰是这 8 个必填键;builtin 工厂 `mk` 与
4059
4069
  # center overlay 都按该接口铸,无第 9 键来源。
4060
- required: [name, source, builtin, summary, toolset, tools, promptSummary, enabled]
4070
+ # server 7.7.0([C132]/[3235]):+available/unavailableReason 两键——运行面可用性投影,与请求该
4071
+ # 场景时的 501 拒绝臂同一谓词(server `scenarioAvailability()` 单一属主);`enabled`=声明面、
4072
+ # `available`=运行面,两轴独立。封闭集从 8 键升 9 必填+1 条件键。
4073
+ required: [name, source, builtin, summary, toolset, tools, promptSummary, enabled, available]
4061
4074
  additionalProperties: false
4062
4075
  properties:
4063
4076
  name: { type: string }
@@ -4068,6 +4081,8 @@ components:
4068
4081
  tools: { type: array, items: { type: string } }
4069
4082
  promptSummary: { type: string, description: 'a human-readable prompt DIGEST — NOT the full prompt.' }
4070
4083
  enabled: { type: boolean }
4084
+ available: { type: boolean, description: 'whether this deployment can actually RUN the scenario right now — same predicate as the 501 rejection arm on submit; false ⟺ submitting is guaranteed to 501. Independent axis from `enabled` (declared vs runnable).' }
4085
+ unavailableReason: { type: string, description: 'machine-readable reason, present ONLY when available:false (absent = no reason). Closed set, currently the single member `git_client_unconfigured`; branch on the key, never match the English prose. Additive-open: treat an unknown word as "unavailable, reason unknown".' }
4071
4086
 
4072
4087
  RunStatus:
4073
4088
  type: string
@@ -4200,7 +4215,7 @@ components:
4200
4215
  type: string
4201
4216
  description: >-
4202
4217
  [2400] TR-16 (declared 2026-08-02) — CALLER-MINTED uuidv7 task id: the DURABLE second tier of
4203
- idempotency on POST /v1/runs (`src/http/routes/runs.ts:196-217`). The `Idempotency-Key` header's
4218
+ idempotency on POST /v1/runs (`src/http/routes/runs.ts`). The `Idempotency-Key` header's
4204
4219
  cache is in-memory and PER-POD, so a dispatch gateway that re-routes a retry to ANOTHER instance
4205
4220
  after a network error would start a SECOND run; THIS replay reads the durable run store, so it
4206
4221
  survives the re-route and a restart — it is the prerequisite for gateway failover. A hit replays the
@@ -4390,7 +4405,7 @@ components:
4390
4405
  description: Quality-gate cascade (cheap→strong). Mutually exclusive with `verify`. Not on the stream.
4391
4406
  suggestNextPrompts:
4392
4407
  description: >
4393
- E12 (shell-host; service spec-fields.ts:9 / runs.ts:392, SHIPPED) — opt-in post-completion "what to ask
4408
+ E12 (shell-host; service spec-fields.ts / runs.ts, SHIPPED) — opt-in post-completion "what to ask
4394
4409
  next" suggestions. After a COMPLETED run, core runs an LLM pass and the service appends a `suggestions`
4395
4410
  event to the durable run-events tail. `true` = core defaults; the object form tunes count / generating
4396
4411
  role. 🔴 Mutually exclusive with `verify`/`cascade` (those return a result, not a streamed run → 400).
@@ -4421,8 +4436,12 @@ components:
4421
4436
  callability; the advertised schema stays the empty object); `swap` advertises the REAL schema on the
4422
4437
  next request. A provider whose decoding is CONSTRAINED by the advertised schema loops UNBOUNDED under
4423
4438
  `static` (every round emits `{}` and the next round still advertises an empty schema), which is why
4424
- this is a per-task knob. OMIT to inherit the engine chain (`spec ?? env ?? static`) — sending an
4425
- explicit value takes that decision away from the deployment env lane. Unknown value => 400 fail-loud.
4439
+ this is a per-task knob. OMIT to inherit the engine chain,
4440
+ `spec ?? env(SEMA_TOOL_MATERIALIZE_STRATEGY) ?? "swap"` — sending an explicit value takes that
4441
+ decision away from the deployment env lane. 🔴 The chain-tail default is **`swap`** (this text used
4442
+ to say `static`): it defaulted to `static` for exactly one core release window and core 5.15.0
4443
+ (BREAKING) put it back to `swap`; the server floor is core >= 5.16.0, so every supported deployment
4444
+ resolves an omitted value to `swap`. Unknown value => 400 fail-loud.
4426
4445
  clientContext:
4427
4446
  type: object
4428
4447
  description: Non-authoritative client hints (never trusted for authz).
@@ -4465,7 +4484,7 @@ components:
4465
4484
  type: boolean
4466
4485
  description: >
4467
4486
  🔴 PER-TASK opt-in for forwarding subagent CONTENT onto the parent's event stream (server
4468
- `src/http/wire-types.ts:157`, consumed at `boot/resolve-spec.ts:557` — read STRICTLY as `=== true`).
4487
+ `src/http/wire-types.ts`, consumed at `boot/resolve-spec.ts` — read STRICTLY as `=== true`).
4469
4488
  ⚠️ The capability bit of the same name says "this body key is ACCEPTED", NOT "forwarding is on":
4470
4489
  it is hard-coded true, while forwarding itself is OFF unless this key is sent. Without it the parent
4471
4490
  stream carries progress-only frames and a subagent-transcript view stays empty forever.
@@ -4476,11 +4495,11 @@ components:
4476
4495
  - type: object
4477
4496
  additionalProperties: false
4478
4497
  properties:
4479
- ttlMs: { type: integer, description: 'Retention window; server CLAMPS to ≤ 24 h (task-workflow.ts:77-83).' }
4498
+ ttlMs: { type: integer, description: 'Retention window; server CLAMPS to ≤ 24 h (task-workflow.ts).' }
4480
4499
  max: { type: integer, description: 'Retained session count; server CLAMPS to ≤ 64.' }
4481
4500
  description: >
4482
4501
  🔴 PER-TASK opt-in to RETAIN finished subagent sessions so they can be resumed (server
4483
- `src/http/wire-types.ts:169`, normalized at `boot/resolve-spec.ts:564`). This is the OTHER HALF of
4502
+ `src/http/wire-types.ts`, normalized at `boot/resolve-spec.ts`). This is the OTHER HALF of
4484
4503
  `POST /v1/runs/{runId}/subagents/{handle}/resume`: the capability bit `subagentResume` can be true and
4485
4504
  the route mounted, and the call still 409s `resume.retain_off` — retention is per-run and OFF by default.
4486
4505
  An over-large ask is CLAMPED, not rejected. (Declared 2026-08-02; the error code and the capability bit
@@ -4572,7 +4591,7 @@ components:
4572
4591
  # 🔴 **刻意不封闭**(census 轴一裁定,2026-07-30)。此前是「没写」= JSON Schema 默认开放,分不清
4573
4592
  # 「决定了要开」还是「忘了」;现在显式写成 true,让这条决定可读、可复审。理由:server 对引擎
4574
4593
  # `TaskResult` 是**整体透传**(runs.ts `withModelUsage` 只做 `{...result, stats:{...}}` 的加法),
4575
- # 而 SDK 的 `TaskResult`/`TaskStats` 两侧都带 `[key: string]: unknown` 开集(types.ts:119)——把
4594
+ # 而 SDK 的 `TaskResult`/`TaskStats` 两侧都带 `[key: string]: unknown` 开集(types.ts)——把
4576
4595
  # 这里改成 false 会让 spec 与本仓自己的类型面互相打脸,并且把「引擎加字段」判成 wire 违约(它不是)。
4577
4596
  # 收紧这一面的正解不是 additionalProperties,是把引擎真发的键**逐个登记**(字段级 drift 门的活)。
4578
4597
  additionalProperties: true
@@ -4684,7 +4703,7 @@ components:
4684
4703
 
4685
4704
  RunReceipt:
4686
4705
  type: object
4687
- # 封闭(census 轴一,2026-07-30):producer 铸造点 routes/runs.ts:180 与 idem 重放臂各自逐字写死
4706
+ # 封闭(census 轴一,2026-07-30):producer 铸造点 routes/runs.ts 与 idem 重放臂各自逐字写死
4688
4707
  # `{taskId, sessionId, status}` —— 三键全在场、无第四键 ⇒ required 已完整,additionalProperties 收紧到
4689
4708
  # false 后「server 多发一个 spec 没登记的键」当场红(此前 ①档只校已声明键的类型,多键完全不可见)。
4690
4709
  additionalProperties: false
@@ -4701,7 +4720,7 @@ components:
4701
4720
  Run state (`GET /v1/runs/:id`) — live shape per the pinned wire contract Drift 2: `result` is the NESTED TaskResult
4702
4721
  object (output text in result.result, stats in result.stats; NO top-level stats), and the error text
4703
4722
  field is `error` (not errorMessage; errorCode is floated up from result.errorCode).
4704
- # 封闭(census 轴一,2026-07-30):铸造点 routes/runs.ts:337-351 是一个逐键写死的字面量,恰好 10 键
4723
+ # 封闭(census 轴一,2026-07-30):铸造点 routes/runs.ts 是一个逐键写死的字面量,恰好 10 键
4705
4724
  # (taskId/sessionId/status/result/supervisorCost/suggestions/errorCode/error/jobId/source),与本
4706
4725
  # properties 一一对上;只有前三键无条件在场(其余走 `?? undefined` ⇒ JSON 丢键)⇒ required 维持 3 键。
4707
4726
  additionalProperties: false
@@ -4765,7 +4784,7 @@ components:
4765
4784
  description: >
4766
4785
  Synchronous ack for `POST /v1/runs/:id/cancel`. `status` is "cancelling" (accepted) or a terminal status
4767
4786
  (idempotent no-op). NOTE: "cancelling" is NOT a RunStatus enum member — it only appears here.
4768
- # 封闭(census 轴一,2026-07-30):cancel 面的 7 个 sendJson 铸造点(routes/runs.ts:417/436/480/483/
4787
+ # 封闭(census 轴一,2026-07-30):cancel 面的 7 个 sendJson 铸造点(routes/runs.ts
4769
4788
  # 495/505/512)键集的并集恰好 = {taskId, status, note?, errorCode?};taskId/status 无条件在场。
4770
4789
  additionalProperties: false
4771
4790
  required: [taskId, status]
@@ -4866,7 +4885,7 @@ components:
4866
4885
  securityContext: { type: object, additionalProperties: true, description: 'k8s-shaped, genuinely arbitrary — Record<string,unknown> server-side.' }
4867
4886
 
4868
4887
  ImageSelectResult:
4869
- # 新增(2026-07-25 回填批)。🔴 census 批2 四段:closed against the real handler (images.ts:407-415) — ALL
4888
+ # 新增(2026-07-25 回填批)。🔴 census 批2 四段:closed against the real handler (images.ts) — ALL
4870
4889
  # seven keys are unconditionally assigned every 200 (required tightened from none); `manifestSha` can be
4871
4890
  # null (ImageIndexEntry.manifestSha is `string | null`, this was typed non-nullable — a spec lie); and
4872
4891
  # `podContract`/`capabilities` now share the SAME closed schemas as ImageIndexEntry (no more drift/no
@@ -4943,7 +4962,7 @@ components:
4943
4962
  description: >
4944
4963
  Turn trace envelope (`GET /v1/tasks/:id/turns`). Key is `turns` (resource-named). `retainedFrom` marks
4945
4964
  the lowest retained seq. Turn ITEM shape is draft (co-design with artifacts, Q7).
4946
- # 封闭 + required 补全(census 轴一,2026-07-30):铸造点 routes/trace-usage.ts:233
4965
+ # 封闭 + required 补全(census 轴一,2026-07-30):铸造点 routes/trace-usage.ts
4947
4966
  # `{ turns, ...(nextCursor?{nextCursor}:{}) , retainedFrom }` —— `retainedFrom` 是**无条件**键
4948
4967
  # (`RunStore.retainedFrom(): Promise<number>`,四个实现都返回数字),此前只 required 了 `turns`,
4949
4968
  # 于是「server 少发 retainedFrom」这种断页判据丢失对本门不可见。
@@ -4959,7 +4978,7 @@ components:
4959
4978
  TaskList:
4960
4979
  type: object
4961
4980
  description: Paginated run summaries (`GET /v1/tasks`). Envelope key is `tasks` (resource-named, NOT `items`).
4962
- # 封闭(census 轴一,2026-07-30):信封铸造点 routes/trace-usage.ts:201 只有 tasks + 条件 nextCursor。
4981
+ # 封闭(census 轴一,2026-07-30):信封铸造点 routes/trace-usage.ts 只有 tasks + 条件 nextCursor。
4963
4982
  # 行本身(`TaskSummary`)保持开集 —— 它的 TS 侧有索引签名(引擎透传),那份「开」是真的。
4964
4983
  additionalProperties: false
4965
4984
  required: [tasks]
@@ -5020,7 +5039,7 @@ components:
5020
5039
 
5021
5040
  ArtifactList:
5022
5041
  type: object
5023
- # 封闭(census 轴一,2026-07-30):铸造点 routes/trace-usage.ts:214 只有 artifacts 一键(无分页)。
5042
+ # 封闭(census 轴一,2026-07-30):铸造点 routes/trace-usage.ts 只有 artifacts 一键(无分页)。
5024
5043
  # 行本身(`Artifact`)保持开集(TS 侧索引签名)。
5025
5044
  additionalProperties: false
5026
5045
  required: [artifacts]
@@ -5032,7 +5051,7 @@ components:
5032
5051
  LeaderReceipt:
5033
5052
  type: object
5034
5053
  description: >
5035
- 202 receipt of POST /v1/leader (server leader/endpoint.ts:130 — exact literal
5054
+ 202 receipt of POST /v1/leader (server leader/endpoint.ts — exact literal
5036
5055
  `{ leaderRunId, status: "running" }`; the POST ack is always the "running" value, the other
5037
5056
  `status` enum members only ever appear on the GET twin below).
5038
5057
  # 🔴 census 批2 五段:CLOSED — the POST handler's literal never carries a third key.
@@ -5045,12 +5064,12 @@ components:
5045
5064
  LeaderRecord:
5046
5065
  type: object
5047
5066
  description: >
5048
- 200 body of GET /v1/leader/{leaderRunId} (server leader/endpoint.ts:142-150 — exact literal
5067
+ 200 body of GET /v1/leader/{leaderRunId} (server leader/endpoint.ts — exact literal
5049
5068
  `{ id, status, ...(result?{result}:{}), ...(error?{error}:{}) }`).
5050
5069
  # 🔴 census 批2 五段:两处修——① CLOSED(result/error 是 OMIT-when-absent 的可选键,不是额外键);
5051
5070
  # ② `status` 的第四值 `needs_human`(LEADER-REPAIRLOOP-INTEGRATION §5/§10.6 的第三终态,
5052
5071
  # `statusForLeaderResult` 在 candidate_only/needs_human_oracle/conflict 三个 repairTerminal 上产出)
5053
- # 此前 SPEC LIES BY OMISSION——TS 侧 `LeaderRecord.status` 早就带这个第四值(types.ts:1607,亲验
5072
+ # 此前 SPEC LIES BY OMISSION——TS 侧 `LeaderRecord.status` 早就带这个第四值(types.ts,亲验
5054
5073
  # 已在场),只有 spec 的 enum 一直缺它,是个只有场景真触发才会现形的假红洞(一个真实合法帧被判违约)。
5055
5074
  required: [id, status]
5056
5075
  additionalProperties: false
@@ -5113,7 +5132,7 @@ components:
5113
5132
  type: object
5114
5133
  description: >
5115
5134
  The legacy D-lane approval row of GET /v1/approvals/{approvalId} (server `ApprovalRow`,
5116
- plugins/approval-store-sql.ts:43 — the row is `sendJson`'d verbatim, approvals-assistant.ts:668).
5135
+ plugins/approval-store-sql.ts — the row is `sendJson`'d verbatim, approvals-assistant.ts).
5117
5136
  Present only on deployments running the legacy `approvalStore` leg (no `checkpointStore` wired);
5118
5137
  the durable F4 lane has no by-id GET (only list + decide).
5119
5138
  # 🔴 census 批2 五段:CLOSED — byte-identical to the server row type, no extra keys.
@@ -5167,7 +5186,7 @@ components:
5167
5186
  legacy verbs in 1.0.0; the live face is `client.memory.exportScope` / `.sync`
5168
5187
  (`GET /v1/memory/export`, `POST /v1/memory/sync/{scope}`), which this flag does NOT gate.
5169
5188
  Kept on the wire so an older shell that still reads it honestly hides the dead affordances.
5170
- workflows: { type: boolean, description: "S8 self-orchestration workflow routes are mounted." }
5189
+ workflows: { type: boolean, description: "ENGINE-CAN: this worker's engine can orchestrate S8 self-orchestration workflows (server: `workflowsCapable ?? Boolean(workflowRunStore)`). The MOUNTING of the durable /v1/workflows read routes is the separate `workflowsList` bit below; the two coincide in today's wiring but are declared as orthogonal axes." }
5171
5190
  workflowsList: { type: boolean, description: "`GET /v1/workflows` list face (separate from the per-run reads)." }
5172
5191
  resumeAt: { type: boolean, description: "E18 resume-at-message anchors are resolvable." }
5173
5192
  rewindFiles: { type: boolean, description: "E19 working-tree snapshot/restore is wired." }
@@ -5178,7 +5197,7 @@ components:
5178
5197
  sessionSearch: { type: boolean, description: "Server-side session search." }
5179
5198
  sessionFork: { type: boolean, description: "Session fork verb." }
5180
5199
  sessionDelete: { type: boolean, description: "Session delete verb." }
5181
- sessionInit: { type: boolean, description: "Session pre-initialization verb." }
5200
+ sessionInit: { type: boolean, description: "`GET /v1/sessions/{sessionId}/init` startup bundle (E11 shell-host contract) is available. Pure config assembly (model + mode), always served — the server hard-codes true. NOTE: this advertises an ENDPOINT, not a `session_init` stream frame; no such frame exists on any leg." }
5182
5201
  sessionPolicy: { type: boolean, description: "E6 per-session operator-tightened tool rules are durable." }
5183
5202
  usage: { type: boolean, description: "QUOTA face wired (server costQuota dep — cost/token budget enforcement + per-principal accounting). NOT an analytics-availability probe ([1871] B15): per-turn analytics (turn_end.usage, model_usage frames) are engine-side and unconditional, and GET /v1/usage always answers 200 (with `enabled: false` when the quota face is off). Gate quota UI on this key; never gate analytics on it." }
5184
5203
  fleet:
@@ -5219,7 +5238,7 @@ components:
5219
5238
  sessionEvents:
5220
5239
  type: boolean
5221
5240
  description: >
5222
- 🔴 契约调和车1 H② (2026-08-02): server HAS been emitting this key (`src/http/routes/capabilities.ts:70`)
5241
+ 🔴 契约调和车1 H② (2026-08-02): server HAS been emitting this key (the `sessionEvents` bit in `src/http/routes/capabilities.ts`)
5223
5242
  while both this spec and the SDK type were missing it. Gates `GET /v1/sessions/{sessionId}/events`
5224
5243
  (the session-level SSE head subscription). Predicate is a THREE-way AND: a durable session-watch seam
5225
5244
  plus a session store exposing BOTH `getLeafId` and `ownerOf` — the route's 501 uses the same
@@ -5253,8 +5272,8 @@ components:
5253
5272
  streamApproval:
5254
5273
  type: boolean
5255
5274
  description: >
5256
- design/172 IN-STREAM approval protocol is on this worker (server >=7.3.0,
5257
- `src/http/routes/capabilities.ts:233` — `resolveStreamApprovalGate(...).active`). TRUE ⇒ the live
5275
+ design/172 IN-STREAM approval protocol is on this worker (server >=7.3.0, the `streamApproval` bit in
5276
+ `src/http/routes/capabilities.ts` — `resolveStreamApprovalGate(...).active`). TRUE ⇒ the live
5258
5277
  stream may carry `approval_request` / `approval_revoke` frames and `POST
5259
5278
  /v1/tasks/{taskId}/asks/{askId}/decision` settles them. FALSE/absent ⇒ that endpoint answers **501
5260
5279
  `feature.approval_ask_disabled`** on every call and the two frames never appear — the SAME predicate
@@ -5264,15 +5283,22 @@ components:
5264
5283
  `backend.kind != "local"` (the ask ledger must be DURABLE — an in-memory ledger loses accepted
5265
5284
  decisions on restart) AND the park facility is present (checkpoint store + DURABLE_APPROVAL, the
5266
5285
  degradation target when a window expires; without it the outcome would be a fail-closed deny, worse
5267
- than today's live card). `STREAM_APPROVAL_ENABLED` is **default OFF**, so on virtually every
5268
- deployment today this bit is `false` and the legacy `tool_approval` live-card leg is the only
5269
- approval face. Orthogonal to `toolApproval` (the live-card leg) and to `approvals` (the durable
5270
- checkpoint leg) — all three can differ.
5286
+ than today's live card). The deployment default for `STREAM_APPROVAL_ENABLED` reads in TWO
5287
+ SEGMENTS: on server **<= 7.4.0 it is default OFF**, so on virtually every deployment of those
5288
+ versions this bit is `false` and the legacy `tool_approval` live-card leg is the only approval face;
5289
+ on server **>= 7.5.0 it is default ON** (published to npm 2026-08-07, disclosed as BREAKING in the
5290
+ server CHANGELOG together with the ask window default 60s -> 300s), so a deployment meeting the
5291
+ other four conjuncts serves the frames and the decision endpoint out of the box. BOTH segments are
5292
+ live deployment realities — the first is not a historical footnote, it still describes every running
5293
+ 7.3.x/7.4.x install, and a consumer that reads only one of them is wrong about half the fleet.
5294
+ `STREAM_APPROVAL_ENABLED=false` is the clean revert key. Do NOT infer from the version number or
5295
+ from this prose — READ THIS BIT; it is the conjunction's actual value. Orthogonal to `toolApproval`
5296
+ (the live-card leg) and to `approvals` (the durable checkpoint leg) — all three can differ.
5271
5297
  promptSuggestions: { type: boolean, description: "Follow-up prompt suggestions are generated." }
5272
5298
  modelUsage:
5273
5299
  type: boolean
5274
5300
  description: >
5275
- Per-model usage echo is available (server `src/http/routes/capabilities.ts:197`
5301
+ Per-model usage echo is available (server: the `modelUsage` bit in `src/http/routes/capabilities.ts`,
5276
5302
  `Boolean(runStore && modelUsage)` — BOTH the durable run store and the tracker that feeds it;
5277
5303
  absent either, a consumer only gets aggregate cost).
5278
5304
  🔴 CORRECTED (契约调和车1 H③, 2026-08-02): this bit does NOT gate a read route — the route this
@@ -5288,21 +5314,25 @@ components:
5288
5314
  s3PublicEndpoint:
5289
5315
  type: ["string", "null"]
5290
5316
  description: >
5291
- Public base URL for artifact/snapshot links, when the deployment exposes one. `null` = not configured
5292
- (links are then worker-relative). One of only two NON-boolean capability values the server emits.
5317
+ SendUserFile's S3 public-link base (`config.sendUserFile.publicEndpoint`), when the deployment
5318
+ configures one. `null` = not configured. 🔴 CORRECTED: it has NOTHING to do with artifact/snapshot
5319
+ links, and there is no "links are then worker-relative" fallback — an unconfigured endpoint makes
5320
+ SendUserFile fail loudly and the sibling `sendUserFile` bit report false. A NON-boolean capability
5321
+ value (there are several — see `taskSettings`, `fleet`, `workspace`, `version`, `scenarios`).
5293
5322
  taskSettings:
5294
5323
  type: object
5295
5324
  description: >
5296
5325
  Which `settings.json` sub-faces this worker honours (CC-parity v1). OPEN object — unknown keys may
5297
- appear. The other non-boolean capability value.
5326
+ appear. One of the NON-boolean capability values (not "the other one": `fleet`, `workspace`,
5327
+ `s3PublicEndpoint`, `version` and `scenarios` are non-boolean too — read each key's own shape).
5298
5328
  additionalProperties: true
5299
5329
  properties:
5300
5330
  permissions: { type: boolean }
5301
5331
  permissionMode: { type: boolean }
5302
5332
  model: { type: boolean }
5303
5333
  outputStyle: { type: boolean }
5304
- env: { type: boolean }
5305
- hooks: { type: boolean }
5334
+ env: { type: boolean, description: 'TRUE on the single-user host lane (server emits `cwdHonored(config)`). It is NOT hard-coded false any more — the old always-false value predated the per-task shellEnv seam and hid a live capability.' }
5335
+ hooks: { type: boolean, description: 'Single-user deployments only (`requirePrincipal !== true`).' }
5306
5336
  memoryWrite:
5307
5337
  type: boolean
5308
5338
  description: >
@@ -5388,7 +5418,7 @@ components:
5388
5418
  description: >
5389
5419
  core `MemoryEntry` (@sema-agent/core memory-engine/types.d.ts) — one memory-engine (DB twins)
5390
5420
  note. Used verbatim by both GET /v1/memory/export's `entries` and POST /v1/memory/sync/{scope}'s
5391
- `serverEntries` (server never re-shapes it — memory-policy.ts:57 / memory-sync.ts:79 pass the core
5421
+ `serverEntries` (server never re-shapes it — memory-policy.ts / memory-sync.ts pass the core
5392
5422
  array straight to `sendJson`).
5393
5423
  required: [id, slug, frontmatter, body, rev, scope]
5394
5424
  additionalProperties: false
@@ -5403,8 +5433,8 @@ components:
5403
5433
  MemorySyncResponse:
5404
5434
  type: object
5405
5435
  description: >
5406
- 200 body of POST /v1/memory/sync/{scope} — core `MemorySyncResponse` (server memory-sync.ts:74-86),
5407
- `sendJson`'d verbatim (memory-policy.ts:107). `pullTruncated` is OMIT-when-absent (present+`true`
5436
+ 200 body of POST /v1/memory/sync/{scope} — core `MemorySyncResponse` (server memory-sync.ts),
5437
+ `sendJson`'d verbatim (memory-policy.ts). `pullTruncated` is OMIT-when-absent (present+`true`
5408
5438
  only when `serverEntries` was cut by `?pull.limit`).
5409
5439
  required: [applied, conflicts, serverEntries, serverDeletes, cursor]
5410
5440
  additionalProperties: false
@@ -5457,7 +5487,7 @@ components:
5457
5487
  MetricsSummaryOps:
5458
5488
  type: object
5459
5489
  description: >
5460
- 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server.ts:794; server
5490
+ 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server.ts; server
5461
5491
  observability/metrics.ts `MetricsSummary` interface + `summarize()`, all keys unconditional).
5462
5492
  Ops/observability face (`x-sdk: none` on the path) — closed here for census fidelity, but this
5463
5493
  does NOT change its SDK-wrapping status.
@@ -5508,7 +5538,7 @@ components:
5508
5538
  A non-secret `@model` catalog entry (GET /v1/models; service src/http/routes/capabilities.ts). `name` = the `@handle` the
5509
5539
  user types in the objective body to pick this model per-task. 🔴 NON-SECRET — name/capabilities only, never
5510
5540
  baseUrl/apiKey/headers (the service strips them).
5511
- # 🔴 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):铸造点 `routes/capabilities.ts:295-313`
5541
+ # 🔴 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):铸造点 `routes/capabilities.ts`
5512
5542
  # 是逐键字面量投影(白名单,不是透传——「open set」旧注不成立,新键要先过这道 spec)。core Model 类型
5513
5543
  # 里 id/provider/reasoning 都是必填、`vision` 由 `input` 恒算 ⇒ 五键无条件;contextWindow/maxOutputTokens
5514
5544
  # 0/缺省即省略,supportedEffortLevels 仅 reasoning 模型。
@@ -5548,7 +5578,7 @@ components:
5548
5578
 
5549
5579
  ElicitRespondAck:
5550
5580
  type: object
5551
- description: The 200 ack from a successful elicitation respond (service elicitation.ts:267).
5581
+ description: The 200 ack from a successful elicitation respond (service elicitation.ts).
5552
5582
  # 封闭(census 批2 第三段,2026-07-30):铸造点是三键字面量,三键全部无条件。
5553
5583
  additionalProperties: false
5554
5584
  required: [elicitationId, delivery, action]
@@ -5560,7 +5590,7 @@ components:
5560
5590
  QuestionAnswer:
5561
5591
  type: object
5562
5592
  description: >
5563
- The shell's answer to a live `question` frame (= core QuestionAnswer, ask-question.ts:69). The service validates
5593
+ The shell's answer to a live `question` frame (= core QuestionAnswer, ask-question.ts). The service validates
5564
5594
  only this OUTER shape (`answers[]` of `{header, selected:string[], note?}`); core owns the semantic fence
5565
5595
  (`selected ⊆ the offered options`, `note` wrapped in an untrusted-DATA fence).
5566
5596
  required: [answers]
@@ -5582,7 +5612,7 @@ components:
5582
5612
 
5583
5613
  QuestionRespondAck:
5584
5614
  type: object
5585
- description: The 200 ack from a successful question respond (service question.ts:226).
5615
+ description: The 200 ack from a successful question respond (service question.ts).
5586
5616
  # 封闭(census 批2 第三段,2026-07-30):铸造点是两键字面量。
5587
5617
  additionalProperties: false
5588
5618
  required: [questionId, delivery]
@@ -5597,7 +5627,7 @@ components:
5597
5627
  the REQUEST principal. When no quota window is configured → enabled=false + ONLY windowSec/maxTask*
5598
5628
  (the amount/limit fields are ABSENT) — render must tolerate the gap.
5599
5629
  # 🔴 封闭 + required 补铸造点(census 批2 第二段,2026-07-30)。两条腿都是逐字字面量
5600
- # (`routes/observability.ts:68` 无配额臂 4 键 / `:72` 有配额臂 11 键),11 个属性就是并集。
5630
+ # (`routes/observability.ts` —— 无配额臂 4 键 / 有配额臂 11 键),11 个属性就是并集。
5601
5631
  # required 从 `[enabled]` 抬到四键:`windowSec`/`maxTaskCostUsd`/`maxTaskTokens` 在**两条腿上都**
5602
5632
  # 无条件发送(config 直读),只列 `enabled` 是把恒在键写成了可缺 —— 消费方因此得写永远为真的判空。
5603
5633
  required: [enabled, windowSec, maxTaskCostUsd, maxTaskTokens]
@@ -5623,7 +5653,7 @@ components:
5623
5653
  admission + approval enforcement is 100% server-side). Top level CLOSED; the two pass-through
5624
5654
  collections (`commandPolicy` rows, `approvalRequire` entries) stay permissive — they are operator config
5625
5655
  echoed verbatim, not a service-minted shape.
5626
- # 🔴 封闭 + required(census 批2 第二段,2026-07-30):铸造点 `routes/memory-policy.ts:131` 是一个
5656
+ # 🔴 封闭 + required(census 批2 第二段,2026-07-30):铸造点 `routes/memory-policy.ts` 是一个
5627
5657
  # 四键字面量,四个键**全部无条件**发送(`autonomy` 走 `?? null`,`commandPolicy` 走 `?? []`)——
5628
5658
  # 此前 required 是空的,于是「一个丢了三个键的响应」也能干干净净通过。
5629
5659
  required: [autonomy, commandPolicy, approvalRequire, limits]
@@ -5647,7 +5677,7 @@ components:
5647
5677
  type: object
5648
5678
  description: >
5649
5679
  Effective ceilings (single-task + principal-level + quota window). CLOSED (the literal at
5650
- `routes/memory-policy.ts:135` has exactly these four keys); the two `maxPrincipal*`/`costQuota*`
5680
+ `routes/memory-policy.ts` has exactly these four keys); the two `maxPrincipal*`/`costQuota*`
5651
5681
  entries stay OUT of `required` — they are optional config and are omitted when unset.
5652
5682
  additionalProperties: false
5653
5683
  required: [maxTaskCostUsd, maxTaskTokens]
@@ -5670,7 +5700,7 @@ components:
5670
5700
  GET /v1/workflows list row = core summarizeWorkflowRun projection (owner-scoped; scope = the creating
5671
5701
  principal). All times are epoch MS.
5672
5702
  🔴 census 批2 四段(2026-07-30):CLOSED against core's real `summarizeWorkflowRun` (sema-core
5673
- src/core/workflow-run-store.ts:79-97) — `originatingSessionId`/`agentFailures` were emitted on the
5703
+ src/core/workflow-run-store.ts) — `originatingSessionId`/`agentFailures` were emitted on the
5674
5704
  wire (both conditional-spread, present only when defined) but absent from this schema (two-side same-gap).
5675
5705
  required: [id, scope, status, phaseCount, agentCount, tokens, startedAt, createdAt]
5676
5706
  additionalProperties: false
@@ -5700,7 +5730,7 @@ components:
5700
5730
  🔴 `arg` is a SHORT primary-arg SUMMARY (command→name, path→basename, url→origin+path), secret-scrubbed
5701
5731
  and truncated (~80 code points) by core. It exists for the monitor's `Read(path)` / `Bash(grep …)`
5702
5732
  display — it is NOT the full args. RENDER it; NEVER re-feed it to a model.
5703
- 🔴 census 批2 四段:CLOSED against core's real `ToolActivity` (sema-core src/core/types.ts:2332-2348) —
5733
+ 🔴 census 批2 四段:CLOSED against core's real `ToolActivity` (sema-core src/core/types.ts) —
5704
5734
  `at` (the [843]④a epoch-ms stamp, carried verbatim by both the start and end beat) was on the wire
5705
5735
  but absent from this schema.
5706
5736
  required: [phase, toolCallId, toolName]
@@ -5716,7 +5746,7 @@ components:
5716
5746
  WorkflowAgentRow:
5717
5747
  # 新增(2026-07-25 回填批)—— 取代 `WorkflowRun.agents` 的 `items: {}`(零形状)。
5718
5748
  # 🔴 census 批2 四段:CLOSED against the REAL wire projection — server's `summarizeWorkflowDetail`
5719
- # (src/http/routes/workflows.ts:358-394), not core's raw `WorkflowAgentRun`. The two differ: the server
5749
+ # (src/http/routes/workflows.ts), not core's raw `WorkflowAgentRun`. The two differ: the server
5720
5750
  # ADDS `displayStatus`/`callKey`/`groupId`/`lastActivityAt`/`durationMs`/`queuedAt`/`startedAt`/`endedAt`/
5721
5751
  # `replayed` (all present on the wire but previously undeclared — same-side gap, no server change needed)
5722
5752
  # and DROPS core's `errorCode`/`errorMessage`/`attempts`/`lastAttemptReason`/`sessionId` (never projected
@@ -5753,7 +5783,7 @@ components:
5753
5783
 
5754
5784
  WorkflowPhaseProgress:
5755
5785
  # 新(census 批2 四段):此前 `WorkflowRun.phases` 是 `items: {}`(零形状)。真形 = server
5756
- # `summarizeWorkflowDetail`'s phases.map projection (workflows.ts:335-346) — a DERIVED progress view,
5786
+ # `summarizeWorkflowDetail`'s phases.map projection (workflows.ts) — a DERIVED progress view,
5757
5787
  # not core's raw `WorkflowPhase` (drops `detail`/`model`/(phase-level) `agentFailures`; adds `done`/`total`).
5758
5788
  type: object
5759
5789
  description: One phase's progress in a workflow's detail (server-derived — done/total of the agents grouped under it).
@@ -5770,7 +5800,7 @@ components:
5770
5800
 
5771
5801
  WorkflowGroupNode:
5772
5802
  # 新(census 批2 四段):`WorkflowRun.groups` was undeclared entirely. Real shape = server's rebuilt nested
5773
- # tree (workflows.ts:399-441, GroupNode) — recursive, acyclic (a dangling/cyclic parentGroupId is grafted
5803
+ # tree (workflows.ts, GroupNode) — recursive, acyclic (a dangling/cyclic parentGroupId is grafted
5774
5804
  # onto the root by the server, so this shape never actually cycles on the wire).
5775
5805
  type: object
5776
5806
  description: One node of the workflow's nested `ctx.workflow` group tree (rebuilt server-side from `parentGroupId`).
@@ -5788,7 +5818,7 @@ components:
5788
5818
 
5789
5819
  WorkflowRunStats:
5790
5820
  # 新(census 批2 四段):此前 inline `stats` 只钉了 `tokens`/`nested.tokens` 两键;真形 = core
5791
- # `WorkflowRunStats`(sema-core src/orchestration/workflow-types.ts:105-110)——own/nested 各自还有
5821
+ # `WorkflowRunStats`(sema-core src/orchestration/workflow-types.ts)——own/nested 各自还有
5792
5822
  # `turns`/`costMicroUsd`,nested 另有 `tasks`。四键(own turns/costMicroUsd + nested 两键)此前两侧同缺。
5793
5823
  type: object
5794
5824
  description: 'Cumulative workflow usage. `own` (top-level) and `nested` (delegated sub-agents) are kept SEPARATE (R-5) — total spend = tokens + nested.tokens.'
@@ -5810,7 +5840,7 @@ components:
5810
5840
 
5811
5841
  WorkflowRun:
5812
5842
  # 🔴 census 批2 四段:CLOSED against the REAL wire projection — server's `summarizeWorkflowDetail`
5813
- # (src/http/routes/workflows.ts:317-475), which is a DERIVED view over core's `WorkflowRun`, not the raw
5843
+ # (src/http/routes/workflows.ts), which is a DERIVED view over core's `WorkflowRun`, not the raw
5814
5844
  # record. This schema previously mirrored (a stale subset of) the core type; the actual wire adds
5815
5845
  # `durationMs`/`unphased`/`groups` and drops core's `sourceTaskId`/`originatingSessionId`/`effectiveArgs`/
5816
5846
  # `resultFull`/`completionId`/`resume`/`journalSkips` (never projected by this route — genuinely absent
@@ -5933,7 +5963,7 @@ components:
5933
5963
  SessionNotifyResult:
5934
5964
  # 4.3.0(SM-9):把 notify 的两态提成**具名** schema —— 此前只有 path 内联的两个匿名 200/202 形,
5935
5965
  # SDK 侧的返回型则是裸 `Record<string, unknown>`,`delivery` 这条承重语义(已送达 vs 排队)在类型面
5936
- # 整个丢失。铸造点 `routes/notify-wake.ts:98`(live)/ `:116`(parked),两臂都是字面量。
5966
+ # 整个丢失。铸造点 `routes/notify-wake.ts`(live / parked 两臂),都是字面量。
5937
5967
  description: >
5938
5968
  POST /v1/sessions/{sessionId}/notify 的判别联合。分支 `delivery`:`live` = 会话有活流、通知已送达
5939
5969
  (HTTP 200);`parked` = 无活流,排队到下次开流才 drain(HTTP 202)。消费方不得把两者都渲染成"已通知"。
@@ -5984,7 +6014,7 @@ components:
5984
6014
  nextOffset: { type: integer, description: 'Pagination cursor; present only when the page is exactly `limit` rows.' }
5985
6015
  WorkflowJournalEntry:
5986
6016
  # 新(census 批2 四段):GET /v1/workflows/:id/journal row, the non-truncated arm (server `projectResult`,
5987
- # workflows.ts:127-134) — one agent()-call's cached TaskResult, bounded + redacted.
6017
+ # workflows.ts) — one agent()-call's cached TaskResult, bounded + redacted.
5988
6018
  type: object
5989
6019
  description: One journal entry — a completed agent() call's projected TaskResult (bounded + redacted).
5990
6020
  required: [callKey, ordinal, status]
@@ -5999,7 +6029,7 @@ components:
5999
6029
  turns: { type: integer, description: 'present only when the result carried stats.' }
6000
6030
 
6001
6031
  WorkflowJournalEntryTruncated:
6002
- # 新(census 批2 四段):the truncated arm (workflows.ts:141-142/159-162) — an honest stub for a row whose
6032
+ # 新(census 批2 四段):the truncated arm (workflows.ts) — an honest stub for a row whose
6003
6033
  # stored TaskResult exceeded the per-row 64KiB gate (SQL stores) or byte length (file/in-memory
6004
6034
  # fallback): the server never pulls the oversized payload into process memory.
6005
6035
  type: object
@@ -6016,17 +6046,17 @@ components:
6016
6046
  type: object
6017
6047
  description: >
6018
6048
  One frame of GET /v1/workflows/:id/stream (streamWorkflowRun). FIRST frame is `event:meta` with
6019
- `data: {type:"meta", version, runId}` (`src/http/routes/workflows.ts:487` — the `type` is double-emitted
6049
+ `data: {type:"meta", version, runId}` (`src/http/routes/workflows.ts` — the `type` is double-emitted
6020
6050
  on purpose: a proxy that forwards only `data:` lines would otherwise leave a `data.type`-dispatching
6021
6051
  consumer unable to recognise the opener). Subsequent frames carry a core WorkflowEvent verbatim (its
6022
6052
  `type` is the SSE event name). NOT resumable (replica-local, in-process; no id/Last-Event-ID).
6023
6053
  `data` is permissive (core-internal, evolves).
6024
6054
  🔴 There IS exactly one TERMINAL frame and it means FAILURE: `event: error` with
6025
6055
  `data: {type:"error", errorCode:"workflow.stream_error", message:"workflow stream error"}`
6026
- (`routes/workflows.ts:517`). Branch on `errorCode` — NEVER anchor on `message`; the errorCode was added
6056
+ (`routes/workflows.ts`). Branch on `errorCode` — NEVER anchor on `message`; the errorCode was added
6027
6057
  precisely because consumers were otherwise forced to anchor prose or to conflate "the stream broke" with
6028
6058
  "the stream ended normally", i.e. to report a FAILED workflow as finished. (Declared 2026-08-02.)
6029
- Keep-alive is a REAL NAMED FRAME `event: heartbeat` / `data: {}` @15 s (`:496`), not an SSE comment.
6059
+ Keep-alive is a REAL NAMED FRAME `event: heartbeat` / `data: {}` @15 s (`routes/workflows.ts`), not an SSE comment.
6030
6060
  NOTE: on the wire this is SSE framing (event name + JSON data), not a JSON body — schema describes one
6031
6061
  decoded frame.
6032
6062
  required: [event, data]
@@ -6039,7 +6069,7 @@ components:
6039
6069
  FleetTaskStatus:
6040
6070
  type: string
6041
6071
  description: >
6042
- The fleet vocabulary the shell renders (service FleetTaskStatus, fleet-bus.ts:27-36). The service maps its
6072
+ The fleet vocabulary the shell renders (service FleetTaskStatus, fleet-bus.ts). The service maps its
6043
6073
  run/workflow status onto this NEUTRAL set: `awaiting approval` = a needs-review/plan-approval park,
6044
6074
  `waiting` = a durable suspend. CLOSED on the wire; render OPEN (branch known, fall back generically).
6045
6075
  enum: [queued, running, waiting, stopping, 'awaiting approval', idle, completed, failed, killed]
@@ -6047,7 +6077,7 @@ components:
6047
6077
  type: object
6048
6078
  description: 'Upload receipt (POST /v1/attachments 201). `name` is the SANITIZED basename the server will materialize under `attachments/`.'
6049
6079
  # 🔴 census 批2 五段(2026-07-30):CLOSED against the real emitter — `sendJson(res, 201, { id, name,
6050
- # mime, sha256, sizeBytes })` (server attachments.ts:90) is an EXACT 5-key object literal, never a
6080
+ # mime, sha256, sizeBytes })` (server attachments.ts) is an EXACT 5-key object literal, never a
6051
6081
  # superset.
6052
6082
  required: [id, name, mime, sha256, sizeBytes]
6053
6083
  additionalProperties: false
@@ -6060,7 +6090,7 @@ components:
6060
6090
  FleetTaskRow:
6061
6091
  type: object
6062
6092
  description: >
6063
- One active task row (service FleetTaskRow fleet-bus.ts:40-54) — MINUS the wire-STRIPPED `scope` (a
6093
+ One active task row (service FleetTaskRow fleet-bus.ts) — MINUS the wire-STRIPPED `scope` (a
6064
6094
  tenant-isolation field the service removes AFTER the owner-gate; never rendered, never on this wire). A
6065
6095
  `task` frame carries the POST-MERGE FULL row → replace by `id`, NOT a sparse patch. A subagent child row
6066
6096
  sets `parentId` (E2 parentToolCallId) + an id of `"<runId> <taskId>"`.
@@ -6125,14 +6155,14 @@ components:
6125
6155
  FleetWorkflowRow:
6126
6156
  type: object
6127
6157
  description: >
6128
- One workflow row (service FleetWorkflowRow fleet-bus.ts:57-69) — MINUS the wire-stripped `scope`. `status`
6158
+ One workflow row (service FleetWorkflowRow fleet-bus.ts) — MINUS the wire-stripped `scope`. `status`
6129
6159
  is the NEUTRAL workflow run status (running|completed|failed), open on read.
6130
6160
  required: [id, name, status]
6131
6161
  additionalProperties: true
6132
6162
  properties:
6133
6163
  # 🔴 FW-6(2026-08-02):这里此前有一个 `sourceLane` —— **幻影键,已删**。`sourceLane` 只在
6134
- # `FleetTaskRow`(`src/fleet/fleet-bus.ts:146`)上,`FleetWorkflowRow`(`:150-170`)全文没有它,
6135
- # `publishWorkflow`(`:338-342`)也从不写它;SDK 侧同样没有(SDK 是对的那一边)。
6164
+ # `FleetTaskRow`(`src/fleet/fleet-bus.ts`)上,`FleetWorkflowRow` 全文没有它,
6165
+ # 同文件的 `publishWorkflow` 也从不写它;SDK 侧同样没有(SDK 是对的那一边)。
6136
6166
  # 按它 codegen 的第三方会得到一个永远 undefined 的 `workflowRow.sourceLane`,并照 FleetTaskRow 那条
6137
6167
  # 「run-leg composite rows only」的描述写出一个**永不触发**的 workflow 行去重分支。
6138
6168
  id: { type: string }
@@ -6141,7 +6171,7 @@ components:
6141
6171
  status: { type: string, description: 'Neutral workflow run status (running|completed|failed; open).' }
6142
6172
  doneCount: { type: integer }
6143
6173
  # 🔴 FW-6(2026-08-02):补上一个 server 真发、SDK 真声明、只有 spec 漏了的键。
6144
- # `src/fleet/fleet-bus.ts:167` 声明,`src/orchestration/workflow-notify-journal.ts:511` 真发。
6174
+ # `src/fleet/fleet-bus.ts` 声明,`src/orchestration/workflow-notify-journal.ts` 真发。
6145
6175
  startedCount: { type: integer, description: 'Agents actually STARTED (vs `totalCount` = planned, queued included). CC 2.1.220 的 `⚠ Large workflow` 规模告警用它当分母 —— 漏了它那条告警根本渲染不出来。' }
6146
6176
  totalCount: { type: integer }
6147
6177
  failedCount: { type: integer }
@@ -6179,7 +6209,7 @@ components:
6179
6209
  hook_notice: '#/components/schemas/FleetFrame_hook_notice'
6180
6210
  FleetFrame_meta:
6181
6211
  type: object
6182
- # 🔴 FW-10(2026-08-02):此前只声明三键,而铸造点 `src/http/routes/fleet.ts:79` **无条件**发五键 ——
6212
+ # 🔴 FW-10(2026-08-02):此前只声明三键,而铸造点 `src/http/routes/fleet.ts` **无条件**发五键 ——
6183
6213
  # `sessionScoped` / `bgNotifyFailClosed` 不在任何条件分支里。这两个键正是「server 契约方复审 F1
6184
6214
  # (行为错·最高)」的当事键:它们曾被一次白名单重构静默剥掉,导致 cli 的让位臂全程死代码。
6185
6215
  # SDK 侧已上锁(`test/fleet.test.ts` 有专门的透传回归钉),spec 侧一直空白 —— 按 spec 生成的客户端
@@ -6334,8 +6364,8 @@ components:
6334
6364
  type:
6335
6365
  type: string
6336
6366
  description: >
6337
- 契约调和车1 G(2026-08-02;server [2373]C-12, `src/trace/project.ts:763` + the meta frame at
6338
- `routes/trace-usage.ts:258`): the frame's own event name, DOUBLE-EMITTED inside `data`. Always
6367
+ 契约调和车1 G(2026-08-02;server [2373]C-12, `src/trace/project.ts` + the meta frame at
6368
+ `routes/trace-usage.ts`): the frame's own event name, DOUBLE-EMITTED inside `data`. Always
6339
6369
  present on server >= 5.0.0; OPTIONAL here because the SDK's supported floor is server 3.0.0
6340
6370
  (where only the `error` frame carried it). Equal to the SSE `event:` line verbatim — its reason
6341
6371
  to exist is the proxy/relay case that strips the `event:` line and forwards only the data JSON,
@@ -6344,7 +6374,7 @@ components:
6344
6374
  PendingList:
6345
6375
  type: object
6346
6376
  description: Envelope for pending HITL checkpoints (key `pending`, NOT a bare array — the pinned wire contract).
6347
- # 封闭(census 轴一,2026-07-30):三个铸造点(routes/approvals-assistant.ts:89/620/624)都只发
6377
+ # 封闭(census 轴一,2026-07-30):三个铸造点(routes/approvals-assistant.ts)都只发
6348
6378
  # `{ pending }`。行本身(`PendingCheckpoint`)保持开集(TS 侧索引签名)。
6349
6379
  additionalProperties: false
6350
6380
  required: [pending]
@@ -6357,7 +6387,7 @@ components:
6357
6387
  type: object
6358
6388
  description: Envelope for a session's approval exemptions (; key `exemptions`, NOT a bare array).
6359
6389
  required: [exemptions]
6360
- # 封闭:铸造点 `routes/approvals-assistant.ts:138` 是单键字面量。
6390
+ # 封闭:铸造点 `routes/approvals-assistant.ts` 是单键字面量。
6361
6391
  additionalProperties: false
6362
6392
  properties:
6363
6393
  exemptions:
@@ -6373,7 +6403,7 @@ components:
6373
6403
  · the RESUMED-TASK shape (`http/server.ts` driveResumeIntoRunLog return) — `{taskId?, sessionId, status,
6374
6404
  errorCode?, errorMessage?, retriable?}`, plus `rememberApplied` when the request carried
6375
6405
  `remember:"session"` AND the worker has an exemption store;
6376
- · the PARKED-BACKGROUND-AGENT shape (`parked-decide.ts:272`, server >=1.267) — `{taskId, status:"resuming",
6406
+ · the PARKED-BACKGROUND-AGENT shape (`parked-decide.ts`, server >=1.267) — `{taskId, status:"resuming",
6377
6407
  decision}`, an ACCEPTED (not completed) receipt; the revive drives asynchronously.
6378
6408
  `status` is the only key BOTH shapes guarantee: `taskId` rides `getActiveTaskId` on the resumed leg (omitted
6379
6409
  when there is no active row) and `sessionId` is absent from the parked shape.
@@ -6415,7 +6445,7 @@ components:
6415
6445
  MEMORY only — deny/neverAuto always outrank it; it never loosens the session-policy layer.
6416
6446
  required: [toolName, grantedBy, createdAt]
6417
6447
  # 封闭:三个 store 实现(sql/pg/local)的 `list()` 都逐字铸这三键
6418
- # (`plugins/approval-exemption-store.ts:67`),行不是原样透传的 DB 行。
6448
+ # (`plugins/approval-exemption-store.ts`),行不是原样透传的 DB 行。
6419
6449
  additionalProperties: false
6420
6450
  properties:
6421
6451
  toolName: { type: string, description: Canonical tool name the exemption covers. }
@@ -6430,7 +6460,7 @@ components:
6430
6460
  The `GET /v1/approvals` operator queue row (true shape). NEVER includes a capability token.
6431
6461
  ⚠️ `createdAt`/`deadline` are EPOCH MILLISECONDS (numbers), not ISO strings.
6432
6462
  🔴 真矛盾撞获+纠正(2026-07-25,core [1640] 审计撞获 + server 复核确认 src/http/routes/approvals-assistant.ts→
6433
- listPending() 真实实现 `src/plugins/tidb-checkpoint-store.ts:54-79`):这份 schema 此前声称
6463
+ listPending() 真实实现 `src/plugins/tidb-checkpoint-store.ts`):这份 schema 此前声称
6434
6464
  "MIRRORS core CheckpointSummary,顶层 gateKind/severity,NEVER a riskDescriptor object"——
6435
6465
  与真实实现完全相反。真实的 `PendingCheckpoint`(service 自定义类型,不是 core 的
6436
6466
  `CheckpointSummary`)**没有 token 字段**(store 层注释:"deliberately NO token")、**没有
@@ -6559,7 +6589,7 @@ components:
6559
6589
  type: object
6560
6590
  description: 'ASSISTANT-WIRE-CONTRACT §2 — `GET /v1/assistant/inbox` envelope. Already severity-sorted by core.'
6561
6591
  required: [inbox]
6562
- # 封闭:铸造点 `routes/approvals-assistant.ts:210` 是单键字面量(行本身 InboxRow 已封闭)。
6592
+ # 封闭:铸造点 `routes/approvals-assistant.ts` 是单键字面量(行本身 InboxRow 已封闭)。
6563
6593
  additionalProperties: false
6564
6594
  properties:
6565
6595
  inbox:
@@ -6663,7 +6693,7 @@ components:
6663
6693
  ASSISTANT-WIRE-CONTRACT §3 — `GET /v1/assistant/tasks` envelope. Triage-ordered by core (needs-attention
6664
6694
  first, then severity DESC, then spend DESC). Empty `{tasks:[]}` when no TiDB run store.
6665
6695
  required: [tasks]
6666
- # 封闭:两个铸造点(`routes/approvals-assistant.ts:224` 的无 run-store 空臂 + `:273`)都是单键字面量。
6696
+ # 封闭:两个铸造点(`routes/approvals-assistant.ts` 的无 run-store 空臂 + 正常臂)都是单键字面量。
6667
6697
  additionalProperties: false
6668
6698
  properties:
6669
6699
  tasks:
@@ -6754,12 +6784,12 @@ components:
6754
6784
  SessionSummary:
6755
6785
  type: object
6756
6786
  description: >
6757
- E16 (shell-host sessionPicker; service SessionSummary tidb-run-store.ts:42) — a `GET /v1/sessions` list row
6787
+ E16 (shell-host sessionPicker; service SessionSummary tidb-run-store.ts) — a `GET /v1/sessions` list row
6758
6788
  aggregating a session's run ledger (last activity + most-recent objective preview + status). owner-scoped.
6759
6789
  All timestamps ISO. `objectivePreview` is service-redacted + truncated (genuinely `null` when no run).
6760
6790
  required: [sessionId, owner, lastActivityAt, firstActivityAt, runCount, objectivePreview, lastStatus]
6761
- # 🔴 封闭(census 批2 第二段,2026-07-30)。铸造点=`security.ts:22 SessionListItem`,9 键逐字对上;
6762
- # 路由 `routes/sessions-list.ts:148` 把 store 行**原样**放进 `sessions[]`(零投影),所以「多键」
6791
+ # 🔴 封闭(census 批2 第二段,2026-07-30)。铸造点=`security.ts SessionListItem`,9 键逐字对上;
6792
+ # 路由 `routes/sessions-list.ts` 把 store 行**原样**放进 `sessions[]`(零投影),所以「多键」
6763
6793
  # 只能来自 store 实现自己加字段 —— 那正是要抓的漂。`lastRunId`/`title` 刻意**不进 required**:
6764
6794
  # 两者由 store 侧 SQL 投影(legacy `task_run` 聚合腿的 lister 不一定发),required 会把一条
6765
6795
  # 合法的退化面响应判红。
@@ -6833,7 +6863,7 @@ components:
6833
6863
  type: object
6834
6864
  description: Keyset page envelope for `GET /v1/sessions`. `nextCursor` absent = last page.
6835
6865
  required: [sessions]
6836
- # 封闭:铸造点 `routes/sessions-list.ts:148` 是逐字两键字面量(`nextCursor` 条件在场)。
6866
+ # 封闭:铸造点 `routes/sessions-list.ts` 是逐字两键字面量(`nextCursor` 条件在场)。
6837
6867
  additionalProperties: false
6838
6868
  properties:
6839
6869
  sessions:
@@ -6844,7 +6874,7 @@ components:
6844
6874
  SessionPermissionRules:
6845
6875
  type: object
6846
6876
  description: >
6847
- E6 (shell-host permissionMode; core SessionPermissionRules session-policy-store.ts:23) — operator-tightened
6877
+ E6 (shell-host permissionMode; core SessionPermissionRules session-policy-store.ts) — operator-tightened
6848
6878
  per-session tool-permission rules (core reads them at prepare-time, SUBTRACT-only). 5 optional `string[]`
6849
6879
  fields. tighten-only invariant is core-enforced (a loosen needs an operator).
6850
6880
  properties:
@@ -6868,7 +6898,7 @@ components:
6868
6898
  # SessionPermissionRules + 内联的 `rev`)声明的键不在它作用域内 ⇒ 只写 `false` 会把合法的
6869
6899
  # `toolAllow`/`rev` 判成违约。镜像的一致性由 spec.test.ts 的通用「封闭 allOf 臂必须镜像被引
6870
6900
  # schema 全部键」门看守(以后往 SessionPermissionRules 加键,忘了同步镜像就当场红)。
6871
- # 为什么值得封闭:PUT 腿 `routes/sessions.ts:551` 只挑这 5 个已知字段落店,GET 腿回的是 store
6901
+ # 为什么值得封闭:PUT 腿 `routes/sessions.ts` 只挑这 5 个已知字段落店,GET 腿回的是 store
6872
6902
  # 原样 —— 「store 多回一个键」正是本门要抓的那类漂(消费方按 spec 生成的类型会漏掉它)。
6873
6903
  additionalProperties: false
6874
6904
  properties:
@@ -6882,10 +6912,10 @@ components:
6882
6912
  McpServerStatus:
6883
6913
  type: object
6884
6914
  description: >
6885
- E9 (shell-host mcpStatus; core McpServerStatus core/mcp.ts:76) — one MCP server's status at materialization
6915
+ E9 (shell-host mcpStatus; core McpServerStatus core/mcp.ts) — one MCP server's status at materialization
6886
6916
  time. `status` is connected | failed (core never emits disabled). `error` (failed only) is service-redacted
6887
6917
  + length-bounded.
6888
- # 封闭三层(census 批2 第三段,2026-07-30):行铸造点 `routes/sessions.ts:157-165` 逐键条件展开这 5 键;
6918
+ # 封闭三层(census 批2 第三段,2026-07-30):行铸造点 `routes/sessions.ts` 逐键条件展开这 5 键;
6889
6919
  # `serverInfo` 不是透传——core 自己投影成两键字面量(core dist mcp.js:986 `{name, version}`)。
6890
6920
  required: [name, status]
6891
6921
  additionalProperties: false
@@ -6907,8 +6937,7 @@ components:
6907
6937
  description: >
6908
6938
  `GET /v1/sessions/:id/mcp` envelope. `asOf` = THIS materialization moment (ISO). `degraded:true` (servers
6909
6939
  empty) ⇒ materialize timed out/failed (NOT "no MCP"). No MCP configured ⇒ `servers:[]` without `degraded`.
6910
- # 封闭(census 批2 第三段,2026-07-30):三个铸造臂(`routes/sessions.ts:134` 空面板 / `:152` 超时
6911
- # degraded / `:155-166` 正常)键集并集恰是这 3 键;degraded 只在超时臂铸。
6940
+ # 封闭(census 批2 第三段,2026-07-30):三个铸造臂(`routes/sessions.ts` 的空面板 / 超时 degraded / 正常)键集并集恰是这 3 键;degraded 只在超时臂铸。
6912
6941
  required: [asOf, servers]
6913
6942
  additionalProperties: false
6914
6943
  properties:
@@ -6937,8 +6966,8 @@ components:
6937
6966
  description: >
6938
6967
  One E19 file snapshot keyed by a `SessionTreeEntry.id`. `manifest` = `[relPath, blobHash]` tuples; the blob
6939
6968
  BYTES are NOT inlined (they ride the content-addressed blob routes).
6940
- # 封闭(census 批2 第三段,2026-07-30):server 侧类型+铸造点(`session-sync.ts:206` PULL 出 /
6941
- # `routes/session-sync.ts:53` PUSH 校验形)都恰是这两键。
6969
+ # 封闭(census 批2 第三段,2026-07-30):server 侧类型+铸造点(`session-sync.ts` PULL 出 /
6970
+ # `routes/session-sync.ts` PUSH 校验形)都恰是这两键。
6942
6971
  additionalProperties: false
6943
6972
  required: [key, manifest]
6944
6973
  properties:
@@ -6955,7 +6984,7 @@ components:
6955
6984
  SyncAnchor:
6956
6985
  type: object
6957
6986
  description: One E18 resume-at anchor. `owner` is RE-KEYED to the importing principal on a PUSH (§9).
6958
- # 封闭(census 批2 第三段,2026-07-30):server 侧行类型 `session-sync.ts:52` 恰是这三键(owner 可 null 不可缺)。
6987
+ # 封闭(census 批2 第三段,2026-07-30):server 侧行类型 `session-sync.ts` 恰是这三键(owner 可 null 不可缺)。
6959
6988
  additionalProperties: false
6960
6989
  required: [eventId, entryId, owner]
6961
6990
  properties:
@@ -6966,7 +6995,7 @@ components:
6966
6995
  SessionRulesRecord:
6967
6996
  type: object
6968
6997
  description: A (principal, rules) policy record — one row across ALL principals (E6 `listBySession`). Replayed verbatim on import (the cloud applies the tighten-only gate).
6969
- # 封闭(census 批2 第三段,2026-07-30):core `SessionRulesRecord`(session-policy-store.d.ts:19)恰是
6998
+ # 封闭(census 批2 第三段,2026-07-30):core `SessionRulesRecord`(session-policy-store.d.ts)恰是
6970
6999
  # 这两键;`principal: undefined`(会话默认规则)在 JSON 里=整键省略。
6971
7000
  additionalProperties: false
6972
7001
  required: [rules]
@@ -6979,7 +7008,7 @@ components:
6979
7008
  description: >
6980
7009
  2c session-sync `GET …/sync/manifest` payload (wrapped server-side as `{ manifest }`). The THIN cross-backend
6981
7010
  snapshot: entry IDS (oldest-first, NOT payloads) + per-snapshot relPath→blobHash + policy + anchors + leaf.
6982
- # 封闭(census 批2 第三段,2026-07-30):铸造点 `session-sync.ts:226`(exportSessionManifest 尾部
7011
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `session-sync.ts`(exportSessionManifest 尾部
6983
7012
  # 字面量)恰是这 7 键,全部无条件(leafId 缺席位=null 不省略)。
6984
7013
  additionalProperties: false
6985
7014
  required: [sessionId, entryIds, entryCount, leafId, snapshots, policy, anchors]
@@ -7011,7 +7040,7 @@ components:
7011
7040
  §7 — how a SOURCE log relates to a DESTINATION log, decided over the entry-ID SETS. A discriminated union on
7012
7041
  `relation`. `fresh`/`identical` carry nothing else; `fast_forward` the appended tail; `stale` the dst entries
7013
7042
  the src lacks; `fork` the common ancestor + each side's exclusive ids.
7014
- # 五臂全封闭(census 批2 第三段,2026-07-30):铸造点=分类器 `session-sync-kernel.ts:126-160`,
7043
+ # 五臂全封闭(census 批2 第三段,2026-07-30):铸造点=分类器 `session-sync-kernel.ts`,
7015
7044
  # 五个 return 全是字面量,臂形与 core 导出的 `SyncRelation` 判别联合逐键一致。
7016
7045
  oneOf:
7017
7046
  - type: object
@@ -7056,8 +7085,8 @@ components:
7056
7085
  description: >
7057
7086
  `POST …/sync/import` Phase-A result. `identical` → no `stagingId` (skip Phase B); `fresh`/`fast_forward` →
7058
7087
  `{ stagingId, relation }`. `relation` is the BARE classifier tag (not the full object).
7059
- # 封闭(census 批2 第三段,2026-07-30):两个铸造臂——identical 臂 `routes/session-sync.ts:385-389`
7060
- # (relation+basis+payloadVerified,无 stagingId)/ staged 臂 `:472`(stagingId+relation)。
7088
+ # 封闭(census 批2 第三段,2026-07-30):两个铸造臂——identical 臂 `routes/session-sync.ts`
7089
+ # (relation+basis+payloadVerified,无 stagingId)/ 同文件的 staged 臂(stagingId+relation)。
7061
7090
  additionalProperties: false
7062
7091
  required: [relation]
7063
7092
  properties:
@@ -7068,7 +7097,7 @@ components:
7068
7097
  type: string
7069
7098
  # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:`entry-ids+digest` 不是"将来值"——1.277.0 落
7070
7099
  # digest 半场的**同一车**里铸造点就是 `basis: payloadVerified ? "entry-ids+digest" : "entry-ids"`
7071
- # (routes/session-sync.ts:387),旧注却把它写成 x-open-enum 的假想例。补进闭集;x-open-enum 保留
7100
+ # (routes/session-sync.ts),旧注却把它写成 x-open-enum 的假想例。补进闭集;x-open-enum 保留
7072
7101
  # (真正的将来值仍按未知值降级)。
7073
7102
  enum: [entry-ids, entry-ids+digest]
7074
7103
  x-open-enum: true
@@ -7090,7 +7119,7 @@ components:
7090
7119
  ImportCommitted:
7091
7120
  type: object
7092
7121
  description: '`POST …/sync/import/:stagingId/entries` (Phase B) commit result — the AUTHORITATIVE in-txn re-classify tag.'
7093
- # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts:608`(`{ relation: committed.relation }`)单键字面量。
7122
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts`(`{ relation: committed.relation }`)单键字面量。
7094
7123
  additionalProperties: false
7095
7124
  required: [relation]
7096
7125
  properties:
@@ -7129,10 +7158,13 @@ components:
7129
7158
  `quota_exhausted` (429 — the E4 fleet quota-LEASE budget, distinct from the cost quota).
7130
7159
  NOTE: budget (`budget.precall`/`budget.exceeded`) and `cancelled` are RESULT-level codes on
7131
7160
  TaskResult/RunRecord, NOT HTTP errors (pinned wire contract).
7132
- 🔴 STABLE PREFIX FAMILIES (wire-contract Appendix A, criterion 3) — nine prefixes are a stable
7161
+ 🔴 STABLE PREFIX FAMILIES (wire-contract Appendix A, criterion 3) — ELEVEN prefixes are a stable
7133
7162
  COARSE branch surface, so falling back on the prefix (unknown code → look at its prefix → only then
7134
- at the status) is always safe: `auth.` `request.` `not_found.` `conflict.` `limit.` `capability.`
7135
- `feature.` `internal.` `state.`. The SDK maps each family to a typed class, so a code this SDK has
7163
+ at the status) is always safe: `auth.` `request.` `not_found.` `conflict.` `gone.` `parking.`
7164
+ `limit.` `capability.` `feature.` `internal.` `state.`. (`gone.` = 410, "the channel closed, switch
7165
+ channels"; `parking.` = 425, "intermediate state — you retry to learn the landing, not to re-submit".
7166
+ Both families, and both status codes, first reached the wire with server 7.3.0's in-stream approval
7167
+ decision endpoint.) The SDK maps each family to a typed class, so a code this SDK has
7136
7168
  never seen still degrades INFORMATIVELY (e.g. a future `capability.xyz_required` already lands on
7137
7169
  CapabilityUnavailableError). Family examples not named elsewhere in this document:
7138
7170
  `request.payload_too_large` (413 — a field or the whole body is over the server cap; shorten and
@@ -7148,11 +7180,18 @@ components:
7148
7180
  description: On a 409 — the task currently holding the session lock (see the Conflict response note).
7149
7181
 
7150
7182
  # ── Streaming event taxonomy (the UX 命脉). Each SSE `data:` line is one JSON-serialized AgentEvent. ──
7151
- # TWO vocabularies (the pinned wire contract Drift 3):
7183
+ # TWO vocabularies (the pinned wire contract Drift 3). ⚠️ The two lines below are the SHAPE-DEFINING
7184
+ # contrast (per-token deltas vs turn-aggregated), NOT an enumeration of either leg's arm set — the union
7185
+ # under `AgentEvent` below is the authoritative list, and each `Event_*` schema states its own legs.
7152
7186
  # live (POST /v1/tasks/stream): text_delta / reasoning_delta (delta = string fragment, core-native)
7153
- # + tool_start/tool_end/turn_end/compacted/done
7187
+ # + the lifecycle/tool/observation arms + the out-of-band NAMED frames
7188
+ # (question / elicitation / tool_approval / approval_request), which
7189
+ # interleave on the same connection but are NOT AgentEvent arms
7154
7190
  # durable (GET /v1/runs/:id/events): text / reasoning (turn-AGGREGATED, no deltas)
7155
- # + tool_start/tool_end/turn_end/compacted/suspended/done/failed
7191
+ # + the same lifecycle/tool arms + the durable-only appends
7192
+ # (prompt_assembled / config_assembled / model_usage / suggestions /
7193
+ # needs_review / task_notification / diagnostics / workflow_complete)
7194
+ # + suspended/failed
7156
7195
  AgentEvent:
7157
7196
  oneOf:
7158
7197
  - $ref: '#/components/schemas/Event_meta'
@@ -7198,6 +7237,16 @@ components:
7198
7237
  # [2854] core 5.14.0 TaskEvent 16->18 (server 7.3.0 pickup): both new arms, projected.
7199
7238
  - $ref: '#/components/schemas/Event_human_input'
7200
7239
  - $ref: '#/components/schemas/Event_wiring_manifest'
7240
+ # 🔴 #185a (2026-08-08): the three APPROVAL arms of the DURABLE leg. They were replayed by
7241
+ # GET /v1/runs/:id/events all along (the server appends them and the read boundary re-emits
7242
+ # `{type, ...data}`) but neither this union nor the SDK's declared them, so a generated client
7243
+ # had no arm to switch on and dropped approval cards. Each arm is `allOf: [<the frame schema>, the
7244
+ # narrowed discriminant]` — the key set stays owned by ToolApprovalFrame / ApprovalRequestFrame,
7245
+ # so a frame gaining a key (7.5.0's governanceForced) needs no edit here.
7246
+ # `approval_revoke` is deliberately ABSENT: it is live-only and never appended.
7247
+ - $ref: '#/components/schemas/Event_tool_approval'
7248
+ - $ref: '#/components/schemas/Event_tool_approval_complete'
7249
+ - $ref: '#/components/schemas/Event_approval_request'
7201
7250
  discriminator:
7202
7251
  propertyName: type
7203
7252
  mapping:
@@ -7236,15 +7285,18 @@ components:
7236
7285
  workflow_complete: '#/components/schemas/Event_workflow_complete'
7237
7286
  human_input: '#/components/schemas/Event_human_input'
7238
7287
  wiring_manifest: '#/components/schemas/Event_wiring_manifest'
7288
+ tool_approval: '#/components/schemas/Event_tool_approval'
7289
+ tool_approval_complete: '#/components/schemas/Event_tool_approval_complete'
7290
+ approval_request: '#/components/schemas/Event_approval_request'
7239
7291
 
7240
7292
  # design/99 §E2 (SHIPPED, core TaskEventIdentity) — additive per-event identity on every CONTENT event (NOT on
7241
7293
  # the lifecycle arms turn_end/compacted/suspended/done/failed). eventId = uuidv7 dedup/resume handle (the durable
7242
- # `text` re-stamps the turn's first text_delta eventId, runs.ts:250); parentToolCallId = sub-agent attribution.
7294
+ # `text` re-stamps the turn's first text_delta eventId, runs.ts); parentToolCallId = sub-agent attribution.
7243
7295
  EventIdentity:
7244
7296
  type: object
7245
7297
  # 🔴 census 轴一(2026-07-30)亲读铸造点补的两键:LIVE 腿的白名单化 catch-all
7246
- # (server `routes/tasks.ts:640-648`,text_delta / turn_end / message_committed / context_usage 四臂)
7247
- # 传的是**四个** identity 键,而 durable 腿的 `identityFields`(trace/project.ts:101)只传前两个
7298
+ # (server `routes/tasks.ts`,text_delta / turn_end / message_committed / context_usage 四臂)
7299
+ # 传的是**四个** identity 键,而 durable 腿的 `identityFields`(trace/project.ts)只传前两个
7248
7300
  # ——「两腿两策」是那段代码自己写下的口径。此前 spec 只声明两键 ⇒ 在封闭这些臂时会把真键判成违约。
7249
7301
  # 声明面取两腿并集(逐臂再收窄不值当),缺席即缺席。
7250
7302
  properties:
@@ -7260,7 +7312,7 @@ components:
7260
7312
  condition: durable-ledger leg only (no ledger => neither header nor frame). sessionId rides along
7261
7313
  (needed to bind subsequent turns when the server minted a fresh session). Always precedes any content
7262
7314
  frame. NOT an engine event — server-minted, no EventIdentity.
7263
- # 封闭:铸造点 routes/tasks.ts:122 是逐字写死的三键字面量(sessionId 条件在场)。
7315
+ # 封闭:铸造点 routes/tasks.ts 是逐字写死的三键字面量(sessionId 条件在场)。
7264
7316
  additionalProperties: false
7265
7317
  required: [type, taskId]
7266
7318
  properties:
@@ -7275,7 +7327,7 @@ components:
7275
7327
  # `additionalProperties` 只认**同一个 subschema** 的 `properties`,`allOf` 分支($ref EventIdentity)
7276
7328
  # 声明的键不在它的作用域内 —— 只写 `additionalProperties: false` 会把合法的 eventId 判成违约
7277
7329
  # (draft-07 的 ajv 也不支持能跨 allOf 结算的 `unevaluatedProperties`,写了会被静默忽略 = 假绿)。
7278
- # 镜像取两腿并集(超集,宁可漏抓不可假红);`$ref` 保留(spec.test.ts:92 的 §E2 门要它)。
7330
+ # 镜像取两腿并集(超集,宁可漏抓不可假红);`$ref` 保留(spec.test.ts 的 §E2 门要它)。
7279
7331
  # 镜像与 EventIdentity 的一致性由 spec.test.ts 的「封闭臂必须镜像全部 identity 键」门看守。
7280
7332
  additionalProperties: false
7281
7333
  required: [type, text]
@@ -7334,7 +7386,7 @@ components:
7334
7386
  type: { const: tool_start }
7335
7387
  toolCallId: { type: string }
7336
7388
  toolName: { type: string }
7337
- # 🔴 census 轴一(2026-07-30)回填:server 白名单 builder `toolStartEventData`(trace/project.ts:112)
7389
+ # 🔴 census 轴一(2026-07-30)回填:server 白名单 builder `toolStartEventData`(trace/project.ts)
7338
7390
  # 真发 `label`(core 展示名,与 tool_end 同待遇),spec 此前漏声明 —— 封闭前必须先补,否则真帧变假红。
7339
7391
  label: { type: string, description: 'core 展示名(缺席 ⇒ 用 toolName 渲染)。' }
7340
7392
  args: {}
@@ -7356,7 +7408,7 @@ components:
7356
7408
  type: { const: tool_end }
7357
7409
  toolCallId: { type: string }
7358
7410
  toolName: { type: string }
7359
- # 🔴 census 轴一(2026-07-30)回填:`toolEndEventData`(trace/project.ts:125)真发 `label` 与
7411
+ # 🔴 census 轴一(2026-07-30)回填:`toolEndEventData`(trace/project.ts)真发 `label` 与
7360
7412
  # `structured`,spec 此前两者都漏 —— SDK 的 events.ts 早已声明,漂移只在 spec 这一侧。
7361
7413
  label: { type: string, description: 'core 展示名(同 tool_start.label)。' }
7362
7414
  structured: {} # core 1.203 CC 卡片明细;与 output 同一 UNTRUSTED RAW 待遇(已 redactDeep),形状开放
@@ -7364,13 +7416,49 @@ components:
7364
7416
  output: {} # NON-UNIFORM: string | (TextContent|ImageContent)[] | (truncated) string. Absent ⇒ no body.
7365
7417
  truncated: { type: boolean }
7366
7418
  totalChars: { type: integer, description: 'core >=1.442 (RB-210): honest ORIGINAL size when truncated — sum of each block''s true size (text=chars, image/document=base64 bytes), no JSON-wrapping overhead. Absent on older cores / untruncated results.' } # true ⇒ output was size-bounded to a truncated string.
7419
+ # 🔴 core >=5.18.1 (#187). Closed set, mirrored here as `enum` (the vocabulary's owner is the engine).
7420
+ # ABSENCE CARRIES NO SEMANTICS: it is either an older caller, or a posture arm (headless auto-deny /
7421
+ # blanket onAsk) that deliberately leaves it unset — the two are indistinguishable, so absent is neither
7422
+ # "human" nor "no approval happened". Read the run row / checkpoint face to judge a run's fate.
7423
+ settledBy:
7424
+ type: string
7425
+ enum: [human, timeout, aborted]
7426
+ description: >
7427
+ core >=5.18.1 (#187) — HOW this call was settled when it went through an approval:
7428
+ `human` = somebody actually answered; `timeout` = the approval window elapsed;
7429
+ `aborted` = abort / unclonable arguments / out-of-contract. Present ONLY on the settled call.
7430
+ Absent = an older caller OR a posture arm that does not fill it — do NOT infer a semantic
7431
+ from the missing key.
7432
+ # 🔴 core >=5.9.0 (W3 / [2535]). OPEN SET — deliberately a bare `type: string`, NOT an enum.
7433
+ # This arm is `additionalProperties: false`, and the server's `toolEndEventData` has been emitting
7434
+ # `errorCode` since 5.9.0, so omitting it made every legitimate frame carrying one fail strict
7435
+ # validation (same family as the label/structured census drift, and as `permissionRules` in #187).
7436
+ # Writing an `enum` here would be WORSE than omitting it: the vocabulary's owner is the TOOL, not the
7437
+ # engine — any tool's `ToolResult.details.code` reaches the wire, so an enum turns every legitimate
7438
+ # unknown code into a violation while looking "stricter".
7439
+ errorCode:
7440
+ type: string
7441
+ description: >
7442
+ core >=5.9.0 (W3) — machine-readable short code for WHY this tool call failed; meaningful only
7443
+ when `isError` is true. OPEN SET: a verbatim passthrough of any tool's `ToolResult.details.code`.
7444
+ The engine's own codes (`gate.parked`, `tool.not_found`) are only two examples; the fs tools
7445
+ already emit `path_not_in_root` / `readonly_out_of_root`, and self-registered tools may add more.
7446
+ Consumers MUST branch with a `default` arm and MUST NOT exhaustively switch on known values.
7447
+ The service enforces only a length bound (>128 chars = malformed, the whole key is DROPPED rather
7448
+ than truncated — truncating would mint a code core never sent). NOTE: this is a DIFFERENT
7449
+ namespace from the `errorCode` on HTTP error bodies — and NEITHER is closed. The HTTP one is
7450
+ owned by the SERVICE (Appendix A is machine-reconciled against source, so the CURRENT catalog is
7451
+ complete and family prefixes match status codes), so consumers fall back by prefix then by status
7452
+ code and new codes may be added across versions. This one is owned by the TOOL: no prefix families,
7453
+ so the only fallback is a `default` arm.
7454
+ Absent = no machine-readable failure code (a success frame, or an older human-text-only shape).
7367
7455
  eventId: { type: string }
7368
7456
  parentToolCallId: { type: string }
7369
7457
  sourceTaskId: { type: string }
7370
7458
  bgAgentId: { type: string }
7371
7459
  Event_turn_end:
7372
7460
  type: object
7373
- # 🔴 census 轴一(2026-07-30):`turn_end` 走 LIVE 腿的白名单 catch-all(routes/tasks.ts:646),该分支
7461
+ # 🔴 census 轴一(2026-07-30):`turn_end` 走 LIVE 腿的白名单 catch-all(routes/tasks.ts),该分支
7374
7462
  # 对四个 identity 键做条件 spread —— 即「core 若真在 turn_end 上盖了 identity,它会上 wire」。
7375
7463
  # 处置:**不** allOf EventIdentity(契约面继续不承诺生命周期臂带 identity,spec.test.ts §E2 那条钉
7376
7464
  # 保持有效),但把四键作为本地可选属性声明出来,好让下面的封闭对那条真实代码路径不产生假红。
@@ -7399,7 +7487,7 @@ components:
7399
7487
  cacheReadTokens: { type: integer }
7400
7488
  cacheWriteTokens: { type: integer }
7401
7489
  costMicroUsd: { type: integer }
7402
- # 🔴 census 轴一(2026-07-30)回填:`turnEndEventData`(trace/project.ts:449)是 2026-07-25 才补的
7490
+ # 🔴 census 轴一(2026-07-30)回填:`turnEndEventData`(trace/project.ts)是 2026-07-25 才补的
7403
7491
  # builder,它带上来的这两键 spec 从未声明过 —— 而它们恰恰是**诚实缺席**语义的载体:
7404
7492
  # `usageMissing` 缺了,「没有 usage」会被读成「用量是 0」而不是「未知」。
7405
7493
  usageMissing: { const: true, description: '本轮用量不可信/缺失的诚实标记。缺席 ⇒ usage 可信。' }
@@ -7411,14 +7499,14 @@ components:
7411
7499
  Event_compacted:
7412
7500
  type: object
7413
7501
  # 🔴 **刻意不封闭**(SDK events.ts 的 compacted 臂带 `[k: string]: unknown`),但 census 轴一
7414
- # (2026-07-30)把 `compactedEventData`(trace/project.ts:340-407)真发的键**全部登记**了一遍 ——
7502
+ # (2026-07-30)把 `compactedEventData`(trace/project.ts)真发的键**全部登记**了一遍 ——
7415
7503
  # 此前 spec 只有 tokensBefore 一键,连 fixture 天天在喂的 `trigger` 都不在声明面上。
7416
7504
  additionalProperties: true
7417
7505
  required: [type]
7418
7506
  properties:
7419
7507
  type: { const: compacted }
7420
7508
  tokensBefore: { type: integer }
7421
- trigger: { type: string, description: 'auto(逼近窗口上限)| manual(/compact)。缺席按 auto 渲染。' }
7509
+ trigger: { type: string, description: '开集(engine-shaped,别当闭合枚举):`auto`(逼近窗口上限)/ `manual`(`POST /v1/runs/:id/compact`,发在该 run 自己的流上)/ `forced`(core ≥5.16.0 的 prompt-too-long 恢复腿)。缺席按 auto 渲染;未知值也按 auto 渲染,别崩。' }
7422
7510
  preserved_segment:
7423
7511
  type: object
7424
7512
  required: [firstKeptEntryId]
@@ -7453,7 +7541,7 @@ components:
7453
7541
  Event_suggestions:
7454
7542
  type: object
7455
7543
  description: >
7456
- E12 (shell-host; service runs.ts:392 + src/http/routes/runs.ts, opt-in via `TaskRequest.suggestNextPrompts`) —
7544
+ E12 (shell-host; service runs.ts + src/http/routes/runs.ts, opt-in via `TaskRequest.suggestNextPrompts`) —
7457
7545
  post-completion "what to ask next" prompt suggestions. Appended to the DURABLE run-events tail AFTER a
7458
7546
  `done` with `status:"completed"` (core's fire-and-forget LLM pass). 🔴 UNTRUSTED model text, service-
7459
7547
  redacted, UI-ONLY — render as clickable next-prompt chips; NEVER re-feed to a model. Empty/absent ⇒ none.
@@ -7499,7 +7587,7 @@ components:
7499
7587
  CLIENT wire」已是 doc-rot,SDK events.ts 早已改口):server **确实**在三条腿上转发本臂
7500
7588
  (`taskProgressEventData`,bg runs.ts append / resume append / 同步 live SSE),此外才是 fleet 子行。
7501
7589
  allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
7502
- # 🔴 census 轴一(2026-07-30)三处纠正,依据=亲读 `taskProgressEventData`(trace/project.ts:146-166):
7590
+ # 🔴 census 轴一(2026-07-30)三处纠正,依据=亲读 `taskProgressEventData`(trace/project.ts):
7503
7591
  # ① required 过严 —— builder 里 `usage`/`status` 都是**条件 spread**,只有 `taskId` 无条件在场;
7504
7592
  # spec 却把它们列进 required ⇒ 一个没带 usage 的真 tick 会被判违约(方向反了的假红)。
7505
7593
  # ② `status` 写成 `const: running` 太窄 —— core [1414]#3 的 settle 终态 tick("completed"/"failed")
@@ -7651,6 +7739,19 @@ components:
7651
7739
  properties:
7652
7740
  backgroundAgentStore: { type: boolean }
7653
7741
  hostChildEventSink: { type: boolean }
7742
+ permissionRules:
7743
+ type: object
7744
+ additionalProperties: false
7745
+ description: >
7746
+ core >=5.18.0 (design/179), server >=7.6.0 — is a PERSISTED PERMISSION RULE store wired on this leg,
7747
+ i.e. can a "don't ask again" answer to an approval actually be remembered? TENANT-visible: the engine
7748
+ keeps this section OUTSIDE the operator-only `governance` section and the server projects it on BOTH
7749
+ faces. Read it before offering a "don't ask again" affordance — with `false` there is nowhere to
7750
+ redeem it. The engine ALWAYS emits the section, so `false` is a REAL reading ("no rule lane on this
7751
+ worker"), NOT "this engine is too old". The section is optional HERE only because this spec's support
7752
+ floor is server 3.0.0, which predates it; absent = the worker never sent it.
7753
+ properties:
7754
+ storeWired: { type: boolean }
7654
7755
  governance:
7655
7756
  type: object
7656
7757
  additionalProperties: false
@@ -7741,8 +7842,8 @@ components:
7741
7842
  Event_suspended:
7742
7843
  type: object
7743
7844
  description: HITL — the run paused on an F4 gate or AskUserQuestion. Resolve via approvals.decide, then resume.
7744
- # 封闭(census 轴一,2026-07-30)。两个铸造点都是写死的字面量:server.ts:2147 `{gate}`、
7745
- # server.ts:2161 `{gate:null, reopened}` —— 后者的 `reopened` 此前 spec 完全没有,是本次亲读补上的。
7845
+ # 封闭(census 轴一,2026-07-30)。两个铸造点都是写死的字面量:server.ts `{gate}`、
7846
+ # server.ts `{gate:null, reopened}` —— 后者的 `reopened` 此前 spec 完全没有,是本次亲读补上的。
7746
7847
  additionalProperties: false
7747
7848
  required: [type]
7748
7849
  properties:
@@ -7755,7 +7856,7 @@ components:
7755
7856
  reopened:
7756
7857
  type: ['string', 'null']
7757
7858
  description: >
7758
- server.ts:2161 —— 一条 resume 腿把 run 重新挂回 suspended 时,带上「为什么重开」的 errorCode
7859
+ server.ts —— 一条 resume 腿把 run 重新挂回 suspended 时,带上「为什么重开」的 errorCode
7759
7860
  (无则显式 null)。只出现在这条重开臂上;普通挂起帧不带此键。
7760
7861
  ModelUsageDelta:
7761
7862
  # 新增(2026-07-25 回填批)。SDK 的 facade 审计把这个形状命名了一次(此前是在两个使用点各写一遍的匿名内联形:
@@ -7897,7 +7998,7 @@ components:
7897
7998
  description: >
7898
7999
  Per-model usage delta (live-verified on canary). `usage` is keyed BY MODEL NAME — same source as
7899
8000
  `done.result.stats.modelUsage` (lets a multi-model / role-routed run attribute spend per model).
7900
- # 封闭:铸造点 trace/project.ts:27 `append("model_usage", { usage: delta })` 只有这一键
8001
+ # 封闭:铸造点 trace/project.ts `append("model_usage", { usage: delta })` 只有这一键
7901
8002
  #(map 的**值**仍是开集 ModelUsageDelta —— 那层的「开」是真的,不动)。
7902
8003
  additionalProperties: false
7903
8004
  required: [type, usage]
@@ -7997,9 +8098,16 @@ components:
7997
8098
  ToolApprovalFrame:
7998
8099
  type: object
7999
8100
  description: >
8000
- LIVE tool-approval frame (SSE, `type` IS the event name). NOT an AgentEvent arm — it interleaves on the
8001
- run's stream when TOOL_APPROVAL_ENABLED. The shell renders `tool_approval` as the three-choice card and
8002
- dismisses it on `tool_approval_complete`. Respond via POST /v1/tool-approvals/{approvalId}/respond.
8101
+ Tool-approval frame (SSE, `type` IS the event name). On the LIVE stream it interleaves out-of-band when
8102
+ TOOL_APPROVAL_ENABLED and is NOT an AgentEvent arm there (the live leg dispatches by SSE `event:` name).
8103
+ On the BACKGROUND/durable leg the server appends it to the events tail so it also replays through
8104
+ GET /v1/runs/:id/events — and there it IS a declared arm as of #185a, via `Event_tool_approval` /
8105
+ `Event_tool_approval_complete` (each = THIS schema plus the narrowed discriminant, so the keys live
8106
+ here only). Before #185a the durable response was typed solely as `AgentEvent` with no such arm: the
8107
+ payload carried `type` (the read boundary emits `{type, ...data}`) so runtime discrimination worked,
8108
+ but a generated client had nothing to switch on and dropped approval cards. That gap is closed.
8109
+ The shell renders `tool_approval` as the three-choice card and dismisses it on `tool_approval_complete`.
8110
+ Respond via POST /v1/tool-approvals/{approvalId}/respond.
8003
8111
  required: [type, approvalId]
8004
8112
  additionalProperties: true
8005
8113
  properties:
@@ -8021,6 +8129,25 @@ components:
8021
8129
  args:
8022
8130
  description: '"tool_approval" only: the call''s args, secret-redacted. UNTRUSTED for display. Absent (with argsOmitted) when over the byte cap or unserializable.'
8023
8131
  argsOmitted: { type: boolean, enum: [true] }
8132
+ governanceForced:
8133
+ type: boolean
8134
+ enum: [true]
8135
+ description: >
8136
+ "tool_approval" only, ADDITIVE, two-stage presence: ABSENT on server <= 7.4.0; from server >= 7.5.0
8137
+ ([2942]/[2943]) present with value `true` when this ask's gate comes from the OPERATOR GOVERNANCE
8138
+ layer (the deployment-side AUTONOMY / commandPolicy / MANUAL_MODE_SHELL_GATE / SENSITIVE_WRITE_PATTERNS
8139
+ knobs) rather than from a model default gate or the request's own client permission posture.
8140
+ The only authority on presence is an observed frame, never a version string.
8141
+
8142
+ Use it to render a "forced by governance" badge: such a gate CANNOT be removed by a client posture,
8143
+ so the shell must present it as non-bypassable instead of pointing the user at `permissionMode`.
8144
+
8145
+ 🔴 ABSENT != false (same discipline as the risk axes): the key is present ONLY when true. Absence
8146
+ means "no evidence of a governance origin" — it covers both genuinely non-governance asks (e.g. a tool
8147
+ listed in the deployment's APPROVAL_REQUIRE) AND shapes the server cannot discriminate (e.g. when the
8148
+ governance shell gate sits at the "classify" tier, a shell ask may come from the classifier or from
8149
+ another gate; the server leaves the key absent rather than guessing). Never render absence as
8150
+ "this gate can be bypassed by a posture".
8024
8151
  fromSubagent:
8025
8152
  type: boolean
8026
8153
  enum: [true]
@@ -8029,20 +8156,36 @@ components:
8029
8156
  type: string
8030
8157
  description: 'Child''s core session id (same id domain as task_progress.taskId) — row-join value + fallback discriminator against a server predating fromSubagent.'
8031
8158
  sourceAgentName: { type: string, description: 'Display name of the child agent, redacted. UNTRUSTED.' }
8159
+ delegation:
8160
+ type: object
8161
+ description: >
8162
+ core >= 5.9.0 W1 ([2535]), "tool_approval" only, ADDITIVE: the ask's DELEGATION provenance chain
8163
+ (the innermost grandchild frame wins). Present on the same door as `fromSubagent`. Read-only display
8164
+ enrichment; `agentName` is UNTRUSTED for display like `sourceAgentName`.
8165
+ additionalProperties: true
8166
+ required: [parentToolCallId, depth]
8167
+ properties:
8168
+ parentToolCallId: { type: string }
8169
+ depth: { type: integer }
8170
+ agentName: { type: string }
8032
8171
  outcome:
8033
8172
  type: string
8034
8173
  enum: [allowed, denied, expired]
8035
- description: '"tool_approval_complete" only. `expired` = TTL/abort/disconnect — a fail-closed DENY the shell should render as such, not as "still pending".'
8174
+ description: '"tool_approval_complete" only. Cosmetic dismiss reason. `expired` = TTL/abort/disconnect; what the ENGINE received for it DEPENDS on the deployment: where the design/172 ask ledger is on (server >= 7.3.0 under STREAM_APPROVAL_ENABLED) the server''s expireAsk CAS re-adjudicates the outcome to "unavailable" and the run PARKS; without that ledger the historical fail-closed DENY stands. Never derive the run''s fate from this frame.'
8036
8175
 
8037
- # ── design/172 流内审批协议(#151;UNRELEASED,候 server 7.3.0 + STREAM_APPROVAL_ENABLED,默认 OFF)──
8176
+ # ── design/172 流内审批协议(#151)—— 已随 server 7.3.0 上线;开关 STREAM_APPROVAL_ENABLED 的默认值
8177
+ # 分两段:server ≤7.4.0 默认 OFF,≥7.5.0 默认 ON(BREAKING,见 server CHANGELOG)。──
8038
8178
  # 与上面的 ToolApprovalFrame 是**并行加帧,不替换**:开关打开时同一只 ask 出两帧(先 tool_approval,
8039
8179
  # 后 approval_request),两帧同带 `approvalId` ⇒ 消费端按它去重、优先渲染新帧。
8040
8180
  ApprovalRequestFrame:
8041
8181
  type: object
8042
8182
  description: >
8043
- IN-STREAM approval card (SSE, `type` IS the event name; design/172 §3.1). NOT an AgentEvent arm — it
8044
- interleaves on the live token stream like tool_approval/question/elicitation. Settle it via
8045
- POST /v1/tasks/{taskId}/asks/{askId}/decision.
8183
+ IN-STREAM approval card (SSE, `type` IS the event name; design/172 §3.1). TWO LEGS: on the LIVE token
8184
+ stream it is an out-of-band named frame (NOT an AgentEvent arm) interleaved like
8185
+ tool_approval/question/elicitation; on the DURABLE leg the server appends it, so the same frame replays
8186
+ through GET /v1/runs/:id/events — and there it IS registered, as the `Event_approval_request` arm
8187
+ (#185a; that arm allOf-refs THIS schema, so the keys are never restated).
8188
+ Settle it via POST /v1/tasks/{taskId}/asks/{askId}/decision.
8046
8189
 
8047
8190
  WINDOW: `expiresAtMs` is cast ONCE and never recomputed; `expiresInMs` = max(0, expiresAtMs - serverNowMs)
8048
8191
  and is therefore MONOTONICALLY DECREASING across replays — a reconnect NEVER renews the window.
@@ -8115,6 +8258,15 @@ components:
8115
8258
  argsOmitted: { type: boolean, enum: [true], description: 'Present (true) only when args exceeded the byte cap. Never false.' }
8116
8259
  toolCallId: { type: string, description: 'The engine''s tool-call id (the `call_…` on the assistant message''s tool_use block) — same domain as ToolApprovalFrame.toolCallId.' }
8117
8260
  risk: { $ref: '#/components/schemas/ApprovalRiskAxes' }
8261
+ governanceForced:
8262
+ type: boolean
8263
+ enum: [true]
8264
+ description: >
8265
+ ADDITIVE, two-stage presence (ABSENT on server <= 7.4.0; present with `true` from server >= 7.5.0):
8266
+ this ask's gate comes from the OPERATOR GOVERNANCE layer and cannot be removed by a client permission
8267
+ posture. Semantics, the ABSENT != false clause and the discrimination boundary are stated verbatim on
8268
+ ToolApprovalFrame.governanceForced. It sits at the card's top level rather than inside `risk` because
8269
+ `risk` describes the ENGINE's judgement of the call, while this key says WHO imposed the gate.
8118
8270
  fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
8119
8271
  sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
8120
8272
  sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
@@ -8344,8 +8496,14 @@ components:
8344
8496
  QuestionFrame:
8345
8497
  type: object
8346
8498
  description: >
8347
- LIVE AskUserQuestion frame (SSE, `type` IS the event name). NOT an AgentEvent arm. Respond via
8348
- POST /v1/questions/{questionId}/respond; unanswered asks fall back to the headless default.
8499
+ AskUserQuestion frame (SSE, `type` IS the event name). TWO LEGS: on the LIVE stream it is an out-of-band
8500
+ named frame (NOT an AgentEvent arm); on the DURABLE leg the server persists it, so the same frame replays
8501
+ through GET /v1/runs/:id/events — and there it IS registered, as the closed `Event_question` /
8502
+ `Event_question_complete` arms (keep the key sets mirrored; a gate enforces it).
8503
+ Respond via POST /v1/questions/{questionId}/respond. 🔴 An UNANSWERED ask does NOT "fall back to the headless
8504
+ default" (server 7.4.0 / #166): the deployment reports `unavailable` ("nobody was reachable") and CORE
8505
+ picks the landing — a DURABLE leg PARKS a checkpoint for an operator to answer later, a non-durable leg
8506
+ continues on the `declined_unavailable` synthetic-continuation card. The run never hangs either way.
8349
8507
  required: [type, questionId]
8350
8508
  additionalProperties: true
8351
8509
  properties:
@@ -8354,11 +8512,14 @@ components:
8354
8512
  questions:
8355
8513
  type: array
8356
8514
  items: { $ref: '#/components/schemas/AskQuestion' }
8357
- description: '"question" only: the model''s structured questions, secret-redacted. UNTRUSTED — never re-feed to a model. Absent on an over-cap payload (the ask then headless-defaults and the shell never renders it).'
8515
+ description: '"question" only: the model''s structured questions, secret-redacted. UNTRUSTED — never re-feed to a model. Absent on an over-cap payload — and then NO frame is emitted at all: the ask reports `unavailable` (see this schema''s description).'
8358
8516
  outcome:
8359
8517
  type: string
8360
8518
  enum: [answered, unanswered]
8361
- description: '"question_complete" only: `answered` = a human responded; `unanswered` = ttl/abort/throttle released it (the model got the headless default). Cosmetic dismiss reason.'
8519
+ description: '"question_complete" only: `answered` = a human responded; `unanswered` = ttl/abort/throttle released it and core was told NOBODY WAS REACHABLE (`unavailable`) — on a durable deployment that PARKS rather than handing the model a default. Cosmetic dismiss reason only; never derive the run''s fate from it.'
8520
+ serverNowMs:
8521
+ type: integer
8522
+ description: 'server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame has no core-stream eventId to carry, so this is its own time coordinate. Absent on older servers.'
8362
8523
 
8363
8524
  AskQuestion:
8364
8525
  type: object
@@ -8385,8 +8546,11 @@ components:
8385
8546
  ElicitationFrame:
8386
8547
  type: object
8387
8548
  description: >
8388
- LIVE MCP elicitation frame (SSE, `type` IS the event name). NOT an AgentEvent arm. Respond via
8389
- POST /v1/elicitations/{elicitationId}/respond.
8549
+ MCP elicitation frame (SSE, `type` IS the event name). TWO LEGS: on the LIVE stream it is an
8550
+ out-of-band named frame (NOT an AgentEvent arm); on the DURABLE leg the server persists it, so the same
8551
+ frame replays through GET /v1/runs/:id/events — and there it IS registered, as the closed
8552
+ `Event_elicitation` / `Event_elicitation_complete` arms (keep the key sets mirrored; a gate enforces it).
8553
+ Respond via POST /v1/elicitations/{elicitationId}/respond.
8390
8554
  required: [type, elicitationId, mcpServerName]
8391
8555
  additionalProperties: true
8392
8556
  properties:
@@ -8403,6 +8567,9 @@ components:
8403
8567
  type: string
8404
8568
  enum: [accept, decline, cancel]
8405
8569
  description: '"elicitation_complete" only: how it resolved (dialog dismiss reason).'
8570
+ serverNowMs:
8571
+ type: integer
8572
+ description: 'server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame carries no core-stream `eventId`, so this is its own time coordinate. Absent on older servers.'
8406
8573
 
8407
8574
  Event_steering_injected:
8408
8575
  type: object
@@ -8427,7 +8594,7 @@ components:
8427
8594
  2026-08-02) — do NOT assume the full TaskResult:
8428
8595
  · `TaskResult` — the normal terminal (output string at `.result`, stats at `.stats`), service Drift 4;
8429
8596
  · `ActiveRunConflictDoneResult` — the SSE lane's REJECTION terminal: when the session already has an
8430
- active run, the submit's 409 is encoded as this done frame (`src/http/routes/tasks.ts:234`), and it
8597
+ active run, the submit's 409 is encoded as this done frame (`src/http/routes/tasks.ts`), and it
8431
8598
  carries NO `taskId` / `sessionId` / `stats`.
8432
8599
  Narrow on the presence of `stats` (required on TaskResult, absent from the conflict shape), or on
8433
8600
  `errorCode === "conflict.session_active_run"`. `status` does NOT discriminate — both shapes carry it.
@@ -8442,17 +8609,17 @@ components:
8442
8609
  replay:
8443
8610
  type: boolean
8444
8611
  description: >
8445
- 契约调和车1 F(2026-08-02;server `src/http/routes/tasks.ts:71` 与 `:919` 两处铸造点)— `true`
8612
+ 契约调和车1 F(2026-08-02;server `src/http/routes/tasks.ts` 的两处铸造点)— `true`
8446
8613
  marks the IDEMPOTENT-REPLAY leg: this terminal was NOT produced by a live run of your submit. Either
8447
- the `Idempotency-Key` hit the in-flight/settled cache (:71), or your call was deduplicated into a
8448
- concurrent identical submit and received no live events at all (:919). The `result` is the original
8614
+ the `Idempotency-Key` hit the in-flight/settled cache, or your call was deduplicated into a
8615
+ concurrent identical submit and received no live events at all. The `result` is the original
8449
8616
  run's terminal, verbatim. ABSENT on every live leg (never `false`) — a UI can use it to say "replayed"
8450
8617
  instead of implying the work just ran twice.
8451
8618
 
8452
8619
  ActiveRunConflictDoneResult:
8453
8620
  type: object
8454
8621
  description: >
8455
- The SSE lane's REJECTION `done.result` (server `src/http/active-run-conflict.ts:64` `toDoneFrameResult`,
8622
+ The SSE lane's REJECTION `done.result` (server `src/http/active-run-conflict.ts` `toDoneFrameResult`,
8456
8623
  server >= 3.21). Same material as the 409 `conflict.session_active_run` body, minus the token-free
8457
8624
  addressing rule's exceptions: a checkpoint token NEVER appears on the wire. It is NOT a TaskResult —
8458
8625
  there is no run to report on, so `taskId`/`sessionId`/`stats` are structurally absent.
@@ -8499,11 +8666,11 @@ components:
8499
8666
 
8500
8667
  # ══════════════════════════════════════════════════════════════════════════════════════════════
8501
8668
  # 🔴 census 轴一(2026-07-30)补的**四个未登记臂**。它们不是「将来会有」——**今天就在 wire 上**:
8502
- # · `context_usage` LIVE(routes/tasks.ts:653)+ DURABLE(trace/ledger-sink.ts:144 / server.ts:2115)
8503
- # · `config_assembled` DURABLE(trace/project.ts:43)
8504
- # · `needs_review` DURABLE(http/server.ts:2148)
8505
- # · `message_committed` LIVE(routes/tasks.ts:650)
8506
- # durable 腿的读边界(routes/runs.ts:99 `formatEvent`)对**每一条**落库事件原样下发 `{type, ...data}`
8669
+ # · `context_usage` LIVE(routes/tasks.ts)+ DURABLE(trace/ledger-sink.ts / server.ts)
8670
+ # · `config_assembled` DURABLE(trace/project.ts)
8671
+ # · `needs_review` DURABLE(http/server.ts)
8672
+ # · `message_committed` LIVE(routes/tasks.ts)
8673
+ # durable 腿的读边界(routes/runs.ts `formatEvent`)对**每一条**落库事件原样下发 `{type, ...data}`
8507
8674
  # (唯一归一是 brain_status→status),所以「本仓 append 了什么」= 「wire 上会出现什么」。而本文件的
8508
8675
  # `AgentEvent` 是闭集 oneOf ⇒ 一个真实的 context_usage 帧对着 spec 校验**当场零臂命中**。
8509
8676
  # ⚠️ 元教训:`spec-union-arm-gate` 比的是 spec ↔ SDK 两侧的臂集,这四个臂**两侧同缺**,所以那道门
@@ -8512,7 +8679,7 @@ components:
8512
8679
  Event_context_usage:
8513
8680
  type: object
8514
8681
  description: >
8515
- core 1.414 first-class arm, whitelisted by `contextUsageEventData` (server trace/project.ts:431).
8682
+ core 1.414 first-class arm, whitelisted by `contextUsageEventData` (server trace/project.ts).
8516
8683
  🔴 Two engine-stated conventions, pass them through, never re-derive: (a) `usedTokens > compactAtTokens`
8517
8684
  IS the predicate the engine itself feeds `shouldCompact`; (b) `windowTokens` is the AUTOCOMPACT window,
8518
8685
  NOT the model's context size (rendering it as "model context" gives the user a wrong denominator).
@@ -8526,7 +8693,7 @@ components:
8526
8693
  usedTokens: { type: integer }
8527
8694
  windowTokens: { type: integer, description: 'autocompact 窗,不是模型上下文大小。' }
8528
8695
  compactAtTokens: { type: integer }
8529
- # LIVE 腿(routes/tasks.ts:653)在这条臂上挂 identity;durable 腿不挂。同 turn_end 的处置:
8696
+ # LIVE 腿(routes/tasks.ts)在这条臂上挂 identity;durable 腿不挂。同 turn_end 的处置:
8530
8697
  # 本地可选声明,不 allOf(不把 identity 承诺进契约面)。
8531
8698
  eventId: { type: string }
8532
8699
  parentToolCallId: { type: string }
@@ -8536,7 +8703,7 @@ components:
8536
8703
  type: object
8537
8704
  description: >
8538
8705
  Compaction result frame ([2373]A-2, server 5.0.0 three-leg parity batch: live SSE + durable bg append +
8539
- durable resume append all share one builder, `compactionOutcomeEventData`, server trace/project.ts:375 —
8706
+ durable resume append all share one builder, `compactionOutcomeEventData`, server trace/project.ts —
8540
8707
  replays no longer drop it). `outcome`/`trigger` are engine-shaped passthrough strings coerced via
8541
8708
  String(); `reason` is optional, secret-redacted. Identity: TWO keys only (`eventId`,
8542
8709
  `parentToolCallId`) — this arm is NOT on the live-leg four-key whitelist that `context_usage` sits on
@@ -8555,7 +8722,7 @@ components:
8555
8722
  Event_config_assembled:
8556
8723
  type: object
8557
8724
  description: >
8558
- DURABLE only ([1301]③, server trace/project.ts:43) — the per-turn companion record of `prompt_assembled`:
8725
+ DURABLE only ([1301]③, server trace/project.ts) — the per-turn companion record of `prompt_assembled`:
8559
8726
  which config catalog version produced this turn's effective fields, and why anything was overridden.
8560
8727
  Bounded frame: the builder DROPS the whole record over 32 KiB rather than shipping a truncated half-shape.
8561
8728
  `fields` / `overrideReasons` are engine/registry-shaped passthrough — treat as opaque display data.
@@ -8570,7 +8737,7 @@ components:
8570
8737
  Event_needs_review:
8571
8738
  type: object
8572
8739
  description: >
8573
- DURABLE only (design/80 D-B, server http/server.ts:2148) — a RESUMED plan_review leg got re-gated into
8740
+ DURABLE only (design/80 D-B, server http/server.ts) — a RESUMED plan_review leg got re-gated into
8574
8741
  another review pause. Same payload shape as `suspended`: the gate verbatim, or an explicit `null`.
8575
8742
  NOTE `needs_review` is BOTH a `RunStatus` terminal and a `CheckpointGate.kind`; this arm is the third
8576
8743
  use of the name — the stream frame announcing that pause.
@@ -8586,7 +8753,7 @@ components:
8586
8753
  Event_message_committed:
8587
8754
  type: object
8588
8755
  description: >
8589
- LIVE only (server routes/tasks.ts:650, whitelisted in the same catch-all as text_delta/turn_end/
8756
+ LIVE only (server routes/tasks.ts, whitelisted in the same catch-all as text_delta/turn_end/
8590
8757
  context_usage) — a session-history entry was committed. `entryId` is the durable entry handle the
8591
8758
  R8 rewind anchor is keyed on ("rewind to the prompt" targets a `role:"user"` entry).
8592
8759
  additionalProperties: false
@@ -8609,8 +8776,8 @@ components:
8609
8776
  Event_error:
8610
8777
  type: object
8611
8778
  description: >
8612
- STREAM-CONTROL frame, NOT a run failure (server src/http/sse-log.ts:86; the trace leg emits the same
8613
- shape at routes/trace-usage.ts:294). The server writes it as a NAMED frame (`event: error`) whose payload
8779
+ STREAM-CONTROL frame, NOT a run failure (server src/http/sse-log.ts; the trace leg emits the same
8780
+ shape at routes/trace-usage.ts). The server writes it as a NAMED frame (`event: error`) whose payload
8614
8781
  echoes `type`, so both an `event:`-dispatching client and a payload-dispatching one see it.
8615
8782
  🔴 Its arrival means the RUN IS STILL ALIVE — the 15-minute stream cap fired. The correct handling is to
8616
8783
  RECONNECT with `Last-Event-ID` and keep reading (the durable leg stamps `id:` on every frame); rendering
@@ -8626,12 +8793,15 @@ components:
8626
8793
  Event_question:
8627
8794
  type: object
8628
8795
  description: >
8629
- AskUserQuestion OPEN frame (server src/question.ts:38 `QuestionFrame`). The live leg dispatches it by SSE
8630
- event name (routes/tasks.ts:677, the same out-of-band channel as `elicitation`/`tool_approval`); the
8631
- DURABLE leg persists it via `append(type, rest)` (src/runs.ts:498), so the same frame also replays on
8796
+ AskUserQuestion OPEN frame (server src/question.ts `QuestionFrame`). The live leg dispatches it by SSE
8797
+ event name (routes/tasks.ts, the same out-of-band channel as `elicitation`/`tool_approval`); the
8798
+ DURABLE leg persists it via `append(type, rest)` (src/runs.ts), so the same frame also replays on
8632
8799
  GET /v1/runs/:id/events — that half is what this schema registers.
8633
8800
  🔴 UNTRUSTED-for-display: `questions` is secret-redacted, render only, NEVER re-feed to a model.
8634
- `questions` absent ⇒ the payload exceeded the 16 KiB cap and was dropped (the ask headless-defaults).
8801
+ `questions` absent ⇒ the payload exceeded the 16 KiB cap and was dropped — and then NO frame is emitted
8802
+ at all: the ask reports `unavailable` (server 7.4.0 / #166), which on a DURABLE deployment PARKS a
8803
+ checkpoint for an operator and on a non-durable one continues via the `declined_unavailable`
8804
+ synthetic-continuation card. It does NOT "headless-default".
8635
8805
  additionalProperties: false
8636
8806
  required: [type, questionId]
8637
8807
  properties:
@@ -8640,22 +8810,38 @@ components:
8640
8810
  questions:
8641
8811
  type: array
8642
8812
  items: { $ref: '#/components/schemas/AskQuestion' }
8813
+ serverNowMs:
8814
+ type: integer
8815
+ description: >
8816
+ server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame carries no
8817
+ core-stream `eventId`, so this is its own time coordinate. 🔴 It MUST be declared here: this schema
8818
+ is `additionalProperties: false`, so omitting a key the server really sends makes a strict validator
8819
+ reject a legitimate frame. Absent on servers < 7.3.0.
8643
8820
  Event_question_complete:
8644
8821
  type: object
8645
8822
  description: >
8646
8823
  AskUserQuestion CLOSE frame (dialog-dismiss signal). `answered` = a human responded; `unanswered` =
8647
- ttl / abort / throttle released it and the model got the headless default. Cosmetic — it does not change
8648
- the run's status.
8824
+ ttl / abort / throttle released it and core was told NOBODY WAS REACHABLE (`unavailable`) — on a durable
8825
+ deployment that PARKS rather than handing the model a default (server 7.4.0 / #166; the old "the model
8826
+ got the headless default" reading is wrong). Cosmetic dismiss reason — it does not itself change the
8827
+ run's status, and the run's fate must never be derived from it.
8649
8828
  additionalProperties: false
8650
8829
  required: [type, questionId]
8651
8830
  properties:
8652
8831
  type: { const: question_complete }
8653
8832
  questionId: { type: string }
8654
8833
  outcome: { type: string, enum: [answered, unanswered] }
8834
+ serverNowMs:
8835
+ type: integer
8836
+ description: >
8837
+ server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame carries no
8838
+ core-stream `eventId`, so this is its own time coordinate. 🔴 It MUST be declared here: this schema
8839
+ is `additionalProperties: false`, so omitting a key the server really sends makes a strict validator
8840
+ reject a legitimate frame. Absent on servers < 7.3.0.
8655
8841
  Event_elicitation:
8656
8842
  type: object
8657
8843
  description: >
8658
- Inbound-MCP elicitation OPEN frame (server src/elicitation.ts:42 `ElicitationFrame`). Same two legs as
8844
+ Inbound-MCP elicitation OPEN frame (server src/elicitation.ts `ElicitationFrame`). Same two legs as
8659
8845
  `question`: live dispatches by event name, the durable leg persists + replays it.
8660
8846
  🔴 UNTRUSTED: `message` is fenced + secret-redacted; `requestedSchema` is an OPAQUE passthrough (never
8661
8847
  interpreted/validated) and must still be fenced on display. v1 is always `mode: "form"` (url-mode is
@@ -8669,6 +8855,13 @@ components:
8669
8855
  message: { type: string }
8670
8856
  requestedSchema: {}
8671
8857
  mode: { type: string, enum: [form] }
8858
+ serverNowMs:
8859
+ type: integer
8860
+ description: >
8861
+ server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame carries no
8862
+ core-stream `eventId`, so this is its own time coordinate. 🔴 It MUST be declared here: this schema
8863
+ is `additionalProperties: false`, so omitting a key the server really sends makes a strict validator
8864
+ reject a legitimate frame. Absent on servers < 7.3.0.
8672
8865
  Event_elicitation_complete:
8673
8866
  type: object
8674
8867
  description: 'Inbound-MCP elicitation CLOSE frame; `action` is how it settled (the dialog-dismiss reason).'
@@ -8679,11 +8872,18 @@ components:
8679
8872
  elicitationId: { type: string }
8680
8873
  mcpServerName: { type: string }
8681
8874
  action: { type: string, enum: [accept, decline, cancel] }
8875
+ serverNowMs:
8876
+ type: integer
8877
+ description: >
8878
+ server >= 7.3.0, ADDITIVE: the server-signed emit timestamp. A server-minted side-frame carries no
8879
+ core-stream `eventId`, so this is its own time coordinate. 🔴 It MUST be declared here: this schema
8880
+ is `additionalProperties: false`, so omitting a key the server really sends makes a strict validator
8881
+ reject a legitimate frame. Absent on servers < 7.3.0.
8682
8882
  Event_workflow_complete:
8683
8883
  type: object
8684
8884
  description: >
8685
8885
  Out-of-band delivery of an async workflow's completion (server
8686
- src/orchestration/workflow-completion-inbox.ts:522). Delivery is STREAM-OPEN driven: the session's inbox
8886
+ src/orchestration/workflow-completion-inbox.ts). Delivery is STREAM-OPEN driven: the session's inbox
8687
8887
  is drained on its NEXT leg — the sync leg writes a live frame, the bg/resume legs `append` it as a durable
8688
8888
  event the tailing client replays.
8689
8889
  🔴 AT-LEAST-ONCE (two-phase: the entry is acked only after a successful emit, and two legs racing the same
@@ -8697,6 +8897,67 @@ components:
8697
8897
  status: { type: string }
8698
8898
  summary: { type: string }
8699
8899
 
8900
+ # ── 🔴 #185a (2026-08-08): the DURABLE leg's three APPROVAL arms ────────────────────────────────
8901
+ # Shape rule for all three: `allOf: [<frame schema>, {the narrowed discriminant}]`. The keys are NOT
8902
+ # restated here — ToolApprovalFrame / ApprovalRequestFrame stay their single owner, so an additive
8903
+ # frame key (7.5.0's `governanceForced`) lands on the arm for free. The arms stay OPEN
8904
+ # (the frame schemas are `additionalProperties: true`), which is also why they need no local mirror
8905
+ # of the allOf branch keys (that duty only binds closed schemas).
8906
+ # Presence is CONDITIONAL and differs per family — an arm being declared says the payload's SHAPE,
8907
+ # never that the frame will arrive: `tool_approval*` needs TOOL_APPROVAL_ENABLED, `approval_request`
8908
+ # needs the design/172 protocol (server >= 7.3.0; default OFF <= 7.4.0 / ON >= 7.5.0, and the only
8909
+ # authority is the measured `capabilities.streamApproval` bit, never a version string).
8910
+ Event_tool_approval:
8911
+ description: >
8912
+ DURABLE-leg replay of the legacy tool-approval OPEN frame. Same payload as ToolApprovalFrame with the
8913
+ discriminant narrowed to `tool_approval`. Settle it via POST /v1/tool-approvals/{approvalId}/respond.
8914
+ Anchor the card on `toolCallId` (NOT `approvalId`); render `governanceForced` as a non-bypassable
8915
+ "forced by governance" badge. UNTRUSTED for display: `message`/`args` are redacted but model-authored.
8916
+ allOf:
8917
+ - $ref: '#/components/schemas/ToolApprovalFrame'
8918
+ - type: object
8919
+ required: [type]
8920
+ properties:
8921
+ type: { type: string, enum: [tool_approval] }
8922
+ Event_tool_approval_complete:
8923
+ description: >
8924
+ DURABLE-leg replay of the legacy tool-approval CLOSE frame (ToolApprovalFrame with the discriminant
8925
+ narrowed to `tool_approval_complete`). `outcome` is a COSMETIC dismiss reason: for `expired`, a
8926
+ deployment carrying the design/172 ask ledger re-adjudicates to "unavailable" and the run PARKS, while
8927
+ one without it keeps the historical fail-closed DENY. NEVER derive the run's fate from this frame.
8928
+ allOf:
8929
+ - $ref: '#/components/schemas/ToolApprovalFrame'
8930
+ - type: object
8931
+ required: [type]
8932
+ properties:
8933
+ type: { type: string, enum: [tool_approval_complete] }
8934
+ Event_approval_request:
8935
+ description: >
8936
+ The design/172 in-stream approval card as an AgentEvent arm. PARALLEL to `tool_approval`, not a
8937
+ replacement: with the protocol on, one ask emits BOTH frames (legacy first) carrying the SAME
8938
+ `approvalId` — dedupe on it and prefer this arm. The decision key is `askId`; settle via
8939
+ POST /v1/tasks/{taskId}/asks/{askId}/decision.
8940
+
8941
+ 🔴 THIS ARM IS THE OPEN ENVELOPE, NOT `ApprovalRequestFrame`, AND THAT IS DELIBERATE. The v1 frame
8942
+ schema pins `schemaVersion: 1` and `kind: permission`, but a stream carries whatever the server sends:
8943
+ binding the arm to v1 would (a) make a generated client REJECT a legitimate future-version frame at the
8944
+ `oneOf`, and (b) tell a TypeScript/codegen consumer that `card` and its risk axes are guaranteed present
8945
+ on a frame that never validated as v1 — the exact bypass of the "unknown shapes get a GENERIC card,
8946
+ NEVER auto-deny" rule stated on ApprovalRequestFrame and ApprovalFrameEnvelope.
8947
+ ⇒ Read this arm as the envelope, THEN check `schemaVersion == 1 && kind == "permission"` (SDK:
8948
+ `isApprovalRequestFrameV1`) before touching `card`/`askId`/the window keys — the validated v1 payload is
8949
+ described by `ApprovalRequestFrame`. If it does not validate, render the generic card.
8950
+
8951
+ 🔴 A frame replayed from the durable events tail is for TIMELINE RENDERING ONLY, never a card-set
8952
+ baseline: its `expiresInMs` is frozen at mint time, and the full reconciliation baseline is the
8953
+ open-stream preamble.
8954
+ allOf:
8955
+ - $ref: '#/components/schemas/ApprovalFrameEnvelope'
8956
+ - type: object
8957
+ required: [type]
8958
+ properties:
8959
+ type: { type: string, enum: [approval_request] }
8960
+
8700
8961
  # ── resources/*.ts schema backfill, batch 2 (2026-07-28) — the 28-item WIRE_PENDING ratchet zeroed.
8701
8962
  # Each schema below is cross-checked against the server source (file:line cited in its description
8702
8963
  # or in the SDK resource JSDoc it mirrors). Wire objects are OPEN sets (additionalProperties: true).
@@ -8727,7 +8988,7 @@ components:
8727
8988
  description: >
8728
8989
  The 200 body of POST /v1/approvals/{sessionId}/decide when the pending checkpoint belongs to a
8729
8990
  PARKED background agent (server 1.267, ASSISTANT-WIRE-CONTRACT §4a parked variant; server
8730
- parked-decide.ts:272 — exact literal shape). ACCEPTANCE semantics: the revive drives
8991
+ parked-decide.ts — exact literal shape). ACCEPTANCE semantics: the revive drives
8731
8992
  ASYNCHRONOUSLY — 200 means accepted, not completed; a failed drive honestly re-parks the row and
8732
8993
  the pending re-appears on the list. Task-level suspends keep the legacy resumed shape —
8733
8994
  discriminate by `status === "resuming"`.
@@ -8827,19 +9088,19 @@ components:
8827
9088
  detail: { type: string, description: 'Redacted supplement (e.g. why skipped). Additive / tolerate-absent.' }
8828
9089
 
8829
9090
  BakeStatus:
8830
- # 新(census 批2 四段)——server `BakeStatus`(plugins/store-contracts.ts:83), the COARSE lifecycle
9091
+ # 新(census 批2 四段)——server `BakeStatus`(plugins/store-contracts.ts), the COARSE lifecycle
8831
9092
  # bakeView/claimResponse/bakesCreate all key on.
8832
9093
  type: string
8833
9094
  enum: [queued, running, done, failed]
8834
9095
 
8835
9096
  BakeState:
8836
- # 新(census 批2 四段)——server `BakeState`(store-contracts.ts:85-93), build.sh's finer 8-value state;
9097
+ # 新(census 批2 四段)——server `BakeState`(store-contracts.ts), build.sh's finer 8-value state;
8837
9098
  # null before the runner's first `state` line lands.
8838
9099
  type: [string, 'null']
8839
9100
  enum: [PENDING, BUILDING, PUSHING, VERIFYING, REGISTERING, COMPLETE, FAILED, CANCELLED, null]
8840
9101
 
8841
9102
  BakeErrorCode:
8842
- # 新(census 批2 四段)——server `BakeErrorCode`(store-contracts.ts:96), the closed structured terminal
9103
+ # 新(census 批2 四段)——server `BakeErrorCode`(store-contracts.ts), the closed structured terminal
8843
9104
  # code set (§P2.14 #2); null = success or an uncategorized failure.
8844
9105
  type: [string, 'null']
8845
9106
  enum: [band-conflict, duplicate, disk, timeout, cancelled, null]
@@ -8875,7 +9136,7 @@ components:
8875
9136
 
8876
9137
  BakeRecordView:
8877
9138
  # 新(census 批2 四段)——GET /v1/images/bakes/{bakeId}'s `{ bake }` envelope. Exact key set = server
8878
- # `bakeView()` (images.ts:527-551): the durable BakeRecord MINUS the runner-internal secret/lease fields
9139
+ # `bakeView()` (images.ts): the durable BakeRecord MINUS the runner-internal secret/lease fields
8879
9140
  # (ingestSecret/runnerId/leaseUntil/cancelRequested/argv — an ops surface, not the claim credential).
8880
9141
  type: object
8881
9142
  description: The operator poll view of one bake (server bakeView projection over the durable BakeRecord).
@@ -8908,7 +9169,7 @@ components:
8908
9169
 
8909
9170
  BakeSubmitAck:
8910
9171
  # 新(census 批2 四段)——POST /v1/images/bakes 202 body. All three code paths (attach-to-in-flight,
8911
- # atomic-admission-created, dryRun) produce this SAME shape (images.ts:193/206/213/217).
9172
+ # atomic-admission-created, dryRun) produce this SAME shape (images.ts).
8912
9173
  type: object
8913
9174
  description: Acknowledgement of a submitted/attached-to bake.
8914
9175
  required: [bakeId, eventsUrl, status, state]
@@ -8920,7 +9181,7 @@ components:
8920
9181
  state: { $ref: '#/components/schemas/BakeState' }
8921
9182
 
8922
9183
  BakeCancelAck:
8923
- # 新(census 批2 四段)——POST /v1/images/bakes/{bakeId}/cancel 202 body (images.ts:239-241). `note` is
9184
+ # 新(census 批2 四段)——POST /v1/images/bakes/{bakeId}/cancel 202 body (images.ts). `note` is
8924
9185
  # ALWAYS present (a ternary VALUE, not a conditional key) — the spec previously implied it was optional.
8925
9186
  type: object
8926
9187
  description: Cooperative-cancel acknowledgement (a durable flag; the runner kills the build at its next heartbeat).
@@ -8933,7 +9194,7 @@ components:
8933
9194
 
8934
9195
  BakeIngestAck:
8935
9196
  # 新(census 批2 四段)——POST /v1/images/bakes/{bakeId}/ingest 200 body. 🔴 FOUR distinct shapes share
8936
- # this one status code (images.ts:302-334): a heartbeat ack carries no `seq`; a non-terminal frame ack
9197
+ # this one status code (images.ts): a heartbeat ack carries no `seq`; a non-terminal frame ack
8937
9198
  # carries `seq` but no `terminal`; a terminal `done` carries `terminal`+`indexId` (success) OR
8938
9199
  # `terminal`+`needsFlip` (the bounded-retry-exhausted non-throw path, still 200 — the runner re-POSTs the
8939
9200
  # idempotent done to re-drive the flip). `cancelRequested`+`leaseValid` ride on every non-heartbeat shape
@@ -9102,7 +9363,7 @@ components:
9102
9363
  nextBefore?}`; server src/http/routes/sessions-list.ts — exact projection). A principal caller is PINNED to its
9103
9364
  own scope (?scope= ignored); a fleet credential may pass ?scope= (default = the single-user
9104
9365
  sentinel "_"). Page with `before` = a previous page's `nextBefore` (uuidv7; malformed → 400).
9105
- # 🔴 census 批2 五段:CLOSED — exact projection (sessions-list.ts:94-105); taskId/sessionId are the
9366
+ # 🔴 census 批2 五段:CLOSED — exact projection (sessions-list.ts); taskId/sessionId are the
9106
9367
  # only OMIT-when-absent keys, no others.
9107
9368
  required: [id, filename, size, ttlSec, track, bucket, key, createdAt]
9108
9369
  additionalProperties: false
@@ -9123,7 +9384,7 @@ components:
9123
9384
  enum: [now, next, later]
9124
9385
  description: >-
9125
9386
  [2400] TR-4 — per-message queue priority on POST /v1/runs/{taskId}/steer. ACCEPTED + ENUM-VALIDATED
9126
- FAIL-LOUD (`routes/runs.ts:603`; anything else is 400 `request.field_invalid`) and echoed back on the
9387
+ FAIL-LOUD (`routes/runs.ts`; anything else is 400 `request.field_invalid`) and echoed back on the
9127
9388
  receipt, but ADVISORY today: core's steer is a single-slot last-writer-wins with no per-message priority
9128
9389
  or addressable drop, so a faithful priority queue (and DELETE /v1/runs/{taskId}/queue/{messageId}) waits
9129
9390
  on a core seam.
@@ -9133,16 +9394,16 @@ components:
9133
9394
  description: >
9134
9395
  [2400] TR-4 — the POST /v1/runs/{taskId}/steer receipt, DISCRIMINATED on `delivery` (three server mint
9135
9396
  points meaning three different things; branch on this, NOT on the status code):
9136
- `applied` (200, `routes/runs.ts:648`) — injected into the run LIVE on this replica, drains at the next
9397
+ `applied` (200, `routes/runs.ts`) — injected into the run LIVE on this replica, drains at the next
9137
9398
  turn boundary (`status:"running"`);
9138
- `queued` (202, `:641`) — the run is durably SUSPENDED, so the steer was PARKED on its pending checkpoint
9399
+ `queued` (202) — the run is durably SUSPENDED, so the steer was PARKED on its pending checkpoint
9139
9400
  and is injected on resume (`status:"suspended"`);
9140
- `parked_for_wake` (202, `:734`) — the run already ENDED; the server minted a `task_done` checkpoint and
9401
+ `parked_for_wake` (202) — the run already ENDED; the server minted a `task_done` checkpoint and
9141
9402
  parked the message on it, delivered ONLY by POST /v1/sessions/{sessionId}/wake — nothing happens until
9142
9403
  the session is woken.
9143
9404
  `messageId` is server-minted and stable across all three (dedup / correlation handle).
9144
9405
  required: [taskId, status, delivery, messageId]
9145
- # 封闭:三个铸造点是逐字面量拼出来的对象(routes/runs.ts:641/648/734),不是引擎透传形。
9406
+ # 封闭:三个铸造点是逐字面量拼出来的对象(routes/runs.ts),不是引擎透传形。
9146
9407
  additionalProperties: false
9147
9408
  properties:
9148
9409
  taskId: { type: string }
@@ -9176,8 +9437,7 @@ components:
9176
9437
  TaskOutput-tool text (the child's final assistant body once terminal) — UNTRUSTED model output,
9177
9438
  same posture as a run `result`. `output` = the engine registry's UnifiedTaskOutput projection
9178
9439
  passed through VERBATIM.
9179
- # 封闭(census 批2 第三段,2026-07-30):两个铸造点(`routes/runs.ts:934` 窄面 4 键 / `:1155` generic
9180
- # 面 4 键 + 条件 `...g14`)顶层键集就是这 5 个。`output` 子对象**保持开**——它是 registry 投影的
9440
+ # 封闭(census 批2 第三段,2026-07-30):两个铸造点(`routes/runs.ts` 的窄面 4 键 / generic 面 4 键 + 条件 `...g14`)顶层键集就是这 5 个。`output` 子对象**保持开**——它是 registry 投影的
9181
9441
  # verbatim 透传(deps 签名 `details: unknown`),不是 server 铸的形。
9182
9442
  required: [taskId, target, content, output]
9183
9443
  additionalProperties: false
@@ -9216,7 +9476,7 @@ components:
9216
9476
  exact literal: `status:"running"`, `delivery:"accepted"`). ACCEPTANCE, not an outcome: compaction
9217
9477
  runs at the next turn boundary and a `compacted{trigger:"manual"}` event rides the run stream ONLY
9218
9478
  if anything was summarized (mooted/failed → no event).
9219
- # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts:793` 是
9479
+ # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts` 是
9220
9480
  # 四键字面量,四键**全部无条件**——此前 required 只列 taskId,把恒在键写成了可缺。
9221
9481
  required: [taskId, status, delivery, note]
9222
9482
  additionalProperties: false
@@ -9233,7 +9493,7 @@ components:
9233
9493
  `toolCallId` echoed, `delivery:"requested"`). Fire-and-forget: acceptance only — the
9234
9494
  authoritative outcome is the tool call's own `tool_end.structured {detached:true, task_id}`;
9235
9495
  a non-detachable env makes the request a fail-safe no-op.
9236
- # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts:848` 是
9496
+ # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts` 是
9237
9497
  # 四键字面量,四键全部无条件(toolCallId=请求回显,body 校验过必在)。
9238
9498
  required: [taskId, toolCallId, delivery, note]
9239
9499
  additionalProperties: false
@@ -9321,10 +9581,10 @@ components:
9321
9581
  ToolApprovalRespondAck:
9322
9582
  type: object
9323
9583
  description: >
9324
- The 200 ack of POST /v1/tool-approvals/{approvalId}/respond (server tool-approval.ts:521 — exact
9584
+ The 200 ack of POST /v1/tool-approvals/{approvalId}/respond (server tool-approval.ts — exact
9325
9585
  shape, `decision` echoed). Owner-gated with a 404 and NO existence oracle: non-owner and
9326
9586
  unknown/settled/expired/wrong-replica ids are indistinguishable.
9327
- # 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts:521` 三恒在键 + 两条件键
9587
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts` 三恒在键 + 两条件键
9328
9588
  # (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里),键集恰是这 5 个。
9329
9589
  required: [approvalId, delivery, decision]
9330
9590
  additionalProperties: false
@@ -9348,25 +9608,25 @@ components:
9348
9608
  UsageMetric:
9349
9609
  type: string
9350
9610
  enum: [tasks, tokensIn, tokensOut, costUsd]
9351
- description: 'The ?metric= axis of GET /v1/usage/series (server usage-analytics.ts:67). Default costUsd.'
9611
+ description: 'The ?metric= axis of GET /v1/usage/series (server usage-analytics.ts). Default costUsd.'
9352
9612
 
9353
9613
  UsageGranularity:
9354
9614
  type: string
9355
9615
  enum: [hour, day]
9356
- description: 'The ?granularity= axis of GET /v1/usage/series (UTC buckets; server usage-analytics.ts:68). Default day.'
9616
+ description: 'The ?granularity= axis of GET /v1/usage/series (UTC buckets; server usage-analytics.ts). Default day.'
9357
9617
 
9358
9618
  UsageDimension:
9359
9619
  type: string
9360
9620
  enum: [principal, model]
9361
9621
  description: >
9362
- The ?dimension= axis of GET /v1/usage/breakdown (server usage-analytics.ts:85). Default principal.
9622
+ The ?dimension= axis of GET /v1/usage/breakdown (server usage-analytics.ts). Default principal.
9363
9623
  A JWT principal querying dimension=principal only ever sees its own bucket (owner is enforced
9364
9624
  upstream — the semantics hold naturally, no leak surface).
9365
9625
 
9366
9626
  UsageTotals:
9367
9627
  type: object
9368
9628
  description: >
9369
- Window totals (server usage-analytics.ts:31 UsageTotals). tokensIn = Σ(inputTokens +
9629
+ Window totals (server usage-analytics.ts UsageTotals). tokensIn = Σ(inputTokens +
9370
9630
  cacheReadTokens + cacheWriteTokens) — the billing view (cache hits count); tokensOut = Σ
9371
9631
  outputTokens; costUsd = Σ costMicroUsd / 1e6 (authoritative micro-USD ledger, not an estimate).
9372
9632
  required: [tasks, tokensIn, tokensOut, costUsd, estimated]
@@ -9401,9 +9661,9 @@ components:
9401
9661
  allOf:
9402
9662
  - $ref: '#/components/schemas/UsageWindowBase'
9403
9663
  - $ref: '#/components/schemas/UsageTotals'
9404
- # 🔴 封闭 + 本地镜像(census 批2 第二段,2026-07-30)。铸造点 `routes/trace-usage.ts:141` 逐字是
9664
+ # 🔴 封闭 + 本地镜像(census 批2 第二段,2026-07-30)。铸造点 `routes/trace-usage.ts` 逐字是
9405
9665
  # `{ ...base, ...usageSummary(scan.rows) }`,两半都是本仓自己的字面量:`base` = from/to/truncated?
9406
- # (`:139`),`usageSummary` = `finish(UsageTotals)` 的五键(`usage-analytics.ts:98`)。
9666
+ # (`usage-analytics.ts`),`usageSummary` = `finish(UsageTotals)` 的五键(`usage-analytics.ts`)。
9407
9667
  # 镜像的理由与 Event_reasoning 处那条长注逐字同一条(`additionalProperties` 不结算 allOf 分支);
9408
9668
  # 一致性由 spec.test.ts 的通用「封闭 allOf 臂必须镜像被引 schema 全部键」门看守。
9409
9669
  additionalProperties: false
@@ -9433,13 +9693,13 @@ components:
9433
9693
  type: array
9434
9694
  items:
9435
9695
  type: object
9436
- # 封闭:桶行铸造点 `usage-analytics.ts:118` 是两键字面量 `{ t, v }`。
9696
+ # 封闭:桶行铸造点 `usage-analytics.ts` 是两键字面量 `{ t, v }`。
9437
9697
  additionalProperties: false
9438
9698
  required: [t, v]
9439
9699
  properties:
9440
9700
  t: { type: string, description: 'ISO-8601 bucket start (UTC).' }
9441
9701
  v: { type: number }
9442
- # 封闭 + 本地镜像:铸造点 `routes/trace-usage.ts:149` = `{ ...base, metric, granularity, series }`。见 UsageSummary 处的注。
9702
+ # 封闭 + 本地镜像:铸造点 `routes/trace-usage.ts` = `{ ...base, metric, granularity, series }`。见 UsageSummary 处的注。
9443
9703
  additionalProperties: false
9444
9704
  properties:
9445
9705
  from: { type: string }
@@ -9471,7 +9731,7 @@ components:
9471
9731
  properties:
9472
9732
  key: { type: string, description: 'The bucket key (a principal or a model id).' }
9473
9733
  - $ref: '#/components/schemas/UsageTotals'
9474
- # 封闭 + 本地镜像:桶行铸造点 `usage-analytics.ts:126` 是 `{ key, ...UsageTotals }`。
9734
+ # 封闭 + 本地镜像:桶行铸造点 `usage-analytics.ts` 是 `{ key, ...UsageTotals }`。
9475
9735
  additionalProperties: false
9476
9736
  properties:
9477
9737
  key: { type: string }
@@ -9480,7 +9740,7 @@ components:
9480
9740
  tokensOut: { type: number }
9481
9741
  costUsd: { type: number }
9482
9742
  estimated: { type: boolean }
9483
- # 封闭 + 本地镜像:铸造点 `routes/trace-usage.ts:157` = `{ ...base, dimension, breakdown }`。见 UsageSummary 处的注。
9743
+ # 封闭 + 本地镜像:铸造点 `routes/trace-usage.ts` = `{ ...base, dimension, breakdown }`。见 UsageSummary 处的注。
9484
9744
  additionalProperties: false
9485
9745
  properties:
9486
9746
  from: { type: string }
@@ -9496,7 +9756,7 @@ components:
9496
9756
  exact literal: `status:"running"`, `delivery:"applied"`, `marker` = the engine-minted delivery
9497
9757
  marker; NO `note`, unlike the run-subagent verb).
9498
9758
  # 🔴 census 批2 五段:CLOSED — all 5 keys are unconditional in the literal
9499
- # `{ runId: wfRunId, label, status: "running", delivery: "applied", marker }` (workflows.ts:279);
9759
+ # `{ runId: wfRunId, label, status: "running", delivery: "applied", marker }` (workflows.ts);
9500
9760
  # this schema previously existed but no path `$ref`'d it (see the path fix above).
9501
9761
  required: [runId, label, status, delivery, marker]
9502
9762
  additionalProperties: false
@@ -9513,7 +9773,7 @@ components:
9513
9773
  # type)对方法返回位的内联形都是盲的 —— 于是它漏了这里一直标为 required 的 `sessionId`,CI 全绿。
9514
9774
  type: object
9515
9775
  description: >
9516
- GET /v1/sessions/{sessionId}/workspace 的 200 信封(server src/http/routes/sessions.ts:624-629)。
9776
+ GET /v1/sessions/{sessionId}/workspace 的 200 信封(server src/http/routes/sessions.ts)。
9517
9777
  ⚠️ `total` = **本页行数**(`snapshots.length`),不是快照总数,且这条腿没有任何分页游标 —— 与同族
9518
9778
  `WorkspaceTreePage.total`(真·全量)**同名反义**。`latest` 与展示行同源(防 ghost key)。
9519
9779
  additionalProperties: false
@@ -9530,7 +9790,7 @@ components:
9530
9790
  # 4.3.0(CAPS-OPS-8):同上,tree 腿的 200 信封。
9531
9791
  type: object
9532
9792
  description: >
9533
- GET /v1/sessions/{sessionId}/workspace/{key}/tree 的 200 信封(server src/http/routes/sessions.ts:651-657)。
9793
+ GET /v1/sessions/{sessionId}/workspace/{key}/tree 的 200 信封(server src/http/routes/sessions.ts)。
9534
9794
  `total` 在这条腿上是**全量**(`all.length`)—— 与 `WorkspaceSnapshotPage.total` 语义相反。
9535
9795
  `key` 回的是解析后的真实键(请求可传 `latest` 别名)。
9536
9796
  additionalProperties: false