@sema-agent/sdk 7.3.0 → 8.0.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 (49) hide show
  1. package/README.md +99 -0
  2. package/dist/errors.d.ts +32 -6
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +35 -3
  5. package/dist/errors.js.map +1 -1
  6. package/dist/events.d.ts +31 -7
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/events.js.map +1 -1
  9. package/dist/index.d.ts +6 -4
  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 +6 -0
  14. package/dist/resources/approvals.d.ts.map +1 -1
  15. package/dist/resources/approvals.js.map +1 -1
  16. package/dist/resources/fleet.d.ts +39 -6
  17. package/dist/resources/fleet.d.ts.map +1 -1
  18. package/dist/resources/fleet.js.map +1 -1
  19. package/dist/resources/images.d.ts +5 -1
  20. package/dist/resources/images.d.ts.map +1 -1
  21. package/dist/resources/images.js +11 -7
  22. package/dist/resources/images.js.map +1 -1
  23. package/dist/resources/rules.d.ts +33 -8
  24. package/dist/resources/rules.d.ts.map +1 -1
  25. package/dist/resources/rules.js.map +1 -1
  26. package/dist/resources/runs.d.ts +6 -1
  27. package/dist/resources/runs.d.ts.map +1 -1
  28. package/dist/resources/runs.js +13 -7
  29. package/dist/resources/runs.js.map +1 -1
  30. package/dist/resources/sessions.d.ts +19 -1
  31. package/dist/resources/sessions.d.ts.map +1 -1
  32. package/dist/resources/sessions.js +22 -0
  33. package/dist/resources/sessions.js.map +1 -1
  34. package/dist/resources/tool-approvals.d.ts +22 -7
  35. package/dist/resources/tool-approvals.d.ts.map +1 -1
  36. package/dist/resources/tool-approvals.js +7 -2
  37. package/dist/resources/tool-approvals.js.map +1 -1
  38. package/dist/resources/trace.d.ts +9 -8
  39. package/dist/resources/trace.d.ts.map +1 -1
  40. package/dist/resources/trace.js +15 -14
  41. package/dist/resources/trace.js.map +1 -1
  42. package/dist/sse.d.ts +25 -2
  43. package/dist/sse.d.ts.map +1 -1
  44. package/dist/sse.js +30 -16
  45. package/dist/sse.js.map +1 -1
  46. package/dist/types.d.ts +74 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/openapi.yaml +387 -65
  49. package/package.json +1 -1
package/openapi.yaml CHANGED
@@ -1962,6 +1962,73 @@ paths:
1962
1962
  schema: { $ref: '#/components/schemas/McpStatusPanel' }
1963
1963
  '401': { $ref: '#/components/responses/Unauthorized' }
1964
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
+
1965
2032
  # ── 2c session-sync (P1d) — the cloud as a SYNC PEER ──────────────────────────────────────────────────────────
1966
2033
  # The local shell's local backend ⇄ this cloud service. PULL a session's whole state out, or PUSH the local
1967
2034
  # peer's in. Gate the whole surface off `capabilities.sessionSync` — a FOUR-way conjunction (durable backend
@@ -2466,7 +2533,11 @@ paths:
2466
2533
  member since core 5.7.0/RB-459: the content-ask `answer` payload was refused) — your binding does
2467
2534
  NOT match the current pending action (TOCTOU/swap). The SDK splits these by `errorCode` into
2468
2535
  ConflictError vs ApprovalBindingMismatchError. binding_mismatch = SAFETY signal: refetch + re-present
2469
- to the human, NEVER auto-retry.
2536
+ to the human, NEVER auto-retry. (S-57, server >= 7.54.0: the `field:"boundCallId"` shape is
2537
+ intercepted in steady state by the `approval_stale` second trigger source below — the server
2538
+ compares the echoed boundCallId against the current pending pre-CAS and answers 409
2539
+ `approval_stale` + `currentPending`; this code's boundCallId arm covers only the remaining race
2540
+ window between that compare and the core CAS. The hash/answer arms are unchanged.)
2470
2541
  FOURTH member ([2400] CB-2, declared 2026-08-02): `gate_not_tool_approval` — the suspension this
2471
2542
  sessionId is parked on is NOT a tool-approval gate (`src/http/server.ts` rejects pre-CAS when the
2472
2543
  gate kind is neither `human` nor `irreversible_ask`: nothing is consumed, no model leg burns). It is
@@ -2474,6 +2545,14 @@ paths:
2474
2545
  `gate_not_plan_review` (§4c) → SDK `GateNotToolApprovalError`. Client action: route by the row's
2475
2546
  `gateKind` — `plan_review` → `POST /v1/assistant/tasks/{taskId}/plan_review`, `resource_limit` →
2476
2547
  `POST /v1/assistant/tasks/{taskId}/resume`. Retrying the same body never succeeds.
2548
+ SIXTH group (A-075.10, TIME-BASED retry-later pair): `resume.usage_window_exhausted` (#449 G1,
2549
+ core 5.60.1 — the deployment governance window on this run's ledger key is full; NOTHING was
2550
+ consumed or unpinned, the SAME token with the SAME decision redeems once the window slides) and
2551
+ `resume.preflight_rejected` (#376, core 5.65 retry-later form). Both are PRE-CAS (the checkpoint
2552
+ stays pending) and their body ADDITIVELY carries `retryAfterSec` (seconds, ceil, min 1; the
2553
+ server mints the key ONLY on these two codes with a finite positive wait) → SDK
2554
+ `ResumeRetryLaterError` (a `SubagentResumeConflictError` subclass so existing family catches
2555
+ keep matching; `.retryAfterSec` is the actionable wait).
2477
2556
  FIFTH group ([2400] CB-2): the core `checkpoint.*` family the server mirrors verbatim
2478
2557
  (`src/http/server.ts`) — `checkpoint.gate_mismatch` / `checkpoint.reopen_failed` /
