@sema-agent/sdk 7.2.0-rc.1 → 7.3.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 (38) hide show
  1. package/README.md +23 -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 +70 -4
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/events.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  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 +17 -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 +10 -4
  26. package/dist/resources/sessions.d.ts.map +1 -1
  27. package/dist/resources/sessions.js.map +1 -1
  28. package/dist/resources/tool-approvals.d.ts +22 -1
  29. package/dist/resources/tool-approvals.d.ts.map +1 -1
  30. package/dist/resources/tool-approvals.js +3 -0
  31. package/dist/resources/tool-approvals.js.map +1 -1
  32. package/dist/resources/workspace.d.ts +7 -1
  33. package/dist/resources/workspace.d.ts.map +1 -1
  34. package/dist/resources/workspace.js.map +1 -1
  35. package/dist/types.d.ts +95 -6
  36. package/dist/types.d.ts.map +1 -1
  37. package/openapi.yaml +522 -67
  38. 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.
@@ -1916,12 +2064,18 @@ paths:
1916
2064
  get:
1917
2065
  tags: [sessions]
1918
2066
  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
2067
+ 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).
2068
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot-scoped blob PULL; use GET …/sync/history/blobs/{hash}. Live on ≤7.51 only.'
2069
+ description: >
2070
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): a blob is scoped by the session's
2071
+ HISTORY GRAPH now, not by a per-snapshot manifest key, so this arm was replaced by
2072
+ `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}` (sessionSyncGetHistoryBlob — same
2073
+ octet-stream body, same no-oracle 404 posture, scope check = "referenced by this session's
2074
+ exportHistory envelope"). On ≥7.52 this path no longer matches the sync route family and answers 404
2075
+ `not_found.route`. SDK: `sessions.sync.getHistoryBlob` (the legacy `getBlob` verb stays, deprecated,
2076
+ for ≤7.51 servers inside the support range).
2077
+ Historical shape (≤7.51): one content-addressed blob's RAW bytes; the cloud served a hash ONLY if
2078
+ THIS session's `:key` manifest referenced it (foreign/unreferenced → 404 `blob not found`). 400 on a
1925
2079
  non-64-hex hash or an undecodable key/hash. Owner-scoped (404).
1926
2080
  responses:
1927
2081
  '200':
@@ -1934,6 +2088,43 @@ paths:
1934
2088
  '404': { $ref: '#/components/responses/NotFound' }
1935
2089
  '501': { $ref: '#/components/responses/NotImplemented' }
1936
2090
 
2091
+ /v1/sessions/{sessionId}/sync/history/blobs/{hash}:
2092
+ parameters:
2093
+ - $ref: '#/components/parameters/PrincipalHeader'
2094
+ - in: path
2095
+ name: sessionId
2096
+ required: true
2097
+ schema: { type: string }
2098
+ - in: path
2099
+ name: hash
2100
+ required: true
2101
+ schema: { type: string, pattern: '^[0-9a-f]{64}$' }
2102
+ description: The content-address (SHA-256, 64 lowercase-hex).
2103
+ get:
2104
+ tags: [sessions]
2105
+ operationId: sessionSyncGetHistoryBlob
2106
+ x-status: live # S-15/design/381 (server 7.52.0): the history-graph-scoped successor of the retired snapshots/{key}/blobs PULL arm.
2107
+ summary: PULL one content-addressed blob's raw bytes (history-graph-scoped; server ≥7.52).
2108
+ description: >
2109
+ 2c session-sync PULL, S-15 epoch — one content-addressed blob's RAW bytes
2110
+ (`application/octet-stream`). 🔴 SCOPE BOUNDARY (the security boundary — a content-addressed blob is
2111
+ shared across sessions): the cloud serves a hash ONLY if THIS session's HISTORY GRAPH references it
2112
+ (some tracked file version's `blobHash` in its exportHistory envelope). A foreign / unreferenced hash
2113
+ → 404 `not_found.blob`, never a blind read; a server whose file-history store has no blob faces
2114
+ (e.g. the local file backend) answers the SAME 404 shape (no oracle — "nothing servable" and "not
2115
+ referenced" are deliberately indistinguishable). 400 `request.id_invalid` on a non-64-hex hash.
2116
+ Owner-scoped (404). Replaces the retired `GET …/sync/snapshots/{key}/blobs/{hash}`.
2117
+ responses:
2118
+ '200':
2119
+ description: The blob bytes.
2120
+ content:
2121
+ application/octet-stream:
2122
+ schema: { type: string, format: binary }
2123
+ '400': { $ref: '#/components/responses/BadRequest' }
2124
+ '401': { $ref: '#/components/responses/Unauthorized' }
2125
+ '404': { $ref: '#/components/responses/NotFound' }
2126
+ '501': { $ref: '#/components/responses/NotImplemented' }
2127
+
1937
2128
  /v1/sessions/{sessionId}/sync/blobs/{hash}:
1938
2129
  parameters:
1939
2130
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -1955,7 +2146,12 @@ paths:
1955
2146
  body). The cloud verifies `sha256(body) === :hash` (the content-address integrity guard) BEFORE storing — a
1956
2147
  mismatch → 400 `blob hash mismatch`. own-or-FRESH gate. Per-blob cap 64 MiB → 413 mid-stream beyond it. A
1957
2148
  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`).
2149
+ PUSH path is the BARE `blobs/:hash` (the PULL is `history/blobs/:hash` since server 7.52,
2150
+ `snapshots/:key/blobs/:hash` before). 🔴 server ≥7.52 (S-15 换店): the blob faces ride the
2151
+ file-HISTORY store — a deployment whose store has no blob faces (the local file backend) answers 501
2152
+ `capability.snapshot_store_required` ("blob upload requires a durable file-history store") while the
2153
+ rest of the sync surface keeps working; on ≤7.51 the same code meant a missing snapshot store. Same
2154
+ code, same operator disposition (wire a backend with blob faces).
1959
2155
  requestBody:
1960
2156
  required: true
1961
2157
  content:
@@ -2028,6 +2224,14 @@ paths:
2028
2224
  2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
2029
2225
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
2030
2226
  422 `missing_blob` — PUT it first), guards the import lease + any active run (409), then mints a stagingId.
2227
+ ⚠️ S-15 EPOCH DEBT (registered 2026-08-30, codex adversarial review; migration is a SEPARATE batch):
2228
+ server ≥7.52's Phase A actually consumes `{entryIds, fileHistory?, policy, anchors, resolution?,
2229
+ logDigest?}` (routes/session-sync.ts) — the `snapshots` key below is SILENTLY IGNORED there (an
2230
+ unknown body key), so on ≥7.52 a push lands entries/policy/anchors and the session itself but NO
2231
+ file snapshots/history; the snapshot-epoch shape below is what ≤7.51 servers consume. The full
2232
+ migration (SessionManifest/SessionBundle/push-pull orchestration + `fileHistory: FileHistoryExport |
2233
+ null` + epoch negotiation) is tracked as its own debt — this op's schema deliberately stays the
2234
+ ≤7.51 truth until that batch lands rather than half-changing.
2031
2235
  `identical` → `{ relation:"identical", basis, payloadVerified }` with NO stagingId (skip Phase B);
2032
2236
  `basis` is `"entry-ids+digest"` when the caller supplied a comparable digest and it matched, else
2033
2237
  `"entry-ids"` (id-set equality only) — see the `ImportStaged` schema, which has carried these two
@@ -2282,6 +2486,23 @@ paths:
2282
2486
  already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client action:
2283
2487
  refetch the inbox; this pending is gone. SDK → ApprovalStaleError. Note the bare CAS 409 has NO
2284
2488
  `errorCode`, which is exactly how the three are told apart.
2489
+ S-02 (server ≥7.52, A-075.94③): the stale body may carry the ADDITIVE pointer key
2490
+ `currentPending` ($ref ApprovalStaleCurrentPending — the SESSION'S current pending's D-1
2491
+ coordinates `{toolName, boundCallId, boundInputHash?}`, same names/shapes as the §7b queue row) so
2492
+ a shell relocates and re-decides in ONE hop instead of refetching the whole queue. NEVER carries a
2493
+ checkpointToken. Absent when there is no pending / the gate is not a tool approval / the row read
2494
+ failed (loud fail-open ledger arm) / an older server. Owner domain = the route's owner gate (no
2495
+ new disclosure surface). SDK: `ApprovalStaleError.currentPending`, whole-or-absent guard.
2496
+ REACHABILITY (codex 复审 R1-[medium] 采纳成文): the stale arm fires only for callers that ECHO a
2497
+ `checkpointToken`; this SDK's ApprovalDecision deliberately has no such field (removed in 1.0.0 —
2498
+ resume credentials never ride the compliant decide), so decides issued through this SDK never
2499
+ trigger it. The key is documented for the wire contract's sake: legacy shells and non-SDK callers
2500
+ that still echo tokens DO reach it, and the SDK's read side stays typed for whatever arrives.
2501
+ ALSO an S-02 behavior NARROWING on this arm: an echoed `checkpointToken` that does NOT belong to
2502
+ this session, when the session has NO pending, now folds into the byte-identical 404
2503
+ `not_found.approval` (it used to 409 — a single-bit cross-tenant "who is parked on an approval"
2504
+ oracle); a legitimately-owned old token (a shell retrying its own already-decided row) keeps the
2505
+ attributable 409 + `terminal:"resolved"` verbatim.
2285
2506
  content:
2286
2507
  application/json:
2287
2508
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -5024,6 +5245,27 @@ components:
5024
5245
  CC Rewind "code"-only (core 1.166.0 TaskSpec.rewindFilesTo). Restore the working tree to this user-message
5025
5246
  entryId's snapshot WITHOUT forking the conversation (no setLeafId). Use instead of resumeAt+rewindFiles when
5026
5247
  the user wants only files reverted. Target = a user-message SessionTreeEntry.id.
5248
+ restoreFiles:
5249
+ type: boolean
5250
+ description: >-
5251
+ S-15 epoch request key (design/381; server ≥7.52 validates boolean and passes through to core —
5252
+ codex review R1-[medium]: `capabilities.restoreFiles` told clients to send this key while neither
5253
+ this schema nor the SDK type could express it). With `resumeAt`: restore the TRACKED SET (files
5254
+ this session edited) to that boundary on fork — S-15 narrowed restore whole-tree ⇒ tracked-set and
5255
+ moved the request key off `rewindFiles` (the old `rewindFiles`+`resumeAt` spelling gets core's
5256
+ typed migration rejection on ≥7.52). Probe `capabilities.restoreFiles` first: bit absent = older
5257
+ server, send the old spelling; never send both spellings.
5258
+ acceptPartialRestore:
5259
+ type: boolean
5260
+ description: >-
5261
+ S-15 companion key (design/381 DV-15; server ≥7.52 validates boolean and passes through —
5262
+ semantics owned by core's restore contract, dist typings verbatim): partial-restore consent.
5263
+ Absent/false = when the convergence ends with ≥1 per-file refusal/failure the run fails loud
5264
+ with the terminal `rewind.restore_failed` carrying the per-file ledger; true = the run keeps
5265
+ going and the same ledger is disclosed as a `restore_partial` note on `TaskResult.rewindNotes`.
5266
+ 🔴 Applies to BOTH restore legs (codex R2 correction): `resumeAt`+`restoreFiles` AND the
5267
+ code-only `rewindFilesTo` — a code-only caller omitting it gets the loud terminal too. Recovery
5268
+ either way: re-run the same restore (per-file idempotent, converges).
5027
5269
  permissionMode:
5028
5270
  type: string
5029
5271
  enum: [default, plan, acceptEdits, bypassPermissions, auto]
@@ -5370,6 +5612,14 @@ components:
5370
5612
  description: >
5371
5613
  The gate kind that produced `checkpointToken` when the run suspended. Deliberately UNTYPED here —
5372
5614
  the shape is core's and still evolving; treat it as opaque and branch on `status`/`errorCode` instead.
5615
+ toolCallId:
5616
+ type: string
5617
+ description: >
5618
+ [4913] (server 7.41+) — the PENDING tool call's id when this result is a durable park
5619
+ (`status` suspended/needs_review with a `tool_approval` pending action); same key and meaning as the
5620
+ `tool_approval` frame's `toolCallId` (server >= 1.307). ABSENT (key omitted, never null) on a
5621
+ tool-less park (resource_limit / plan_review / task_done), on every terminal result, and when the
5622
+ server-side checkpoint read failed — consumers test presence.
5373
5623
  degraded:
5374
5624
  type: object
5375
5625
  description: >
@@ -6084,9 +6334,29 @@ components:
6084
6334
  the store but no membership authority deliberately reports false (and does not mount the routes).
6085
6335
  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
6336
  workflowsList: { type: boolean, description: "`GET /v1/workflows` list face (separate from the per-run reads)." }
6337
+ workflowModels:
6338
+ type: array
6339
+ items: { type: string }
6340
+ description: >-
6341
+ [5673]③/[5697] (server ≥7.52): the model VOCABULARY for workflow-script `agent({modelName})` —
6342
+ teaching face = pre-dispatch validation face, single-sourced (`workflowModelAllowlistFor`: an
6343
+ explicit SELF_ORCHESTRATION_MODELS always wins / single-user = every catalog key / multi-tenant
6344
+ fail-closed = []) ∩ the live per-prepare expanded catalog — every listed word passes BOTH gates,
6345
+ and a configured-but-absent word is filtered (config mistakes surface as core rejections and ops
6346
+ diagnostics, never as vocabulary). Empty array = honest "nothing usable here" (e.g. multi-tenant
6347
+ with no explicit allowlist); ABSENT = an older server (degrade to not teaching, not validating).
6087
6348
  resumeAt: { type: boolean, description: "E18 resume-at-message anchors are resolvable." }
6088
6349
  rewindFiles: { type: boolean, description: "E19 working-tree snapshot/restore is wired." }
6089
6350
  rewindFilesTo: { type: boolean, description: "Restore to a SPECIFIC entry id (not just the latest snapshot)." }
6351
+ restoreFiles:
6352
+ type: boolean
6353
+ description: >-
6354
+ S-15 epoch discriminator (server ≥7.52, `Boolean(fileHistoryStore)`): this server understands the
6355
+ `restoreFiles` REQUEST KEY / tracked-set restore semantics (design/381 — restore narrowed
6356
+ whole-tree ⇒ tracked set; the retired `rewindFiles`+`resumeAt` spelling gets core's typed
6357
+ migration rejection there). New clients negotiate on THIS bit and send `restoreFiles` (+
6358
+ `resumeAt`); absent ⇒ an older server, send the old spelling. `rewindFiles` keeps its name (same
6359
+ affordance — "rewind can move files"); this bit is what tells the two epochs apart.
6090
6360
  manualCompact: { type: boolean, description: "`POST /v1/runs/:id/compact` (needs a run store)." }
6091
6361
  sessions: { type: boolean, description: "Session read face (`GET /v1/sessions/:id`)." }
6092
6362
  sessionList: { type: boolean, description: "`GET /v1/sessions` list/picker face." }
@@ -6118,10 +6388,15 @@ components:
6118
6388
  steerPriority: { type: string }
6119
6389
  workspace:
6120
6390
  description: >
6121
- #3 (server >=1.299): workspace-browse capability family — an OBJECT when the E19 snapshot-store
6391
+ #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
6122
6392
  read faces + a durable session store are wired ({browse, archive, maxFileBytes}); `false`/absent
6123
6393
  otherwise (same predicate as the routes' 501). `maxFileBytes` = the single-file read cap (over-cap
6124
6394
  ⇒ 413 workspace_file_too_large envelope; the archive endpoint is the escape hatch).
6395
+ 🔴 RETIRED (server ≥7.52, S-15/design/381): the whole-tree snapshot epoch this face projected was
6396
+ replaced by per-edited-file rewind history, so this key is now ALWAYS false and the four
6397
+ /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired` (route shape
6398
+ kept as a tombstone — a named refusal, not a 404 that reads like a typo). Consumption posture
6399
+ unchanged: probe this bit, false ⇒ don't render the panel.
6125
6400
  oneOf:
6126
6401
  - type: boolean
6127
6402
  enum: [false]
@@ -6253,8 +6528,14 @@ components:
6253
6528
  sessionSync:
6254
6529
  type: boolean
6255
6530
  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.
6531
+ 2c session-sync (P1d) — the `/v1/sessions/{sessionId}/sync/*` peer routes resolve (server ≥7.52
6532
+ predicate: durable backend + entry-export/import-staging seams + a file-HISTORY store wired —
6533
+ S-15 换店; ≤7.51 the last conjunct was the file-snapshot store). Gate the whole sync surface off
6534
+ this. ⚠️ The two BLOB faces (PUT …/sync/blobs/{hash}, GET …/sync/history/blobs/{hash})
6535
+ additionally need the store's blob faces (`syncBlobFaces` — the SQL/cloud backends; the local
6536
+ file backend has none): without them the PUT answers 501 `capability.snapshot_store_required`
6537
+ and the history PULL answers the no-oracle 404 `not_found.blob`, while the REST of the sync
6538
+ surface keeps working.
6258
6539
 
6259
6540
  SkillSpec:
6260
6541
  type: object
@@ -6691,6 +6972,21 @@ components:
6691
6972
  replayed: { type: boolean, description: 'set when this agent''s result was REPLAYED from a resume journal rather than freshly run.' }
6692
6973
  prompt: { type: string, description: 'what the worker was ASKED (core-redacted + bounded). UNTRUSTED display text.' }
6693
6974
  output: { type: string, description: 'the worker''s final OUTPUT (core-redacted + bounded). UNTRUSTED display text.' }
6975
+ pendingApproval:
6976
+ type: object
6977
+ description: >-
6978
+ S-43(案A)E 段(server ≥7.52,core 5.65 供给;[5454] 键形冻结)— the suspended approvals this leg
6979
+ is CURRENTLY parked on. ABSENT = none pending. Three-state on purpose: absence ≠ empty array — an
6980
+ empty array would make "this engine does not supply the seat" and "truly nothing pending" the same
6981
+ shape. Server-side zod narrow-read projection: a drifted/half-written record omits the WHOLE key
6982
+ rather than projecting "a pendingApproval with nothing decidable in it".
6983
+ required: [askIds, oldestCreatedAtMs]
6984
+ additionalProperties: false
6985
+ properties:
6986
+ askIds: { type: array, items: { type: string }, minItems: 1 }
6987
+ oldestCreatedAtMs: { type: number }
6988
+ 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.' }
6989
+ 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
6990
 
6695
6991
  WorkflowPhaseProgress:
6696
6992
  # 新(census 批2 四段):此前 `WorkflowRun.phases` 是 `items: {}`(零形状)。真形 = server
@@ -7167,12 +7463,18 @@ components:
7167
7463
  description: >
7168
7464
  FIRST frame — `{type, version, scoped, sessionScoped, bgNotifyFailClosed}` (all five UNCONDITIONAL on
7169
7465
  server >= 1.247). `scoped:true` ⇒ owner-scoped view; `false` ⇒ fleet-wide (operator/trace token).
7466
+ `session` (server 7.31+, [4119] P-31 Q2): additive echo of THE session id in effect on THIS connection
7467
+ — present iff the connection carried `?session=`. This is the connection-level zero-content
7468
+ self-attestation bit client-core's P-31 residual family asks for ([5427]): a keyed host feeds a stream
7469
+ into a ledger, compares this echo against the ledger's session anchor, and can trust
7470
+ `sessionScoped`/`bgNotifyFailClosed` without content-based inference.
7170
7471
  required: [type, version, scoped, sessionScoped, bgNotifyFailClosed]
7171
7472
  properties:
7172
7473
  type: { const: meta }
7173
7474
  version: { type: integer }
7174
7475
  scoped: { type: boolean }
7175
7476
  sessionScoped: { type: boolean, description: 'true ⇒ this connection carried `?session=` (rows and notifications are filtered by the host session).' }
7477
+ 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
7478
  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
7479
  FleetFrame_snapshot:
7178
7480
  type: object
@@ -7351,12 +7653,55 @@ components:
7351
7653
  description: Envelope for pending HITL checkpoints (key `pending`, NOT a bare array — the pinned wire contract).
7352
7654
  # 封闭(census 轴一,2026-07-30):三个铸造点(routes/approvals-assistant.ts)都只发
7353
7655
  # `{ pending }`。行本身(`PendingCheckpoint`)保持开集(TS 侧索引签名)。
7656
+ # #315(server ≥7.43):第二顶层键 `livePending` = streamApproval 窗内的流内 ask(此前对轮询消费端
7657
+ # 结构性不可见)。ADDITIVE / tolerate-absent:键缺席=「协调器未装配(本部署无 live 审批面)」或
7658
+ # 旧 server;空数组=「有面、此刻无待批」——两义可判别,消费端测 presence。**分数组不混编**:
7659
+ # live 行的决议口=POST /v1/tool-approvals/{approvalId}/respond,durable 行=decide 口;数组名即路由判据。
7354
7660
  additionalProperties: false
7355
7661
  required: [pending]
7356
7662
  properties:
7357
7663
  pending:
7358
7664
  type: array
7359
7665
  items: { $ref: '#/components/schemas/PendingCheckpoint' }
7666
+ livePending:
7667
+ type: array
7668
+ items: { $ref: '#/components/schemas/LivePendingRow' }
7669
+
7670
+ LivePendingRow:
7671
+ type: object
7672
+ description: >-
7673
+ A live (in-stream) pending ask inside its streamApproval window (#315, server ≥7.43). Decide it via
7674
+ POST /v1/tool-approvals/{approvalId}/respond — NOT the /decide route (that is the durable rows' door).
7675
+ Deliberately narrow: tool input/args are NOT listed (no second redaction surface — details ride the
7676
+ SSE tool_approval frame). Optional flags are only-if-true (absent is NEVER encoded as false).
7677
+ additionalProperties: false
7678
+ required: [approvalId, toolName, ts, expiresAtMs]
7679
+ properties:
7680
+ approvalId: { type: string, description: 'The SAME id the respond endpoint takes (wire uuidv7).' }
7681
+ toolName: { type: string }
7682
+ ts: { type: number, description: 'Registration time (ms epoch).' }
7683
+ expiresAtMs: { type: number, description: 'Absolute window deadline — the SAME number minted once for the tool_approval frame (never recomputed).' }
7684
+ sessionId: { type: string }
7685
+ requiresRealApproval: { type: boolean, enum: [true] }
7686
+ governanceForced: { type: boolean, enum: [true] }
7687
+ fromSubagent: { type: boolean, enum: [true], description: 'The ask originates from a delegated subagent.' }
7688
+ 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 (三键恒等亲证在案).' }
7689
+
7690
+ ApprovalStaleCurrentPending:
7691
+ type: object
7692
+ description: >-
7693
+ S-02 (server ≥7.52) — the ADDITIVE pointer key `currentPending` on the decide 409 `approval_stale`
7694
+ body: the session's CURRENT pending's three D-1 coordinates (same names/shapes as the §7b queue row),
7695
+ so a shell relocates and re-decides in ONE hop instead of refetching the whole queue. 🔴 NEVER includes
7696
+ a checkpointToken (resume credentials do not leave the server). Whole-or-absent: the server mints it
7697
+ only from a pending, owner-checked tool-approval row (no pending / non-tool gate / row read failure —
7698
+ a loud fail-open ledger arm — all omit the key).
7699
+ required: [toolName, boundCallId]
7700
+ additionalProperties: false
7701
+ properties:
7702
+ toolName: { type: string, description: 'The current pending''s gated tool (same source/value as the pending row''s toolName).' }
7703
+ boundCallId: { type: string, description: 'The current pending''s pendingAction.toolCallId — the D-1 anchor to echo back on the relocated decide.' }
7704
+ boundInputHash: { type: string, description: 'Server-minted; present only when the row has one. Echo verbatim, never recompute.' }
7360
7705
 
7361
7706
  ExemptionList:
7362
7707
  type: object
@@ -7374,14 +7719,25 @@ components:
7374
7719
  description: >
7375
7720
  The 200 body of `POST /v1/approvals/{sessionId}/decide` (census 批2 第二段, 2026-07-30 — this op's 200 was
7376
7721
  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,
7722
+ THREE real shapes share this envelope (#316, server >= 7.37 — [4660] Inkglow: the task-level leg used to
7723
+ drive the ENTIRE resume synchronously before responding, so its 200 took a full model round-trip with no
7724
+ upper bound; it now answers at the ACCEPTANCE point instead):
7725
+ · the TASK-LEVEL ACCEPTANCE shape (server >= 7.37, the DEFAULT on durable deployments;
7726
+ `http/server.ts` driveResumeIntoRunLog acceptance point) — `{taskId, sessionId, status:"resuming",
7727
+ bindingEnforced:true}`. Emitted AFTER every rejection-capable step has settled (lease 429, markResuming
7728
+ CAS 409, core pre-CAS guards + atomic CAS — those still reject synchronously); only the model leg runs
7729
+ on after the response. Follow the run via poll/SSE (`GET /v1/runs/:id` / `/events`) — `errorCode` /
7730
+ `errorMessage` / `retriable` NEVER ride this shape, failures land on the run row and its stream.
7731
+ · the RESUMED-TASK TERMINAL shape (pre-7.37 servers on every decide; >= 7.37 ONLY on deployments with no
7732
+ durable run row to follow — there the leg honestly stays synchronous) — `{taskId?, sessionId, status,
7379
7733
  errorCode?, errorMessage?, retriable?}`, plus `rememberApplied` when the request carried
7380
7734
  `remember:"session"` AND the worker has an exemption store;
7381
7735
  · the PARKED-BACKGROUND-AGENT shape (`parked-decide.ts`, server >=1.267) — `{taskId, status:"resuming",
7382
7736
  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.
7737
+ `status` is the only key ALL shapes guarantee. ⚠️ `status === "resuming"` alone no longer discriminates
7738
+ the parked shape: branch on `decision` (parked receipt only) vs `sessionId`/`bindingEnforced` (task-level
7739
+ acceptance only). `taskId` rides `getActiveTaskId` on the terminal leg (omitted when there is no active
7740
+ row) and `sessionId` is absent from the parked shape.
7385
7741
  required: [status]
7386
7742
  additionalProperties: false
7387
7743
  properties:
@@ -7397,8 +7753,9 @@ components:
7397
7753
  status:
7398
7754
  type: string
7399
7755
  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).
7756
+ OPEN string — do NOT narrow. Task-level acceptance (server >= 7.37): the literal `resuming` (= accepted,
7757
+ resume in flight — follow the run stream). Terminal leg: the run's resulting status (`completed`/`failed`/
7758
+ `suspended`/`needs_review`). Parked leg: the literal `resuming` (= accepted, revive in flight).
7402
7759
  decision:
7403
7760
  type: string
7404
7761
  enum: [approve, deny]
@@ -8257,6 +8614,7 @@ components:
8257
8614
  - $ref: '#/components/schemas/Event_status'
8258
8615
  - $ref: '#/components/schemas/Event_task_progress'
8259
8616
  - $ref: '#/components/schemas/Event_workspace_changed'
8617
+ - $ref: '#/components/schemas/Event_engine_notice'
8260
8618
  - $ref: '#/components/schemas/Event_suspended'
8261
8619
  # 🔴 以下 6 臂是 2026-07-25 补的:`events.ts` 的 `AgentEvent` 联合有 21 个 `type` 字面量,本文件此前只声明
8262
8620
  # 15 个 —— 照本文件生成客户端的消费方会把 6 种合法帧当成非法值(事件面是最吃重的 wire 面,这个缺口比
@@ -8313,6 +8671,7 @@ components:
8313
8671
  status: '#/components/schemas/Event_status'
8314
8672
  task_progress: '#/components/schemas/Event_task_progress'
8315
8673
  workspace_changed: '#/components/schemas/Event_workspace_changed'
8674
+ engine_notice: '#/components/schemas/Event_engine_notice'
8316
8675
  suspended: '#/components/schemas/Event_suspended'
8317
8676
  file_link: '#/components/schemas/Event_file_link'
8318
8677
  prompt_assembled: '#/components/schemas/Event_prompt_assembled'
@@ -8619,9 +8978,18 @@ components:
8619
8978
  required: [type, phase]
8620
8979
  properties:
8621
8980
  type: { const: status }
8622
- phase: { type: string, description: "rate_limited | retrying | reconnecting | circuit_open (open set)" }
8981
+ # 🔴 A-057.27:6 员(此前只列 4,漏 recovered/gave_up 两个**终局**值 —— 一段重试序列恒恰一终局帧,
8982
+ # 两者对渲染方是同一句话「把重试行撤下来」)。闭集声明、开集读。
8983
+ phase: { type: string, description: "rate_limited | retrying | reconnecting | circuit_open | recovered | gave_up (closed in core, read as open set)" }
8623
8984
  detail: { type: string, description: "neutral human hint (NO provider/HTTP detail)" }
8624
8985
  retryInSec: { type: number }
8986
+ # 🔴 A-057.26:以下四键 server 真在发(builder brainStatusEventData,三腿共享),而本 schema 是
8987
+ # `additionalProperties: false` ⇒ 不声明 = 一个**合法**的真帧会被严格校验器判违约(假红),正是本仓
8988
+ # 元门(封闭 schema 必须镜像分支键)要防的那一形。缺席不铸键,故一律 optional。
8989
+ attempt: { type: number, description: "1-based index of the attempt that just FAILED (this wait precedes attempt+1)" }
8990
+ maxRetries: { type: number, description: "retry budget of the lane in force (refused-connect short lane = 2)" }
8991
+ retryInMs: { type: number, description: "same number as retryInSec at ms precision (countdown rendering)" }
8992
+ errClass: { type: string, description: "connect_refused | transport | rate_limit | server | http | output_cap — retry-wait frames ONLY; never inferred from phase" }
8625
8993
  eventId: { type: string }
8626
8994
  parentToolCallId: { type: string }
8627
8995
  sourceTaskId: { type: string }
@@ -8846,6 +9214,36 @@ components:
8846
9214
  parentToolCallId: { type: string }
8847
9215
  sourceTaskId: { type: string }
8848
9216
  bgAgentId: { type: string }
9217
+ Event_engine_notice:
9218
+ type: object
9219
+ description: >
9220
+ server >= 7.36 (#310, board [4630]) — an engine STRUCTURED NOTICE judged to be for THIS SESSION'S end user,
9221
+ routed to the session's event projection by `sessionId`. Present on BOTH legs (live SSE and the durable
9222
+ ledger replayed by `GET /v1/runs/:id/events`) — reconnect replay shows it again, so consume idempotently
9223
+ (same discipline as `workspace_changed`).
9224
+ RENDER BY `code` + `detail`; `message` is a FALLBACK ONLY (core states that `memory.session_polluted`'s
9225
+ message varies with the memoryProvenance mode — matching on message text WILL break). `code` is an OPEN
9226
+ set (the server whitelist grows with core's code register): render a known code specially, fall back to
9227
+ `message` for an unknown one — never drop the frame.
9228
+ Starter whitelist (server 7.36): `memory.session_polluted` `{reason, sessionId?}` ·
9229
+ `memory.harvest_quarantined` `{count, moved, escalated, reason?}` — `moved` and `escalated` MUST NOT be
9230
+ subtracted from each other (an in-place tombstone counts as both) · `memory.delegation_static_mark_waived`
9231
+ `{reason, subagentType?, sessionId?}`.
9232
+ A notice whose `detail.sessionId` is absent is HONESTLY NOT DELIVERED (the server never guesses a session),
9233
+ so the frame's absence does NOT mean "did not happen" — the operator-facing structured log always has the
9234
+ full family. `detail` is already redacted + size-bounded by the server; still treat it as external text.
9235
+ additionalProperties: false
9236
+ required: [type, code, message, detail, sessionId, ts]
9237
+ properties:
9238
+ type: { const: engine_notice }
9239
+ code: { type: string, description: 'Stable dot-namespaced machine code. OPEN set — never drop an unknown one.' }
9240
+ message: { type: string, description: 'Core-minted human line. FALLBACK DISPLAY ONLY, never a match key.' }
9241
+ detail:
9242
+ type: object
9243
+ additionalProperties: true
9244
+ description: 'Machine-readable facts (per code). Redacted + bounded server-side; open set of keys.'
9245
+ sessionId: { type: string, description: 'Owning session — this frame only ever appears on that session stream.' }
9246
+ ts: { type: integer, format: int64, description: 'SERVER observation time (ms epoch), NOT the engine mint time.' }
8849
9247
  Event_workspace_changed:
8850
9248
  type: object
8851
9249
  description: >
@@ -9508,6 +9906,12 @@ components:
9508
9906
  fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
9509
9907
  sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
9510
9908
  sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
9909
+ # A-057.④(#314):三键 server 早已随卡投出(与 tool_approval 帧同一窄读函数 ⇒ 帧与 card 同值),
9910
+ # 但本 schema 只在帧上公示过 —— 按 spec 生成的卡消费方拿不到。定义与帧上同构段逐字同源,
9911
+ # 长 description 不复制(单一真源在 ToolApprovalFrame 的同名键)。
9912
+ 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).' }
9913
+ 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.' }
9914
+ 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
9915
  delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
9512
9916
 
9513
9917
  RuleSuggestion:
@@ -9973,6 +10377,23 @@ components:
9973
10377
  activeTaskStatus:
9974
10378
  type: string
9975
10379
  description: '`running` | `suspended` | `needs_review` — best-effort (absent when the store lookup degraded).'
10380
+ msSinceLastActivity:
10381
+ type: number
10382
+ description: >
10383
+ (#245 S1, server >= 7.18 — 既存违例回填 2026-08-20: server has been emitting this on the done frame
10384
+ since the key landed on the 409 body; the CLOSED schema here just never declared it, so every real
10385
+ frame carrying it "violated" spec. Same key, same semantics as the 409 body.) Milliseconds since the
10386
+ occupying run's last recorded activity — the ONLY liveness-evidence bit on this frame (same-replica
10387
+ best-effort). Absent = no evidence, NOT "fresh"; never narrow absence to 0.
10388
+ stale:
10389
+ const: true
10390
+ description: >
10391
+ (#316(c), server >= 7.37; [4664]② hole (c)) The occupying run row looks DEAD by the same staleness
10392
+ criterion the poll face (`GET /v1/runs/:id`) uses to fold a terminal status — heartbeat silence past
10393
+ the reap window. This frame does NOT fold the status (`activeTaskStatus` stays verbatim, the reaper
10394
+ owns the transition); the bit lets a shell reconcile "conflict says running / poll says failed"
10395
+ inside the self-heal window. ADDITIVE, present ONLY as `true`; absent = no staleness evidence (parked
10396
+ rows are out of reap scope and never carry it — absence is NOT "alive").
9976
10397
  pendingGate:
9977
10398
  allOf: [{ $ref: '#/components/schemas/PendingGateMaterial' }]
9978
10399
  description: >
@@ -10889,6 +11310,40 @@ components:
10889
11310
  priority: { $ref: '#/components/schemas/SteerPriority' }
10890
11311
  note: { type: string, description: 'Human-readable note on the two parked legs; absent on `applied`.' }
10891
11312
 
11313
+ InterruptReceipt:
11314
+ description: >
11315
+ The POST /v1/runs/{taskId}/interrupt 202 receipt — TWO mutually exclusive shapes (oneOf, discriminated
11316
+ by which arm's keys are present, NOT by status code; both are 202 — codex 复审 R1-[medium]:单对象全
11317
+ 可选形会让 `{taskId,status}` 与混臂形都合法,生成式消费方靠不住判别键). TEXT form (cut + steer):
11318
+ `delivery:"queued-immediate"` + `messageId` (≡ the engine inputId — correlate with the run stream's
11319
+ `task.turn_interrupted` `detail.inputId`; acceptance = enqueued, the cut itself is best-effort).
11320
+ BARE (halt) form: `turnCut` and deliberately NO delivery/messageId (no input frame exists to correlate
11321
+ — A-075.78 成文;halt's acceptance is NOT a queue insert, so the receipt can honestly answer whether
11322
+ THIS call cut a live turn). `status` = the run row's word as the server read it at acceptance. Honest
11323
+ window: a halt racing the run's natural last moments reports the natural completion, still carrying
11324
+ the haltedByUser seat.
11325
+ # 封闭两臂:两个铸造点都是逐字面量拼出的对象(server routes/runs.ts interrupt 段),不是引擎透传形。
11326
+ oneOf:
11327
+ - type: object
11328
+ description: TEXT form (cut + steer) — the run continues.
11329
+ required: [taskId, status, delivery, messageId]
11330
+ additionalProperties: false
11331
+ properties:
11332
+ taskId: { type: string }
11333
+ status: { type: string, description: 'The run status the server saw at acceptance time.' }
11334
+ delivery: { type: string, enum: [queued-immediate] }
11335
+ 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).' }
11336
+ note: { type: string, description: 'Contract note (逐句照 core 契约,不多许诺一个字).' }
11337
+ - type: object
11338
+ description: BARE (halt) form (cut + stop) — the run closes at this boundary.
11339
+ required: [taskId, status, turnCut]
11340
+ additionalProperties: false
11341
+ properties:
11342
+ taskId: { type: string }
11343
+ status: { type: string, description: 'The run status the server saw at acceptance time.' }
11344
+ turnCut: { type: boolean, description: 'true ⇔ THIS call cut a live turn; repeat calls answer false once the stop is latched (halt is naturally idempotent).' }
11345
+ note: { type: string, description: 'Contract note (逐句照 core 契约,不多许诺一个字).' }
11346
+
10892
11347
  SubagentSteerReceipt:
10893
11348
  type: object
10894
11349
  description: >
@@ -11619,7 +12074,7 @@ components:
11619
12074
  required: [rule, scope]
11620
12075
  properties:
11621
12076
  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.' }
12077
+ 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
12078
  principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
11624
12079
 
11625
12080
  RuleRevokeResult: