@sema-agent/sdk 7.2.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +64 -0
  2. package/dist/errors.d.ts +25 -2
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +43 -2
  5. package/dist/errors.js.map +1 -1
  6. package/dist/events.d.ts +82 -4
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/events.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/resources/approvals.d.ts +23 -4
  14. package/dist/resources/approvals.d.ts.map +1 -1
  15. package/dist/resources/approvals.js +14 -1
  16. package/dist/resources/approvals.js.map +1 -1
  17. package/dist/resources/runs.d.ts +57 -0
  18. package/dist/resources/runs.d.ts.map +1 -1
  19. package/dist/resources/runs.js +62 -0
  20. package/dist/resources/runs.js.map +1 -1
  21. package/dist/resources/session-sync.d.ts +25 -3
  22. package/dist/resources/session-sync.d.ts.map +1 -1
  23. package/dist/resources/session-sync.js +35 -3
  24. package/dist/resources/session-sync.js.map +1 -1
  25. package/dist/resources/sessions.d.ts +29 -5
  26. package/dist/resources/sessions.d.ts.map +1 -1
  27. package/dist/resources/sessions.js +22 -0
  28. package/dist/resources/sessions.js.map +1 -1
  29. package/dist/resources/tool-approvals.d.ts +22 -1
  30. package/dist/resources/tool-approvals.d.ts.map +1 -1
  31. package/dist/resources/tool-approvals.js +3 -0
  32. package/dist/resources/tool-approvals.js.map +1 -1
  33. package/dist/resources/workspace.d.ts +7 -1
  34. package/dist/resources/workspace.d.ts.map +1 -1
  35. package/dist/resources/workspace.js.map +1 -1
  36. package/dist/types.d.ts +149 -6
  37. package/dist/types.d.ts.map +1 -1
  38. package/openapi.yaml +704 -67
  39. package/package.json +1 -1
package/openapi.yaml CHANGED
@@ -487,10 +487,12 @@ paths:
487
487
  # `steering.duplicate_input_id` 语义**完全依赖这个头** —— 不声明它,就是声明了一条谁也走不到的臂。
488
488
  # ⚠️ 与 submit-class(`/v1/tasks`、`/v1/runs`)不同,这里的键**不是重试许可证**:steer 是
489
489
  # AT-MOST-ONCE,SDK 不自动铸键也不因为有键就重试;它纯粹是服务端的去重令牌。
490
- # ⚠️⚠️ **作用域只到 parked 两腿**(codex R2 复审报出,亲读 server 证实):只有
491
- # `setPendingSteer(token, scope, steerInput())` 那两条腿把派生的 `inputId` 交给 core;live 腿走的是
492
- # `live.steer(text, { trusted })`,**没传 inputId**,core 的 `TaskStream.steer()` 今天也不收 ——
493
- # 所以 200 `applied` 腿**不去重**。这条限定写进下面 409 的描述里,别在别处把它说成整个 verb 的幂等。
490
+ # ⚠️⚠️ **作用域(A-075.78 doc-rot 修,2026-08-30 亲读 server runs.ts live 腿实证)**:自 server 7.23.0
491
+ # (core 5.35.0 #257)起,派生的 `inputId` **两腿都传** —— parked 腿经 `setPendingSteer`(7.3.0 起就有),
492
+ # live 腿经 `live.steer(text, { trusted, inputId })`。此前这里写「live 腿不传 inputId,200 applied 腿
493
+ # 不去重」——那句自 7.23.0 起过期(SDK `resources/runs.ts` 的 SCOPE 段早已翻面,本注迟到)。两腿判重
494
+ # 窗口强度不同(parked=durable 行;live=流对象生命周期,不跨重启/副本/resume);对 7.19.0–7.22.0 的
495
+ # 老 server 旧限定仍逐字成立 —— 细节见下面 409 描述的 SCOPE 段。
494
496
  - $ref: '#/components/parameters/IdempotencyKey'
495
497
  post:
496
498
  tags: [runs]
@@ -590,13 +592,19 @@ paths:
590
592
  sent is already parked with DIFFERENT steering content (same key + same content is an idempotent
591
593
  no-op and 202s). The engine refuses rather than silently picking a side. Reissue with a fresh
592
594
  key — this is a new instruction, not a retry.