2479
2558
  `checkpoint.reopen_revote` / `checkpoint.already_exists` / `checkpoint.resume_aborted` /
@@ -2493,11 +2572,15 @@ paths:
2493
2572
  checkpointToken. Absent when there is no pending / the gate is not a tool approval / the row read
2494
2573
  failed (loud fail-open ledger arm) / an older server. Owner domain = the route's owner gate (no
2495
2574
  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.
2575
+ REACHABILITY (REWRITTEN for S-57, server >= 7.54.0): the stale arm now has TWO trigger sources —
2576
+ (1) a caller echoing a legacy `checkpointToken` (this SDK's ApprovalDecision deliberately has no
2577
+ such field, removed in 1.0.0 — resume credentials never ride the compliant decide), and
2578
+ (2) an echoed `boundCallId` that does not match the session's CURRENT pending row (the COMPLIANT
2579
+ SDK shape — the server compares pre-CAS and answers this 409 + `currentPending` directly; this
2580
+ form never carries `terminal`). So a decide issued through this SDK DOES reach this 409 under
2581
+ coordinate rotation (before S-57 that shape fell through to `approval_binding_mismatch` +
2582
+ `field:"boundCallId"` with no pointer key; that arm now covers only the race window between the
2583
+ server-side compare and the core CAS).
2501
2584
  ALSO an S-02 behavior NARROWING on this arm: an echoed `checkpointToken` that does NOT belong to
2502
2585
  this session, when the session has NO pending, now folds into the byte-identical 404
2503
2586
  `not_found.approval` (it used to 409 — a single-bit cross-tenant "who is parked on an approval"
@@ -5697,15 +5780,42 @@ components:
5697
5780
  Run state (`GET /v1/runs/:id`) — live shape per the pinned wire contract Drift 2: `result` is the NESTED TaskResult
5698
5781
  object (output text in result.result, stats in result.stats; NO top-level stats), and the error text
5699
5782
  field is `error` (not errorMessage; errorCode is floated up from result.errorCode).
5700
- # 封闭(census 轴一,2026-07-30):铸造点 routes/runs.ts 是一个逐键写死的字面量,恰好 10 键
5701
- # (taskId/sessionId/status/result/supervisorCost/suggestions/errorCode/error/jobId/source),与本
5702
- # properties 一一对上;只有前三键无条件在场(其余走 `?? undefined` ⇒ JSON 丢键)⇒ required 维持 3 键。
5783
+ # 封闭(census 轴一,2026-07-30;A-075.21 回填后 12 键):铸造点 routes/runs.ts 是一个逐键写死的
5784
+ # 字面量(taskId/sessionId/status/msSinceLastActivity/crossSliceUsage/result/supervisorCost/
5785
+ # suggestions/errorCode/error/jobId/source),与本 properties 一一对上;只有 taskId/sessionId/status
5786
+ # 无条件在场(其余走 `?? undefined` / 条件 spread ⇒ JSON 丢键)⇒ required 维持 3 键。
5787
+ # (A-075.21:此前的「恰好 10 键」对 msSinceLastActivity(#245 S1)/crossSliceUsage([3833] S-4)
5788
+ # 两条铸键路径结构性失明 —— server test/producer-contract.test.ts 的 S-4 登记钉逐字记着这笔账。)
5703
5789
  additionalProperties: false
5704
5790
  required: [taskId, sessionId, status]
5705
5791
  properties:
5706
5792
  taskId: { type: string }
5707
5793
  sessionId: { type: string }
5708
5794
  status: { $ref: '#/components/schemas/RunStatus' }
5795
+ msSinceLastActivity:
5796
+ type: integer
5797
+ description: >
5798
+ #245 S1 (server >= 7.18) — ms since the run's last activity (poll-face liveness evidence; same key
5799
+ and semantics as the 409 conflict body). SAME-REPLICA BEST-EFFORT: absent when the poll lands on a
5800
+ replica that never ran this run. Absence = cannot prove, NOT 0/fresh.
5801
+ crossSliceUsage:
5802
+ type: object
5803
+ description: >
5804
+ [3833] S-4 (server >= 7.48.0) — cross-slice governance-window usage, present only on a running
5805
+ (non-stale) row polled on the replica that registered the window. Reading discipline: values are
5806
+ LOWER BOUNDS (ledgered at turn boundaries, delegation subtree excluded); the three axes come in
5807
+ PAIRS (an unconfigured cap omits its pair; spentMicroUsd may also be absent when unpriced — 0
5808
+ would lie); WHOLE-KEY ABSENCE does NOT mean "nothing spent" (no cap / not running / wrong replica
5809
+ / ledger unavailable are indistinguishable); on resumed legs the totals stay frozen on the first
5810
+ slice's ledger, never overwritten by current env.
5811
+ additionalProperties: false
5812
+ properties:
5813
+ totalTokens: { type: integer }
5814
+ spentTokens: { type: integer }
5815
+ totalBudgetMicroUsd: { type: integer, description: 'Integer micro-USD (display divides by 1e6).' }
5816
+ spentMicroUsd: { type: integer, description: 'Integer micro-USD; absent on unpriced deployments (unknown, not 0).' }
5817
+ maxSlices: { type: integer }
5818
+ sliceCount: { type: integer }
5709
5819
  result: { $ref: '#/components/schemas/TaskResult' }
5710
5820
  errorCode:
5711
5821
  type: string
@@ -6358,6 +6468,20 @@ components:
6358
6468
  `resumeAt`); absent ⇒ an older server, send the old spelling. `rewindFiles` keeps its name (same
6359
6469
  affordance — "rewind can move files"); this bit is what tells the two epochs apart.
6360
6470
  manualCompact: { type: boolean, description: "`POST /v1/runs/:id/compact` (needs a run store)." }
6471
+ configCatalog:
6472
+ type: boolean
6473
+ description: >-
6474
+ S-32 (server >=7.53, `routes/capabilities.ts:731`): the `GET /v1/config/catalog` operator lane
6475
+ (DESIGN-278 §5 S2 — deployment config self-description) is mounted on this DEPLOYMENT. TWO-term
6476
+ conjunction, written against the route's own refusal arms: ① the `CONFIG_CATALOG_ENABLED` knob is
6477
+ on (default on; a compliance deployment that turns it off makes the path a 404 — the valve-family
6478
+ precedent) AND ② the operator principal list is NON-EMPTY (with an empty list the route 403s every
6479
+ identity). 🔴 DEPLOYMENT-LEVEL AVAILABILITY, NOT THE CALLER'S AUTHORIZATION: the caller's own
6480
+ identity is NOT in the predicate — the route additionally requires the CURRENT principal to be an
6481
+ explicit operator (`routes/config-catalog.ts` `explicitOperatorOk`, else 403 `auth.operator_only`),
6482
+ so a non-operator reads `true` and still gets 403. Show/invoke the catalog only for an identity you
6483
+ have ALREADY established as an operator; the bit tells you whether that operator's control exists
6484
+ here at all. `false` does not say WHICH term failed. ABSENT = an older server (<7.53), not "off".
6361
6485
  sessions: { type: boolean, description: "Session read face (`GET /v1/sessions/:id`)." }
6362
6486
  sessionList: { type: boolean, description: "`GET /v1/sessions` list/picker face." }
6363
6487
  sessionSearch: { type: boolean, description: "Server-side session search." }
@@ -7190,19 +7314,25 @@ components:
7190
7314
  note: { type: string }
7191
7315
  SessionWakeResult:
7192
7316
  # 4.3.0(SM-9):wake 200 形提成具名 schema(形与下面 /wake 路径的 200 逐字同源)。
7317
+ # A-075.1(车AE,2026-09-01):#354 只修了 inline 路径 200,这只具名形停在旧二键 required、封闭形下
7318
+ # 缺 `bindingEnforced` ⇒「逐字同源」自称为假、严格生成客户端拒收真响应。现与 inline 重新逐字对齐
7319
+ # (spec.test.ts 的 A-075.1 组机器看守同源等式)。
7193
7320
  type: object
7194
7321
  description: >
7195
7322
  POST /v1/sessions/{sessionId}/wake 的 200 形。⚠️ 200 也可能带 `errorCode`(core 侧 `wake.*` 拒了) ——
7196
- 按机器码分支,别只看 HTTP 状态码。
7323
+ 按机器码分支,别只看 HTTP 状态码。ACCEPTANCE form (durable row present, [3833] S-1): status
7324
+ "resuming", the leg keeps running server-side. SYNCHRONOUS form (no durable run row): status is the
7325
+ run's resulting terminal enum. `bindingEnforced` is always true on BOTH forms (both mint points).
7197
7326
  additionalProperties: false
7198
- required: [sessionId, status]
7327
+ required: [sessionId, status, bindingEnforced]
7199
7328
  properties:
7200
- sessionId: { type: string }
7201
- status: { type: string, description: 'The run''s resulting status (core TaskStatus verbatim).' }
7202
7329
  taskId: { type: string, description: 'The active run id; OMITTED when no active row.' }
7203
- errorCode: { type: string, description: 'Failure code when the resumed leg failed / was refused.' }
7204
- errorMessage: { type: string, description: 'Human-readable failure detail (server-side secret-redacted).' }
7205
- retriable: { type: boolean, description: 'true = the park is STILL pending (re-parked); refetch and retry.' }
7330
+ sessionId: { type: string }
7331
+ 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).' }
7332
+ bindingEnforced: { type: boolean, description: '[2400] HITL-12 generation probe — this server enforces decision-action binding. Always true (present on BOTH 200 forms).' }
7333
+ errorCode: { type: string, description: 'Failure code when the resumed leg failed (e.g. `cancelled`). Synchronous form only — never on the acceptance form.' }
7334
+ errorMessage: { type: string, description: 'Human-readable failure detail (server-side secret-redacted). Synchronous form only.' }
7335
+ retriable: { type: boolean, description: 'true = the park is STILL pending (re-parked); refetch and retry. Synchronous form only.' }
7206
7336
  WorkflowJournalRow:
7207
7337
  # 🔴 census 批2 四段:CLOSED against server's real projected/truncated row shapes — a row is EITHER the
7208
7338
  # projected result OR an honest truncated stub(超 64KiB 的行从不进程内存,投影为 truncated stub)。
@@ -7596,27 +7726,29 @@ components:
7596
7726
  parentToolCallId: { type: string, description: 'The parent tool_use id VERBATIM (E2 domain, engine-minted) — [1832] P1-2, server >= 1.290 / engine 1.432; the terminal notification frame carries it too. Additive / tolerate-absent.' }
7597
7727
  ts: { type: integer }
7598
7728
  FleetFrame_hook_notice:
7599
- type: object
7600
7729
  description: >
7601
- server 1.280.0 (cli [1786]) — a hook adjudication FAILED TO COMPLETE this cycle (observability frame,
7602
- pure observe: it does not block, resume, or inject anything). Semantics are "could not evaluate", NOT
7603
- "evaluated to fail" — render as "this cycle went unguarded, allowed through", never "the guard is broken".
7604
- Wire is FLAT (`send("hook_notice", { type, ...wire, ts })`, http/server.ts SSE outlet — the server-internal
7605
- `{notice}` nesting never reaches the wire). Visibility is fail-CLOSED like bg_notification: the owner keys
7606
- ownerScope/ownerSessionId gate delivery and are STRIPPED before send.
7607
- required: [type, kind, event, reason, ts]
7730
+ Hook observability frame, TWO kinds (both pure observe — never block, resume, or inject anything;
7731
+ visibility fail-CLOSED like bg_notification: the owner keys ownerScope/ownerSessionId gate delivery
7732
+ and are STRIPPED before send; wire is FLAT — `send("hook_notice", { type, ...wire, ts })`, the
7733
+ server-internal `{notice}` nesting never reaches it). codex R1-F4: modeled as the discriminated union
7734
+ it really is — the common `{type, ts}` frame base composed with each kind's payload arm — so a
7735
+ generated client gets the SAME per-kind narrowing as the TS type (a second-family frame missing
7736
+ hookName/entryType is invalid, not silently accepted). `kind` is CLOSED two-member on the server but
7737
+ OPEN ON READ: degrade an unknown future kind to a generic observability row.
7738
+ oneOf:
7739
+ - allOf:
7740
+ - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
7741
+ - $ref: '#/components/schemas/FleetHookDecisionUnavailable'
7742
+ - allOf:
7743
+ - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
7744
+ - $ref: '#/components/schemas/FleetHookNonBlockingFailure'
7745
+
7746
+ FleetHookNoticeFrameBase:
7747
+ type: object
7748
+ description: 'The hook_notice frame envelope shared by both kinds: the SSE frame discriminant and the server timestamp.'
7749
+ required: [type, ts]
7608
7750
  properties:
7609
7751
  type: { const: hook_notice }
7610
- kind:
7611
- type: string
7612
- enum: [hook_decision_unavailable]
7613
- description: 'Only one kind today; an enum slot so the next observability frame kind needs no new frame arm.'
7614
- event: { type: string, description: 'Which hook event (Stop / PreToolUse / …) — defined by "a hook adjudication did not complete", not any single feature.' }
7615
- reason:
7616
- type: string
7617
- enum: [no_content, unparsed, skipped]
7618
- description: 'Machine-branchable cause (same taxonomy as the server''s structured log lines). CLOSED on the server side but OPEN ON READ — an unknown future value must not crash a consumer.'
7619
- detail: { type: string, description: 'Redacted supplement (e.g. why skipped). Additive / tolerate-absent.' }
7620
7752
  ts: { type: integer }
7621
7753
 
7622
7754
  TraceStreamEvent:
@@ -7902,6 +8034,22 @@ components:
7902
8034
  presence may differ between the two legs for one ask. Derived from hot-reloadable governance knobs —
7903
8035
  long subscribers follow the stream's re-emitted frames. Absence ≠ "not governance-forced"; it means
7904
8036
  "no governance-origin evidence". Never written as false.
8037
+ hasBidiControls:
8038
+ type: boolean
8039
+ enum: [true]
8040
+ description: >
8041
+ S-30① (server >=7.53; core 5.60.0 #438), ADDITIVE, present ONLY when true: the parked action's
8042
+ execution payload contains at least one DIRECTIONAL formatting control (Trojan Source — what a
8043
+ human reads may not be the byte order that executes). Value is ENTIRELY core-minted (the
8044
+ `PendingAction.hasBidiControls` bit at park time; the SQL store denormalises it into the row's
8045
+ `has_bidi_controls` column and reads `=== 1`, the local file store reads the parked
8046
+ `pendingAction` bit `=== true` — neither recomputes). Rides BOTH durable
8047
+ read faces through the same projection (this row on `GET /v1/approvals` and the `pending` frame of
8048
+ `/v1/approvals/stream`). Never null, never false: ABSENCE = "not detected" (clean / core's bounded
8049
+ scan did not reach it / a row parked before the column existed) and MUST NOT be read as "confirmed
8050
+ clean". The live-card leg's sibling key is `inputHasBidi` (E-14, computed by the server over the
8051
+ frame's own serialised args) — a different name on purpose. Display/triage only; never part of
8052
+ resume / gate / CAS.
7905
8053
 
7906
8054
  # ── assistant-scheduler wire (ASSISTANT-WIRE-CONTRACT.md, service main 6cd0164 / core 1.110.0, LIVE) ──
7907
8055
  CheckpointSummary:
@@ -7966,6 +8114,17 @@ components:
7966
8114
  severity: { type: integer, minimum: 1, maximum: 5, description: 'Sort key; only human/irreversible_ask carry it.' }
7967
8115
  spentMicroUsd: { type: number, description: 'Accumulated spend on the suspend chain (micro-USD).' }
7968
8116
  deadline: { type: number, description: 'Awaiting-human SLA deadline (epoch ms).' }
8117
+ hasBidiControls:
8118
+ type: boolean
8119
+ enum: [true]
8120
+ description: >-
8121
+ server >=7.53 (S-30① / core 5.60.0 #438), ADDITIVE, present ONLY when true: the parked action's
8122
+ payload carries a DIRECTIONAL formatting control (Trojan Source). Third read face of the same
8123
+ core-minted bit (`routes/approvals-assistant.ts` inbox whitelist arm `s.hasBidiControls === true`;
8124
+ `summarizeCheckpoint` back-fills it for pre-bit rows on THIS face, so the absent set differs from
8125
+ the /v1/approvals row's — the reading of absence does not). Absence = "not detected", never
8126
+ "confirmed clean". Declared here because this schema is CLOSED — without the key a strict validator
8127
+ rejects every >=7.53 inbox row that carries it.
7969
8128
  objective: { type: ['string', 'null'], description: 'task objective, null if unavailable.' }
7970
8129
  input:
7971
8130
  description: >
@@ -8334,6 +8493,53 @@ components:
8334
8493
  items: { $ref: '#/components/schemas/McpServerStatus' }
8335
8494
  degraded: { type: boolean }
8336
8495
 
8496
+ SessionMemoryStatus:
8497
+ type: object
8498
+ description: >
8499
+ core 7.0.2 `SessionMemoryStatus` (design/383 §S-7, #511 item 1; `dist/core/memory-engine/engine.d.ts`) —
8500
+ SAME NAME, SAME SHAPE as core's export. Every key is optional, and each key's absence has ITS OWN
8501
+ meaning (below, read off `engine.js sessionMemoryStatus`): two of them (`optOutSource`, `lastCaptureAt`)
8502
+ are legitimately absent on a healthy session, the other three are absent only when their source could
8503
+ not be read. A fault never coins a `false`/`0` stand-in.
8504
+ # 封闭:core 的接口恰是这五键;server 逐键条件拷贝(禁 spread)⇒ 闭集本身。
8505
+ additionalProperties: false
8506
+ properties:
8507
+ captureOptedOut:
8508
+ type: boolean
8509
+ description: 'TRUE = a standing capture opt-out record; FALSE = store readable, no record (capture on). ABSENT = the record store faulted — indeterminate (`optOutSource: "fault"` accompanies).'
8510
+ committedCount:
8511
+ type: integer
8512
+ description: 'Committed entries carrying this session''s lineage contribution. Absent = ledger unreadable.'
8513
+ foldedCount:
8514
+ type: integer
8515
+ 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.'
8516
+ optOutSource:
8517
+ type: string
8518
+ enum: [record, fault]
8519
+ 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.'
8520
+ lastCaptureAt:
8521
+ type: integer
8522
+ 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.'
8523
+
8524
+ SessionMemoryStatusResponse:
8525
+ type: object
8526
+ description: >
8527
+ `GET /v1/sessions/{sessionId}/memory-status` 200 body (server >=7.53, S-53 seam①): `sessionId` + the
8528
+ five `SessionMemoryStatus` keys copied ONE BY ONE when present (the server forbids spreading and forbids
8529
+ defaults — the five conditional copies ARE the closed set). Per-key absence meaning = exactly
8530
+ `SessionMemoryStatus` (a healthy zero-history session omits `optOutSource` and `lastCaptureAt`). Keys
8531
+ are mirrored locally rather than via allOf so the closed schema stays self-describing (spec rule:
8532
+ closed + allOf must mirror every key).
8533
+ additionalProperties: false
8534
+ required: [sessionId]
8535
+ properties:
8536
+ sessionId: { type: string, description: 'The session the status is about (echo of the path segment, percent-decoded).' }
8537
+ captureOptedOut: { type: boolean, description: 'See SessionMemoryStatus.captureOptedOut — absent = indeterminate, never a coined false.' }
8538
+ committedCount: { type: integer, description: 'See SessionMemoryStatus.committedCount.' }
8539
+ foldedCount: { type: integer, description: 'See SessionMemoryStatus.foldedCount.' }
8540
+ optOutSource: { type: string, enum: [record, fault], description: 'See SessionMemoryStatus.optOutSource — absent = no opt-out record (healthy), never a fault by itself.' }
8541
+ 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.' }
8542
+
8337
8543
  # ── 2c session-sync (P1d) — the cloud-as-a-SYNC-PEER schemas ──────────────────────────────────────────────
8338
8544
  SyncEntry:
8339
8545
  type: object
@@ -8651,10 +8857,12 @@ components:
8651
8857
  # had no arm to switch on and dropped approval cards. Each arm is `allOf: [<the frame schema>, the
8652
8858
  # narrowed discriminant]` — the key set stays owned by ToolApprovalFrame / ApprovalRequestFrame,
8653
8859
  # so a frame gaining a key (7.5.0's governanceForced) needs no edit here.
8654
- # `approval_revoke` is deliberately ABSENT: it is live-only and never appended.
8860
+ # `approval_revoke` joined as the FOURTH arm ([5924]/S-58, server >= 7.55.0): the bg/resume durable
8861
+ # legs append it to the run's ledger now (the sync leg stays live-SSE-only), so it replays here too.
8655
8862
  - $ref: '#/components/schemas/Event_tool_approval'
8656
8863
  - $ref: '#/components/schemas/Event_tool_approval_complete'
8657
8864
  - $ref: '#/components/schemas/Event_approval_request'
8865
+ - $ref: '#/components/schemas/Event_approval_revoke'
8658
8866
  discriminator:
8659
8867
  propertyName: type
8660
8868
  mapping:
@@ -8697,6 +8905,7 @@ components:
8697
8905
  tool_approval: '#/components/schemas/Event_tool_approval'
8698
8906
  tool_approval_complete: '#/components/schemas/Event_tool_approval_complete'
8699
8907
  approval_request: '#/components/schemas/Event_approval_request'
8908
+ approval_revoke: '#/components/schemas/Event_approval_revoke'
8700
8909
 
8701
8910
  # design/99 §E2 (SHIPPED, core TaskEventIdentity) — additive per-event identity on every CONTENT event (NOT on
8702
8911
  # the lifecycle arms turn_end/compacted/suspended/done/failed). eventId = uuidv7 dedup/resume handle (the durable
@@ -9017,6 +9226,16 @@ components:
9017
9226
  properties:
9018
9227
  type: { const: task_progress }
9019
9228
  taskId: { type: string, description: 'The subagent task id (the delegated sub-run).' }
9229
+ seq:
9230
+ type: integer
9231
+ description: >-
9232
+ core 5.36.0 (#258), server-forwarded: the GENERATION number of the child's `a*` registry row
9233
+ (fresh spawn = 1, +1 per revival; same axis as `TaskNotificationPayload.seq` and the fleet
9234
+ `BackgroundChildEvent.seq`). Lets a consumer tell a late first frame of a known generation (same
9235
+ value) from an un-folded revival (greater). ABSENT = the run has no `a*` row (sync delegated child /
9236
+ workflow `wa*` / top-level) — no generation concept exists, do NOT read absence as cycle 1.
9237
+ core-minted number, no user content. (codex R2 of the SDK 7.3.1 batch: this key was really on the
9238
+ wire and this closed arm omitted it — a strict validator rejected every generation-tagged tick.)
9020
9239
  taskType:
9021
9240
  type: string
9022
9241
  description: >
@@ -9034,6 +9253,15 @@ components:
9034
9253
  durationMs: { type: integer }
9035
9254
  status: { type: string, description: 'running 为主;core [1414]#3 的 settle 终态 tick 亦可为 completed/failed(开集,按未知值兜底渲染)。' }
9036
9255
  name: { type: string, description: '子代的人类展示名(UNTRUSTED,已 redactSecrets)。缺席 ⇒ 无名,别回退到 objective。' }
9256
+ model:
9257
+ type: string
9258
+ description: >-
9259
+ server >=7.53 (core 7.0.1 #508①, P-42): the resolved catalog id of the model this child leg was
9260
+ PREPARED with — same value/meaning as `TaskResult.model` (a mid-run degrade does NOT rewrite it; the
9261
+ switch is disclosed on `TaskResult.degraded`). Minted unconditionally by both core task_progress
9262
+ sites (running tick + settle terminal tick) and forwarded verbatim by the server whitelist on all
9263
+ three live legs + the durable ledger row. ABSENT = an older producer (core <7.0.1 / server <7.53)
9264
+ or unstated — never read absence as "no model". core-minted, no user content.
9037
9265
  parentTaskId: { type: string, description: '嵌套子代的上级 taskId;缺席 ⇒ 一级子代。' }
9038
9266
  currentAction: { type: string, description: '子代最近一次 tool_start 的一行人话("Bash npm test");UNTRUSTED,已 redactSecrets。' }
9039
9267
  workflowRunId: { type: string, description: 'core 1.401:后台 workflow 子代的不透明 run id(下游据此跳过与 fleet 行的重复渲染)。' }
@@ -10021,9 +10249,14 @@ components:
10021
10249
  clears that batch's local cards (whitelist-pick by `askIds`) and waits for the new askIds a resume mints.
10022
10250
 
10023
10251
  Siblings already DECIDED are NOT in `askIds` (an accepted decision is not affected by revocation).
10024
- Revoke frames CAN BE LOST (frames produced by the reconciler / orphan legs have no seq to allocate and
10025
- ride the live plane only) — the structural compensation is the open-stream preamble baseline (see
10026
- ApprovalRequestFrame), not a redelivery of this frame.
10252
+ Emission is PER CONTEXT (S-58, server >= 7.55.0 — the old "reconciler/orphan legs have no seq, live
10253
+ plane only" wording pinned a mint-site constraint on the delivery plane and is retired): the sync leg
10254
+ stays live-SSE-only, the bg/resume durable legs append the frame to the run's own ledger and
10255
+ GET /v1/runs/:id/events replays it (see Event_approval_revoke). Revoke frames CAN STILL BE LOST on any
10256
+ leg (shell offline, connection drop, emit-target churn, ledger write failure — the frame is a
10257
+ notification, the row is the source of truth) — the structural compensation is the open-stream preamble
10258
+ baseline (see ApprovalRequestFrame), not a redelivery of this frame; a ledger-replayed revoke frame is
10259
+ for timeline rendering only.
10027
10260
  required: [type, schemaVersion, batchId, askIds, reason, serverNowMs]
10028
10261
  additionalProperties: true
10029
10262
  properties:
@@ -10038,6 +10271,15 @@ components:
10038
10271
  `aborted` = the run was cancelled. Read as OPEN: an unknown reason is handled as a generic revoke
10039
10272
  (clear the cards), never specially.
10040
10273
  serverNowMs: { type: integer }
10274
+ taskId:
10275
+ type: string
10276
+ description: >
10277
+ S-58 (server >= 7.55.0, always sent there; ABSENT on <= 7.54.0 — the frame family was live-only
10278
+ then, hence not in `required`): the ORIGIN run''s wire id (= the `taskId` of the same ask''s card
10279
+ frame; server buildRevokeFrame''s originTaskId). The dispatch set is session-shaped ((owner,
10280
+ sessionId) closure) and can contain OTHER runs'' contexts — durable legs ledger the frame into
10281
+ their own run, so without this key an orphan revoke row (no paired card frame) could not be
10282
+ attributed or filtered; consumers ignore frames whose taskId is not theirs.
10041
10283
 
10042
10284
  AskDecidedConflictBody:
10043
10285
  allOf:
@@ -10704,6 +10946,30 @@ components:
10704
10946
  required: [type]
10705
10947
  properties:
10706
10948
  type: { type: string, enum: [approval_request] }
10949
+ Event_approval_revoke:
10950
+ description: >
10951
+ The design/172 batch-level card REVOCATION frame as an AgentEvent arm ([5924]/S-58, server >= 7.55.0).
10952
+ Emission is PER CONTEXT: the sync stream leg carries it live-SSE-only (unchanged, never ledgered),
10953
+ while the bg/resume durable legs append it to the run's own ledger — so GET /v1/runs/:id/events
10954
+ tails/replays it, and a durable consumer can finally tell "actively revoked (halt/cancel/park
10955
+ degradation)" from "stream ended". On <= 7.54.0 the durable leg never carries this frame.
10956
+
10957
+ 🔴 THIS ARM IS THE OPEN ENVELOPE, NOT `ApprovalRevokeFrame` — same ruling as Event_approval_request:
10958
+ narrow with `schemaVersion == 1` (SDK: `isApprovalRevokeFrameV1`) before touching
10959
+ `batchId`/`askIds`/`taskId`; a frame that does not narrow is handled as a generic revoke (clear that
10960
+ batch's local cards), never specially.
10961
+
10962
+ 🔴 Consumption discipline: (a) attribute/filter by `taskId` (always present >= 7.55.0) — a run that
10963
+ joined the dispatch set late can carry an ORPHAN revoke ledger row with no paired card frame; "not
10964
+ mine" is safely ignored. (b) A replayed revoke frame is for TIMELINE RENDERING ONLY — the card-set
10965
+ reconciliation baseline stays the open-stream preamble (the frame can be lost on any leg: it is a
10966
+ notification, the row is the source of truth). (c) Siblings already DECIDED are never in `askIds`.
10967
+ allOf:
10968
+ - $ref: '#/components/schemas/ApprovalFrameEnvelope'
10969
+ - type: object
10970
+ required: [type]
10971
+ properties:
10972
+ type: { type: string, enum: [approval_revoke] }
10707
10973
 
10708
10974
  # ── resources/*.ts schema backfill, batch 2 (2026-07-28) — the 28-item WIRE_PENDING ratchet zeroed.
10709
10975
  # Each schema below is cross-checked against the server source (file:line cited in its description
@@ -10729,6 +10995,14 @@ components:
10729
10995
  sessionId: { type: string, description: '`resolved` frames: which pending settled.' }
10730
10996
  toolCallId: { type: string, description: '`resolved` frames: the settled pending''s bound tool-call id (absent when the pending carried none).' }
10731
10997
  count: { type: integer, description: '`synced` frames: how many pendings the connect-time snapshot carried.' }
10998
+ hasBidiControls:
10999
+ type: boolean
11000
+ enum: [true]
11001
+ description: >-
11002
+ `pending` frames only (server >=7.53, S-30①): the spread PendingCheckpoint row's bidi-presence bit —
11003
+ SAME projection as the `GET /v1/approvals` row (`projectPendingForWire`), declared here explicitly so
11004
+ a stream consumer sees it typed instead of through the open index signature. Present ONLY when true;
11005
+ absence = "not detected", never "confirmed clean" (see PendingCheckpoint.hasBidiControls).
10732
11006
 
10733
11007
  ParkedDecideAccepted:
10734
11008
  type: object
@@ -10812,21 +11086,31 @@ components:
10812
11086
  parentToolCallId: { type: string, description: 'The parent tool_use id VERBATIM (E2 domain, engine-minted) — [1832] P1-2, server >= 1.290 / engine 1.432. Additive / tolerate-absent.' }
10813
11087
 
10814
11088
  FleetHookNotice:
11089
+ description: >
11090
+ Hook observability payload, TWO kinds (service fleet-bus.ts `HookNotice` minus the wire-stripped
11091
+ ownerScope/ownerSessionId — the payload view of FleetFrame_hook_notice; a discriminated union on
11092
+ `kind`, mirroring the SDK type verbatim). Both kinds are pure observe — never block, resume, or
11093
+ inject. Visibility is fail-CLOSED: a ?session= subscriber only receives notices provably owned by
11094
+ that session. `kind` is a CLOSED two-member set on the server (exhaustive-switch discipline) but
11095
+ OPEN ON READ: degrade an unknown future kind to a generic observability row, never crash.
11096
+ oneOf:
11097
+ - $ref: '#/components/schemas/FleetHookDecisionUnavailable'
11098
+ - $ref: '#/components/schemas/FleetHookNonBlockingFailure'
11099
+ discriminator:
11100
+ propertyName: kind
11101
+ mapping:
11102
+ hook_decision_unavailable: '#/components/schemas/FleetHookDecisionUnavailable'
11103
+ hook_non_blocking_failure: '#/components/schemas/FleetHookNonBlockingFailure'
11104
+
11105
+ FleetHookDecisionUnavailable:
10815
11106
  type: object
10816
11107
  description: >
10817
- A hook adjudication FAILED TO COMPLETE this cycle (service fleet-bus.ts `HookNotice` minus the
10818
- wire-stripped ownerScope/ownerSessionId — the payload view of FleetFrame_hook_notice). Semantics
10819
- are "could not evaluate", NOT "evaluated to fail": render as "this cycle went unguarded, allowed
10820
- through", never "the guard is broken". Pure observe — it does not block, resume, or inject.
10821
- Visibility is fail-CLOSED: a ?session= subscriber only receives notices provably owned by that
10822
- session.
11108
+ Kind 1 — a hook adjudication FAILED TO COMPLETE this cycle ("could not evaluate", NOT "evaluated to
11109
+ fail": render as "this cycle went unguarded, allowed through", never "the guard is broken").
10823
11110
  required: [kind, event, reason]
10824
11111
  additionalProperties: true
10825
11112
  properties:
10826
- kind:
10827
- type: string
10828
- enum: [hook_decision_unavailable]
10829
- description: 'Only one kind today; an enum slot so the next observability frame kind needs no new frame arm.'
11113
+ kind: { type: string, enum: [hook_decision_unavailable] }
10830
11114
  event: { type: string, description: 'Which hook event (Stop / PreToolUse / …) — defined by "a hook adjudication did not complete", not any single feature.' }
10831
11115
  reason:
10832
11116
  # 🔴 NO `enum` — same ruling as `FleetTaskRow.retiredBy` (codex R1-F3, family case): the description
@@ -10834,9 +11118,31 @@ components:
10834
11118
  # so a closed enum here would make a strict generated client drop the whole hook_notice frame the day the
10835
11119
  # server mints a fourth cause word — and that frame is the ONLY disclosure that a guard cycle went unwatched.
10836
11120
  type: string
10837
- description: 'Machine-branchable cause. CLOSED on the server side (fleet-bus.ts HookNotice.reason) but OPEN ON READ — THE KNOWN WORDS TODAY ARE no_content | unparsed | skipped (named here rather than pinned as an `enum`, deliberately — see the comment above): branch known values, render anything else as the generic "could not evaluate".'
11121
+ description: 'Machine-branchable cause. CLOSED on the server side (fleet-bus.ts) but OPEN ON READ — KNOWN WORDS TODAY: no_content | unparsed | skipped (named here rather than pinned as an `enum`, deliberately — see the comment above): branch known values, render anything else as the generic "could not evaluate".'
10838
11122
  detail: { type: string, description: 'Redacted supplement (e.g. why skipped). Additive / tolerate-absent.' }
10839
11123
 
11124
+ FleetHookNonBlockingFailure:
11125
+ type: object
11126
+ description: >
11127
+ Kind 2 (server >= 7.48.0, #281 — A-075.17): the CONFIGURED HOOK ITSELF BROKE (non-blocking failure:
11128
+ exit nonzero-and-not-2 / timeout / spawn failure / bad JSON). Render "<hookName> hook error" + stderr
11129
+ (CC-parity attachment shape) — this one IS "this hook is broken", unlike kind 1.
11130
+ required: [kind, event, reason, hookName, entryType]
11131
+ additionalProperties: true
11132
+ properties:
11133
+ kind: { type: string, enum: [hook_non_blocking_failure] }
11134
+ event: { type: string, description: 'Which hook event (PreToolUse / Stop / …).' }
11135
+ reason:
11136
+ # 🔴 NO `enum` — same open-on-read ruling as kind 1 (SDK type is `... | (string & {})`).
11137
+ type: string
11138
+ description: 'Machine-branchable failure shape. CLOSED server-side, OPEN ON READ — KNOWN WORDS TODAY: exit_nonzero | timeout | spawn_failed | bad_json.'
11139
+ hookName: { type: string, description: 'Hook identity (statusMessage, else command/URL/prompt; redacted + bounded, http entries stripped of query/fragment). Always sent.' }
11140
+ entryType: { type: string, description: 'command | http | prompt | agent — without it, hookName''s nature is a guess. Closed server-side, open on read. Always sent.' }
11141
+ exitCode: { type: integer, description: 'Absent for timeout/spawn_failed (no exit code exists — the server never mints a legit-looking 0).' }
11142
+ toolName: { type: string, description: 'Present on tool events (PreToolUse/PostToolUse/PostToolUseFailure).' }
11143
+ stderr: { type: string, description: 'Redacted + truncated stderr digest (512-char cap incl. marker). Empty stderr => absent.' }
11144
+ detail: { type: string, description: 'Redacted supplement (e.g. where an exit-2 had no venue). Additive / tolerate-absent.' }
11145
+
10840
11146
  BakeStatus:
10841
11147
  # 新(census 批2 四段)——server `BakeStatus`(plugins/store-contracts.ts), the COARSE lifecycle
10842
11148
  # bakeView/claimResponse/bakesCreate all key on.
@@ -11946,29 +12252,45 @@ components:
11946
12252
  never hand it to another user.
11947
12253
  expiresAtMs: { type: integer, description: 'Absolute server-clock expiry (the window is 10 minutes — one interaction, not a session).' }
11948
12254
 
12255
+ CcImportRedeemedMember:
12256
+ type: object
12257
+ description: >
12258
+ One member of the redeem 200 (core `RedeemedBatchMember`, the 200-REACHABLE arm verbatim — core d.ts
12259
+ permission-rule-consent.d.ts). The union's third arm `{status:"refused", reason}` is STRUCTURALLY
12260
+ UNREACHABLE on this route's 200: any refused member makes the server treat the whole redeem as
12261
+ indeterminate, release the claim and answer 503 `state.rule_import_retry` (server rules-consent.ts) —
12262
+ so this schema honestly describes the two-value `status` only.
12263
+ additionalProperties: false
12264
+ required: [candidateIndex, rule, scope, status, alreadyRedeemed, dot]
12265
+ properties:
12266
+ candidateIndex: { type: integer, description: 'Index into the prepare preview''s candidate set (candidate space).' }
12267
+ rule: { type: string, description: 'Canonical rule text.' }
12268
+ scope: { $ref: '#/components/schemas/RuleScope' }
12269
+ status:
12270
+ type: string
12271
+ enum: [persisted, deduped]
12272
+ description: '`persisted` = landed this redeem; `deduped` = an equal rule was already in the store (the consent took effect — not a failure).'
12273
+ alreadyRedeemed: { type: boolean, description: 'true = this outcome was REPLAYED verbatim from the ticket row (lost-response retry; idempotency evidence — zero double-mint).' }
12274
+ dot: { $ref: '#/components/schemas/RuleDot' }
12275
+
11949
12276
  CcImportResult:
11950
12277
  type: object
11951
12278
  description: >
11952
- What the import ACTUALLY did (core `ImportResult`) — a different moment and a different contract from
11953
- the preview, because dedup, concurrency and redemption-time validation can each move an entry between
11954
- the two.
12279
+ What the import ACTUALLY did (server >= 7.46.0, BREAKING #2 there / core 5.58.0 redeemRuleBatch
12280
+ members passthrough) — a different moment and a different contract from the preview, because dedup,
12281
+ concurrency and redemption-time validation can each move an entry between the two.
11955
12282
 
11956
- ⚠️ `skippedAtRedeem` is empty on every success arm of THIS lane: the server treats a non-empty one as
11957
- INDETERMINATE, releases the claim and answers 503 `state.rule_import_retry` instead of returning a 200
11958
- that quietly means "half of it did not land". The field stays on the shape because it is core's.
12283
+ 🔴 SHAPE CHANGE (A-075.13): the old three-bucket form `{persisted[], deduped[], skippedAtRedeem[],
12284
+ rev}` left the wire in server 7.46.0 — a consumer reading `result.persisted.length` gets undefined.
12285
+ This schema describes the real wire since then; the SDK type changed with it (a staleness fix, not a
12286
+ behavior change).
11959
12287
  additionalProperties: false
11960
- required: [persisted, deduped, skippedAtRedeem, rev]
12288
+ required: [members, rev]
11961
12289
  properties:
11962
- persisted:
11963
- type: array
11964
- items: { $ref: '#/components/schemas/RuleCandidate' }
11965
- deduped:
12290
+ members:
11966
12291
  type: array
11967
- items: { $ref: '#/components/schemas/RuleCandidate' }
11968
- description: 'Already in the store — not a failure.'
11969
- skippedAtRedeem:
11970
- type: array
11971
- items: { $ref: '#/components/schemas/CcImportSkippedRule' }
12292
+ items: { $ref: '#/components/schemas/CcImportRedeemedMember' }
12293
+ description: 'Per-candidate outcome (discriminate on `status`; refused never rides a 200 — see CcImportRedeemedMember).'
11972
12294
  rev: { type: integer, description: 'The rule store''s monotonic revision after the batch.' }
11973
12295
 
11974
12296
  CcImportRedeemResult: