@sema-agent/sdk 9.6.0 → 9.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.yaml CHANGED
@@ -2079,6 +2079,77 @@ paths:
2079
2079
  schema: { $ref: '#/components/schemas/McpStatusPanel' }
2080
2080
  '401': { $ref: '#/components/responses/Unauthorized' }
2081
2081
 
2082
+ /v1/sessions/{sessionId}/mcp/reconnect:
2083
+ parameters:
2084
+ - $ref: '#/components/parameters/PrincipalHeader'
2085
+ - in: path
2086
+ name: sessionId
2087
+ required: true
2088
+ schema: { type: string }
2089
+ post:
2090
+ tags: [sessions]
2091
+ operationId: sessionsMcpReconnect
2092
+ x-status: live # S-381 (server 7.85.0; core 7.22.0 #857); SDK sessions.mcpReconnect.
2093
+ summary: Re-dial ONE named MCP server inside a live session (a TRANSACTION, not a refresh button).
2094
+ description: >
2095
+ S-381 (server >= 7.85.0; core 7.22.0 #857) — re-dial the transport of ONE NAMED MCP server WITHOUT
2096
+ ending the task. Body is `{ server }`: required, non-empty, at most 190 characters, and the body gate
2097
+ is STRICT — a misspelled key is refused, never stripped-and-ignored, and there is NO all-servers form
2098
+ (re-dialing everything would CLOSE the healthy connections, and a stdio server that owns a device
2099
+ cannot be re-dialed while the old connection still holds it). A bad body ⇒ 400 `request.body_shape`.
2100
+ 🔴 A TRANSACTION, NOT A REFRESH BUTTON (core's own words — this sentence belongs in the UI copy): the
2101
+ old connection is closed BEFORE the new dial, so a FAILED re-dial spends a working connection — the
2102
+ server stays disconnected, ITS TOOLS ARE WITHDRAWN from the model's roster, and in-flight calls fail.
2103
+ Offering "reconnect" on a HEALTHY server is offering a transaction.
2104
+ 🔴 `outcome` IS THE ONLY DISCRIMINATOR AND ALL THREE STATES ARE HTTP 200 (the status answers only
2105
+ "did the verb reach the engine", the same law as steer's "branch on `delivery`, not the status"):
2106
+ `accepted` = core's `reconnected`; `refused` = core's `refused` OR `not_declared` (an unknown server
2107
+ name is a ROSTER fact, not an engine-capability fact); `unsupported` = THE SERVER'S OWN WORD (core has
2108
+ none) for "this run declared no MCP at all" (core throws `mcp.reconnect_unavailable`) — and that arm is
2109
+ an ALWAYS-FIVE-KEY body `{taskId, sessionId, server, outcome, reason}`: with no core result the server
2110
+ coins NO `prefix`/`toolCount`/`added`/`removed`/`toolNames`. Consumers MUST branch on `outcome` and
2111
+ must NOT read `body.added` unconditionally.
2112
+ 🔴 `toolNames` is PRESENT-IFF core supplied `tools` (NOT keyed off `outcome`): `[]` (a failed dial) and
2113
+ ABSENT (an arm that never touched the connection) are two different sentences. It is the roster THIS
2114
+ re-dial asserted for its own `prefix` domain, NOT "what the model finally sees" — core filters again by
2115
+ the deployment's exclusion table before mounting, and a refused axis-fold mounts none, so this key is a
2116
+ SUPERSET in both shapes. For "what actually got mounted" read a roster face.
2117
+ Next action comes from `status` (machine-readable: `errorCode` is core's `McpFailureKind` word — do not
2118
+ narrow it, switch with a default arm; `httpStatus` 401 is a re-authorize door); `reason` is HUMAN TEXT
2119
+ (server-redacted, length-bounded) and must NOT be parsed.
2120
+ Gates, the same family as steer / capture-optout: 501 `capability.run_store_required` (no durable run
2121
+ ledger) · 501 `capability.session_ownership_required` (no `sessionStorage.ownerOf`) · 400
2122
+ `request.path_malformed` (bad percent-encoding in the id) · 404 `not_found.session` — unknown session
2123
+ AND someone else's session answer the SAME code and SAME message (deliberate: no existence oracle); an
2124
+ EXPLICIT operator may re-dial any tenant's session · 422 `steering.invalid_content` · 409
2125
+ `steering.not_running` with FOUR arms behind ONE code (parked awaiting a decision / already terminal /
2126
+ live on ANOTHER replica / no live steerable face here). A re-dial is LEG-LOCAL — a durable resume dials
2127
+ every declared server afresh — so "park a re-dial until resume" is structurally not a shape, and this
2128
+ face deliberately has no park leg. Probe with `capabilities.mcpReconnect`; absent ⇒ probe by 404/405.
2129
+ requestBody:
2130
+ required: true
2131
+ content:
2132
+ application/json:
2133
+ schema:
2134
+ type: object
2135
+ additionalProperties: false
2136
+ required: [server]
2137
+ properties:
2138
+ server: { type: string, minLength: 1, maxLength: 190, description: 'The ONE named MCP server to re-dial (the declared name). Required; there is no all-servers form. The cap is load-bearing: the string is relayed into an operator notice detail.' }
2139
+ responses:
2140
+ '200':
2141
+ description: >-
2142
+ The re-dial receipt. Branch on `outcome`; the `unsupported` arm is an always-five-key body.
2143
+ content:
2144
+ application/json:
2145
+ schema: { $ref: '#/components/schemas/SessionMcpReconnectResult' }
2146
+ '400': { $ref: '#/components/responses/BadRequest' }
2147
+ '401': { $ref: '#/components/responses/Unauthorized' }
2148
+ '404': { $ref: '#/components/responses/NotFound' }
2149
+ '409': { $ref: '#/components/responses/Conflict' }
2150
+ '422': { $ref: '#/components/responses/UnprocessableEntity' }
2151
+ '501': { $ref: '#/components/responses/NotImplemented' }
2152
+
2082
2153
  /v1/sessions/{sessionId}/memory-status:
2083
2154
  parameters:
2084
2155
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -2578,9 +2649,24 @@ paths:
2578
2649
  `data.type` — proxy-safe dispatch. Cross-replica by
2579
2650
  construction (polls the SHARED checkpoint table). 15-min cap + heartbeats; a DB blip retries, never
2580
2651
  kills the stream. Scope = the caller's principal (operator/trace token = fleet-wide).
2652
+ 🔴 TWO MORE EVENTS (server >= 7.87.0 / S-454 P1; cli L-397): this leg now has TWO diff sources, not one.
2653
+ `live_pending` = a LIVE (in-stream) ask entered or its wire projection changed (UPSERT semantics;
2654
+ payload = a `LivePendingRow` spread plus `serverNowMs`, AND IT CARRIES `frame`, so a consumer can render
2655
+ the card straight from the push). `live_resolved` = a live ask left (settled / window expired / entry
2656
+ gone — ALL THREE ARE ONE FRAME: "why it left" is for the consumer to fetch, the push does not conclude
2657
+ it; payload = `{type, approvalId, serverNowMs}`). First-tick order: `pending…` → `live_pending…` →
2658
+ `synced` (and `synced.count` still counts ONLY the durable queue).
2659
+ 🔴 THE TWO WORDS DELIBERATELY DO NOT REUSE `pending`/`resolved`: those are keyed by
2660
+ `(sessionId, toolCallId)` and decided at `POST /v1/approvals/{sessionId}/decide`, while a live row is
2661
+ keyed by `approvalId` and decided at `POST /v1/tool-approvals/{approvalId}/respond`. Sharing one word
2662
+ would eventually send a live card to the decide door — a 404 with no wire evidence of the mistake.
2663
+ ⚠️ Both live words come from an IN-PROCESS window projection, so their cross-replica reach is exactly
2664
+ that of `GET /v1/approvals`'s `livePending` — narrower than the durable pair above. The deployment
2665
+ without a live approval face (coordinator unwired) emits neither, and the stream's bytes are identical
2666
+ to 7.86.0's (capability absent = zero change, not an empty event).
2581
2667
  responses:
2582
2668
  '200':
2583
- description: text/event-stream of meta/pending/resolved frames.
2669
+ description: text/event-stream of meta/pending/resolved/live_pending/live_resolved frames.
2584
2670
  content:
2585
2671
  text/event-stream:
2586
2672
  schema: { type: string }
@@ -2621,9 +2707,15 @@ paths:
2621
2707
  string; `remember` not `"session"`; `checkpointToken`/`boundCallId`/`boundInputHash` not a string)
2622
2708
  · "request.field_conflict" (`remember` without `decision:"approve"`; TC-5.4 answer coherence —
2623
2709
  approving a pending AskUserQuestion REQUIRES `answer`, and `answer` is only valid on that approve)
2624
- · "remember_not_in_proof" / "updated_input_not_in_proof" (`remember` / `updatedInput` on a
2625
- direct-door worker — not covered by the decision proof) · "remember_requires_binding" (`remember`
2626
- without the D-1 binding echo). PARKED variant (server 5.12.0, core 5.7.0 RB-459):
2710
+ · "remember_not_in_proof" / "updated_input_not_in_proof" / "checkpoint_token_not_in_proof"
2711
+ (`remember` / `updatedInput` / `checkpointToken` on a direct-door worker — not covered by the
2712
+ decision proof; the third one is server >= 7.87.0 / S-452, and its message points at the
2713
+ replacement: bind the decision with `boundCallId` + `boundInputHash`. Refusing it loses nothing —
2714
+ the token was only ever a staleness check, and on the direct door that check is already covered by
2715
+ the proof's binding. A compliant client never sends it: this SDK deleted the field at 1.0.0)
2716
+ · "request.field_invalid" ALSO covers an unrecognised key inside `answer` (server >= 7.87.0 /
2717
+ S-452: the closed answer shape — the message names the key paths and the body carries `unknownKeys`)
2718
+ · "remember_requires_binding" (`remember` without the D-1 binding echo). PARKED variant (server 5.12.0, core 5.7.0 RB-459):
2627
2719
  `decide.parked_answer_required` — the parked child's pending action is an AskUserQuestion and the
2628
2720
  approve carried no `answer` (the ONLY pre-claim rejection left on that leg; the retired codes
2629
2721
  `decide.parked_answer_unsupported` / `decide.parked_question_unsupported` are gone — an answer HAS
@@ -3015,8 +3107,15 @@ paths:
3015
3107
  schema: { $ref: '#/components/schemas/AssistantTaskStatus' }
3016
3108
  '400':
3017
3109
  description: >
3018
- Bad/missing `decision`; `edit` without a non-empty `editedPlan`; `editedPlan` present on
3019
- approve/reject (FORBIDDEN); non-string `reason`.
3110
+ `ErrorResponse`-shaped. Codes: "request.body_shape" (bad/missing `decision`, OR an unrecognised
3111
+ top-level key — server >= 7.86.0 / S-438 key closure, the body carries `unknownKeys` /
3112
+ `unsupportedKeys`) · "request.field_conflict" (`edit` without a non-empty `editedPlan`;
3113
+ `editedPlan` present on approve/reject; `permissionModeAfter` sent with `edit`/`reject`;
3114
+ `permissionModeAfter` on a task that was not submitted with `permissionMode:"plan"`) ·
3115
+ "request.field_invalid" (non-string `reason`; `permissionModeAfter` outside the two-word closed
3116
+ set — zero widening arm) · "request.path_malformed" (a bad percent-encoded path segment —
3117
+ server >= 7.86.0 / S-430 turned that family from 500 `internal.error` into a 400 across 16 doors).
3118
+ 413 `reason_too_large` rides its own status.
3020
3119
  content:
3021
3120
  application/json:
3022
3121
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -5033,11 +5132,16 @@ paths:
5033
5132
  tags: [metrics]
5034
5133
  operationId: metricsSummary
5035
5134
  x-status: draft # ops surface; uses a metricsToken (NOT a principal). Shape loose.
5036
- x-sdk: none # DELIBERATELY not in @sema-agent/sdk (2026-07-27 定性): ops/observability face, credential = metricsToken not a principal; README walks curl. Revocable — delete this key + the gate exemption to bring it in.
5037
5135
  summary: Worker health/cost summary (ops).
5038
5136
  description: >
5039
- Uses `metricsToken` (operator), not `x-agent-principal`. Shape is loose/ops-defined. Deliberately
5040
- NOT wrapped by the SDK (see the `x-sdk` key) — ops tooling reads it with curl.
5137
+ Uses `metricsToken` (operator) OR a full authToken, not `x-agent-principal`.
5138
+ ⚠️ The `x-sdk: none` key that used to sit here ("deliberately not in @sema-agent/sdk", 2026-07-27) was
5139
+ STALE and is deleted at sdk 9.7.0: the SDK has wrapped this face since the 2026-07-17 coverage batch
5140
+ (`client.metrics.summary()`), and since 9.7.0 the two S-451 keys are named on the returned type
5141
+ (`MetricsSummaryOps`). The path stays on the spec-path gate's spec-only exemption list for a DIFFERENT
5142
+ reason, now written there: that gate's SDK-side extractor only collects `/v1/...` literals, so a
5143
+ non-`/v1` path can never match — the exemption compensates for the EXTRACTOR's scope, not for a
5144
+ missing verb.
5041
5145
  security:
5042
5146
  - metricsToken: []
5043
5147
  responses:
@@ -5045,11 +5149,9 @@ paths:
5045
5149
  description: Metrics summary (shape draft).
5046
5150
  content:
5047
5151
  application/json:
5048
- # 🔴 census 批2 五段:CLOSED against the real emitter — `sendJson(res, 200, { model,
5049
- # ...deps.metrics.summarize() })` (server.ts) is a fixed key set (server observability/
5050
- # metrics.ts `MetricsSummary`), not an ops free-for-all. Left `x-status: draft` / `x-sdk: none`
5051
- # untouched (still deliberately outside the SDK's wrapped surface) — closing the SHAPE and
5052
- # keeping it un-wrapped are independent axes.
5152
+ # 🔴 census 批2 五段把它按真铸点 CLOSED(`{ model, ...deps.metrics.summarize() }` 是定键集,
5153
+ # 不是 ops free-for-all)。sdk 9.7.0 **改开**并补上生产方早已多出来的三键 —— 理由与 receipts
5154
+ # 在 `MetricsSummaryOps` 的 description 里(投影型响应体开集;请求体仍封闭)。
5053
5155
  schema: { $ref: '#/components/schemas/MetricsSummaryOps' }
5054
5156
  '401': { $ref: '#/components/responses/Unauthorized' }
5055
5157
 
@@ -5316,6 +5418,20 @@ components:
5316
5418
  waitMs: { type: integer }
5317
5419
  decision: { type: string }
5318
5420
  toolName: { type: string }
5421
+ toolCallId:
5422
+ type: string
5423
+ description: >-
5424
+ core 7.23.0 (#897), server >= 7.87.0 — the engine's own call id, passed through verbatim.
5425
+ 🔴 ABSENT MEANS "NOT JOINABLE", NOT "no call": three of the four mint sites (an inherited
5426
+ grant being reused, the durable-resume leg, the park self-check's synthetic row) may have
5427
+ no call identity to give. 🔴 JOIN BY THIS KEY — never by row count or position: a
5428
+ multi-leg aggregation (verify / cascade / repair-loop) pushes each leg's gates as a
5429
+ BLOCK, so the order is "leg completion order concatenated", and a consumer's own
5430
+ permission ledger legitimately has a DIFFERENT number of rows. A right-hand key existing
5431
+ is not a reason to mint a parallel ledger elsewhere: the engine's is authoritative.
5432
+ ⚠️ Since server 7.84.0 (S-394) the model-authored `toolArg` here goes through the SAME
5433
+ redactor as `result`, so credential-shaped BYTES CHANGE (anything else is byte-identical)
5434
+ — a consumer hashing these fields for identity must move off them.
5319
5435
  required: [turns, tokens]
5320
5436
  # 🔴 OPEN: the engine may carry fields not named here (live-observed, e.g. extra costBreakdown axes) — the SDK
5321
5437
  # passes them through rather than dropping, so consumers must not assume this list is exhaustive.
@@ -5705,6 +5821,33 @@ components:
5705
5821
  `memory.capture_optout_denied` (terminal). A worker older than 7.70.0 IGNORES the key silently, so
5706
5822
  it is not a capability probe. Rides the persisted body onto resume legs.
5707
5823
  Mid-run flip: POST /v1/runs/{taskId}/memory/capture-optout (the same record, a second ingress).
5824
+ memory:
5825
+ type: string
5826
+ enum: [off]
5827
+ description: >
5828
+ server >= 7.84.0 (S-405 / core #863 W3; mint `src/http/wire-types.ts:309`) — the REQUEST-level
5829
+ "no memory face at all" declaration: "off" means THIS RUN mounts no memory face (no `# Memory`
5830
+ section, no index, none of the three memory tools, no write admission). A ONE-MEMBER closed set:
5831
+ there is NO "on" spelling, absence is the only way to say "memory as usual".
5832
+ 🔴 THREE ORTHOGONAL AXES, do not mix them up: this key = "this RUN mounts no memory face";
5833
+ `memoryCapture:"off"` = "memory stays mounted, but THIS SESSION's content never enters long-term
5834
+ memory"; `memoryWrite:false` = "this run reads memory as usual, the harvest just does not commit".
5835
+ 🔴 SENDING BOTH IS NOT BUYING BOTH (contract §12.7, verified against core `prepare-memory.js:48/:55`):
5836
+ core conjoins the capture leg with the engine mount, so on a turn carrying `memory:"off"` the
5837
+ capture opt-out's ONE-WAY session record DOES NOT LAND ⇒ the next turn without this key captures as
5838
+ usual. To make the session permanently uncaptured, do NOT send this key on that turn.
5839
+ ⚠️ Honest scope: this is "no memory face", NOT a filesystem ban — if the memory root sits inside the
5840
+ workspace this run was granted, ordinary Read/Write still go through the ordinary fences.
5841
+ ⚠️ The FACT IS KEPT, not erased: the server passes it through as `TaskSpec.memory.enabled:false`
5842
+ (it does not drop the spec) ⇒ the terminal observation face reads `disabled`, not `no-spec`.
5843
+ ⚠️ Same name, different thing from the legacy `GET /v1/capabilities.memory` bit (always false — the
5844
+ RETIRED MemoryStore verb family). Do not read that bit to decide whether this key is available.
5845
+ 🔴 NO CAPABILITY BIT: 7.78.1–7.83.x can trial-by-400 (the spelling gate refuses); older workers
5846
+ cannot be probed (they ignore it silently).
5847
+ 🔴 The spelling gate is loud on BOTH legs: "OFF" / a boolean / anything else is a 400
5848
+ `request.field_invalid` (fresh admission and the resume replay share ONE predicate,
5849
+ `singleMemberOffFieldIssue`; on the resume leg it folds to 409 `resume_blocked_by_policy`).
5850
+ Rides the persisted body onto resume legs.
5708
5851
  settings:
5709
5852
  type: object
5710
5853
  description: >
@@ -6661,9 +6804,106 @@ components:
6661
6804
  properties:
6662
6805
  id: { type: string }
6663
6806
  status: { type: string, enum: [running, completed, failed, needs_human] }
6664
- result: {}
6807
+ result:
6808
+ allOf: [{ $ref: '#/components/schemas/LeaderRunResult' }]
6809
+ description: >
6810
+ The leader run's terminal payload. sdk 9.7.0 declares the two segments a consumer actually renders
6811
+ (`conflict` / `salvaged`) and leaves the rest open — see `LeaderRunResult`. (The SDK's TS side keeps
6812
+ `LeaderRecord.result: unknown` for one more version: pointing it at the named type is a NARROWING,
6813
+ which RELEASE.md reserves for a major. Cast it: `record.result as LeaderRunResult`.)
6665
6814
  error: { type: string }
6666
6815
 
6816
+ LeaderRunResult:
6817
+ type: object
6818
+ additionalProperties: true
6819
+ description: >
6820
+ The `result` of a leader run (server `leader/leader.ts` `LeaderResult`). Only the segments a consumer
6821
+ really renders are declared; the rest passes through (leader is a gated v2 lane and its result carries
6822
+ server-internal shapes — mirroring all of them here would just create a second copy that drifts).
6823
+ 🔴 S-113 / clay C-R63 ④ (server >= 7.83.0): A MERGE THAT DOES NOT APPLY IS HANDED TO THE USER (the CC
6824
+ shape). The apply conflict no longer starts a conflict-resolver model: `ok:false` plus the `conflict`
6825
+ segment, and the run's status is the EXISTING third terminal `needs_human` (no new status word). The
6826
+ user takes `workers[].branch` + `files` and merges/cherry-picks. SAME BATCH, DELETED WITH ZERO ALIASES:
6827
+ `merge.conflictsResolved`, `conflictResolverCostUsd`, and the `merge-conflict` member of
6828
+ `replan.trigger` — a consumer that read them goes tsc-red, and that IS the notice.
6829
+ properties:
6830
+ ok: { type: boolean, description: 'Did the whole wave succeed (always false on a conflict terminal).' }
6831
+ conflict:
6832
+ type: object
6833
+ additionalProperties: true
6834
+ description: >
6835
+ The wave that would not merge (server >= 7.83.0; ONE mint — `conflictSegment` — shared by the main
6836
+ path and the collapse path, so "the same situation, two different terminals" is structurally gone).
6837
+ ABSENT = no conflict on this wave, OR an older server — the two look the same, so do not infer a
6838
+ version from absence.
6839
+ required: [baseSha, files, workers]
6840
+ properties:
6841
+ baseSha: { type: string, description: "The user's merge base = this wave's durable base." }
6842
+ files: { type: array, items: { type: string }, description: 'Paths carrying conflict markers / .rej (CONTENTS NOT INCLUDED).' }
6843
+ filesTruncated:
6844
+ type: boolean
6845
+ enum: [true]
6846
+ description: >-
6847
+ Present only when the list hit the CAP. 🔴 ABSENCE DOES NOT MEAN "this is all of it" (corrected
6848
+ at sdk 9.7.0 after a codex finding was verified against the server's own probe arm in
6849
+ `leader/merge.ts`): this flag reflects the CAP and nothing else. The list is assembled by two
6850
+ shell probes (`grep -rIl` for conflict markers + `find -name '*.rej'`), and when either leg
6851
+ fails the server honestly returns a possibly EMPTY or partial `files` while logging one line
6852
+ (`merge_conflict_files_probe_failed`) — there is NO discriminator for that shape on the wire.
6853
+ ⇒ A `conflict` segment with an empty (or implausibly short) `files` must NOT be rendered as
6854
+ "no conflicting files"; render "the list may be incomplete" and point the user at `rejHead`
6855
+ and `workers[].branch`. A bit that separates "probe failed" from "there really are no more"
6856
+ would have to come from the server; this SDK does not mint a second judge for it.
6857
+ workers:
6858
+ type: array
6859
+ description: 'EVERY completed worker. `applied:false` = that worker''s patch is not in the tree (the conflicting one, or one queued behind it — apply is sequential and stops at the first failure). The branch comes from the provisioning registry, so it always has a value: the user checks it out.'
6860
+ items:
6861
+ type: object
6862
+ additionalProperties: true
6863
+ required: [workerId, branch, applied]
6864
+ properties:
6865
+ workerId: { type: string }
6866
+ branch: { type: string }
6867
+ applied: { type: boolean }
6868
+ rejHead: { type: string, description: 'Head of the .rej (through the server''s existing artifact redaction, <= 2 KiB) — FOR A HUMAN TO LOOK AT, not to parse.' }
6869
+ salvaged:
6870
+ type: array
6871
+ description: >
6872
+ Patches of the workers that did not merge (best-effort; present only when non-empty). 🔴 Since
6873
+ 7.83.0 a CONFLICT terminal carries EVERY worker's patch (it used to carry only the unfinished ones):
6874
+ the integration sandbox is destroyed in a `finally`, so the branches are not guaranteed to still be
6875
+ on a remote — the patch itself is the deliverable, and the consumer should give the user a way to
6876
+ save it. ⚠️ `patch` is WORKER-AUTHORED CODE (untrusted text): render it, save it, never feed it back
6877
+ to a model.
6878
+ items:
6879
+ type: object
6880
+ additionalProperties: true
6881
+ required: [workerId, sessionId, patch]
6882
+ properties:
6883
+ workerId: { type: string }
6884
+ sessionId: { type: string }
6885
+ patch: { type: string }
6886
+ suspended:
6887
+ type: array
6888
+ description: 'Resume handles of suspended workers (C4) — their checkpoints and paused envs are intact.'
6889
+ items:
6890
+ type: object
6891
+ additionalProperties: true
6892
+ required: [workerId, sessionId]
6893
+ properties:
6894
+ workerId: { type: string }
6895
+ sessionId: { type: string }
6896
+ route: { type: string, description: 'single = a one-subtask plan; fanout = N.' }
6897
+ error: { type: string, description: 'Planning/provisioning failed before fan-out.' }
6898
+ replan:
6899
+ type: object
6900
+ additionalProperties: true
6901
+ description: 'Replan-lite: which trigger won arbitration and what was done. 🔴 The closed set LOST `merge-conflict` in 7.83.0 (S-113, hard breaking, zero aliases): a merge that does not apply is no longer replanned, it is handed to the user.'
6902
+ properties:
6903
+ trigger: { type: string, description: 'suspended | budget | verify-fail | worker-failed.' }
6904
+ redispatched: { type: array, items: { type: string } }
6905
+ collapsed: { type: string }
6906
+
6667
6907
  ApprovalDecision:
6668
6908
  type: object
6669
6909
  description: HITL decision (`POST /v1/approvals/:sessionId/decide`).
@@ -6675,12 +6915,23 @@ components:
6675
6915
  AskUserQuestion answer — TRUE WIRE SHAPE (worker-validated, 400 otherwise; found in prod
6676
6916
  integration, service-pinned): {answers:[{header, selected:string[], note?}]} where header matches
6677
6917
  the question's header. Omit for a plain F4 approve/deny.
6918
+ 🔴 CLOSED SHAPE (server >= 7.87.0 / S-452): an unrecognised key ANYWHERE in this object (including
6919
+ inside `answers[]`) is REFUSED — 400 `request.field_invalid`, naming the KEY PATHS in the message
6920
+ and in the `unknownKeys` machine table. Refused rather than dropped, because a misspelled
6921
+ `selected` would silently drop part of the operator's own typed answer with a 200 on top (#157
6922
+ family). Accepted: `answers`, `answers[].header`, `answers[].selected`, `answers[].note`.
6923
+ ⚠️ THE TWO LEGS DIFFER IN STRICTNESS and the live one is not the contract: on this DURABLE leg
6924
+ `answers[]` must be non-empty and each `selected` non-empty (an empty array is a 400), while the
6925
+ LIVE leg (`POST /v1/questions/{id}/respond`) only checks that it is an array (an empty one has a
6926
+ headless-default meaning there).
6678
6927
  type: object
6928
+ additionalProperties: false
6679
6929
  properties:
6680
6930
  answers:
6681
6931
  type: array
6682
6932
  items:
6683
6933
  type: object
6934
+ additionalProperties: false
6684
6935
  required: [header, selected]
6685
6936
  properties:
6686
6937
  header: { type: string }
@@ -6976,6 +7227,28 @@ components:
6976
7227
  mcpElicitation: { type: boolean, description: "MCP elicitation round-trips are supported." }
6977
7228
  askUserQuestion: { type: boolean, description: "AskUserQuestion gate is available (durable when approvals are)." }
6978
7229
  toolApproval: { type: boolean, description: "Tool-approval gating is active." }
7230
+ approvalsStreamLive:
7231
+ type: boolean
7232
+ description: >
7233
+ server >= 7.87.1 (S-455, the consumption half of S-454; mint `routes/capabilities.ts:667` =
7234
+ `projectApprovalsStreamLiveCapability(deps)`, predicate's sole owner `capabilities/approvals-push.ts`)
7235
+ — whether this deployment's approvals push face carries the LIVE event family. TRUE ⟺
7236
+ `GET /v1/approvals/stream` carries `live_pending` / `live_resolved` in addition to the durable
7237
+ park rows (same read function as `GET /v1/approvals`'s `livePending`). FACILITY DEPENDENCY, TWO
7238
+ conjuncts, each against its own leg's own presence predicate: ① the approvals foundation (same as
7239
+ the `approvals` bit; `checkpointStore` — absent ⇒ the whole `/v1/approvals/*` domain 404s, there
7240
+ is no stream at all) AND ② the live coordinator (same as the `toolApproval` bit;
7241
+ `TOOL_APPROVAL_ENABLED` — absent ⇒ the stream exists but that source is entirely absent, bytes
7242
+ identical to pre-S-454).
7243
+ 🔴 DELIBERATELY NOT FOLDED into `approvals` or `toolApproval`: `approvals` true with this bit false
7244
+ is the COMMON case (tool-approval not enabled); `toolApproval` true with no checkpoint foundation
7245
+ means the stream DOES NOT EXIST AT ALL — reading either bit alone would lie to half the fleet.
7246
+ ⚠️ True only promises the SOURCE IS PRESENT: not that any ask is pending right now, and not
7247
+ cross-replica visibility (the live half is an IN-PROCESS PROJECTION, the same limit as
7248
+ `livePending`; the durable park rows are the cross-replica half).
7249
+ ⚠️ ABSENT = an older server (<7.87.1) ⇒ cannot tell whether the push face carries the live family;
7250
+ do NOT read "no `live_pending` frame arrived yet" as "this deployment never pushes live" — the two
7251
+ are indistinguishable on an older server. Fall back to `GET /v1/approvals`'s `livePending` pull face.
6979
7252
  streamApproval:
6980
7253
  type: boolean
6981
7254
  description: >
@@ -7328,6 +7601,66 @@ components:
7328
7601
  `WriteProtectionCapability`). `null` = this process cannot say; an ABSENT key means an OLDER worker,
7329
7602
  and "no key" is NOT "no table" — an absent seat on the engine side is precisely the DEFAULT TABLE
7330
7603
  BEING IN PLACE.
7604
+ webSearch:
7605
+ type: object
7606
+ additionalProperties: true
7607
+ description: >
7608
+ server >= 7.82.1 (S-382; mint `routes/capabilities.ts:236` =
7609
+ `projectWebSearchCapability(deps.webSearchProvider)`) — THIS DEPLOYMENT'S DEFAULT WEBSEARCH BACKEND.
7610
+ The closed set is TYPED FROM the server's `WEB_SEARCH_PROVIDERS` tuple (`plugins/web-search.ts:70`,
7611
+ the repo's single provider word table) plus `"none"`; the predicate is the very
7612
+ `webSearchConfigFromEnv()` product boot USED TO BUILD the backend, so "the bit says brave while
7613
+ tavily is wired" is structurally impossible.
7614
+ 🔴 `backend:"none"` AND AN ABSENT KEY ARE OPPOSITE DISPOSITIONS — never fold them: `"none"` = this
7615
+ box has NO search backend (render "unavailable"); ABSENT = an older worker (< 7.82.1) ⇒ render
7616
+ "cannot tell". Reading absence as `"none"` renders a correctly-configured older deployment as
7617
+ having no search.
7618
+ 🔴 NO INTERNALS: the endpoint (a SearXNG instance URL), the API key and the quota never ride this
7619
+ wire, and the per-request `settings.webSearch` override is deliberately NOT echoed — this bit
7620
+ advertises the DEPLOYMENT DEFAULT and must not drift with the caller.
7621
+ required: [backend]
7622
+ properties:
7623
+ backend: { type: string, description: 'brave | tavily | searxng | none — closed set typed from the server''s provider tuple; unknown words are a newer server, not an error.' }
7624
+ mcpReconnect:
7625
+ type: boolean
7626
+ description: >
7627
+ server >= 7.85.0 (S-381 / core 7.22.0 #857; mint `routes/capabilities.ts:264` =
7628
+ `Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf)`) — presence of
7629
+ `POST /v1/sessions/{sessionId}/mcp/reconnect`. FACILITY DEPENDENCY, two conjuncts: a durable run
7630
+ ledger (the verb reaches the live stream through the run row) AND the session-ownership face (the
7631
+ inbound owner gate is fail-closed; without it the whole door is 501). Both 501s derive from the SAME
7632
+ deps as this bit, so "says yes but 501s" is structurally impossible.
7633
+ 🔴 True only promises the ENDPOINT IS THERE: not that there is a live run right now (otherwise 409
7634
+ `steering.not_running`), and not that this run declared any MCP server at all (undeclared ⇒ HTTP 200
7635
+ with `outcome:"unsupported"`, which is not an error). ABSENT = an older worker ⇒ probe by 404/405,
7636
+ do not render a dead affordance.
7637
+ executionLane:
7638
+ type: object
7639
+ additionalProperties: true
7640
+ description: >
7641
+ server >= 7.86.0 (S-426; mint `routes/capabilities.ts:602` =
7642
+ `projectExecutionLaneCapability(deps.config)`) — THE EXECUTION LANE, SELF-REPORTED. Before it, "do
7643
+ this engine's tools run on THIS machine" could only be guessed from a three-way circumstantial
7644
+ conjunction (shell spawned it AND no remote URL AND `REMOTE_EXEC` in {unset, host}) — none of which
7645
+ the engine said, and which drifts the moment the shell talks to a remote engine (it reads ITS OWN
7646
+ box's env).
7647
+ `provider` is typed from the server's `RemoteExecProvider` (`execution-lane-caps.ts:50`, derived
7648
+ from the `ServiceConfigFlat["remoteExec"]` discriminated union); `REMOTE_EXEC` UNSET ⇒ `"host"` is
7649
+ CONTRACT, not a fallback. Honest cost: the server internally has a finer word (`in-process` — that
7650
+ lane mounts NO hands at all) which this bit folds into `host`, so a consumer cannot tell "hands on
7651
+ this box" from "no hands at all"; that is a DIFFERENT AXIS, not a reason to re-point the lane word.
7652
+ `toolsOnThisHost` is owned by `task-cwd.ts`'s `toolsRunOnThisHost` — the SAME FUNCTION that decides
7653
+ whether a self-scanned skill gets a `baseDir`.
7654
+ 🔴 THE IMPLICATION IS ONE-WAY: `toolsOnThisHost:false` ⇒ `skills[].baseDir` is never sent (that is
7655
+ the promise); the converse does NOT hold — `true` is only necessary (a flat `skills/<name>.md` has
7656
+ no directory of its own; plugin-sourced skills go through a loader that never passes the switch),
7657
+ so under `true` read `baseDir` as PRESENT-IFF.
7658
+ 🔴 NO INTERNALS: k8s apiUrl / ssh host / adb serial / image / credentials / mountPath never ride here.
7659
+ An ABSENT key must NOT be read as either side — that is exactly the state this bit was minted to end.
7660
+ required: [provider, toolsOnThisHost]
7661
+ properties:
7662
+ provider: { type: string, description: 'host | e2b | k8s | ssh | adb | local-docker | device — typed from the server''s RemoteExecProvider; `REMOTE_EXEC` unset ⇒ `host`.' }
7663
+ toolsOnThisHost: { type: boolean, description: 'The tools run on the process that answered this request (server-absolute paths mean something to the model''s tools).' }
7331
7664
  WriteProtectionCapability:
7332
7665
  type: object
7333
7666
  additionalProperties: false
@@ -7671,16 +8004,24 @@ components:
7671
8004
  MetricsSummaryOps:
7672
8005
  type: object
7673
8006
  description: >
7674
- 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server.ts; server
7675
- observability/metrics.ts `MetricsSummary` interface + `summarize()`, all keys unconditional).
7676
- Ops/observability face (`x-sdk: none` on the path) — closed here for census fidelity, but this
7677
- does NOT change its SDK-wrapping status.
8007
+ 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server `boot/stage-10-http-server`
8008
+ + `observability/metrics.ts` `MetricsSummary` / `summarize()`; every key unconditional).
8009
+ 🔴 OPEN as of sdk 9.7.0, and that is a RULING with a receipt: this schema was closed at the 2026-07-30
8010
+ census and by 7.87.0 the producer had moved THREE keys ahead of it (`limitCeilingAfterAnswer` since
8011
+ #677, `httpErrorsByClass` / `httpErrorsByRoute` since S-451) — i.e. a REAL summary body from a current
8012
+ server failed this schema outright, which is the same disease `LivePendingRow` shipped with. A
8013
+ RESPONSE PROJECTION is therefore declared key-by-key AND left open (the producer ships independently
8014
+ and adds keys additively between SDK releases); REQUEST bodies stay closed, because there the enforcer
8015
+ is the server's own strict gate.
8016
+ ⚠️ The SDK DOES wrap this face (`client.metrics.summary()` → `MetricsSummaryOps`, since the
8017
+ 2026-07-17 coverage batch). The old `x-sdk: none` key on the path said otherwise and was simply stale;
8018
+ it is gone. What remains true is the CREDENTIAL: a `metricsToken` (or a full authToken), not a principal.
7678
8019
  required:
7679
8020
  [model, runsActive, tasks, tokensTotal, costUsd, costUsdByModel, costUnknown, unpricedCalls,
7680
8021
  taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, promptCacheLowHit,
7681
8022
  rateLimited, costQuotaRejected, budgetExceeded, degraded, degenerate, memoryConsolidationOps,
7682
8023
  planCacheRecurrence, cascade, verifications, councilRuns, toolErrors]
7683
- additionalProperties: false
8024
+ additionalProperties: true
7684
8025
  properties:
7685
8026
  model: { type: string }
7686
8027
  runsActive: { type: integer }
@@ -7715,6 +8056,33 @@ components:
7715
8056
  verifications: { type: object, additionalProperties: { type: integer } }
7716
8057
  councilRuns: { type: object, additionalProperties: { type: integer } }
7717
8058
  toolErrors: { type: object, additionalProperties: { type: integer } }
8059
+ limitCeilingAfterAnswer:
8060
+ type: object
8061
+ additionalProperties: { type: integer }
8062
+ description: >-
8063
+ server #677 — answer-time ceiling hits (walltime/turns) by origin. ⚠️ A DIFFERENT DENOMINATOR from
8064
+ `budgetExceeded`: this one is minted on the deps-level notice seat and covers EVERY engine run
8065
+ (background children, delegated nested legs, the leader lane), while `budgetExceeded` is only
8066
+ incremented by the HTTP submit leg's `finalizeTaskResult`. Adding the two is only meaningful inside
8067
+ that narrower total.
8068
+ httpErrorsByClass:
8069
+ type: object
8070
+ additionalProperties: { type: integer }
8071
+ description: >-
8072
+ S-451 (server >= 7.87.0) — HTTP FAILURES in the window, aggregated by status CLASS. The keys are
8073
+ only `"4xx"` / `"5xx"` (2xx/3xx never enter). ⚠️ `"4xx"` INCLUDES `499` (the server's label for
8074
+ "client disconnected before the response finished", which is normal on SSE long-connections) — to
8075
+ look at a real storm, read `httpErrorsByRoute` alongside it. An EMPTY OBJECT means zero failures in
8076
+ the window (the key is always present; absence is NOT how zero is spelled). The whole key absent =
8077
+ an older server.
8078
+ httpErrorsByRoute:
8079
+ type: object
8080
+ additionalProperties: { type: integer }
8081
+ description: >-
8082
+ S-451 (server >= 7.87.0) — the TOP 10 route labels by failures (4xx+5xx combined), descending. The
8083
+ label is the low-cardinality form (ids folded to `:id`; anything off the route table lands on
8084
+ `"other"`). This is the "where did those 180 404s actually land" cell. Empty object = zero failures;
8085
+ key absent = an older server.
7718
8086
 
7719
8087
  ModelInfo:
7720
8088
  type: object
@@ -8528,7 +8896,18 @@ components:
8528
8896
  elapsedMs: { type: integer }
8529
8897
  tokens: { type: integer }
8530
8898
  # tokenDir REMOVED (engine ruling 2026-06-28): dead field, 0 producer / 0 consumer — spec caught up 2026-07-26.
8531
- queuedCount: { type: integer }
8899
+ queuedCount:
8900
+ type: integer
8901
+ description: >-
8902
+ 🔴 PHANTOM KEY — NO server version ever emits it (verified against the published 7.87.0 bytes:
8903
+ the row type declares it and the fleet route's per-key table forwards it, but NO producer writes it
8904
+ — not `publishTask`'s call sites, not core's bg-sink tick, not the durable reconcile leg — and the
8905
+ wire contract does not mention it either). The server deleted it as a DEAD KEY (S-459, zero
8906
+ behavioural impact since `!== undefined` was always false). It stays declared here only because
8907
+ removing an exported field is BREAKING = major-only; it is registered with its deletion condition
8908
+ in `test/_keyset-divergence.ts` (`FLEET_TASK_ROW_PHANTOM_KEYS`) and goes with the next major.
8909
+ DO NOT read it as "queue depth is already reported" — a permanently absent optional key is a FALSE
8910
+ LEAD. Real queue depth needs a NEW key WITH a producer, not this one resurrected.
8532
8911
  awaitingPlanApproval: { type: boolean }
8533
8912
  currentAction: { type: string, description: "The child's most recent tool intent as one human-readable line (UNTRUSTED argument head, redacted server-side)." }
8534
8913
  currentTool:
@@ -8883,9 +9262,21 @@ components:
8883
9262
  description: >-
8884
9263
  A live (in-stream) pending ask inside its streamApproval window (#315, server ≥7.43). Decide it via
8885
9264
  POST /v1/tool-approvals/{approvalId}/respond — NOT the /decide route (that is the durable rows' door).
8886
- Deliberately narrow: tool input/args are NOT listed (no second redaction surface — details ride the
8887
- SSE tool_approval frame). Optional flags are only-if-true (absent is NEVER encoded as false).
8888
- additionalProperties: false
9265
+ Optional flags are only-if-true (absent is NEVER encoded as false).
9266
+ 🔴 THE "tool input/args are NOT listed" NARROWING IS GONE (server >= 7.87.0 / S-454 P1). The old reason
9267
+ was "details ride the stream frame" — but a SUSPENDED ask never has a stream frame (at mint time it had
9268
+ no delivery target at all, so it suspends for a 1h window), and for it that reason is structurally
9269
+ false: without the card body on this row a consumer can list the ask and still not render it. The row
9270
+ therefore carries `frame` — the very card frame that ask WOULD have emitted — for EVERY live row (the
9271
+ rule set does not fork on `suspended`: an ask that does have a live stream carrying one extra copy of
9272
+ its own card is harmless).
9273
+ 🔴 THIS SCHEMA IS OPEN (`additionalProperties: true`) as of sdk 9.7.0, and that is a RULING, not a slip:
9274
+ it was closed, and when 7.87.0 minted `frame` every strict consumer stopped accepting a perfectly legal
9275
+ row — the whole `livePending` array became unreadable rather than "one field short". A producer that
9276
+ ships independently of this SDK adds keys additively between SDK releases, so a RESPONSE projection is
9277
+ declared key-by-key AND left open; the REQUEST bodies stay closed (there the enforcer is the server's
9278
+ own strict gate, so closing is description, not guesswork).
9279
+ additionalProperties: true
8889
9280
  required: [approvalId, toolName, ts, expiresAtMs]
8890
9281
  properties:
8891
9282
  approvalId: { type: string, description: 'The SAME id the respond endpoint takes (wire uuidv7).' }
@@ -8897,6 +9288,20 @@ components:
8897
9288
  governanceForced: { type: boolean, enum: [true] }
8898
9289
  fromSubagent: { type: boolean, enum: [true], description: 'The ask originates from a delegated subagent.' }
8899
9290
  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 (三键恒等亲证在案).' }
9291
+ contentKind: { type: string, description: 'S-62 (server >= 7.6x): content-question kind — `content_ask` when the gated tool is AskUserQuestion (the SAME word as the two durable read faces and core''s summarize face). Absent = an ordinary tool ask. Display/triage only; it NEVER takes part in decision routing.' }
9292
+ frame:
9293
+ allOf: [{ $ref: '#/components/schemas/ToolApprovalFrame' }]
9294
+ description: >-
9295
+ server >= 7.87.0 (S-454 P1; cli L-397) — THE CARD FRAME THIS ASK WOULD HAVE EMITTED. It is the SAME
9296
+ OBJECT as the server's `PendingApproval.frame` (the one construction site in `askBroadcast`, already
9297
+ redacted / byte-capped / window-keys filled), shared by the suspended and the reachable path — so
9298
+ this is NOT a second card projection computed for the list, and there is no second redaction surface.
9299
+ 🔴 INVARIANT: `frame.approvalId === row.approvalId` (verified at 7.87.0's merge review). When one ask
9300
+ was registered as pending more than once and the FIRST registration was cancelled, the frame's id
9301
+ used to point at the dead registration (responding by it 404s); the server now hands out a shallow
9302
+ copy with the id aligned to THIS row whenever the two differ. Always decide by the row's id.
9303
+ 🔴 ABSENT = an older server (<= 7.86.0); from 7.87.0 it is ALWAYS present (a required slot on the
9304
+ entry) and is never minted as `null`.
8900
9305
 
8901
9306
  ApprovalStaleCurrentPending:
8902
9307
  type: object
@@ -9502,12 +9907,38 @@ components:
9502
9907
  description: >
9503
9908
  ASSISTANT-WIRE-CONTRACT §4c — `POST /v1/assistant/tasks/:id/plan_review` body (3-state). `editedPlan` is
9504
9909
  REQUIRED iff `decision==="edit"` (FORBIDDEN otherwise → 400).
9910
+ 🔴 THE BODY KEY SET IS CLOSED (server >= 7.86.0 / S-438): an unrecognised top-level key is refused with
9911
+ 400 `request.body_shape` plus the `unknownKeys` / `unsupportedKeys` machine tables — refused rather
9912
+ than dropped, because a misspelled `permissionModeAfter` would otherwise silently leave the task
9913
+ read-only after its plan was approved.
9505
9914
  required: [decision]
9506
9915
  additionalProperties: false
9507
9916
  properties:
9508
9917
  decision: { type: string, enum: [approve, edit, reject] }
9509
9918
  editedPlan: { type: string, description: 'the operator''s revised plan — required iff decision==="edit".' }
9510
9919
  reason: { type: string }
9920
+ permissionModeAfter:
9921
+ type: string
9922
+ enum: [default, acceptEdits]
9923
+ description: >
9924
+ server >= 7.86.0 (S-433, P1; mints `routes/approvals-assistant.ts:1021/1024/1148`) — WHICH
9925
+ PERMISSION MODE THIS TASK CONTINUES IN once the plan is approved. Before this key, approving a plan
9926
+ was a BLANK CHEQUE: the plan was approved while the read-only clamp stayed on and every write kept
9927
+ asking. `acceptEdits` = writes stop asking; `default` = keep asking (AND is the folded value when
9928
+ the key is absent, so an older shell executes plans unchanged).
9929
+ 🔴 THREE 400s, each with its own code: a value outside the two-word closed set ⇒
9930
+ `request.field_invalid` (ZERO widening arm — this bit decides whether the model has hands after
9931
+ approval, so folding a misspelling into either mode is a silent downgrade on the security axis);
9932
+ sending it with `edit`/`reject` ⇒ `request.field_conflict` (core: those two send the plan back to be
9933
+ redone and the read-only clamp stays — dropping it there would let the caller believe the mode was
9934
+ settled); sending it for a task that was NOT submitted with `permissionMode:"plan"` ⇒
9935
+ `request.field_conflict` (approving the plan lifts no read-only restriction there).
9936
+ ⚠️ DIRECT-DOOR deployments: this key rides the HMAC envelope's TRAILING EXTENSION OBJECT (server
9937
+ 7.87.0: `{answer?, permissionModeAfter?}`, present keys in lexicographic order; 7.86.0's bare
9938
+ trailing slot is DELETED with zero double-read) ⇒ a signer must sign the new shape, and mixed
9939
+ versions are 401 in both directions.
9940
+ ⚠️ The key name is final: `permissionModeAfter` (the build brief once said `nextPermissionMode`,
9941
+ which never shipped — zero aliases).
9511
9942
 
9512
9943
  SessionRecord:
9513
9944
  type: object
@@ -9782,11 +10213,18 @@ components:
9782
10213
  description: >
9783
10214
  `GET /v1/sessions/{sessionId}/memory-status` 200 body (server >=7.53, S-53 seam①): `sessionId` + the
9784
10215
  five `SessionMemoryStatus` keys copied ONE BY ONE when present (the server forbids spreading and forbids
9785
- defaults — the five conditional copies ARE the closed set). Per-key absence meaning = exactly
10216
+ defaults — the five conditional copies ARE the closed set), plus the DEPLOYMENT bit
10217
+ `autoConsolidationArmed` (server >= 7.85.0, always present). Per-key absence meaning = exactly
9786
10218
  `SessionMemoryStatus` (a healthy zero-history session omits `optOutSource` and `lastCaptureAt`). Keys
9787
- are mirrored locally rather than via allOf so the closed schema stays self-describing (spec rule:
9788
- closed + allOf must mirror every key).
9789
- additionalProperties: false
10219
+ are mirrored locally rather than via allOf so the schema stays self-describing (spec rule: a closed
10220
+ schema plus allOf must mirror every key).
10221
+ 🔴 OPEN as of sdk 9.7.0 — same ruling, same receipt as `MetricsSummaryOps` / `LivePendingRow`: this was
10222
+ closed at S-53 and server 7.85.0 then added a sixth key, so a REAL body from a current server failed the
10223
+ schema. A response PROJECTION is declared key-by-key AND left open (the producer ships independently and
10224
+ adds keys additively between SDK releases); request bodies stay closed.
10225
+ ⚠️ SCOPE OF THAT RULING IN THIS BATCH: only the schemas this batch had to GROW were opened (the growth
10226
+ itself is the proof). Re-judging every other closed response schema is a separate car.
10227
+ additionalProperties: true
9790
10228
  required: [sessionId]
9791
10229
  properties:
9792
10230
  sessionId: { type: string, description: 'The session the status is about (echo of the path segment, percent-decoded).' }
@@ -9795,6 +10233,87 @@ components:
9795
10233
  foldedCount: { type: integer, description: 'See SessionMemoryStatus.foldedCount.' }
9796
10234
  optOutSource: { type: string, enum: [record, fault], description: 'See SessionMemoryStatus.optOutSource — absent = no opt-out record (healthy), never a fault by itself.' }
9797
10235
  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.' }
10236
+ autoConsolidationArmed:
10237
+ type: boolean
10238
+ description: >-
10239
+ server >= 7.85.0 (S-403 / core 7.22.0 #863 W1; mint `routes/sessions.ts:744` =
10240
+ `deps.memoryAutoConsolidationArmed`, computed ONCE at boot through core's single reader
10241
+ `autoRunOnRecommendationEffective`) — IS AUTOMATIC MEMORY CONSOLIDATION ARMED ON THIS DEPLOYMENT.
10242
+ 🔴 NOT one of the five core-derived session keys: those are per-SESSION derivations whose absence
10243
+ is meaningful per key; this one is a DEPLOYMENT fact and is ALWAYS PRESENT on >= 7.85.0. Absent =
10244
+ an older worker ⇒ "unknown"; NEVER fold it to `false` (that renders a box which is auto-egressing
10245
+ its whole memory store to the consolidation model as one that is not).
10246
+ 🔴 `true` = a terminal harvest that crosses the threshold auto-runs one host consolidation (memory
10247
+ content auto-egresses to the configured model). A DIFFERENT QUESTION from `captureOptedOut` (does
10248
+ THIS SESSION enter long-term memory) — do not read them together.
10249
+ The same fact is readable by an operator as `wiring_manifest.autoConsolidation` (present-iff armed);
10250
+ one fact, one source — the tenant face has only this key.
10251
+
10252
+ McpReconnectServerStatus:
10253
+ type: object
10254
+ additionalProperties: true
10255
+ description: >
10256
+ The `status` segment of `POST /v1/sessions/{sessionId}/mcp/reconnect` (server >= 7.85.0 / S-381; mint
10257
+ `routes/mcp-reconnect.ts` `projectStatus`) — core's `McpServerStatus` projected BY THIS FACE.
10258
+ ⚠️ The SAME core type is projected TWICE with DIFFERENT key sets and the two are deliberately NOT
10259
+ merged: the `/mcp` panel projects `{name, status, serverInfo?, toolNames?, error?}` only, while this
10260
+ face adds `errorCode` / `httpStatus` / `delivered` / `transportClosed`. Folding them would make the
10261
+ panel's consumers wait for four keys that never come.
10262
+ 🔴 BRANCH ON THE MACHINE FIELDS, NEVER PARSE `reason`: `errorCode` is core's `McpFailureKind` word
10263
+ (owner is the engine — switch with a default arm, do not narrow), `httpStatus` 401 is a re-authorize
10264
+ door, `spawn_failed` is a missing binary.
10265
+ required: [name, status]
10266
+ properties:
10267
+ name: { type: string }
10268
+ status: { type: string, description: 'connected | failed (open set — the word table owner is the engine).' }
10269
+ errorCode: { type: string, description: 'core `McpFailureKind`. Open set; switch with a default arm.' }
10270
+ httpStatus: { type: integer, description: 'Only alongside errorCode `http_status` (401/403 = re-authorize; 5xx = that end is down). ABSENT rather than 0.' }
10271
+ delivered: { type: string, enum: [yes, no, unknown], description: 'Did the request reach that server — read together with errorCode.' }
10272
+ serverInfo:
10273
+ type: object
10274
+ properties:
10275
+ name: { type: string }
10276
+ version: { type: string }
10277
+ error: { type: string, description: 'Remote-authored text: server-redacted and truncated to 200 chars. HUMAN-READ ONLY, never parsed.' }
10278
+ transportClosed: { type: boolean, description: 'Whether that transport is already closed (the old connection is always closed BEFORE the new dial — see the "one transaction" note on the operation).' }
10279
+
10280
+ SessionMcpReconnectResult:
10281
+ type: object
10282
+ additionalProperties: true
10283
+ description: >
10284
+ 200 body of `POST /v1/sessions/{sessionId}/mcp/reconnect` (server >= 7.85.0 / S-381 / core 7.22.0 #857;
10285
+ mint `routes/mcp-reconnect.ts` `projectResult`). Branch on `outcome` — all three states are HTTP 200.
10286
+ 🔴 The `unsupported` arm (this run declared NO MCP) is an ALWAYS-FIVE-KEY body
10287
+ `{taskId, sessionId, server, outcome, reason}`: with no core result the server coins no
10288
+ `prefix`/`toolCount`/`added`/`removed`/`toolNames`, so reading `added` unconditionally is a bug.
10289
+ 🔴 `toolNames` is PRESENT-IFF core supplied `tools` (not keyed off `outcome`) and is a SUPERSET of what
10290
+ the model finally sees (core filters again before mounting) — see the operation description.
10291
+ required: [taskId, sessionId, server, outcome]
10292
+ properties:
10293
+ taskId: { type: string, description: 'The live run this re-dial rode (this session''s active run) — follow it via GET /v1/runs/{taskId}/events.' }
10294
+ sessionId: { type: string, description: 'Echo of the path segment (percent-decoded).' }
10295
+ server: { type: string, description: 'The named server that was re-dialed.' }
10296
+ outcome: { type: string, description: 'accepted | refused | unsupported — the ONLY discriminator (all three are 200). `unsupported` is the SERVER''s word, not core''s.' }
10297
+ prefix: { type: string, description: 'The tool-name prefix domain of that server. Absent on the `unsupported` arm.' }
10298
+ toolCount: { type: integer, description: 'Tool count in that domain after the dial. Absent (never 0) on the `unsupported` arm.' }
10299
+ added: { type: array, items: { type: string } }
10300
+ removed: { type: array, items: { type: string } }
10301
+ toolNames:
10302
+ type: array
10303
+ items: { type: string }
10304
+ description: 'The roster THIS re-dial asserted for its own prefix domain (present-iff core supplied `tools`). A SUPERSET of what gets mounted; `[]` (failed dial) and ABSENT (never touched the connection) are different sentences.'
10305
+ reason: { type: string, description: 'Human sentence (server-redacted, 200-char bound). NEVER parse it — the machine face is `status`.' }
10306
+ status: { $ref: '#/components/schemas/McpReconnectServerStatus' }
10307
+ listingIncomplete:
10308
+ type: object
10309
+ additionalProperties: true
10310
+ description: 'The listing walk did not finish ⇒ do NOT treat `toolNames` as complete. `reason` is core''s word; `pages` is how many pages were walked.'
10311
+ required: [reason, pages]
10312
+ properties:
10313
+ reason: { type: string }
10314
+ pages: { type: integer }
10315
+ budgetMs: { type: integer }
10316
+ error: { type: string }
9798
10317
 
9799
10318
  # ── 2c session-sync (P1d) — the cloud-as-a-SYNC-PEER schemas ──────────────────────────────────────────────
9800
10319
  SyncEntry:
@@ -10042,6 +10561,12 @@ components:
10042
10561
  The two 501 families are deliberately NOT interchangeable: `capability.*` means this deployment did
10043
10562
  not wire that surface (change the deployment / hide the entry point), `feature.*` means the surface
10044
10563
  exists but its switch is off (ask an admin to turn it on).
10564
+ 🔴 `request.path_malformed` IS NOW THE WHOLE FAMILY'S ANSWER (server >= 7.86.0 / S-430): an id
10565
+ segment that is not valid percent-encoding answers 400 on EVERY id-bearing door. Twelve of those
10566
+ doors used to let the bare `decodeURIComponent` throw a `URIError` straight through the top-level
10567
+ catch and answered 500 `internal.error` — a caller-side mistake reported as a server fault, which
10568
+ also made it a retry-worthy-looking code. A consumer that branched on 500 for those paths must move
10569
+ the branch to the 400 (the fix is the caller's either way: re-encode the id).
10045
10570
  error: { type: string }
10046
10571
  errorMessage: { type: string }
10047
10572
  activeTaskId:
@@ -11178,6 +11703,41 @@ components:
11178
11703
  that wires none reports nothing at all. The server never invents `mounted: false` when it cannot
11179
11704
  read the bit — the whole section goes absent instead (a minted false would read as "really not
11180
11705
  mounted").
11706
+ writeProtection:
11707
+ allOf: [{ $ref: '#/components/schemas/WiringManifestWriteProtection' }]
11708
+ description: >
11709
+ core 7.20.1 (#853 / clay C-R55 丙), server >= 7.80.1 — TENANT-visible: which TARGET READING this
11710
+ leg's write-protection judge got. EFFECTIVE half only.
11711
+ 🔴 ABSENCE IS A THIRD STATE (core present-iff a judge was compiled; a deployment with
11712
+ `writeProtectedPaths: []` omits the whole section) — NEVER fold it to `"spelling-only"`, which
11713
+ means "a judge IS there and only reads spellings".
11714
+ ⚠️ Same name, different thing from `capabilities.writeProtection` / the diagnostics endpoint's
11715
+ `writeProtection`: those are THIS SERVER'S assembled NAME TABLE (a boot product); this is core's
11716
+ per-leg SELF-REPORT of the judge's reading.
11717
+ readDeny:
11718
+ allOf: [{ $ref: '#/components/schemas/WiringManifestReadDeny' }]
11719
+ description: >
11720
+ core 7.22.0 (#889 / C-R60), server >= 7.85.0 — OPERATOR face only (stripped on every tenant
11721
+ stream, and deliberately NOT carried by `GET /v1/diagnostics/wiring` either): which BUILT-IN
11722
+ sensitive-path read-deny TIERS materially activate rows on this deployment.
11723
+ 🔴 PRESENT IFF at least one built-in row judges this leg. Absence = no built-in row judges it (a
11724
+ POSITIVE fact), NOT "no read is deny-judged" — a deployment's own `READ_DENY_PATTERNS` is
11725
+ deliberately never reported (pattern TEXT is deployment material, not an enum).
11726
+ 🔴 AN EMPTY ARRAY IS NEVER EMITTED ("off" and "on to nothing" are one fact) ⇒ treat `[]` as a bad
11727
+ producer. The tier vocabulary is owned by core — do not narrow it on the consumer side.
11728
+ ⚠️ Deployment semantics flipped in core 7.22.0: `READ_DENY_BUILTIN_TIERS` UNSET = OFF.
11729
+ autoConsolidation:
11730
+ allOf: [{ $ref: '#/components/schemas/WiringManifestAutoConsolidation' }]
11731
+ description: >
11732
+ core 7.21.0 (#761), server >= 7.82.0 — OPERATOR face only (stripped on every tenant stream): is
11733
+ automatic memory consolidation ARMED.
11734
+ 🔴 PRESENT IFF armed, and the only present shape is `{ onRecommendation: true }` (core's construct
11735
+ gate refuses an armed-but-unrunnable state). ABSENT = NOT ARMED (a positive fact) — never read it
11736
+ as `false`, never as "unknown". Armed means a terminal harvest over the threshold auto-runs one
11737
+ host consolidation: memory content auto-egresses to the configured model, which is why it sits in
11738
+ the operator family beside `governance`.
11739
+ ⚠️ The tenant-readable twin of the same fact is `GET /v1/sessions/{sessionId}/memory-status`'s
11740
+ `autoConsolidationArmed` (always present, a real boolean). One fact, one source, two faces.
11181
11741
  governance:
11182
11742
  type: object
11183
11743
  additionalProperties: false
@@ -14105,6 +14665,16 @@ components:
14105
14665
  WiringManifest:
14106
14666
  type: object
14107
14667
  description: >
14668
+ 🔴 ONE RULE FOR EVERY "EFFECTIVE HALF ONLY" KEY (sdk 9.7.0, after a codex adversarial-review [high]):
14669
+ this schema's only consumer today is `GET /v1/diagnostics/wiring`'s `static` half, and every key marked
14670
+ "effective half only" (`leg`, `ask.effective`, `configFingerprint`, `tools`, `hooks`, `lsp`,
14671
+ `modelGate`, `autoMode`, `writeProtection`, `readDeny`, `autoConsolidation`) is ALWAYS ABSENT on that
14672
+ wire ⇒ THEIR ABSENCE HERE CARRIES NO INFORMATION. The per-key absence readings ("absent = not armed",
14673
+ "absent = a third state", …) hold for the LIVE FRAME (`Event_wiring_manifest`) and must not be carried
14674
+ over to this response. For the facts behind those keys: a tenant reads
14675
+ `GET /v1/sessions/{sessionId}/memory-status` (`autoConsolidationArmed`); an operator subscribes to that
14676
+ leg's live stream (the governance segment and these three sections exist only on a projection that
14677
+ KNOWS the caller's identity).
14108
14678
  core's assembly self-evidence manifest (core >=5.14.0 `WiringManifest`). TWO HALVES, ONE shape (a
14109
14679
  consumer must line the two faces up, so they are not split into separate types) — but which keys
14110
14680
  belong to which half is DETERMINATE, not "usually": `leg`, `ask.effective` and `configFingerprint`
@@ -14248,6 +14818,78 @@ components:
14248
14818
  core 7.18.0 (#789), server >= 7.78.0 — the LSP seam. EFFECTIVE half only; present iff a manager
14249
14819
  seat resolved. `mounted` and `lane` are INDEPENDENT facts — never fold them into one.
14250
14820
  Full per-key contract: see Event_wiring_manifest.lsp.
14821
+ writeProtection:
14822
+ allOf: [{ $ref: '#/components/schemas/WiringManifestWriteProtection' }]
14823
+ description: >
14824
+ core 7.20.1 (#853), server >= 7.80.1 — TENANT-visible: which target reading this leg's
14825
+ write-protection judge got. EFFECTIVE HALF ONLY ⇒ ALWAYS ABSENT ON THIS ENDPOINT, and absence
14826
+ HERE carries no information (see this schema's own description for the one rule that governs
14827
+ every effective-half-only key). The three-state reading ("absence = no judge at all, never fold
14828
+ it to `spelling-only`") is the LIVE FRAME's reading — do not apply it to this response.
14829
+ Full per-key contract: see Event_wiring_manifest.writeProtection.
14830
+ readDeny:
14831
+ allOf: [{ $ref: '#/components/schemas/WiringManifestReadDeny' }]
14832
+ description: >
14833
+ core 7.22.0 (#889), server >= 7.85.0 — OPERATOR face only, effective half only, AND the server
14834
+ explicitly rules it off this endpoint ⇒ always absent here; absence HERE says nothing about the
14835
+ deployment's tiers. The present-iff / never-empty-array reading belongs to the operator LIVE
14836
+ projection. Full per-key contract: see Event_wiring_manifest.readDeny.
14837
+ autoConsolidation:
14838
+ allOf: [{ $ref: '#/components/schemas/WiringManifestAutoConsolidation' }]
14839
+ description: >
14840
+ core 7.21.0 (#761), server >= 7.82.0 — OPERATOR face only, EFFECTIVE HALF ONLY ⇒ ALWAYS ABSENT ON
14841
+ THIS ENDPOINT. 🔴 DO NOT read absence here as "not armed": that renders a deployment which IS
14842
+ auto-egressing its whole memory store as one that is not. The present-iff-armed reading belongs to
14843
+ the operator LIVE projection; for "is this deployment armed", read
14844
+ `GET /v1/sessions/{sessionId}/memory-status`'s `autoConsolidationArmed` (always present, a real
14845
+ boolean). Full per-key contract: see Event_wiring_manifest.autoConsolidation.
14846
+ WiringManifestWriteProtection:
14847
+ # sdk 9.7.0:与 `WiringManifestMcpEntry` / `WiringManifestLspSeam` 同一条裁定 —— 同一段形被 live 帧与
14848
+ # 静态诊断面两处引用,内联就是两份会各自漂的镜像,所以具名单源。
14849
+ type: object
14850
+ additionalProperties: false
14851
+ description: >
14852
+ The `wiring_manifest.writeProtection` section (core 7.20.1 #853 / clay C-R55 丙) — which TARGET
14853
+ READING this leg's write-protection judge got. `"target"` = the judge holds the execution environment
14854
+ and re-judges on the RESOLVED target when the spelling missed (a write that reaches a protected row
14855
+ through an alias or a symlink is cleared); `"spelling-only"` = no environment, the spelling IS the
14856
+ whole verdict. ABSENCE (the section itself) is a THIRD state — no judge was compiled at all.
14857
+ required: [targetView]
14858
+ properties:
14859
+ targetView: { type: string, description: "core's two-word closed set (`spelling-only` / `target`); read as open — the vocabulary owner is the engine." }
14860
+
14861
+ WiringManifestReadDeny:
14862
+ type: object
14863
+ additionalProperties: false
14864
+ description: >
14865
+ The `wiring_manifest.readDeny` section (core 7.22.0 #889 / C-R60) — the BUILT-IN sensitive-path
14866
+ read-deny tiers that MATERIALLY activate rows on this leg, in core's own tier order (NOT the tiers the
14867
+ operator typed: one whose every row the exclude list removed is not named). OPERATOR audience.
14868
+ required: [builtinTiers]
14869
+ properties:
14870
+ builtinTiers:
14871
+ type: array
14872
+ minItems: 1
14873
+ items: { type: string }
14874
+ description: >
14875
+ Tier names, core-owned vocabulary (today `credentials` / `shell-history` / `browser` / `wallet` /
14876
+ `agent-config`) — passed through verbatim, never narrowed here. AN EMPTY ARRAY IS NEVER EMITTED.
14877
+
14878
+ WiringManifestAutoConsolidation:
14879
+ type: object
14880
+ additionalProperties: false
14881
+ description: >
14882
+ The `wiring_manifest.autoConsolidation` section (core 7.21.0 #761) — PRESENT IFF automatic memory
14883
+ consolidation is EFFECTIVELY armed on this deployment. OPERATOR audience: armed means memory content
14884
+ auto-egresses to the configured consolidation model after a qualifying terminal harvest, and it is not
14885
+ observable to the caller at all (the run happens host-side, fire-and-forget, after the harvest).
14886
+ required: [onRecommendation]
14887
+ properties:
14888
+ onRecommendation:
14889
+ type: boolean
14890
+ enum: [true]
14891
+ description: "core's ONLY present shape — the construct gate refuses an armed-but-unrunnable state, so this is a SHAPE, not a value."
14892
+
14251
14893
  WiringManifestHookEntry:
14252
14894
  # sdk 9.5.0:与 `WiringManifestMcpEntry` 同一条裁定 —— 同一份行形被 live 帧与静态诊断面两处引用,
14253
14895
  # 内联就是两份会各自漂的镜像,所以一开始就具名。