593
- 🔴 SCOPE (#193 车8, codex R2 — read this before treating the key as verb-wide idempotency): this
594
- code, and idempotency in general, exist ONLY on the PARKED legs (202 `queued` / `parked_for_wake`),
595
- because only those thread the derived `inputId` into core via `setPendingSteer`. The 200 `applied`
596
- leg calls core's `TaskStream.steer(text, {trusted})`, which takes NO inputId today — so it does
597
- NOT dedupe: a lost 200 retried with the SAME key injects the instruction a SECOND time. Worse, the
598
- receipt's `messageId` is derived from that key, so both receipts are byte-identical and the
599
- duplicate looks like an idempotent hit. Treat a lost 200 as "outcome unknown", not "safe to retry".
595
+ 🔴 SCOPE (#193 车8 → corrected A-075.78, 2026-08-30, server live leg re-read): since server
596
+ 7.23.0 (core 5.35.0 #257) the key dedupes BOTH legs — the parked path threads the derived
597
+ `inputId` into core's `setPendingSteer` (as it has since 7.3.0), and the live path threads the
598
+ SAME id into `TaskStream.steer(text, {trusted, inputId})`, so a lost 200 retried with the same
599
+ key is an idempotent no-op (core injects nothing, no second `human_input` frame) and same-key+
600
+ different-text 409s `steering.duplicate_input_id` on the live leg exactly like the parked one.
601
+ ⚠️ The two dedup windows differ in STRENGTH (core contract, stated rather than papered over):
602
+ parked = DURABLE (the checkpoint row); live = the stream object's lifetime — one run leg in one
603
+ process, never spanning a restart/replica/resume; an id already delivered by the parked leg is
604
+ NOT in the live domain (cross-leg idempotency is the deployment's half).
605
+ ⚠️ Against a server 7.19.0–7.22.0 the OLD caveat applies verbatim: only the parked legs dedupe —
606
+ a lost 200 retried with the same key injects TWICE with byte-identical receipts; treat a lost 200
607
+ there as "outcome unknown", never "safe to retry".
600
608
  content:
601
609
  application/json:
602
610
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -640,6 +648,127 @@ paths:
640
648
  application/json:
641
649
  schema: { $ref: '#/components/schemas/ErrorResponse' }
642
650
 
651
+ /v1/runs/{taskId}/interrupt:
652
+ parameters:
653
+ - $ref: '#/components/parameters/PrincipalHeader'
654
+ - $ref: '#/components/parameters/TaskIdPath'
655
+ # server 收法与 steer 逐字同配方(scoped 口径 + sha256 定长 + `idem-` 前缀)收成 core `inputId`,
656
+ # 🔴 且 text 形 **202 回执的 `messageId` ≡ 这个 inputId** —— `task.turn_interrupted` 帧的
657
+ # `detail.inputId` 用同一个值,回执与帧对得上(并发两刀分得清哪刀是谁的)。与 steer 的缺席臂刻意
658
+ # 不同:本 op **无 key 也现铸 uuidv7 并下传**(每请求各是各的指令,零去重域,重试语义与旧形一致)。
659
+ # bare 形上本头是 no-op(没有输入帧可去重;halt 天然幂等,重复调用引擎自答 turnCut:false)。
660
+ # ⚠️ 键不是重试许可证(steer 同纪律):AT-MOST-ONCE,SDK 不自动铸也不因有键而重试。
661
+ - $ref: '#/components/parameters/IdempotencyKey'
662
+ post:
663
+ tags: [runs]
664
+ operationId: runsInterrupt
665
+ x-status: live # core #504 / server 7.52.0 F1 全弧(A-075.78 SDK 半场,2026-08-30)。
666
+ summary: Interrupt the in-flight TURN of a durable run — cut (+ optionally steer); bare form = halt.
667
+ description: >
668
+ core #504 — the TURN-level verb that run-level cancel is not: finished tools keep their REAL results,
669
+ un-started ones settle as interrupted pairs, in-flight tools are NOT collateral. TWO shapes,
670
+ discriminated by `text` presence.
671
+ WITH text = cut + steer: the in-flight turn is cut and the text goes to the FRONT of the queue — the
672
+ `"now"` tier is the VERB's semantics, not a body field (the steer op's `priority` stays advisory) —
673
+ and the run CONTINUES. 202 `{taskId, status, delivery:"queued-immediate", messageId, note}`;
674
+ `messageId` ≡ the engine inputId, and a real cut is proven ONLY by a `task.turn_interrupted` event on
675
+ the run stream whose `detail.inputId` matches it (core: no false interrupt claims — every real cut
676
+ emits one, an idle cut emits none and the text lands at the next turn boundary).
677
+ BARE form (body `{}` / empty / literal null) = halt (the CC Esc semantic, cut + STOP): the same cut,
678
+ but the run CLOSES at this human-made boundary as `status:"completed"` with
679
+ `TaskResult.haltedByUser:true`; the session continues normally (submit the next objective on the same
680
+ session). 202 `{taskId, status, turnCut, note}` — deliberately NO `delivery`/`messageId` (no input
681
+ frame exists to correlate; the frame's `detail.cause:"user_halt"` arm carries no inputId). A bare body
682
+ with EXTRA keys is a 400 — a typo'd `{txt:…}` must not silently become a verb that stops the run.
683
+ LIVE-ONLY, no park fallback (the root difference from steer — a turn is an in-process concept, a
684
+ parked run has no in-flight turn to cut). Authorization = the cancel door (owner-or-explicit-operator:
685
+ whoever may collaterally cancel the whole run holds the strictly smaller power to cut one turn).
686
+ requestBody:
687
+ required: false
688
+ content:
689
+ application/json:
690
+ schema:
691
+ type: object
692
+ # text 形对多余键宽容(冻结契约,steer 同族);bare 形零键纪律由 400 执法(见下)。
693
+ additionalProperties: true
694
+ properties:
695
+ text:
696
+ type: string
697
+ description: >-
698
+ Optional. Present ⇒ cut + steer (non-empty required; present-but-empty/non-string = 400).
699
+ Absent ⇒ bare halt — the body must then be EXACTLY `{}` (zero keys), empty, or null.
700
+ responses:
701
+ '202':
702
+ description: >-
703
+ Accepted — two shapes share this code; branch on which arm's keys are present (`delivery` vs
704
+ `turnCut`), NOT on the status code. See InterruptReceipt.
705
+ content:
706
+ application/json:
707
+ schema: { $ref: '#/components/schemas/InterruptReceipt' }
708
+ '400':
709
+ description: >-
710
+ errorCode "request.invalid_json" — the body did not parse as JSON;
711
+ errorCode "request.body_shape" — a scalar/array body, a bare body with EXTRA keys, or `text`
712
+ present but empty/non-string (the message is the literal accepted shape: `{}` for bare halt, or
713
+ `{ text: string (non-empty) }` for cut + steer). Malformed is NEVER silently folded into halt —
714
+ that would swap the caller's verb for one that stops the run.
715
+ content:
716
+ application/json:
717
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
718
+ '401': { $ref: '#/components/responses/Unauthorized' }
719
+ '404': { $ref: '#/components/responses/NotFound' }
720
+ '409':
721
+ description: >-
722
+ errorCode "interrupt.nothing_in_flight" — the run is PARKED on a pending decision: no in-flight
723
+ turn to cut, and deliberately NO park fallback — to deliver input to a parked run use
724
+ POST /v1/runs/{taskId}/steer (it parks onto the checkpoint and injects on resume).
725
+ errorCode "interrupt.not_held" — the run is running but THIS replica holds no live interruptible
726
+ turn face: live on ANOTHER replica (stated only when the row's instanceId PROVES it — the message
727
+ then says to resend there), or a verify/cascade execution lane which exposes no stream (those legs
728
+ return a result, not a live stream; the weakest-claim wording covers single-replica deployments).
729
+ Use POST /v1/runs/{taskId}/cancel for a run-level stop.
730
+ errorCode "steering.not_running" (reused verbatim) — terminal / not accepting, or the stream
731
+ settled in the race window ("run just finished on this replica — no in-flight turn to interrupt").
732
+ errorCode "steering.duplicate_input_id" — this Idempotency-Key was already accepted with DIFFERENT
733
+ content or at a DIFFERENT tier (core's replay identity includes the normalized tier: the same key
734
+ that once went through the steer door (default tier) then hits this door (now) is refused —
735
+ "idempotent success without the cut" would be a dispositive lie). Reissue with a fresh key.
736
+ content:
737
+ application/json:
738
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
739
+ '413':
740
+ description: >-
741
+ errorCode "request.payload_too_large" — the worker's global body cap (`MAX_BODY`, 8 MiB), raised
742
+ by the shared JSON reader (the steer op's 413 note applies verbatim; no separate text cap here).
743
+ content:
744
+ application/json:
745
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
746
+ '422':
747
+ description: >-
748
+ TWO codes, text form only (the bare form skips content validation — no text reaches the model,
749
+ and core's userPromptSubmit gate has no jurisdiction over a halt):
750
+ errorCode "steering.invalid_content" — the text carried a control-plane escape (e.g.
751
+ </system-reminder>); validated BEFORE the row lookup, and core re-rejects on the live leg.
752
+ errorCode "steering.blocked_by_hook" (S-51 / core 5.62 design/373 §4.3) — the deployment's
753
+ userPromptSubmit hook refused this input; block / hook timeout / hook crash are ONE code,
754
+ FAIL-CLOSED. The input was NOT accepted (no human_input frame, the inputId is not spent) — a
755
+ retry with different content is legitimate. The hook's own bounded reason is passed through
756
+ verbatim (first-hand triage material). SDK: falls on the SteeringError base via the `steering.`
757
+ prefix family (`.errorCode` carries the discriminator; same registration as
758
+ steering.parked_input_blocked).
759
+ content:
760
+ application/json:
761
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
762
+ '429': { $ref: '#/components/responses/RateLimited' } # rateLimited||quotaExceeded||leaseDenied — steer 同门(mutating + injects into a model run)
763
+ '501':
764
+ description: >-
765
+ errorCode "capability.run_store_required" — no durable run store wired
766
+ (DB_BACKEND=mysql|pg|local); the whole async-run family is off. Probe `capabilities.asyncRuns`,
767
+ don't trial-by-501.
768
+ content:
769
+ application/json:
770
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
771
+
643
772
  /v1/runs/{taskId}/subagents/{target}/steer:
644
773
  parameters:
645
774
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -1523,14 +1652,20 @@ paths:
1523
1652
  schema:
1524
1653
  type: object
1525
1654
  additionalProperties: false
1526
- required: [sessionId, status]
1655
+ # #354 spec-drift fix(2026-08-27,亲读 server 真码):两处铸造点都恒带 `bindingEnforced:true`
1656
+ # (server.ts 受理点 `onAccepted` 与 driveResumeIntoRunLog 终局 return)——此前 additionalProperties:false
1657
+ # 把真响应判成超集;并且 [3833] S-1 起 wake 腿恒声明受理语义(notify-wake.ts 调 resumeWake 第五参=true):
1658
+ # durable 行在场时 200 是**受理形** `{taskId, sessionId, status:"resuming", bindingEnforced}`,腿在后台
1659
+ # 跑到停点(终态跟 GET /v1/runs/:id / events tail);无 durable 行的部署保持同步形(status=终局枚举)。
1660
+ required: [sessionId, status, bindingEnforced]
1527
1661
  properties:
1528
1662
  taskId: { type: string, description: 'The active run id; OMITTED when no active row.' }
1529
1663
  sessionId: { type: string }
1530
- status: { type: string, description: 'The run''s resulting status (completed/failed/suspended/needs_review/blocked — core TaskStatus is passed through verbatim; "timeout" retired in core 5.8.0: walltime exhaustion now lands as failed + errorCode limits.max_walltime_exceeded).' }
1531
- errorCode: { type: string, description: 'Failure code when the resumed leg failed (e.g. `cancelled`).' }
1532
- errorMessage: { type: string, description: 'Human-readable failure detail (server-side secret-redacted).' }
1533
- retriable: { type: boolean, description: 'true = the park is STILL pending (re-parked); refetch and retry.' }
1664
+ status: { type: string, description: 'ACCEPTANCE form (durable row present, [3833] S-1): "resuming" — the leg keeps running server-side; follow GET /v1/runs/:id or the events tail for the outcome. SYNCHRONOUS form (no durable run row): the run''s resulting status (completed/failed/suspended/needs_review/blocked — core TaskStatus verbatim; "timeout" retired in core 5.8.0: walltime exhaustion now lands as failed + errorCode limits.max_walltime_exceeded).' }
1665
+ bindingEnforced: { type: boolean, description: '[2400] HITL-12 generation probe — this server enforces decision-action binding. Always true (present on BOTH 200 forms).' }
1666
+ errorCode: { type: string, description: 'Failure code when the resumed leg failed (e.g. `cancelled`). Synchronous form only — never on the acceptance form.' }
1667
+ errorMessage: { type: string, description: 'Human-readable failure detail (server-side secret-redacted). Synchronous form only.' }
1668
+ retriable: { type: boolean, description: 'true = the park is STILL pending (re-parked); refetch and retry. Synchronous form only.' }
1534
1669
  '400': { $ref: '#/components/responses/BadRequest' }
1535
1670
  '401': { $ref: '#/components/responses/Unauthorized' }
1536
1671
  '404': { description: 'No parked checkpoint for this session (nothing to wake), or session not visible to the caller.' }
@@ -1549,14 +1684,21 @@ paths:
1549
1684
  get:
1550
1685
  tags: [workspace]
1551
1686
  operationId: workspaceList
1552
- x-status: live # #3 workspace browse (server >=1.299; design docs/DESIGN-workspace-browse.md, [1894]).
1553
- summary: List a session's workspace snapshots (newest first).
1554
- description: >
1555
- #3 — the read-only projection over the E19 snapshot store (a snapshot = the working tree at a turn
1556
- boundary; zero new storage). Rows carry `{key, files, bytes?}` — `bytes` only when the SQL size index
1557
- is available (a local file backend omits it honestly). `latest` = the newest snapshot key (absent when
1558
- the session has none). Owner-scoped (non-owner → 404, no oracle); 501 without the snapshot-store read
1559
- faces (probe `capabilities.workspace`).
1687
+ x-status: retired # S-15/design/381 (server 7.52.0): whole-tree snapshot epoch retired — every ≥7.52 deployment answers 501 capability.workspace_retired.
1688
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot listing; ≥7.52 answers 501 capability.workspace_retired. Live on 1.299–7.51 only.'
1689
+ description: >
1690
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this
1691
+ face projected was replaced by per-edited-file rewind HISTORY, which holds only files the agent edited
1692
+ — structurally there is no "whole workspace" manifest to browse anymore. The route SHAPE is kept
1693
+ server-side as a tombstone: every ≥7.52 deployment answers 501 `capability.workspace_retired` (a named
1694
+ refusal, not a 404 that reads like a typo), and `capabilities.workspace` is always false (same
1695
+ predicate). There is NO 1:1 replacement path — a future "rewind coverage" browse would be a NEW face
1696
+ with tracked-set semantics, not this name wired back. Related live faces: rewind restore rides
1697
+ `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file history blobs ride
1698
+ `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`.
1699
+ Historical shape (servers 1.299–7.51): read-only projection over the E19 snapshot store. Rows carry
1700
+ `{key, files, bytes?}`; `latest` = the newest snapshot key. Owner-scoped (non-owner → 404, no
1701
+ oracle); 501 without the snapshot-store read faces (probe `capabilities.workspace`).
1560
1702
  parameters:
1561
1703
  - in: query
1562
1704
  name: limit
@@ -1591,12 +1733,14 @@ paths:
1591
1733
  get:
1592
1734
  tags: [workspace]
1593
1735
  operationId: workspaceTree
1594
- x-status: live
1595
- summary: One snapshot's file tree (stable path order, paginated).
1596
- description: >
1597
- Entries carry `{path, hash, size?}` — `hash` is the sha256 content-address (cross-snapshot change
1598
- highlighting, [1894]②); `size` only when the SQL size index is available. `limit` 1..2000 default 500,
1599
- `offset` cursor; `nextOffset` present = more pages.
1736
+ x-status: retired # S-15 (server 7.52.0): retired with the family — see workspaceList's retirement note.
1737
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot file tree; ≥7.52 answers 501 capability.workspace_retired.'
1738
+ description: >
1739
+ 🔴 RETIRED as of server 7.52.0 — same retirement (and same replacement pointers) as `workspaceList`
1740
+ above: every ≥7.52 deployment answers 501 `capability.workspace_retired`; `capabilities.workspace` is
1741
+ always false. Historical shape (1.299–7.51): entries carry `{path, hash, size?}` — `hash` is the
1742
+ sha256 content-address (cross-snapshot change highlighting, [1894]②); `size` only when the SQL size
1743
+ index is available. `limit` 1..2000 default 500, `offset` cursor; `nextOffset` present = more pages.
1600
1744
  parameters:
1601
1745
  - in: query
1602
1746
  name: limit
@@ -1633,16 +1777,19 @@ paths:
1633
1777
  get:
1634
1778
  tags: [workspace]
1635
1779
  operationId: workspaceFile
1636
- x-status: live
1637
- summary: Read ONE file's bytes from a snapshot (preview face; capped).
1638
- description: >
1639
- Raw bytes with a CONSERVATIVE content-type table (only unambiguous non-active types; html/svg etc.
1640
- deliberately serve as octet-stream — agent output is untrusted; `x-content-type-options: nosniff`
1641
- always set) + the `x-sema-content-binary` discriminator header (first-8KiB NUL sniff, [1894]③).
1642
- Capped at `capabilities.workspace.maxFileBytes` (default 8MiB, WORKSPACE_FILE_MAX_BYTES) — over-cap ⇒
1643
- 413 STRUCTURED envelope `{errorCode: "workspace_file_too_large", sizeBytes, limit}` ([1894]①; the archive
1644
- endpoint is the escape hatch). When the SQL size index knows the size, the 413 fires BEFORE fetching
1645
- the bytes.
1780
+ x-status: retired # S-15 (server 7.52.0): retired with the family — ≥7.52 the tombstone rejects BEFORE any size check, so the 413 envelope below is unreachable there.
1781
+ summary: 'RETIRED (server 7.52.0, S-15) — single-file read; ≥7.52 answers 501 capability.workspace_retired.'
1782
+ description: >
1783
+ 🔴 RETIRED as of server 7.52.0 — same retirement (and replacement pointers) as `workspaceList` above.
1784
+ On ≥7.52 the tombstone 501 fires BEFORE any size check, so the 413 `workspace_file_too_large`
1785
+ envelope below is UNREACHABLE there (the SDK keeps its typed WorkspaceFileTooLargeError for
1786
+ 1.299–7.51 servers still inside the support range). Historical shape (1.299–7.51): raw bytes with a
1787
+ CONSERVATIVE content-type table (html/svg etc. deliberately serve as octet-stream — agent output is
1788
+ untrusted; `x-content-type-options: nosniff` always set) + the `x-sema-content-binary` discriminator
1789
+ header (first-8KiB NUL sniff, [1894]③). Capped at `capabilities.workspace.maxFileBytes` (default
1790
+ 8MiB, WORKSPACE_FILE_MAX_BYTES) — over-cap ⇒ 413 STRUCTURED envelope
1791
+ `{errorCode: "workspace_file_too_large", sizeBytes, limit}` ([1894]①; archive was the escape hatch).
1792
+ With the SQL size index the 413 fired BEFORE fetching the bytes.
1646
1793
  parameters:
1647
1794
  - in: query
1648
1795
  name: path
@@ -1686,15 +1833,16 @@ paths:
1686
1833
  get:
1687
1834
  tags: [workspace]
1688
1835
  operationId: workspaceArchive
1689
- x-status: live
1690
- summary: Download one whole snapshot as a streamed tar (ustar).
1691
- description: >
1692
- Streamed ustar (`content-disposition: attachment`), entries in stable path order, blob-by-blob with
1693
- backpressure — server memory stays O(1 file). Pre-flight checks BEFORE the headers go out: paths not
1694
- representable in ustar (>255 bytes / unsplittable) ⇒ 422 naming the first offenders; missing blobs
1695
- (raced a reap / partial import) ⇒ 502 (retryable). A blob vanishing MID-stream destroys the connection
1696
- (honest truncation — the tar end blocks are missing, consumers can detect it) rather than faking a
1697
- complete archive.
1836
+ x-status: retired # S-15 (server 7.52.0): retired with the family — see workspaceList's retirement note.
1837
+ summary: 'RETIRED (server 7.52.0, S-15) — whole-snapshot tar; ≥7.52 answers 501 capability.workspace_retired.'
1838
+ description: >
1839
+ 🔴 RETIRED as of server 7.52.0 — same retirement (and replacement pointers) as `workspaceList` above:
1840
+ every ≥7.52 deployment answers 501 `capability.workspace_retired`. Historical shape (1.299–7.51):
1841
+ streamed ustar (`content-disposition: attachment`), entries in stable path order, blob-by-blob with
1842
+ backpressure — server memory stays O(1 file). Pre-flight checks BEFORE the headers went out: paths
1843
+ not representable in ustar ⇒ 422 naming the first offenders; missing blobs (raced a reap / partial
1844
+ import) ⇒ 502 (retryable). A blob vanishing MID-stream destroyed the connection (honest truncation)
1845
+ rather than faking a complete archive.
1698
1846
  responses:
1699
1847
  '200':
1700
1848
  description: The tar stream.
@@ -1814,6 +1962,73 @@ paths:
1814
1962
  schema: { $ref: '#/components/schemas/McpStatusPanel' }
1815
1963
  '401': { $ref: '#/components/responses/Unauthorized' }
1816
1964
 
1965
+ /v1/sessions/{sessionId}/memory-status:
1966
+ parameters:
1967
+ - $ref: '#/components/parameters/PrincipalHeader'
1968
+ - in: path
1969
+ name: sessionId
1970
+ required: true
1971
+ schema: { type: string }
1972
+ get:
1973
+ tags: [sessions]
1974
+ operationId: sessionsMemoryStatus
1975
+ x-status: live # S-53 seam① (server 7.53.0; core 7.0.2 #511 item 1 / design/383 §S-7); SDK sessions.memoryStatus.
1976
+ summary: Session memory status — the pure-derivation projection of core's `sessionMemoryStatus`.
1977
+ description: >
1978
+ S-53 seam① (server >=7.53, core 7.0.2 #511 item 1; board [5785]/[5786] key shapes). 200 body =
1979
+ `{sessionId}` + core `MemoryEngine.sessionMemoryStatus(sessionId)` (`SessionMemoryStatus`) projected
1980
+ KEY BY KEY (each of the five optional keys is copied only when present). 🔴 ABSENCE IS HONEST, NOT A
1981
+ DEFAULT — but its meaning is PER KEY (read off core 7.0.2 `engine.js sessionMemoryStatus`, not off a
1982
+ blanket rule): `captureOptedOut` absent = the record store faulted (then `optOutSource:"fault"` rides
1983
+ along; indeterminate ≠ false); `optOutSource` absent = store readable and NO opt-out record (the
1984
+ HEALTHY default — it is only minted as `record`/`fault`); `committedCount` absent = lineage ledger
1985
+ unreadable (a session with no contribution gets `0`, not absence); `lastCaptureAt` absent = ledger
1986
+ unreadable OR no committed contribution at all (a healthy empty session omits it); `foldedCount`
1987
+ absent = ledger unreadable, or the backend cannot enumerate scopes, or the product read failed (`0`
1988
+ when there is nothing to fold). A zero-history session therefore reads
1989
+ `{sessionId, captureOptedOut:false, committedCount:0, foldedCount:0}` — two keys absent and NOTHING
1990
+ degraded. The server never coins a `false`/`0` stand-in for a faulted source. Gate order, same family as the erase
1991
+ face: 401 (multi-tenant, no principal — BEFORE any store read: zero existence oracle) → 501
1992
+ `capability.memory_engine_required` (no owner audit face: a deployment without one must not fake a 404)
1993
+ → 404 `not_found.session` (unknown AND non-owner — SAME code, SAME message; anti-enumeration) → 501
1994
+ `capability.memory_engine_required` (status face absent: engine not wired / the pg+tidb memory backends
1995
+ do not own the control plane — same code as the first 501) → 200. 400 `request.path_malformed` on a bad
1996
+ percent-encoded id segment. Non-billable, non-mutating; served through a short-TTL single-flight cache
1997
+ per session id. 🔴 NO capability bit advertises this face (deliberately — [5785]/[5786] fixed no bit
1998
+ name): the route answers for itself; probe by calling it, a 501 is the honest "not on this deployment".
1999
+ 🔴 VERSION SKEW (codex R2; the SDK support floor is far below 7.53): a supported OLDER server
2000
+ (<7.53) has no such route and answers the version-stable generic fallback 404 `not_found.route` —
2001
+ treat it like the 501 ("face not here"), and distinguish it BY errorCode from this op's own 404
2002
+ `not_found.session` (unknown/non-owner session). Same HTTP status, different codes — never collapse
2003
+ the two.
2004
+ Cross-incarnation caveat (server-side, cannot be fixed at this layer): the capture record and the
2005
+ lineage ledger are keyed by the BARE sessionId and outlive the session, so a re-claimed id reads the
2006
+ PREVIOUS incarnation's counts/opt-out (metadata only, no content bytes).
2007
+ responses:
2008
+ '200':
2009
+ description: >-
2010
+ The session's memory status. Absence is PER-KEY (see SessionMemoryStatusResponse): a healthy
2011
+ zero-history session omits `optOutSource`/`lastCaptureAt` while answering
2012
+ `captureOptedOut:false, committedCount:0, foldedCount:0` — nothing degraded; the other keys are
2013
+ absent only when their source could not be read.
2014
+ content:
2015
+ application/json:
2016
+ schema: { $ref: '#/components/schemas/SessionMemoryStatusResponse' }
2017
+ '400': { $ref: '#/components/responses/BadRequest' }
2018
+ '401': { $ref: '#/components/responses/Unauthorized' }
2019
+ '404': { $ref: '#/components/responses/NotFound' }
2020
+ '500':
2021
+ description: >-
2022
+ errorCode "internal.error" — `routes/sessions.ts`'s catch around `face.status(sessionId)`: core
2023
+ promises the face never throws, so reaching this arm is an assembly defect; the server fails LOUD
2024
+ (500) rather than answering a plausible all-keys-absent body. The SDK maps it to
2025
+ `InternalServerError`. (codex R1 of the 7.3.1 batch: the arm exists in the shipped 7.53.0 route and
2026
+ was missing here.)
2027
+ content:
2028
+ application/json:
2029
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2030
+ '501': { $ref: '#/components/responses/NotImplemented' }
2031
+
1817
2032
  # ── 2c session-sync (P1d) — the cloud as a SYNC PEER ──────────────────────────────────────────────────────────
1818
2033
  # The local shell's local backend ⇄ this cloud service. PULL a session's whole state out, or PUSH the local
1819
2034
  # peer's in. Gate the whole surface off `capabilities.sessionSync` — a FOUR-way conjunction (durable backend
@@ -1916,12 +2131,18 @@ paths:
1916
2131
  get:
1917
2132
  tags: [sessions]
1918
2133
  operationId: sessionSyncGetBlob
1919
- x-status: live
1920
- summary: PULL one content-addressed blob's raw bytes (scope-checked).
1921
- description: >
1922
- 2c session-sync PULL — one content-addressed blob's RAW bytes (`application/octet-stream`). 🔴 SCOPE BOUNDARY:
1923
- a content-addressed blob is shared across sessions, so the cloud serves a hash ONLY if THIS session's `:key`
1924
- manifest references it (a foreign / unreferenced hash → 404 `blob not found`, never a blind read). 400 on a
2134
+ x-status: retired # S-15 (server 7.52.0): the E19-era snapshot-scoped PULL arm was REPLACED by GET …/sync/history/blobs/{hash} — ≥7.52 this path 404s (not_found.route).
2135
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot-scoped blob PULL; use GET …/sync/history/blobs/{hash}. Live on ≤7.51 only.'
2136
+ description: >
2137
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): a blob is scoped by the session's
2138
+ HISTORY GRAPH now, not by a per-snapshot manifest key, so this arm was replaced by
2139
+ `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}` (sessionSyncGetHistoryBlob — same
2140
+ octet-stream body, same no-oracle 404 posture, scope check = "referenced by this session's
2141
+ exportHistory envelope"). On ≥7.52 this path no longer matches the sync route family and answers 404
2142
+ `not_found.route`. SDK: `sessions.sync.getHistoryBlob` (the legacy `getBlob` verb stays, deprecated,
2143
+ for ≤7.51 servers inside the support range).
2144
+ Historical shape (≤7.51): one content-addressed blob's RAW bytes; the cloud served a hash ONLY if
2145
+ THIS session's `:key` manifest referenced it (foreign/unreferenced → 404 `blob not found`). 400 on a
1925
2146
  non-64-hex hash or an undecodable key/hash. Owner-scoped (404).
1926
2147
  responses:
1927
2148
  '200':
@@ -1934,6 +2155,43 @@ paths:
1934
2155
  '404': { $ref: '#/components/responses/NotFound' }
1935
2156
  '501': { $ref: '#/components/responses/NotImplemented' }
1936
2157
 
2158
+ /v1/sessions/{sessionId}/sync/history/blobs/{hash}:
2159
+ parameters:
2160
+ - $ref: '#/components/parameters/PrincipalHeader'
2161
+ - in: path
2162
+ name: sessionId
2163
+ required: true
2164
+ schema: { type: string }
2165
+ - in: path
2166
+ name: hash
2167
+ required: true
2168
+ schema: { type: string, pattern: '^[0-9a-f]{64}$' }
2169
+ description: The content-address (SHA-256, 64 lowercase-hex).
2170
+ get:
2171
+ tags: [sessions]
2172
+ operationId: sessionSyncGetHistoryBlob
2173
+ x-status: live # S-15/design/381 (server 7.52.0): the history-graph-scoped successor of the retired snapshots/{key}/blobs PULL arm.
2174
+ summary: PULL one content-addressed blob's raw bytes (history-graph-scoped; server ≥7.52).
2175
+ description: >
2176
+ 2c session-sync PULL, S-15 epoch — one content-addressed blob's RAW bytes
2177
+ (`application/octet-stream`). 🔴 SCOPE BOUNDARY (the security boundary — a content-addressed blob is
2178
+ shared across sessions): the cloud serves a hash ONLY if THIS session's HISTORY GRAPH references it
2179
+ (some tracked file version's `blobHash` in its exportHistory envelope). A foreign / unreferenced hash
2180
+ → 404 `not_found.blob`, never a blind read; a server whose file-history store has no blob faces
2181
+ (e.g. the local file backend) answers the SAME 404 shape (no oracle — "nothing servable" and "not
2182
+ referenced" are deliberately indistinguishable). 400 `request.id_invalid` on a non-64-hex hash.
2183
+ Owner-scoped (404). Replaces the retired `GET …/sync/snapshots/{key}/blobs/{hash}`.
2184
+ responses:
2185
+ '200':
2186
+ description: The blob bytes.
2187
+ content:
2188
+ application/octet-stream:
2189
+ schema: { type: string, format: binary }
2190
+ '400': { $ref: '#/components/responses/BadRequest' }
2191
+ '401': { $ref: '#/components/responses/Unauthorized' }
2192
+ '404': { $ref: '#/components/responses/NotFound' }
2193
+ '501': { $ref: '#/components/responses/NotImplemented' }
2194
+
1937
2195
  /v1/sessions/{sessionId}/sync/blobs/{hash}:
1938
2196
  parameters:
1939
2197
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -1955,7 +2213,12 @@ paths:
1955
2213
  body). The cloud verifies `sha256(body) === :hash` (the content-address integrity guard) BEFORE storing — a
1956
2214
  mismatch → 400 `blob hash mismatch`. own-or-FRESH gate. Per-blob cap 64 MiB → 413 mid-stream beyond it. A
1957
2215
  backend write failure (e.g. a MinIO non-2xx) → 502 `blob store write failed`. 204 on success (no body). The
1958
- PUSH path is the BARE `blobs/:hash` (the PULL is scoped under `snapshots/:key`).
2216
+ PUSH path is the BARE `blobs/:hash` (the PULL is `history/blobs/:hash` since server 7.52,
2217
+ `snapshots/:key/blobs/:hash` before). 🔴 server ≥7.52 (S-15 换店): the blob faces ride the
2218
+ file-HISTORY store — a deployment whose store has no blob faces (the local file backend) answers 501
2219
+ `capability.snapshot_store_required` ("blob upload requires a durable file-history store") while the
2220
+ rest of the sync surface keeps working; on ≤7.51 the same code meant a missing snapshot store. Same
2221
+ code, same operator disposition (wire a backend with blob faces).
1959
2222
  requestBody:
1960
2223
  required: true
1961
2224
  content:
@@ -2028,6 +2291,14 @@ paths:
2028
2291
  2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
2029
2292
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
2030
2293
  422 `missing_blob` — PUT it first), guards the import lease + any active run (409), then mints a stagingId.
2294
+ ⚠️ S-15 EPOCH DEBT (registered 2026-08-30, codex adversarial review; migration is a SEPARATE batch):
2295
+ server ≥7.52's Phase A actually consumes `{entryIds, fileHistory?, policy, anchors, resolution?,
2296
+ logDigest?}` (routes/session-sync.ts) — the `snapshots` key below is SILENTLY IGNORED there (an
2297
+ unknown body key), so on ≥7.52 a push lands entries/policy/anchors and the session itself but NO
2298
+ file snapshots/history; the snapshot-epoch shape below is what ≤7.51 servers consume. The full
2299
+ migration (SessionManifest/SessionBundle/push-pull orchestration + `fileHistory: FileHistoryExport |
2300
+ null` + epoch negotiation) is tracked as its own debt — this op's schema deliberately stays the
2301
+ ≤7.51 truth until that batch lands rather than half-changing.
2031
2302
  `identical` → `{ relation:"identical", basis, payloadVerified }` with NO stagingId (skip Phase B);
2032
2303
  `basis` is `"entry-ids+digest"` when the caller supplied a comparable digest and it matched, else
2033
2304
  `"entry-ids"` (id-set equality only) — see the `ImportStaged` schema, which has carried these two
@@ -2282,6 +2553,23 @@ paths:
2282
2553
  already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client action:
2283
2554
  refetch the inbox; this pending is gone. SDK → ApprovalStaleError. Note the bare CAS 409 has NO
2284
2555
  `errorCode`, which is exactly how the three are told apart.
2556
+ S-02 (server ≥7.52, A-075.94③): the stale body may carry the ADDITIVE pointer key
2557
+ `currentPending` ($ref ApprovalStaleCurrentPending — the SESSION'S current pending's D-1
2558
+ coordinates `{toolName, boundCallId, boundInputHash?}`, same names/shapes as the §7b queue row) so
2559
+ a shell relocates and re-decides in ONE hop instead of refetching the whole queue. NEVER carries a
2560
+ checkpointToken. Absent when there is no pending / the gate is not a tool approval / the row read
2561
+ failed (loud fail-open ledger arm) / an older server. Owner domain = the route's owner gate (no
2562
+ new disclosure surface). SDK: `ApprovalStaleError.currentPending`, whole-or-absent guard.
2563
+ REACHABILITY (codex 复审 R1-[medium] 采纳成文): the stale arm fires only for callers that ECHO a
2564
+ `checkpointToken`; this SDK's ApprovalDecision deliberately has no such field (removed in 1.0.0 —
2565
+ resume credentials never ride the compliant decide), so decides issued through this SDK never
2566
+ trigger it. The key is documented for the wire contract's sake: legacy shells and non-SDK callers
2567
+ that still echo tokens DO reach it, and the SDK's read side stays typed for whatever arrives.
2568
+ ALSO an S-02 behavior NARROWING on this arm: an echoed `checkpointToken` that does NOT belong to
2569
+ this session, when the session has NO pending, now folds into the byte-identical 404
2570
+ `not_found.approval` (it used to 409 — a single-bit cross-tenant "who is parked on an approval"
2571
+ oracle); a legitimately-owned old token (a shell retrying its own already-decided row) keeps the
2572
+ attributable 409 + `terminal:"resolved"` verbatim.
2285
2573
  content:
2286
2574
  application/json:
2287
2575
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -5024,6 +5312,27 @@ components:
5024
5312
  CC Rewind "code"-only (core 1.166.0 TaskSpec.rewindFilesTo). Restore the working tree to this user-message
5025
5313
  entryId's snapshot WITHOUT forking the conversation (no setLeafId). Use instead of resumeAt+rewindFiles when
5026
5314
  the user wants only files reverted. Target = a user-message SessionTreeEntry.id.
5315
+ restoreFiles:
5316
+ type: boolean
5317
+ description: >-
5318
+ S-15 epoch request key (design/381; server ≥7.52 validates boolean and passes through to core —
5319
+ codex review R1-[medium]: `capabilities.restoreFiles` told clients to send this key while neither
5320
+ this schema nor the SDK type could express it). With `resumeAt`: restore the TRACKED SET (files
5321
+ this session edited) to that boundary on fork — S-15 narrowed restore whole-tree ⇒ tracked-set and
5322
+ moved the request key off `rewindFiles` (the old `rewindFiles`+`resumeAt` spelling gets core's
5323
+ typed migration rejection on ≥7.52). Probe `capabilities.restoreFiles` first: bit absent = older
5324
+ server, send the old spelling; never send both spellings.
5325
+ acceptPartialRestore:
5326
+ type: boolean
5327
+ description: >-
5328
+ S-15 companion key (design/381 DV-15; server ≥7.52 validates boolean and passes through —
5329
+ semantics owned by core's restore contract, dist typings verbatim): partial-restore consent.
5330
+ Absent/false = when the convergence ends with ≥1 per-file refusal/failure the run fails loud
5331
+ with the terminal `rewind.restore_failed` carrying the per-file ledger; true = the run keeps
5332
+ going and the same ledger is disclosed as a `restore_partial` note on `TaskResult.rewindNotes`.
5333
+ 🔴 Applies to BOTH restore legs (codex R2 correction): `resumeAt`+`restoreFiles` AND the
5334
+ code-only `rewindFilesTo` — a code-only caller omitting it gets the loud terminal too. Recovery
5335
+ either way: re-run the same restore (per-file idempotent, converges).
5027
5336
  permissionMode:
5028
5337
  type: string
5029
5338
  enum: [default, plan, acceptEdits, bypassPermissions, auto]
@@ -5370,6 +5679,14 @@ components:
5370
5679
  description: >
5371
5680
  The gate kind that produced `checkpointToken` when the run suspended. Deliberately UNTYPED here —
5372
5681
  the shape is core's and still evolving; treat it as opaque and branch on `status`/`errorCode` instead.
5682
+ toolCallId:
5683
+ type: string
5684
+ description: >
5685
+ [4913] (server 7.41+) — the PENDING tool call's id when this result is a durable park
5686
+ (`status` suspended/needs_review with a `tool_approval` pending action); same key and meaning as the
5687
+ `tool_approval` frame's `toolCallId` (server >= 1.307). ABSENT (key omitted, never null) on a
5688
+ tool-less park (resource_limit / plan_review / task_done), on every terminal result, and when the
5689
+ server-side checkpoint read failed — consumers test presence.
5373
5690
  degraded:
5374
5691
  type: object
5375
5692
  description: >
@@ -6084,10 +6401,44 @@ components:
6084
6401
  the store but no membership authority deliberately reports false (and does not mount the routes).
6085
6402
  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." }
6086
6403
  workflowsList: { type: boolean, description: "`GET /v1/workflows` list face (separate from the per-run reads)." }
6404
+ workflowModels:
6405
+ type: array
6406
+ items: { type: string }
6407
+ description: >-
6408
+ [5673]③/[5697] (server ≥7.52): the model VOCABULARY for workflow-script `agent({modelName})` —
6409
+ teaching face = pre-dispatch validation face, single-sourced (`workflowModelAllowlistFor`: an
6410
+ explicit SELF_ORCHESTRATION_MODELS always wins / single-user = every catalog key / multi-tenant
6411
+ fail-closed = []) ∩ the live per-prepare expanded catalog — every listed word passes BOTH gates,
6412
+ and a configured-but-absent word is filtered (config mistakes surface as core rejections and ops
6413
+ diagnostics, never as vocabulary). Empty array = honest "nothing usable here" (e.g. multi-tenant
6414
+ with no explicit allowlist); ABSENT = an older server (degrade to not teaching, not validating).
6087
6415
  resumeAt: { type: boolean, description: "E18 resume-at-message anchors are resolvable." }
6088
6416
  rewindFiles: { type: boolean, description: "E19 working-tree snapshot/restore is wired." }
6089
6417
  rewindFilesTo: { type: boolean, description: "Restore to a SPECIFIC entry id (not just the latest snapshot)." }
6418
+ restoreFiles:
6419
+ type: boolean
6420
+ description: >-
6421
+ S-15 epoch discriminator (server ≥7.52, `Boolean(fileHistoryStore)`): this server understands the
6422
+ `restoreFiles` REQUEST KEY / tracked-set restore semantics (design/381 — restore narrowed
6423
+ whole-tree ⇒ tracked set; the retired `rewindFiles`+`resumeAt` spelling gets core's typed
6424
+ migration rejection there). New clients negotiate on THIS bit and send `restoreFiles` (+
6425
+ `resumeAt`); absent ⇒ an older server, send the old spelling. `rewindFiles` keeps its name (same
6426
+ affordance — "rewind can move files"); this bit is what tells the two epochs apart.
6090
6427
  manualCompact: { type: boolean, description: "`POST /v1/runs/:id/compact` (needs a run store)." }
6428
+ configCatalog:
6429
+ type: boolean
6430
+ description: >-
6431
+ S-32 (server >=7.53, `routes/capabilities.ts:731`): the `GET /v1/config/catalog` operator lane
6432
+ (DESIGN-278 §5 S2 — deployment config self-description) is mounted on this DEPLOYMENT. TWO-term
6433
+ conjunction, written against the route's own refusal arms: ① the `CONFIG_CATALOG_ENABLED` knob is
6434
+ on (default on; a compliance deployment that turns it off makes the path a 404 — the valve-family
6435
+ precedent) AND ② the operator principal list is NON-EMPTY (with an empty list the route 403s every
6436
+ identity). 🔴 DEPLOYMENT-LEVEL AVAILABILITY, NOT THE CALLER'S AUTHORIZATION: the caller's own
6437
+ identity is NOT in the predicate — the route additionally requires the CURRENT principal to be an
6438
+ explicit operator (`routes/config-catalog.ts` `explicitOperatorOk`, else 403 `auth.operator_only`),
6439
+ so a non-operator reads `true` and still gets 403. Show/invoke the catalog only for an identity you
6440
+ have ALREADY established as an operator; the bit tells you whether that operator's control exists
6441
+ here at all. `false` does not say WHICH term failed. ABSENT = an older server (<7.53), not "off".
6091
6442
  sessions: { type: boolean, description: "Session read face (`GET /v1/sessions/:id`)." }
6092
6443
  sessionList: { type: boolean, description: "`GET /v1/sessions` list/picker face." }
6093
6444
  sessionSearch: { type: boolean, description: "Server-side session search." }
@@ -6118,10 +6469,15 @@ components:
6118
6469
  steerPriority: { type: string }
6119
6470
  workspace:
6120
6471
  description: >
6121
- #3 (server >=1.299): workspace-browse capability family — an OBJECT when the E19 snapshot-store
6472
+ #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
6122
6473
  read faces + a durable session store are wired ({browse, archive, maxFileBytes}); `false`/absent
6123
6474
  otherwise (same predicate as the routes' 501). `maxFileBytes` = the single-file read cap (over-cap
6124
6475
  ⇒ 413 workspace_file_too_large envelope; the archive endpoint is the escape hatch).
6476
+ 🔴 RETIRED (server ≥7.52, S-15/design/381): the whole-tree snapshot epoch this face projected was
6477
+ replaced by per-edited-file rewind history, so this key is now ALWAYS false and the four
6478
+ /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired` (route shape
6479
+ kept as a tombstone — a named refusal, not a 404 that reads like a typo). Consumption posture
6480
+ unchanged: probe this bit, false ⇒ don't render the panel.
6125
6481
  oneOf:
6126
6482
  - type: boolean
6127
6483
  enum: [false]
@@ -6253,8 +6609,14 @@ components:
6253
6609
  sessionSync:
6254
6610
  type: boolean
6255
6611
  description: >
6256
- 2c session-sync (P1d) — the `/v1/sessions/{sessionId}/sync/*` peer routes resolve (durable backend +
6257
- entry-export seam + a file-snapshot store wired; src/http/routes/capabilities.ts). Gate the whole sync surface off this.
6612
+ 2c session-sync (P1d) — the `/v1/sessions/{sessionId}/sync/*` peer routes resolve (server ≥7.52
6613
+ predicate: durable backend + entry-export/import-staging seams + a file-HISTORY store wired —
6614
+ S-15 换店; ≤7.51 the last conjunct was the file-snapshot store). Gate the whole sync surface off
6615
+ this. ⚠️ The two BLOB faces (PUT …/sync/blobs/{hash}, GET …/sync/history/blobs/{hash})
6616
+ additionally need the store's blob faces (`syncBlobFaces` — the SQL/cloud backends; the local
6617
+ file backend has none): without them the PUT answers 501 `capability.snapshot_store_required`
6618
+ and the history PULL answers the no-oracle 404 `not_found.blob`, while the REST of the sync
6619
+ surface keeps working.
6258
6620
 
6259
6621
  SkillSpec:
6260
6622
  type: object
@@ -6691,6 +7053,21 @@ components:
6691
7053
  replayed: { type: boolean, description: 'set when this agent''s result was REPLAYED from a resume journal rather than freshly run.' }
6692
7054
  prompt: { type: string, description: 'what the worker was ASKED (core-redacted + bounded). UNTRUSTED display text.' }
6693
7055
  output: { type: string, description: 'the worker''s final OUTPUT (core-redacted + bounded). UNTRUSTED display text.' }
7056
+ pendingApproval:
7057
+ type: object
7058
+ description: >-
7059
+ S-43(案A)E 段(server ≥7.52,core 5.65 供给;[5454] 键形冻结)— the suspended approvals this leg
7060
+ is CURRENTLY parked on. ABSENT = none pending. Three-state on purpose: absence ≠ empty array — an
7061
+ empty array would make "this engine does not supply the seat" and "truly nothing pending" the same
7062
+ shape. Server-side zod narrow-read projection: a drifted/half-written record omits the WHOLE key
7063
+ rather than projecting "a pendingApproval with nothing decidable in it".
7064
+ required: [askIds, oldestCreatedAtMs]
7065
+ additionalProperties: false
7066
+ properties:
7067
+ askIds: { type: array, items: { type: string }, minItems: 1 }
7068
+ oldestCreatedAtMs: { type: number }
7069
+ approvalWaitedMs: { type: number, description: 'S-43(案A)same batch — CUMULATIVE ms this leg has waited on approvals (#485 timeout-typing consumer; server single-sourced, core does not self-estimate). Absent on engines predating the seat.' }
7070
+ taskRunId: { type: string, description: '#499 (server ≥7.52, core 5.65) — the child ENGINE run identity (= the child TaskResult.runId; LAST-WINS across retries reusing the session). Named taskRunId NOT runId — in the workflow family `runId` already means the workflow run itself (core workflow-types 同名异形防撞段). Absent = stub leg / never reached prepare (additive, key not minted).' }
6694
7071
 
6695
7072
  WorkflowPhaseProgress:
6696
7073
  # 新(census 批2 四段):此前 `WorkflowRun.phases` 是 `items: {}`(零形状)。真形 = server
@@ -7167,12 +7544,18 @@ components:
7167
7544
  description: >
7168
7545
  FIRST frame — `{type, version, scoped, sessionScoped, bgNotifyFailClosed}` (all five UNCONDITIONAL on
7169
7546
  server >= 1.247). `scoped:true` ⇒ owner-scoped view; `false` ⇒ fleet-wide (operator/trace token).
7547
+ `session` (server 7.31+, [4119] P-31 Q2): additive echo of THE session id in effect on THIS connection
7548
+ — present iff the connection carried `?session=`. This is the connection-level zero-content
7549
+ self-attestation bit client-core's P-31 residual family asks for ([5427]): a keyed host feeds a stream
7550
+ into a ledger, compares this echo against the ledger's session anchor, and can trust
7551
+ `sessionScoped`/`bgNotifyFailClosed` without content-based inference.
7170
7552
  required: [type, version, scoped, sessionScoped, bgNotifyFailClosed]
7171
7553
  properties:
7172
7554
  type: { const: meta }
7173
7555
  version: { type: integer }
7174
7556
  scoped: { type: boolean }
7175
7557
  sessionScoped: { type: boolean, description: 'true ⇒ this connection carried `?session=` (rows and notifications are filtered by the host session).' }
7558
+ session: { type: string, description: 'server 7.31+ ([4119] P-31 Q2): the session id in effect on this connection — the verbatim `?session=` value. ABSENT when the connection is not session-bound (never minted empty). Compare against your ledger''s session anchor to close the misrouted-stream trust gap.' }
7176
7559
  bgNotifyFailClosed: { type: boolean, description: 'GENERATION marker — true ⇒ this server''s bg_notification injection path fails CLOSED for scoped subscribers, so a new-generation shell may drop its frame-level own/foreign checks entirely. ABSENT (older server) ⇒ keep them.' }
7177
7560
  FleetFrame_snapshot:
7178
7561
  type: object
@@ -7351,12 +7734,55 @@ components:
7351
7734
  description: Envelope for pending HITL checkpoints (key `pending`, NOT a bare array — the pinned wire contract).
7352
7735
  # 封闭(census 轴一,2026-07-30):三个铸造点(routes/approvals-assistant.ts)都只发
7353
7736
  # `{ pending }`。行本身(`PendingCheckpoint`)保持开集(TS 侧索引签名)。
7737
+ # #315(server ≥7.43):第二顶层键 `livePending` = streamApproval 窗内的流内 ask(此前对轮询消费端
7738
+ # 结构性不可见)。ADDITIVE / tolerate-absent:键缺席=「协调器未装配(本部署无 live 审批面)」或
7739
+ # 旧 server;空数组=「有面、此刻无待批」——两义可判别,消费端测 presence。**分数组不混编**:
7740
+ # live 行的决议口=POST /v1/tool-approvals/{approvalId}/respond,durable 行=decide 口;数组名即路由判据。
7354
7741
  additionalProperties: false
7355
7742
  required: [pending]
7356
7743
  properties:
7357
7744
  pending:
7358
7745
  type: array
7359
7746
  items: { $ref: '#/components/schemas/PendingCheckpoint' }
7747
+ livePending:
7748
+ type: array
7749
+ items: { $ref: '#/components/schemas/LivePendingRow' }
7750
+
7751
+ LivePendingRow:
7752
+ type: object
7753
+ description: >-
7754
+ A live (in-stream) pending ask inside its streamApproval window (#315, server ≥7.43). Decide it via
7755
+ POST /v1/tool-approvals/{approvalId}/respond — NOT the /decide route (that is the durable rows' door).
7756
+ Deliberately narrow: tool input/args are NOT listed (no second redaction surface — details ride the
7757
+ SSE tool_approval frame). Optional flags are only-if-true (absent is NEVER encoded as false).
7758
+ additionalProperties: false
7759
+ required: [approvalId, toolName, ts, expiresAtMs]
7760
+ properties:
7761
+ approvalId: { type: string, description: 'The SAME id the respond endpoint takes (wire uuidv7).' }
7762
+ toolName: { type: string }
7763
+ ts: { type: number, description: 'Registration time (ms epoch).' }
7764
+ expiresAtMs: { type: number, description: 'Absolute window deadline — the SAME number minted once for the tool_approval frame (never recomputed).' }
7765
+ sessionId: { type: string }
7766
+ requiresRealApproval: { type: boolean, enum: [true] }
7767
+ governanceForced: { type: boolean, enum: [true] }
7768
+ fromSubagent: { type: boolean, enum: [true], description: 'The ask originates from a delegated subagent.' }
7769
+ originTaskId: { type: string, description: 'S-52 C2 (server ≥7.52, A-075.87): the delegated child taskId (= the child''s core runSourceTaskId, uuid) — correlates this live ask to the child run row. Minted by the SAME conditional spread as `fromSubagent`: the two keys are always both present or both absent (三键恒等亲证在案).' }
7770
+
7771
+ ApprovalStaleCurrentPending:
7772
+ type: object
7773
+ description: >-
7774
+ S-02 (server ≥7.52) — the ADDITIVE pointer key `currentPending` on the decide 409 `approval_stale`
7775
+ body: the session's CURRENT pending's three D-1 coordinates (same names/shapes as the §7b queue row),
7776
+ so a shell relocates and re-decides in ONE hop instead of refetching the whole queue. 🔴 NEVER includes
7777
+ a checkpointToken (resume credentials do not leave the server). Whole-or-absent: the server mints it
7778
+ only from a pending, owner-checked tool-approval row (no pending / non-tool gate / row read failure —
7779
+ a loud fail-open ledger arm — all omit the key).
7780
+ required: [toolName, boundCallId]
7781
+ additionalProperties: false
7782
+ properties:
7783
+ toolName: { type: string, description: 'The current pending''s gated tool (same source/value as the pending row''s toolName).' }
7784
+ boundCallId: { type: string, description: 'The current pending''s pendingAction.toolCallId — the D-1 anchor to echo back on the relocated decide.' }
7785
+ boundInputHash: { type: string, description: 'Server-minted; present only when the row has one. Echo verbatim, never recompute.' }
7360
7786
 
7361
7787
  ExemptionList:
7362
7788
  type: object
@@ -7374,14 +7800,25 @@ components:
7374
7800
  description: >
7375
7801
  The 200 body of `POST /v1/approvals/{sessionId}/decide` (census 批2 第二段, 2026-07-30 — this op's 200 was
7376
7802
  a bare `{type: object, additionalProperties: true}`, i.e. no field set at all for a generated consumer).
7377
- TWO real shapes share this envelope, discriminate on `status`:
7378
- · the RESUMED-TASK shape (`http/server.ts` driveResumeIntoRunLog return) — `{taskId?, sessionId, status,
7803
+ THREE real shapes share this envelope (#316, server >= 7.37 — [4660] Inkglow: the task-level leg used to
7804
+ drive the ENTIRE resume synchronously before responding, so its 200 took a full model round-trip with no
7805
+ upper bound; it now answers at the ACCEPTANCE point instead):
7806
+ · the TASK-LEVEL ACCEPTANCE shape (server >= 7.37, the DEFAULT on durable deployments;
7807
+ `http/server.ts` driveResumeIntoRunLog acceptance point) — `{taskId, sessionId, status:"resuming",
7808
+ bindingEnforced:true}`. Emitted AFTER every rejection-capable step has settled (lease 429, markResuming
7809
+ CAS 409, core pre-CAS guards + atomic CAS — those still reject synchronously); only the model leg runs
7810
+ on after the response. Follow the run via poll/SSE (`GET /v1/runs/:id` / `/events`) — `errorCode` /
7811
+ `errorMessage` / `retriable` NEVER ride this shape, failures land on the run row and its stream.
7812
+ · the RESUMED-TASK TERMINAL shape (pre-7.37 servers on every decide; >= 7.37 ONLY on deployments with no
7813
+ durable run row to follow — there the leg honestly stays synchronous) — `{taskId?, sessionId, status,
7379
7814
  errorCode?, errorMessage?, retriable?}`, plus `rememberApplied` when the request carried
7380
7815
  `remember:"session"` AND the worker has an exemption store;
7381
7816
  · the PARKED-BACKGROUND-AGENT shape (`parked-decide.ts`, server >=1.267) — `{taskId, status:"resuming",
7382
7817
  decision}`, an ACCEPTED (not completed) receipt; the revive drives asynchronously.
7383
- `status` is the only key BOTH shapes guarantee: `taskId` rides `getActiveTaskId` on the resumed leg (omitted
7384
- when there is no active row) and `sessionId` is absent from the parked shape.
7818
+ `status` is the only key ALL shapes guarantee. ⚠️ `status === "resuming"` alone no longer discriminates
7819
+ the parked shape: branch on `decision` (parked receipt only) vs `sessionId`/`bindingEnforced` (task-level
7820
+ acceptance only). `taskId` rides `getActiveTaskId` on the terminal leg (omitted when there is no active
7821
+ row) and `sessionId` is absent from the parked shape.
7385
7822
  required: [status]
7386
7823
  additionalProperties: false
7387
7824
  properties:
@@ -7397,8 +7834,9 @@ components:
7397
7834
  status:
7398
7835
  type: string
7399
7836
  description: >
7400
- OPEN string — do NOT narrow. Resumed leg: the run's resulting status (`completed`/`failed`/`suspended`/
7401
- `needs_review`). Parked leg: the literal `resuming` (= accepted, revive in flight).
7837
+ OPEN string — do NOT narrow. Task-level acceptance (server >= 7.37): the literal `resuming` (= accepted,
7838
+ resume in flight — follow the run stream). Terminal leg: the run's resulting status (`completed`/`failed`/
7839
+ `suspended`/`needs_review`). Parked leg: the literal `resuming` (= accepted, revive in flight).
7402
7840
  decision:
7403
7841
  type: string
7404
7842
  enum: [approve, deny]
@@ -7545,6 +7983,22 @@ components:
7545
7983
  presence may differ between the two legs for one ask. Derived from hot-reloadable governance knobs —
7546
7984
  long subscribers follow the stream's re-emitted frames. Absence ≠ "not governance-forced"; it means
7547
7985
  "no governance-origin evidence". Never written as false.
7986
+ hasBidiControls:
7987
+ type: boolean
7988
+ enum: [true]
7989
+ description: >
7990
+ S-30① (server >=7.53; core 5.60.0 #438), ADDITIVE, present ONLY when true: the parked action's
7991
+ execution payload contains at least one DIRECTIONAL formatting control (Trojan Source — what a
7992
+ human reads may not be the byte order that executes). Value is ENTIRELY core-minted (the
7993
+ `PendingAction.hasBidiControls` bit at park time; the SQL store denormalises it into the row's
7994
+ `has_bidi_controls` column and reads `=== 1`, the local file store reads the parked
7995
+ `pendingAction` bit `=== true` — neither recomputes). Rides BOTH durable
7996
+ read faces through the same projection (this row on `GET /v1/approvals` and the `pending` frame of
7997
+ `/v1/approvals/stream`). Never null, never false: ABSENCE = "not detected" (clean / core's bounded
7998
+ scan did not reach it / a row parked before the column existed) and MUST NOT be read as "confirmed
7999
+ clean". The live-card leg's sibling key is `inputHasBidi` (E-14, computed by the server over the
8000
+ frame's own serialised args) — a different name on purpose. Display/triage only; never part of
8001
+ resume / gate / CAS.
7548
8002
 
7549
8003
  # ── assistant-scheduler wire (ASSISTANT-WIRE-CONTRACT.md, service main 6cd0164 / core 1.110.0, LIVE) ──
7550
8004
  CheckpointSummary:
@@ -7609,6 +8063,17 @@ components:
7609
8063
  severity: { type: integer, minimum: 1, maximum: 5, description: 'Sort key; only human/irreversible_ask carry it.' }
7610
8064
  spentMicroUsd: { type: number, description: 'Accumulated spend on the suspend chain (micro-USD).' }
7611
8065
  deadline: { type: number, description: 'Awaiting-human SLA deadline (epoch ms).' }
8066
+ hasBidiControls:
8067
+ type: boolean
8068
+ enum: [true]
8069
+ description: >-
8070
+ server >=7.53 (S-30① / core 5.60.0 #438), ADDITIVE, present ONLY when true: the parked action's
8071
+ payload carries a DIRECTIONAL formatting control (Trojan Source). Third read face of the same
8072
+ core-minted bit (`routes/approvals-assistant.ts` inbox whitelist arm `s.hasBidiControls === true`;
8073
+ `summarizeCheckpoint` back-fills it for pre-bit rows on THIS face, so the absent set differs from
8074
+ the /v1/approvals row's — the reading of absence does not). Absence = "not detected", never
8075
+ "confirmed clean". Declared here because this schema is CLOSED — without the key a strict validator
8076
+ rejects every >=7.53 inbox row that carries it.
7612
8077
  objective: { type: ['string', 'null'], description: 'task objective, null if unavailable.' }
7613
8078
  input:
7614
8079
  description: >
@@ -7977,6 +8442,53 @@ components:
7977
8442
  items: { $ref: '#/components/schemas/McpServerStatus' }
7978
8443
  degraded: { type: boolean }
7979
8444
 
8445
+ SessionMemoryStatus:
8446
+ type: object
8447
+ description: >
8448
+ core 7.0.2 `SessionMemoryStatus` (design/383 §S-7, #511 item 1; `dist/core/memory-engine/engine.d.ts`) —
8449
+ SAME NAME, SAME SHAPE as core's export. Every key is optional, and each key's absence has ITS OWN
8450
+ meaning (below, read off `engine.js sessionMemoryStatus`): two of them (`optOutSource`, `lastCaptureAt`)
8451
+ are legitimately absent on a healthy session, the other three are absent only when their source could
8452
+ not be read. A fault never coins a `false`/`0` stand-in.
8453
+ # 封闭:core 的接口恰是这五键;server 逐键条件拷贝(禁 spread)⇒ 闭集本身。
8454
+ additionalProperties: false
8455
+ properties:
8456
+ captureOptedOut:
8457
+ type: boolean
8458
+ description: 'TRUE = a standing capture opt-out record; FALSE = store readable, no record (capture on). ABSENT = the record store faulted — indeterminate (`optOutSource: "fault"` accompanies).'
8459
+ committedCount:
8460
+ type: integer
8461
+ description: 'Committed entries carrying this session''s lineage contribution. Absent = ledger unreadable.'
8462
+ foldedCount:
8463
+ type: integer
8464
+ description: 'Of those, entries already folded into consolidation products (lineage × `distilled.inputs`); `0` when the session has no contribution. Absent = lineage ledger unreadable, or the backend cannot enumerate scopes, or the product read failed.'
8465
+ optOutSource:
8466
+ type: string
8467
+ enum: [record, fault]
8468
+ description: 'WHY `captureOptedOut` reads as it does: `record` = a standing one-way record (rides with `captureOptedOut:true`); `fault` = the store faulted and the capture state is INDETERMINATE (rides with `captureOptedOut` ABSENT). ABSENT = store readable and no record (`captureOptedOut:false`) — the healthy default, NOT a fault. Closed two-value set, same as core.'
8469
+ lastCaptureAt:
8470
+ type: integer
8471
+ description: 'Newest lineage `lastAt` for this session (ms epoch) — rides the same single ledger read as `committedCount`. Absent = ledger unreadable, or no committed contribution exists at all.'
8472
+
8473
+ SessionMemoryStatusResponse:
8474
+ type: object
8475
+ description: >
8476
+ `GET /v1/sessions/{sessionId}/memory-status` 200 body (server >=7.53, S-53 seam①): `sessionId` + the
8477
+ five `SessionMemoryStatus` keys copied ONE BY ONE when present (the server forbids spreading and forbids
8478
+ defaults — the five conditional copies ARE the closed set). Per-key absence meaning = exactly
8479
+ `SessionMemoryStatus` (a healthy zero-history session omits `optOutSource` and `lastCaptureAt`). Keys
8480
+ are mirrored locally rather than via allOf so the closed schema stays self-describing (spec rule:
8481
+ closed + allOf must mirror every key).
8482
+ additionalProperties: false
8483
+ required: [sessionId]
8484
+ properties:
8485
+ sessionId: { type: string, description: 'The session the status is about (echo of the path segment, percent-decoded).' }
8486
+ captureOptedOut: { type: boolean, description: 'See SessionMemoryStatus.captureOptedOut — absent = indeterminate, never a coined false.' }
8487
+ committedCount: { type: integer, description: 'See SessionMemoryStatus.committedCount.' }
8488
+ foldedCount: { type: integer, description: 'See SessionMemoryStatus.foldedCount.' }
8489
+ optOutSource: { type: string, enum: [record, fault], description: 'See SessionMemoryStatus.optOutSource — absent = no opt-out record (healthy), never a fault by itself.' }
8490
+ lastCaptureAt: { type: integer, description: 'See SessionMemoryStatus.lastCaptureAt (ms epoch) — absent on a session with no committed contribution (healthy) as well as on an unreadable ledger.' }
8491
+
7980
8492
  # ── 2c session-sync (P1d) — the cloud-as-a-SYNC-PEER schemas ──────────────────────────────────────────────
7981
8493
  SyncEntry:
7982
8494
  type: object
@@ -8257,6 +8769,7 @@ components:
8257
8769
  - $ref: '#/components/schemas/Event_status'
8258
8770
  - $ref: '#/components/schemas/Event_task_progress'
8259
8771
  - $ref: '#/components/schemas/Event_workspace_changed'
8772
+ - $ref: '#/components/schemas/Event_engine_notice'
8260
8773
  - $ref: '#/components/schemas/Event_suspended'
8261
8774
  # 🔴 以下 6 臂是 2026-07-25 补的:`events.ts` 的 `AgentEvent` 联合有 21 个 `type` 字面量,本文件此前只声明
8262
8775
  # 15 个 —— 照本文件生成客户端的消费方会把 6 种合法帧当成非法值(事件面是最吃重的 wire 面,这个缺口比
@@ -8313,6 +8826,7 @@ components:
8313
8826
  status: '#/components/schemas/Event_status'
8314
8827
  task_progress: '#/components/schemas/Event_task_progress'
8315
8828
  workspace_changed: '#/components/schemas/Event_workspace_changed'
8829
+ engine_notice: '#/components/schemas/Event_engine_notice'
8316
8830
  suspended: '#/components/schemas/Event_suspended'
8317
8831
  file_link: '#/components/schemas/Event_file_link'
8318
8832
  prompt_assembled: '#/components/schemas/Event_prompt_assembled'
@@ -8619,9 +9133,18 @@ components:
8619
9133
  required: [type, phase]
8620
9134
  properties:
8621
9135
  type: { const: status }
8622
- phase: { type: string, description: "rate_limited | retrying | reconnecting | circuit_open (open set)" }
9136
+ # 🔴 A-057.27:6 员(此前只列 4,漏 recovered/gave_up 两个**终局**值 —— 一段重试序列恒恰一终局帧,
9137
+ # 两者对渲染方是同一句话「把重试行撤下来」)。闭集声明、开集读。
9138
+ phase: { type: string, description: "rate_limited | retrying | reconnecting | circuit_open | recovered | gave_up (closed in core, read as open set)" }
8623
9139
  detail: { type: string, description: "neutral human hint (NO provider/HTTP detail)" }
8624
9140
  retryInSec: { type: number }
9141
+ # 🔴 A-057.26:以下四键 server 真在发(builder brainStatusEventData,三腿共享),而本 schema 是
9142
+ # `additionalProperties: false` ⇒ 不声明 = 一个**合法**的真帧会被严格校验器判违约(假红),正是本仓
9143
+ # 元门(封闭 schema 必须镜像分支键)要防的那一形。缺席不铸键,故一律 optional。
9144
+ attempt: { type: number, description: "1-based index of the attempt that just FAILED (this wait precedes attempt+1)" }
9145
+ maxRetries: { type: number, description: "retry budget of the lane in force (refused-connect short lane = 2)" }
9146
+ retryInMs: { type: number, description: "same number as retryInSec at ms precision (countdown rendering)" }
9147
+ errClass: { type: string, description: "connect_refused | transport | rate_limit | server | http | output_cap — retry-wait frames ONLY; never inferred from phase" }
8625
9148
  eventId: { type: string }
8626
9149
  parentToolCallId: { type: string }
8627
9150
  sourceTaskId: { type: string }
@@ -8649,6 +9172,16 @@ components:
8649
9172
  properties:
8650
9173
  type: { const: task_progress }
8651
9174
  taskId: { type: string, description: 'The subagent task id (the delegated sub-run).' }
9175
+ seq:
9176
+ type: integer
9177
+ description: >-
9178
+ core 5.36.0 (#258), server-forwarded: the GENERATION number of the child's `a*` registry row
9179
+ (fresh spawn = 1, +1 per revival; same axis as `TaskNotificationPayload.seq` and the fleet
9180
+ `BackgroundChildEvent.seq`). Lets a consumer tell a late first frame of a known generation (same
9181
+ value) from an un-folded revival (greater). ABSENT = the run has no `a*` row (sync delegated child /
9182
+ workflow `wa*` / top-level) — no generation concept exists, do NOT read absence as cycle 1.
9183
+ core-minted number, no user content. (codex R2 of the SDK 7.3.1 batch: this key was really on the
9184
+ wire and this closed arm omitted it — a strict validator rejected every generation-tagged tick.)
8652
9185
  taskType:
8653
9186
  type: string
8654
9187
  description: >
@@ -8666,6 +9199,15 @@ components:
8666
9199
  durationMs: { type: integer }
8667
9200
  status: { type: string, description: 'running 为主;core [1414]#3 的 settle 终态 tick 亦可为 completed/failed(开集,按未知值兜底渲染)。' }
8668
9201
  name: { type: string, description: '子代的人类展示名(UNTRUSTED,已 redactSecrets)。缺席 ⇒ 无名,别回退到 objective。' }
9202
+ model:
9203
+ type: string
9204
+ description: >-
9205
+ server >=7.53 (core 7.0.1 #508①, P-42): the resolved catalog id of the model this child leg was
9206
+ PREPARED with — same value/meaning as `TaskResult.model` (a mid-run degrade does NOT rewrite it; the
9207
+ switch is disclosed on `TaskResult.degraded`). Minted unconditionally by both core task_progress
9208
+ sites (running tick + settle terminal tick) and forwarded verbatim by the server whitelist on all
9209
+ three live legs + the durable ledger row. ABSENT = an older producer (core <7.0.1 / server <7.53)
9210
+ or unstated — never read absence as "no model". core-minted, no user content.
8669
9211
  parentTaskId: { type: string, description: '嵌套子代的上级 taskId;缺席 ⇒ 一级子代。' }
8670
9212
  currentAction: { type: string, description: '子代最近一次 tool_start 的一行人话("Bash npm test");UNTRUSTED,已 redactSecrets。' }
8671
9213
  workflowRunId: { type: string, description: 'core 1.401:后台 workflow 子代的不透明 run id(下游据此跳过与 fleet 行的重复渲染)。' }
@@ -8846,6 +9388,36 @@ components:
8846
9388
  parentToolCallId: { type: string }
8847
9389
  sourceTaskId: { type: string }
8848
9390
  bgAgentId: { type: string }
9391
+ Event_engine_notice:
9392
+ type: object
9393
+ description: >
9394
+ server >= 7.36 (#310, board [4630]) — an engine STRUCTURED NOTICE judged to be for THIS SESSION'S end user,
9395
+ routed to the session's event projection by `sessionId`. Present on BOTH legs (live SSE and the durable
9396
+ ledger replayed by `GET /v1/runs/:id/events`) — reconnect replay shows it again, so consume idempotently
9397
+ (same discipline as `workspace_changed`).
9398
+ RENDER BY `code` + `detail`; `message` is a FALLBACK ONLY (core states that `memory.session_polluted`'s
9399
+ message varies with the memoryProvenance mode — matching on message text WILL break). `code` is an OPEN
9400
+ set (the server whitelist grows with core's code register): render a known code specially, fall back to
9401
+ `message` for an unknown one — never drop the frame.
9402
+ Starter whitelist (server 7.36): `memory.session_polluted` `{reason, sessionId?}` ·
9403
+ `memory.harvest_quarantined` `{count, moved, escalated, reason?}` — `moved` and `escalated` MUST NOT be
9404
+ subtracted from each other (an in-place tombstone counts as both) · `memory.delegation_static_mark_waived`
9405
+ `{reason, subagentType?, sessionId?}`.
9406
+ A notice whose `detail.sessionId` is absent is HONESTLY NOT DELIVERED (the server never guesses a session),
9407
+ so the frame's absence does NOT mean "did not happen" — the operator-facing structured log always has the
9408
+ full family. `detail` is already redacted + size-bounded by the server; still treat it as external text.
9409
+ additionalProperties: false
9410
+ required: [type, code, message, detail, sessionId, ts]
9411
+ properties:
9412
+ type: { const: engine_notice }
9413
+ code: { type: string, description: 'Stable dot-namespaced machine code. OPEN set — never drop an unknown one.' }
9414
+ message: { type: string, description: 'Core-minted human line. FALLBACK DISPLAY ONLY, never a match key.' }
9415
+ detail:
9416
+ type: object
9417
+ additionalProperties: true
9418
+ description: 'Machine-readable facts (per code). Redacted + bounded server-side; open set of keys.'
9419
+ sessionId: { type: string, description: 'Owning session — this frame only ever appears on that session stream.' }
9420
+ ts: { type: integer, format: int64, description: 'SERVER observation time (ms epoch), NOT the engine mint time.' }
8849
9421
  Event_workspace_changed:
8850
9422
  type: object
8851
9423
  description: >
@@ -9508,6 +10080,12 @@ components:
9508
10080
  fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
9509
10081
  sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
9510
10082
  sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
10083
+ # A-057.④(#314):三键 server 早已随卡投出(与 tool_approval 帧同一窄读函数 ⇒ 帧与 card 同值),
10084
+ # 但本 schema 只在帧上公示过 —— 按 spec 生成的卡消费方拿不到。定义与帧上同构段逐字同源,
10085
+ # 长 description 不复制(单一真源在 ToolApprovalFrame 的同名键)。
10086
+ inputHasBidi: { type: boolean, enum: [true], description: 'server >= 7.45.0 (E-14): the TO-BE-EXECUTED input (redacted args, computed BEFORE the byte cap) contains Unicode bidi override characters — what the eye reads may not be the byte order that runs. DISCLOSURE bit, bytes unchanged; rendering (escape/highlight) is the shell''s. Present (true) or ABSENT; absence != "verified clean" (not detected / serialization failed / older server).' }
10087
+ probeCause: { type: object, additionalProperties: true, description: 'server >= 7.17.0: structured reversibility-probe tightening cause — same shape and semantics as ToolApprovalFrame.probeCause ({code, roots:{shown,total}, further?:{shown,total}}); frame and card carry the SAME value (one narrow-read function server-side). See that key for the full contract.' }
10088
+ ruleEvidence: { type: object, additionalProperties: true, description: 'server >= 7.23.0: rule-provenance evidence — same shape and semantics as ToolApprovalFrame.ruleEvidence (each member a value or a NAMED absence word; display/reconciliation metadata, never adjudication input). Frame and card carry the SAME value. See that key for the full contract.' }
9511
10089
  delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
9512
10090
 
9513
10091
  RuleSuggestion:
@@ -9973,6 +10551,23 @@ components:
9973
10551
  activeTaskStatus:
9974
10552
  type: string
9975
10553
  description: '`running` | `suspended` | `needs_review` — best-effort (absent when the store lookup degraded).'
10554
+ msSinceLastActivity:
10555
+ type: number
10556
+ description: >
10557
+ (#245 S1, server >= 7.18 — 既存违例回填 2026-08-20: server has been emitting this on the done frame
10558
+ since the key landed on the 409 body; the CLOSED schema here just never declared it, so every real
10559
+ frame carrying it "violated" spec. Same key, same semantics as the 409 body.) Milliseconds since the
10560
+ occupying run's last recorded activity — the ONLY liveness-evidence bit on this frame (same-replica
10561
+ best-effort). Absent = no evidence, NOT "fresh"; never narrow absence to 0.
10562
+ stale:
10563
+ const: true
10564
+ description: >
10565
+ (#316(c), server >= 7.37; [4664]② hole (c)) The occupying run row looks DEAD by the same staleness
10566
+ criterion the poll face (`GET /v1/runs/:id`) uses to fold a terminal status — heartbeat silence past
10567
+ the reap window. This frame does NOT fold the status (`activeTaskStatus` stays verbatim, the reaper
10568
+ owns the transition); the bit lets a shell reconcile "conflict says running / poll says failed"
10569
+ inside the self-heal window. ADDITIVE, present ONLY as `true`; absent = no staleness evidence (parked
10570
+ rows are out of reap scope and never carry it — absence is NOT "alive").
9976
10571
  pendingGate:
9977
10572
  allOf: [{ $ref: '#/components/schemas/PendingGateMaterial' }]
9978
10573
  description: >
@@ -10308,6 +10903,14 @@ components:
10308
10903
  sessionId: { type: string, description: '`resolved` frames: which pending settled.' }
10309
10904
  toolCallId: { type: string, description: '`resolved` frames: the settled pending''s bound tool-call id (absent when the pending carried none).' }
10310
10905
  count: { type: integer, description: '`synced` frames: how many pendings the connect-time snapshot carried.' }
10906
+ hasBidiControls:
10907
+ type: boolean
10908
+ enum: [true]
10909
+ description: >-
10910
+ `pending` frames only (server >=7.53, S-30①): the spread PendingCheckpoint row's bidi-presence bit —
10911
+ SAME projection as the `GET /v1/approvals` row (`projectPendingForWire`), declared here explicitly so
10912
+ a stream consumer sees it typed instead of through the open index signature. Present ONLY when true;
10913
+ absence = "not detected", never "confirmed clean" (see PendingCheckpoint.hasBidiControls).
10311
10914
 
10312
10915
  ParkedDecideAccepted:
10313
10916
  type: object
@@ -10889,6 +11492,40 @@ components:
10889
11492
  priority: { $ref: '#/components/schemas/SteerPriority' }
10890
11493
  note: { type: string, description: 'Human-readable note on the two parked legs; absent on `applied`.' }
10891
11494
 
11495
+ InterruptReceipt:
11496
+ description: >
11497
+ The POST /v1/runs/{taskId}/interrupt 202 receipt — TWO mutually exclusive shapes (oneOf, discriminated
11498
+ by which arm's keys are present, NOT by status code; both are 202 — codex 复审 R1-[medium]:单对象全
11499
+ 可选形会让 `{taskId,status}` 与混臂形都合法,生成式消费方靠不住判别键). TEXT form (cut + steer):
11500
+ `delivery:"queued-immediate"` + `messageId` (≡ the engine inputId — correlate with the run stream's
11501
+ `task.turn_interrupted` `detail.inputId`; acceptance = enqueued, the cut itself is best-effort).
11502
+ BARE (halt) form: `turnCut` and deliberately NO delivery/messageId (no input frame exists to correlate
11503
+ — A-075.78 成文;halt's acceptance is NOT a queue insert, so the receipt can honestly answer whether
11504
+ THIS call cut a live turn). `status` = the run row's word as the server read it at acceptance. Honest
11505
+ window: a halt racing the run's natural last moments reports the natural completion, still carrying
11506
+ the haltedByUser seat.
11507
+ # 封闭两臂:两个铸造点都是逐字面量拼出的对象(server routes/runs.ts interrupt 段),不是引擎透传形。
11508
+ oneOf:
11509
+ - type: object
11510
+ description: TEXT form (cut + steer) — the run continues.
11511
+ required: [taskId, status, delivery, messageId]
11512
+ additionalProperties: false
11513
+ properties:
11514
+ taskId: { type: string }
11515
+ status: { type: string, description: 'The run status the server saw at acceptance time.' }
11516
+ delivery: { type: string, enum: [queued-immediate] }
11517
+ messageId: { type: string, description: '≡ the engine inputId (with an Idempotency-Key: `idem-<sha256[:32]>`; without one: a fresh uuidv7, minted and threaded down PER REQUEST — unlike steer''s absent arm, which lets core mint).' }
11518
+ note: { type: string, description: 'Contract note (逐句照 core 契约,不多许诺一个字).' }
11519
+ - type: object
11520
+ description: BARE (halt) form (cut + stop) — the run closes at this boundary.
11521
+ required: [taskId, status, turnCut]
11522
+ additionalProperties: false
11523
+ properties:
11524
+ taskId: { type: string }
11525
+ status: { type: string, description: 'The run status the server saw at acceptance time.' }
11526
+ turnCut: { type: boolean, description: 'true ⇔ THIS call cut a live turn; repeat calls answer false once the stop is latched (halt is naturally idempotent).' }
11527
+ note: { type: string, description: 'Contract note (逐句照 core 契约,不多许诺一个字).' }
11528
+
10892
11529
  SubagentSteerReceipt:
10893
11530
  type: object
10894
11531
  description: >
@@ -11619,7 +12256,7 @@ components:
11619
12256
  required: [rule, scope]
11620
12257
  properties:
11621
12258
  rule: { type: string, minLength: 1, maxLength: 1024, description: 'The canonical rule text, VERBATIM as `RuleListResult.rules[].rule` gave it.' }
11622
- scope: { type: string, minLength: 1, maxLength: 1088, description: 'The scope discriminant string — `global` or `project:<root>` with a NON-EMPTY root (an empty root would match every cwd). VERBATIM as the listing gave it.' }
12259
+ scope: { type: string, minLength: 1, maxLength: 4104, description: 'The scope discriminant string — `global` or `project:<root>` with a NON-EMPTY root (an empty root would match every cwd). VERBATIM as the listing gave it. Cap = "project:".length + server MAX_CWD_CHARS (8+4096) — the 1088 previously written here was the same stale independent copy the server itself fixed (rules.ts MAX_RULE_SCOPE_CHARS 顶注), found by the A-335 anchor sweep.' }
11623
12260
  principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
11624
12261
 
11625
12262
  RuleRevokeResult: