@sema-agent/sdk 9.5.0 → 9.7.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 (42) hide show
  1. package/README.md +61 -1
  2. package/dist/errors.d.ts +20 -2
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +20 -2
  5. package/dist/errors.js.map +1 -1
  6. package/dist/events.d.ts +43 -3
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/events.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/resources/approvals.d.ts +20 -2
  13. package/dist/resources/approvals.d.ts.map +1 -1
  14. package/dist/resources/approvals.js.map +1 -1
  15. package/dist/resources/assistant.d.ts +8 -1
  16. package/dist/resources/assistant.d.ts.map +1 -1
  17. package/dist/resources/assistant.js +8 -1
  18. package/dist/resources/assistant.js.map +1 -1
  19. package/dist/resources/fleet.d.ts +15 -1
  20. package/dist/resources/fleet.d.ts.map +1 -1
  21. package/dist/resources/fleet.js.map +1 -1
  22. package/dist/resources/ops.d.ts +29 -5
  23. package/dist/resources/ops.d.ts.map +1 -1
  24. package/dist/resources/ops.js +12 -1
  25. package/dist/resources/ops.js.map +1 -1
  26. package/dist/resources/sessions.d.ts +23 -1
  27. package/dist/resources/sessions.d.ts.map +1 -1
  28. package/dist/resources/sessions.js +27 -0
  29. package/dist/resources/sessions.js.map +1 -1
  30. package/dist/resources/tool-approvals.d.ts +29 -1
  31. package/dist/resources/tool-approvals.d.ts.map +1 -1
  32. package/dist/resources/tool-approvals.js +2 -0
  33. package/dist/resources/tool-approvals.js.map +1 -1
  34. package/dist/settings.d.ts +32 -1
  35. package/dist/settings.d.ts.map +1 -1
  36. package/dist/sse.d.ts +14 -3
  37. package/dist/sse.d.ts.map +1 -1
  38. package/dist/sse.js.map +1 -1
  39. package/dist/types.d.ts +453 -9
  40. package/dist/types.d.ts.map +1 -1
  41. package/openapi.yaml +738 -35
  42. package/package.json +1 -1
package/openapi.yaml CHANGED
@@ -83,6 +83,25 @@ info:
83
83
  • New contract fields are OPTIONAL with no default (CORE-PRINCIPLES §6).
84
84
  • Async results are delivered by POLL (`GET /v1/runs/:id`) or SSE (`/v1/runs/:id/events`).
85
85
  There is NO generic webhook/callback and NO signing (SERVICE-CORE-CONTEXT §2.4).
86
+ • 🔴 STRING-LENGTH UNIT (applies to EVERY `maxLength`/`minLength` in this file). The producer's own
87
+ gates are JavaScript `.length`, i.e. **UTF-16 code units**; JSON Schema counts **code points**.
88
+ The two agree across the entire BMP and differ only on supplementary characters (emoji, rare CJK,
89
+ musical symbols), each of which costs 2 units but 1 code point. => near a bound, a value made of
90
+ supplementary characters can pass this schema and still be refused by the producer. THAT DIRECTION
91
+ IS DELIBERATE: a schema looser than the producer yields a loud, readable 4xx naming the field, while
92
+ a schema tighter than the producer makes a client refuse a request the producer would have accepted —
93
+ an invisible false refusal whose symptom is indistinguishable from a server rejection (this contract
94
+ shipped exactly that defect on `SkillSpec.content` for several releases). Halving every bound to be
95
+ safe for all-supplementary input would refuse roughly half of the legal range on ordinary text, so the
96
+ bounds here state the producer's number verbatim and this convention states the unit. A client that
97
+ must pre-check exactly counts UTF-16 units itself.
98
+ (`maxLength` is the only keyword involved: it counts code points and that is not configurable. A
99
+ `pattern` could count code units — but only on validators whose regex engine is UTF-16-based and
100
+ non-unicode-mode, which is implementation-specific rather than contract-portable, and it would cost a
101
+ full regex scan of a megabyte-scale payload. The unit is therefore stated, not encoded.)
102
+ • The same unit applies to every BYTE-sounding phrase in this file unless it says UTF-8 explicitly: the
103
+ producer's guards are character/unit counts, so a non-ASCII value is NOT twice as expensive against
104
+ them. Do not pre-check these bounds with a byte length.
86
105
  x-sources:
87
106
  - ../docs/SERVICE-CORE-CONTEXT.md # §2 wire answers, §4 verb surface (authoritative producer input)
88
107
  - ../docs/CORE-PRINCIPLES.md # §2 secret discipline, §3 principal-first, §5 contract-as-anchor, §6 optional-no-default
@@ -2060,6 +2079,77 @@ paths:
2060
2079
  schema: { $ref: '#/components/schemas/McpStatusPanel' }
2061
2080
  '401': { $ref: '#/components/responses/Unauthorized' }
2062
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
+
2063
2153
  /v1/sessions/{sessionId}/memory-status:
2064
2154
  parameters:
2065
2155
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -2559,9 +2649,24 @@ paths:
2559
2649
  `data.type` — proxy-safe dispatch. Cross-replica by
2560
2650
  construction (polls the SHARED checkpoint table). 15-min cap + heartbeats; a DB blip retries, never
2561
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).
2562
2667
  responses:
2563
2668
  '200':
2564
- description: text/event-stream of meta/pending/resolved frames.
2669
+ description: text/event-stream of meta/pending/resolved/live_pending/live_resolved frames.
2565
2670
  content:
2566
2671
  text/event-stream:
2567
2672
  schema: { type: string }
@@ -2602,9 +2707,15 @@ paths:
2602
2707
  string; `remember` not `"session"`; `checkpointToken`/`boundCallId`/`boundInputHash` not a string)
2603
2708
  · "request.field_conflict" (`remember` without `decision:"approve"`; TC-5.4 answer coherence —
2604
2709
  approving a pending AskUserQuestion REQUIRES `answer`, and `answer` is only valid on that approve)
2605
- · "remember_not_in_proof" / "updated_input_not_in_proof" (`remember` / `updatedInput` on a
2606
- direct-door worker — not covered by the decision proof) · "remember_requires_binding" (`remember`
2607
- 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):
2608
2719
  `decide.parked_answer_required` — the parked child's pending action is an AskUserQuestion and the
2609
2720
  approve carried no `answer` (the ONLY pre-claim rejection left on that leg; the retired codes
2610
2721
  `decide.parked_answer_unsupported` / `decide.parked_question_unsupported` are gone — an answer HAS
@@ -2996,8 +3107,15 @@ paths:
2996
3107
  schema: { $ref: '#/components/schemas/AssistantTaskStatus' }
2997
3108
  '400':
2998
3109
  description: >
2999
- Bad/missing `decision`; `edit` without a non-empty `editedPlan`; `editedPlan` present on
3000
- 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.
3001
3119
  content:
3002
3120
  application/json:
3003
3121
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -5014,11 +5132,16 @@ paths:
5014
5132
  tags: [metrics]
5015
5133
  operationId: metricsSummary
5016
5134
  x-status: draft # ops surface; uses a metricsToken (NOT a principal). Shape loose.
5017
- 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.
5018
5135
  summary: Worker health/cost summary (ops).
5019
5136
  description: >
5020
- Uses `metricsToken` (operator), not `x-agent-principal`. Shape is loose/ops-defined. Deliberately
5021
- 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.
5022
5145
  security:
5023
5146
  - metricsToken: []
5024
5147
  responses:
@@ -5026,11 +5149,9 @@ paths:
5026
5149
  description: Metrics summary (shape draft).
5027
5150
  content:
5028
5151
  application/json:
5029
- # 🔴 census 批2 五段:CLOSED against the real emitter — `sendJson(res, 200, { model,
5030
- # ...deps.metrics.summarize() })` (server.ts) is a fixed key set (server observability/
5031
- # metrics.ts `MetricsSummary`), not an ops free-for-all. Left `x-status: draft` / `x-sdk: none`
5032
- # untouched (still deliberately outside the SDK's wrapped surface) — closing the SHAPE and
5033
- # 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 里(投影型响应体开集;请求体仍封闭)。
5034
5155
  schema: { $ref: '#/components/schemas/MetricsSummaryOps' }
5035
5156
  '401': { $ref: '#/components/responses/Unauthorized' }
5036
5157
 
@@ -5297,6 +5418,20 @@ components:
5297
5418
  waitMs: { type: integer }
5298
5419
  decision: { type: string }
5299
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.
5300
5435
  required: [turns, tokens]
5301
5436
  # 🔴 OPEN: the engine may carry fields not named here (live-observed, e.g. extra costBreakdown axes) — the SDK
5302
5437
  # passes them through rather than dropping, so consumers must not assume this list is exhaustive.
@@ -5418,11 +5553,16 @@ components:
5418
5553
  Server cap 16384 chars → 400.
5419
5554
  skills:
5420
5555
  type: array
5421
- maxItems: 10
5422
5556
  description: >
5423
5557
  Per-request user skills (LIVE, service 170c384) — progressive disclosure, same
5424
5558
  mechanism as scenario skills. Merge: scenario WINS on name collision (user's dropped, warn-logged).
5425
- Caps server-enforced; violations 400 with the offending rule named.
5559
+ 🔴 server >= 7.78.1 (hard BREAKING, no alias): the ITEM-COUNT cap (was 10), the `description` 1024-char
5560
+ cap and the `name` 64-char cap are GONE — and so are their three refusal messages, so a consumer branch
5561
+ anchored on them can be deleted (a refusal turned into an acceptance: no harm in the other direction).
5562
+ The only size gate left on this face is `SkillSpec.content` <= 1 MiB (the engine's own load gate);
5563
+ bulk is otherwise bounded by the NON-skill-specific 8 MiB request-body cap. Per-turn cost belongs to the
5564
+ LISTING RENDERER, not to this gate: turn it down with `settings.skillListingBudgetFraction` /
5565
+ `settings.skillListingMaxDescChars`.
5426
5566
  items: { $ref: '#/components/schemas/SkillSpec' }
5427
5567
  reasoningEffort:
5428
5568
  type: string
@@ -5681,6 +5821,33 @@ components:
5681
5821
  `memory.capture_optout_denied` (terminal). A worker older than 7.70.0 IGNORES the key silently, so
5682
5822
  it is not a capability probe. Rides the persisted body onto resume legs.
5683
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.
5684
5851
  settings:
5685
5852
  type: object
5686
5853
  description: >
@@ -5690,7 +5857,24 @@ components:
5690
5857
  service ACTIVELY interprets this bundle (task-settings.ts: permissions + permissions.defaultMode +
5691
5858
  model + outputStyle + env shipped in the 1.26.0 v1 scope; hooks was deferred at that point and wired
5692
5859
  in a later batch — both are live and interpreted today, not a raw uninterpreted passthrough);
5693
- `permissions.defaultMode`'s specific fold is documented on `TaskRequest.permissionMode`. OPEN object.
5860
+ `permissions.defaultMode`'s specific fold is documented on `TaskRequest.permissionMode`.
5861
+ 🔴 ON THIS SUBMIT FACE THE KEY SET IS CLOSED (server >= 7.57.0 for the sub-tree): accepted top-level keys
5862
+ are `env` / `hooks` / `model` / `outputStyle` / `permissions` / `skillListingBudgetFraction` /
5863
+ `skillListingMaxDescChars` / `ultracode` / `webSearch`, and accepted `permissions.*` keys are
5864
+ `allow` / `ask` / `defaultMode` / `deny` / `disableAutoMode`; anything else is 400 `request.body_shape`
5865
+ with the names in the refusal's `unknownKeys` / `unsupportedKeys` arrays (REFUSED, not dropped — a dropped
5866
+ key reads as "in effect" to the caller). The object stays OPEN here because the same `SemaSettings`
5867
+ schema also serves the local `settings.json` wiring, where deferred CC keys are legal.
5868
+ 🔴 `skillListingBudgetFraction` (number in (0,1]) / `skillListingMaxDescChars` (positive integer),
5869
+ server >= 7.78.1: the SKILL-LISTING RENDER BUDGET, passed to the engine with ZERO processing
5870
+ (no clamping, no conversion, no folding) — ABSENT means the key is not written at all (the server does
5871
+ NOT restate the engine defaults, CC-identical `0.01` / `1536`), and an out-of-domain value is
5872
+ 400 `request.field_invalid` on the fresh leg (the resume replay leg drops it instead of 4xx).
5873
+ ⚠️ `skillListingBudgetFraction` is a ONE-WAY knob — turning it DOWN works, turning it UP does not:
5874
+ the engine's listing budget is `min(window x fraction, 8000 bytes)` and 8000 is a STRUCTURAL ceiling of
5875
+ the delivery lane, so raising it only helps a model whose declared window is below 200 000 tokens.
5876
+ The engine's own twin code `config.skill_listing_budget_invalid` (400) is NOT reachable through this
5877
+ submit leg today — it belongs to callers that build a `TaskSpec` directly.
5694
5878
  additionalProperties: true
5695
5879
  cwd: { type: string, description: Working directory for the execution env. }
5696
5880
  selfOrchestration: { type: boolean, description: Permit S8 self-orchestration (run_workflow) inside this task. }
@@ -6620,9 +6804,106 @@ components:
6620
6804
  properties:
6621
6805
  id: { type: string }
6622
6806
  status: { type: string, enum: [running, completed, failed, needs_human] }
6623
- 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`.)
6624
6814
  error: { type: string }
6625
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
+
6626
6907
  ApprovalDecision:
6627
6908
  type: object
6628
6909
  description: HITL decision (`POST /v1/approvals/:sessionId/decide`).
@@ -6634,12 +6915,23 @@ components:
6634
6915
  AskUserQuestion answer — TRUE WIRE SHAPE (worker-validated, 400 otherwise; found in prod
6635
6916
  integration, service-pinned): {answers:[{header, selected:string[], note?}]} where header matches
6636
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).
6637
6927
  type: object
6928
+ additionalProperties: false
6638
6929
  properties:
6639
6930
  answers:
6640
6931
  type: array
6641
6932
  items:
6642
6933
  type: object
6934
+ additionalProperties: false
6643
6935
  required: [header, selected]
6644
6936
  properties:
6645
6937
  header: { type: string }
@@ -7287,6 +7579,66 @@ components:
7287
7579
  `WriteProtectionCapability`). `null` = this process cannot say; an ABSENT key means an OLDER worker,
7288
7580
  and "no key" is NOT "no table" — an absent seat on the engine side is precisely the DEFAULT TABLE
7289
7581
  BEING IN PLACE.
7582
+ webSearch:
7583
+ type: object
7584
+ additionalProperties: true
7585
+ description: >
7586
+ server >= 7.82.1 (S-382; mint `routes/capabilities.ts:236` =
7587
+ `projectWebSearchCapability(deps.webSearchProvider)`) — THIS DEPLOYMENT'S DEFAULT WEBSEARCH BACKEND.
7588
+ The closed set is TYPED FROM the server's `WEB_SEARCH_PROVIDERS` tuple (`plugins/web-search.ts:70`,
7589
+ the repo's single provider word table) plus `"none"`; the predicate is the very
7590
+ `webSearchConfigFromEnv()` product boot USED TO BUILD the backend, so "the bit says brave while
7591
+ tavily is wired" is structurally impossible.
7592
+ 🔴 `backend:"none"` AND AN ABSENT KEY ARE OPPOSITE DISPOSITIONS — never fold them: `"none"` = this
7593
+ box has NO search backend (render "unavailable"); ABSENT = an older worker (< 7.82.1) ⇒ render
7594
+ "cannot tell". Reading absence as `"none"` renders a correctly-configured older deployment as
7595
+ having no search.
7596
+ 🔴 NO INTERNALS: the endpoint (a SearXNG instance URL), the API key and the quota never ride this
7597
+ wire, and the per-request `settings.webSearch` override is deliberately NOT echoed — this bit
7598
+ advertises the DEPLOYMENT DEFAULT and must not drift with the caller.
7599
+ required: [backend]
7600
+ properties:
7601
+ 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.' }
7602
+ mcpReconnect:
7603
+ type: boolean
7604
+ description: >
7605
+ server >= 7.85.0 (S-381 / core 7.22.0 #857; mint `routes/capabilities.ts:264` =
7606
+ `Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf)`) — presence of
7607
+ `POST /v1/sessions/{sessionId}/mcp/reconnect`. FACILITY DEPENDENCY, two conjuncts: a durable run
7608
+ ledger (the verb reaches the live stream through the run row) AND the session-ownership face (the
7609
+ inbound owner gate is fail-closed; without it the whole door is 501). Both 501s derive from the SAME
7610
+ deps as this bit, so "says yes but 501s" is structurally impossible.
7611
+ 🔴 True only promises the ENDPOINT IS THERE: not that there is a live run right now (otherwise 409
7612
+ `steering.not_running`), and not that this run declared any MCP server at all (undeclared ⇒ HTTP 200
7613
+ with `outcome:"unsupported"`, which is not an error). ABSENT = an older worker ⇒ probe by 404/405,
7614
+ do not render a dead affordance.
7615
+ executionLane:
7616
+ type: object
7617
+ additionalProperties: true
7618
+ description: >
7619
+ server >= 7.86.0 (S-426; mint `routes/capabilities.ts:602` =
7620
+ `projectExecutionLaneCapability(deps.config)`) — THE EXECUTION LANE, SELF-REPORTED. Before it, "do
7621
+ this engine's tools run on THIS machine" could only be guessed from a three-way circumstantial
7622
+ conjunction (shell spawned it AND no remote URL AND `REMOTE_EXEC` in {unset, host}) — none of which
7623
+ the engine said, and which drifts the moment the shell talks to a remote engine (it reads ITS OWN
7624
+ box's env).
7625
+ `provider` is typed from the server's `RemoteExecProvider` (`execution-lane-caps.ts:50`, derived
7626
+ from the `ServiceConfigFlat["remoteExec"]` discriminated union); `REMOTE_EXEC` UNSET ⇒ `"host"` is
7627
+ CONTRACT, not a fallback. Honest cost: the server internally has a finer word (`in-process` — that
7628
+ lane mounts NO hands at all) which this bit folds into `host`, so a consumer cannot tell "hands on
7629
+ this box" from "no hands at all"; that is a DIFFERENT AXIS, not a reason to re-point the lane word.
7630
+ `toolsOnThisHost` is owned by `task-cwd.ts`'s `toolsRunOnThisHost` — the SAME FUNCTION that decides
7631
+ whether a self-scanned skill gets a `baseDir`.
7632
+ 🔴 THE IMPLICATION IS ONE-WAY: `toolsOnThisHost:false` ⇒ `skills[].baseDir` is never sent (that is
7633
+ the promise); the converse does NOT hold — `true` is only necessary (a flat `skills/<name>.md` has
7634
+ no directory of its own; plugin-sourced skills go through a loader that never passes the switch),
7635
+ so under `true` read `baseDir` as PRESENT-IFF.
7636
+ 🔴 NO INTERNALS: k8s apiUrl / ssh host / adb serial / image / credentials / mountPath never ride here.
7637
+ An ABSENT key must NOT be read as either side — that is exactly the state this bit was minted to end.
7638
+ required: [provider, toolsOnThisHost]
7639
+ properties:
7640
+ provider: { type: string, description: 'host | e2b | k8s | ssh | adb | local-docker | device — typed from the server''s RemoteExecProvider; `REMOTE_EXEC` unset ⇒ `host`.' }
7641
+ toolsOnThisHost: { type: boolean, description: 'The tools run on the process that answered this request (server-absolute paths mean something to the model''s tools).' }
7290
7642
  WriteProtectionCapability:
7291
7643
  type: object
7292
7644
  additionalProperties: false
@@ -7413,12 +7765,19 @@ components:
7413
7765
 
7414
7766
  SkillSpec:
7415
7767
  type: object
7416
- description: A per-request skill (passed as an object; core-native TaskSpec.skills shape).
7768
+ description: >-
7769
+ A per-request skill (passed as an object; core-native TaskSpec.skills shape).
7770
+ 🔴 server >= 7.78.1: `name` / `description` carry NO length cap any more (the engine's listing renderer
7771
+ trims descriptions and falls back to name-only within its own byte budget — it never REFUSES a skill for
7772
+ being long). `content` keeps the one real gate: 1 MiB, the twin of the engine's load gate (over it the
7773
+ engine refuses the skill outright at load time, so the HTTP face answers first where the caller can read it).
7774
+ The bound is 1 048 576 **UTF-16 code units** (`MAX_SKILL_CONTENT_CHARS`); see the STRING-LENGTH UNIT
7775
+ convention in `info.description` for why this `maxLength` states that number verbatim.
7417
7776
  required: [name, description, content]
7418
7777
  properties:
7419
- name: { type: string, maxLength: 64 }
7420
- description: { type: string, maxLength: 1024 }
7421
- content: { type: string, maxLength: 32768 }
7778
+ name: { type: string, minLength: 1 }
7779
+ description: { type: string }
7780
+ content: { type: string, minLength: 1, maxLength: 1048576 }
7422
7781
 
7423
7782
  McpServerSpec:
7424
7783
  type: object
@@ -7623,16 +7982,24 @@ components:
7623
7982
  MetricsSummaryOps:
7624
7983
  type: object
7625
7984
  description: >
7626
- 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server.ts; server
7627
- observability/metrics.ts `MetricsSummary` interface + `summarize()`, all keys unconditional).
7628
- Ops/observability face (`x-sdk: none` on the path) — closed here for census fidelity, but this
7629
- does NOT change its SDK-wrapping status.
7985
+ 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server `boot/stage-10-http-server`
7986
+ + `observability/metrics.ts` `MetricsSummary` / `summarize()`; every key unconditional).
7987
+ 🔴 OPEN as of sdk 9.7.0, and that is a RULING with a receipt: this schema was closed at the 2026-07-30
7988
+ census and by 7.87.0 the producer had moved THREE keys ahead of it (`limitCeilingAfterAnswer` since
7989
+ #677, `httpErrorsByClass` / `httpErrorsByRoute` since S-451) — i.e. a REAL summary body from a current
7990
+ server failed this schema outright, which is the same disease `LivePendingRow` shipped with. A
7991
+ RESPONSE PROJECTION is therefore declared key-by-key AND left open (the producer ships independently
7992
+ and adds keys additively between SDK releases); REQUEST bodies stay closed, because there the enforcer
7993
+ is the server's own strict gate.
7994
+ ⚠️ The SDK DOES wrap this face (`client.metrics.summary()` → `MetricsSummaryOps`, since the
7995
+ 2026-07-17 coverage batch). The old `x-sdk: none` key on the path said otherwise and was simply stale;
7996
+ it is gone. What remains true is the CREDENTIAL: a `metricsToken` (or a full authToken), not a principal.
7630
7997
  required:
7631
7998
  [model, runsActive, tasks, tokensTotal, costUsd, costUsdByModel, costUnknown, unpricedCalls,
7632
7999
  taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, promptCacheLowHit,
7633
8000
  rateLimited, costQuotaRejected, budgetExceeded, degraded, degenerate, memoryConsolidationOps,
7634
8001
  planCacheRecurrence, cascade, verifications, councilRuns, toolErrors]
7635
- additionalProperties: false
8002
+ additionalProperties: true
7636
8003
  properties:
7637
8004
  model: { type: string }
7638
8005
  runsActive: { type: integer }
@@ -7667,6 +8034,33 @@ components:
7667
8034
  verifications: { type: object, additionalProperties: { type: integer } }
7668
8035
  councilRuns: { type: object, additionalProperties: { type: integer } }
7669
8036
  toolErrors: { type: object, additionalProperties: { type: integer } }
8037
+ limitCeilingAfterAnswer:
8038
+ type: object
8039
+ additionalProperties: { type: integer }
8040
+ description: >-
8041
+ server #677 — answer-time ceiling hits (walltime/turns) by origin. ⚠️ A DIFFERENT DENOMINATOR from
8042
+ `budgetExceeded`: this one is minted on the deps-level notice seat and covers EVERY engine run
8043
+ (background children, delegated nested legs, the leader lane), while `budgetExceeded` is only
8044
+ incremented by the HTTP submit leg's `finalizeTaskResult`. Adding the two is only meaningful inside
8045
+ that narrower total.
8046
+ httpErrorsByClass:
8047
+ type: object
8048
+ additionalProperties: { type: integer }
8049
+ description: >-
8050
+ S-451 (server >= 7.87.0) — HTTP FAILURES in the window, aggregated by status CLASS. The keys are
8051
+ only `"4xx"` / `"5xx"` (2xx/3xx never enter). ⚠️ `"4xx"` INCLUDES `499` (the server's label for
8052
+ "client disconnected before the response finished", which is normal on SSE long-connections) — to
8053
+ look at a real storm, read `httpErrorsByRoute` alongside it. An EMPTY OBJECT means zero failures in
8054
+ the window (the key is always present; absence is NOT how zero is spelled). The whole key absent =
8055
+ an older server.
8056
+ httpErrorsByRoute:
8057
+ type: object
8058
+ additionalProperties: { type: integer }
8059
+ description: >-
8060
+ S-451 (server >= 7.87.0) — the TOP 10 route labels by failures (4xx+5xx combined), descending. The
8061
+ label is the low-cardinality form (ids folded to `:id`; anything off the route table lands on
8062
+ `"other"`). This is the "where did those 180 404s actually land" cell. Empty object = zero failures;
8063
+ key absent = an older server.
7670
8064
 
7671
8065
  ModelInfo:
7672
8066
  type: object
@@ -8480,7 +8874,18 @@ components:
8480
8874
  elapsedMs: { type: integer }
8481
8875
  tokens: { type: integer }
8482
8876
  # tokenDir REMOVED (engine ruling 2026-06-28): dead field, 0 producer / 0 consumer — spec caught up 2026-07-26.
8483
- queuedCount: { type: integer }
8877
+ queuedCount:
8878
+ type: integer
8879
+ description: >-
8880
+ 🔴 PHANTOM KEY — NO server version ever emits it (verified against the published 7.87.0 bytes:
8881
+ the row type declares it and the fleet route's per-key table forwards it, but NO producer writes it
8882
+ — not `publishTask`'s call sites, not core's bg-sink tick, not the durable reconcile leg — and the
8883
+ wire contract does not mention it either). The server deleted it as a DEAD KEY (S-459, zero
8884
+ behavioural impact since `!== undefined` was always false). It stays declared here only because
8885
+ removing an exported field is BREAKING = major-only; it is registered with its deletion condition
8886
+ in `test/_keyset-divergence.ts` (`FLEET_TASK_ROW_PHANTOM_KEYS`) and goes with the next major.
8887
+ DO NOT read it as "queue depth is already reported" — a permanently absent optional key is a FALSE
8888
+ LEAD. Real queue depth needs a NEW key WITH a producer, not this one resurrected.
8484
8889
  awaitingPlanApproval: { type: boolean }
8485
8890
  currentAction: { type: string, description: "The child's most recent tool intent as one human-readable line (UNTRUSTED argument head, redacted server-side)." }
8486
8891
  currentTool:
@@ -8835,9 +9240,21 @@ components:
8835
9240
  description: >-
8836
9241
  A live (in-stream) pending ask inside its streamApproval window (#315, server ≥7.43). Decide it via
8837
9242
  POST /v1/tool-approvals/{approvalId}/respond — NOT the /decide route (that is the durable rows' door).
8838
- Deliberately narrow: tool input/args are NOT listed (no second redaction surface — details ride the
8839
- SSE tool_approval frame). Optional flags are only-if-true (absent is NEVER encoded as false).
8840
- additionalProperties: false
9243
+ Optional flags are only-if-true (absent is NEVER encoded as false).
9244
+ 🔴 THE "tool input/args are NOT listed" NARROWING IS GONE (server >= 7.87.0 / S-454 P1). The old reason
9245
+ was "details ride the stream frame" — but a SUSPENDED ask never has a stream frame (at mint time it had
9246
+ no delivery target at all, so it suspends for a 1h window), and for it that reason is structurally
9247
+ false: without the card body on this row a consumer can list the ask and still not render it. The row
9248
+ therefore carries `frame` — the very card frame that ask WOULD have emitted — for EVERY live row (the
9249
+ rule set does not fork on `suspended`: an ask that does have a live stream carrying one extra copy of
9250
+ its own card is harmless).
9251
+ 🔴 THIS SCHEMA IS OPEN (`additionalProperties: true`) as of sdk 9.7.0, and that is a RULING, not a slip:
9252
+ it was closed, and when 7.87.0 minted `frame` every strict consumer stopped accepting a perfectly legal
9253
+ row — the whole `livePending` array became unreadable rather than "one field short". A producer that
9254
+ ships independently of this SDK adds keys additively between SDK releases, so a RESPONSE projection is
9255
+ declared key-by-key AND left open; the REQUEST bodies stay closed (there the enforcer is the server's
9256
+ own strict gate, so closing is description, not guesswork).
9257
+ additionalProperties: true
8841
9258
  required: [approvalId, toolName, ts, expiresAtMs]
8842
9259
  properties:
8843
9260
  approvalId: { type: string, description: 'The SAME id the respond endpoint takes (wire uuidv7).' }
@@ -8849,6 +9266,20 @@ components:
8849
9266
  governanceForced: { type: boolean, enum: [true] }
8850
9267
  fromSubagent: { type: boolean, enum: [true], description: 'The ask originates from a delegated subagent.' }
8851
9268
  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 (三键恒等亲证在案).' }
9269
+ 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.' }
9270
+ frame:
9271
+ allOf: [{ $ref: '#/components/schemas/ToolApprovalFrame' }]
9272
+ description: >-
9273
+ server >= 7.87.0 (S-454 P1; cli L-397) — THE CARD FRAME THIS ASK WOULD HAVE EMITTED. It is the SAME
9274
+ OBJECT as the server's `PendingApproval.frame` (the one construction site in `askBroadcast`, already
9275
+ redacted / byte-capped / window-keys filled), shared by the suspended and the reachable path — so
9276
+ this is NOT a second card projection computed for the list, and there is no second redaction surface.
9277
+ 🔴 INVARIANT: `frame.approvalId === row.approvalId` (verified at 7.87.0's merge review). When one ask
9278
+ was registered as pending more than once and the FIRST registration was cancelled, the frame's id
9279
+ used to point at the dead registration (responding by it 404s); the server now hands out a shallow
9280
+ copy with the id aligned to THIS row whenever the two differ. Always decide by the row's id.
9281
+ 🔴 ABSENT = an older server (<= 7.86.0); from 7.87.0 it is ALWAYS present (a required slot on the
9282
+ entry) and is never minted as `null`.
8852
9283
 
8853
9284
  ApprovalStaleCurrentPending:
8854
9285
  type: object
@@ -9454,12 +9885,38 @@ components:
9454
9885
  description: >
9455
9886
  ASSISTANT-WIRE-CONTRACT §4c — `POST /v1/assistant/tasks/:id/plan_review` body (3-state). `editedPlan` is
9456
9887
  REQUIRED iff `decision==="edit"` (FORBIDDEN otherwise → 400).
9888
+ 🔴 THE BODY KEY SET IS CLOSED (server >= 7.86.0 / S-438): an unrecognised top-level key is refused with
9889
+ 400 `request.body_shape` plus the `unknownKeys` / `unsupportedKeys` machine tables — refused rather
9890
+ than dropped, because a misspelled `permissionModeAfter` would otherwise silently leave the task
9891
+ read-only after its plan was approved.
9457
9892
  required: [decision]
9458
9893
  additionalProperties: false
9459
9894
  properties:
9460
9895
  decision: { type: string, enum: [approve, edit, reject] }
9461
9896
  editedPlan: { type: string, description: 'the operator''s revised plan — required iff decision==="edit".' }
9462
9897
  reason: { type: string }
9898
+ permissionModeAfter:
9899
+ type: string
9900
+ enum: [default, acceptEdits]
9901
+ description: >
9902
+ server >= 7.86.0 (S-433, P1; mints `routes/approvals-assistant.ts:1021/1024/1148`) — WHICH
9903
+ PERMISSION MODE THIS TASK CONTINUES IN once the plan is approved. Before this key, approving a plan
9904
+ was a BLANK CHEQUE: the plan was approved while the read-only clamp stayed on and every write kept
9905
+ asking. `acceptEdits` = writes stop asking; `default` = keep asking (AND is the folded value when
9906
+ the key is absent, so an older shell executes plans unchanged).
9907
+ 🔴 THREE 400s, each with its own code: a value outside the two-word closed set ⇒
9908
+ `request.field_invalid` (ZERO widening arm — this bit decides whether the model has hands after
9909
+ approval, so folding a misspelling into either mode is a silent downgrade on the security axis);
9910
+ sending it with `edit`/`reject` ⇒ `request.field_conflict` (core: those two send the plan back to be
9911
+ redone and the read-only clamp stays — dropping it there would let the caller believe the mode was
9912
+ settled); sending it for a task that was NOT submitted with `permissionMode:"plan"` ⇒
9913
+ `request.field_conflict` (approving the plan lifts no read-only restriction there).
9914
+ ⚠️ DIRECT-DOOR deployments: this key rides the HMAC envelope's TRAILING EXTENSION OBJECT (server
9915
+ 7.87.0: `{answer?, permissionModeAfter?}`, present keys in lexicographic order; 7.86.0's bare
9916
+ trailing slot is DELETED with zero double-read) ⇒ a signer must sign the new shape, and mixed
9917
+ versions are 401 in both directions.
9918
+ ⚠️ The key name is final: `permissionModeAfter` (the build brief once said `nextPermissionMode`,
9919
+ which never shipped — zero aliases).
9463
9920
 
9464
9921
  SessionRecord:
9465
9922
  type: object
@@ -9734,11 +10191,18 @@ components:
9734
10191
  description: >
9735
10192
  `GET /v1/sessions/{sessionId}/memory-status` 200 body (server >=7.53, S-53 seam①): `sessionId` + the
9736
10193
  five `SessionMemoryStatus` keys copied ONE BY ONE when present (the server forbids spreading and forbids
9737
- defaults — the five conditional copies ARE the closed set). Per-key absence meaning = exactly
10194
+ defaults — the five conditional copies ARE the closed set), plus the DEPLOYMENT bit
10195
+ `autoConsolidationArmed` (server >= 7.85.0, always present). Per-key absence meaning = exactly
9738
10196
  `SessionMemoryStatus` (a healthy zero-history session omits `optOutSource` and `lastCaptureAt`). Keys
9739
- are mirrored locally rather than via allOf so the closed schema stays self-describing (spec rule:
9740
- closed + allOf must mirror every key).
9741
- additionalProperties: false
10197
+ are mirrored locally rather than via allOf so the schema stays self-describing (spec rule: a closed
10198
+ schema plus allOf must mirror every key).
10199
+ 🔴 OPEN as of sdk 9.7.0 — same ruling, same receipt as `MetricsSummaryOps` / `LivePendingRow`: this was
10200
+ closed at S-53 and server 7.85.0 then added a sixth key, so a REAL body from a current server failed the
10201
+ schema. A response PROJECTION is declared key-by-key AND left open (the producer ships independently and
10202
+ adds keys additively between SDK releases); request bodies stay closed.
10203
+ ⚠️ SCOPE OF THAT RULING IN THIS BATCH: only the schemas this batch had to GROW were opened (the growth
10204
+ itself is the proof). Re-judging every other closed response schema is a separate car.
10205
+ additionalProperties: true
9742
10206
  required: [sessionId]
9743
10207
  properties:
9744
10208
  sessionId: { type: string, description: 'The session the status is about (echo of the path segment, percent-decoded).' }
@@ -9747,6 +10211,87 @@ components:
9747
10211
  foldedCount: { type: integer, description: 'See SessionMemoryStatus.foldedCount.' }
9748
10212
  optOutSource: { type: string, enum: [record, fault], description: 'See SessionMemoryStatus.optOutSource — absent = no opt-out record (healthy), never a fault by itself.' }
9749
10213
  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.' }
10214
+ autoConsolidationArmed:
10215
+ type: boolean
10216
+ description: >-
10217
+ server >= 7.85.0 (S-403 / core 7.22.0 #863 W1; mint `routes/sessions.ts:744` =
10218
+ `deps.memoryAutoConsolidationArmed`, computed ONCE at boot through core's single reader
10219
+ `autoRunOnRecommendationEffective`) — IS AUTOMATIC MEMORY CONSOLIDATION ARMED ON THIS DEPLOYMENT.
10220
+ 🔴 NOT one of the five core-derived session keys: those are per-SESSION derivations whose absence
10221
+ is meaningful per key; this one is a DEPLOYMENT fact and is ALWAYS PRESENT on >= 7.85.0. Absent =
10222
+ an older worker ⇒ "unknown"; NEVER fold it to `false` (that renders a box which is auto-egressing
10223
+ its whole memory store to the consolidation model as one that is not).
10224
+ 🔴 `true` = a terminal harvest that crosses the threshold auto-runs one host consolidation (memory
10225
+ content auto-egresses to the configured model). A DIFFERENT QUESTION from `captureOptedOut` (does
10226
+ THIS SESSION enter long-term memory) — do not read them together.
10227
+ The same fact is readable by an operator as `wiring_manifest.autoConsolidation` (present-iff armed);
10228
+ one fact, one source — the tenant face has only this key.
10229
+
10230
+ McpReconnectServerStatus:
10231
+ type: object
10232
+ additionalProperties: true
10233
+ description: >
10234
+ The `status` segment of `POST /v1/sessions/{sessionId}/mcp/reconnect` (server >= 7.85.0 / S-381; mint
10235
+ `routes/mcp-reconnect.ts` `projectStatus`) — core's `McpServerStatus` projected BY THIS FACE.
10236
+ ⚠️ The SAME core type is projected TWICE with DIFFERENT key sets and the two are deliberately NOT
10237
+ merged: the `/mcp` panel projects `{name, status, serverInfo?, toolNames?, error?}` only, while this
10238
+ face adds `errorCode` / `httpStatus` / `delivered` / `transportClosed`. Folding them would make the
10239
+ panel's consumers wait for four keys that never come.
10240
+ 🔴 BRANCH ON THE MACHINE FIELDS, NEVER PARSE `reason`: `errorCode` is core's `McpFailureKind` word
10241
+ (owner is the engine — switch with a default arm, do not narrow), `httpStatus` 401 is a re-authorize
10242
+ door, `spawn_failed` is a missing binary.
10243
+ required: [name, status]
10244
+ properties:
10245
+ name: { type: string }
10246
+ status: { type: string, description: 'connected | failed (open set — the word table owner is the engine).' }
10247
+ errorCode: { type: string, description: 'core `McpFailureKind`. Open set; switch with a default arm.' }
10248
+ httpStatus: { type: integer, description: 'Only alongside errorCode `http_status` (401/403 = re-authorize; 5xx = that end is down). ABSENT rather than 0.' }
10249
+ delivered: { type: string, enum: [yes, no, unknown], description: 'Did the request reach that server — read together with errorCode.' }
10250
+ serverInfo:
10251
+ type: object
10252
+ properties:
10253
+ name: { type: string }
10254
+ version: { type: string }
10255
+ error: { type: string, description: 'Remote-authored text: server-redacted and truncated to 200 chars. HUMAN-READ ONLY, never parsed.' }
10256
+ 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).' }
10257
+
10258
+ SessionMcpReconnectResult:
10259
+ type: object
10260
+ additionalProperties: true
10261
+ description: >
10262
+ 200 body of `POST /v1/sessions/{sessionId}/mcp/reconnect` (server >= 7.85.0 / S-381 / core 7.22.0 #857;
10263
+ mint `routes/mcp-reconnect.ts` `projectResult`). Branch on `outcome` — all three states are HTTP 200.
10264
+ 🔴 The `unsupported` arm (this run declared NO MCP) is an ALWAYS-FIVE-KEY body
10265
+ `{taskId, sessionId, server, outcome, reason}`: with no core result the server coins no
10266
+ `prefix`/`toolCount`/`added`/`removed`/`toolNames`, so reading `added` unconditionally is a bug.
10267
+ 🔴 `toolNames` is PRESENT-IFF core supplied `tools` (not keyed off `outcome`) and is a SUPERSET of what
10268
+ the model finally sees (core filters again before mounting) — see the operation description.
10269
+ required: [taskId, sessionId, server, outcome]
10270
+ properties:
10271
+ taskId: { type: string, description: 'The live run this re-dial rode (this session''s active run) — follow it via GET /v1/runs/{taskId}/events.' }
10272
+ sessionId: { type: string, description: 'Echo of the path segment (percent-decoded).' }
10273
+ server: { type: string, description: 'The named server that was re-dialed.' }
10274
+ outcome: { type: string, description: 'accepted | refused | unsupported — the ONLY discriminator (all three are 200). `unsupported` is the SERVER''s word, not core''s.' }
10275
+ prefix: { type: string, description: 'The tool-name prefix domain of that server. Absent on the `unsupported` arm.' }
10276
+ toolCount: { type: integer, description: 'Tool count in that domain after the dial. Absent (never 0) on the `unsupported` arm.' }
10277
+ added: { type: array, items: { type: string } }
10278
+ removed: { type: array, items: { type: string } }
10279
+ toolNames:
10280
+ type: array
10281
+ items: { type: string }
10282
+ 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.'
10283
+ reason: { type: string, description: 'Human sentence (server-redacted, 200-char bound). NEVER parse it — the machine face is `status`.' }
10284
+ status: { $ref: '#/components/schemas/McpReconnectServerStatus' }
10285
+ listingIncomplete:
10286
+ type: object
10287
+ additionalProperties: true
10288
+ 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.'
10289
+ required: [reason, pages]
10290
+ properties:
10291
+ reason: { type: string }
10292
+ pages: { type: integer }
10293
+ budgetMs: { type: integer }
10294
+ error: { type: string }
9750
10295
 
9751
10296
  # ── 2c session-sync (P1d) — the cloud-as-a-SYNC-PEER schemas ──────────────────────────────────────────────
9752
10297
  SyncEntry:
@@ -9994,6 +10539,12 @@ components:
9994
10539
  The two 501 families are deliberately NOT interchangeable: `capability.*` means this deployment did
9995
10540
  not wire that surface (change the deployment / hide the entry point), `feature.*` means the surface
9996
10541
  exists but its switch is off (ask an admin to turn it on).
10542
+ 🔴 `request.path_malformed` IS NOW THE WHOLE FAMILY'S ANSWER (server >= 7.86.0 / S-430): an id
10543
+ segment that is not valid percent-encoding answers 400 on EVERY id-bearing door. Twelve of those
10544
+ doors used to let the bare `decodeURIComponent` throw a `URIError` straight through the top-level
10545
+ catch and answered 500 `internal.error` — a caller-side mistake reported as a server fault, which
10546
+ also made it a retry-worthy-looking code. A consumer that branched on 500 for those paths must move
10547
+ the branch to the 400 (the fix is the caller's either way: re-encode the id).
9997
10548
  error: { type: string }
9998
10549
  errorMessage: { type: string }
9999
10550
  activeTaskId:
@@ -11130,6 +11681,41 @@ components:
11130
11681
  that wires none reports nothing at all. The server never invents `mounted: false` when it cannot
11131
11682
  read the bit — the whole section goes absent instead (a minted false would read as "really not
11132
11683
  mounted").
11684
+ writeProtection:
11685
+ allOf: [{ $ref: '#/components/schemas/WiringManifestWriteProtection' }]
11686
+ description: >
11687
+ core 7.20.1 (#853 / clay C-R55 丙), server >= 7.80.1 — TENANT-visible: which TARGET READING this
11688
+ leg's write-protection judge got. EFFECTIVE half only.
11689
+ 🔴 ABSENCE IS A THIRD STATE (core present-iff a judge was compiled; a deployment with
11690
+ `writeProtectedPaths: []` omits the whole section) — NEVER fold it to `"spelling-only"`, which
11691
+ means "a judge IS there and only reads spellings".
11692
+ ⚠️ Same name, different thing from `capabilities.writeProtection` / the diagnostics endpoint's
11693
+ `writeProtection`: those are THIS SERVER'S assembled NAME TABLE (a boot product); this is core's
11694
+ per-leg SELF-REPORT of the judge's reading.
11695
+ readDeny:
11696
+ allOf: [{ $ref: '#/components/schemas/WiringManifestReadDeny' }]
11697
+ description: >
11698
+ core 7.22.0 (#889 / C-R60), server >= 7.85.0 — OPERATOR face only (stripped on every tenant
11699
+ stream, and deliberately NOT carried by `GET /v1/diagnostics/wiring` either): which BUILT-IN
11700
+ sensitive-path read-deny TIERS materially activate rows on this deployment.
11701
+ 🔴 PRESENT IFF at least one built-in row judges this leg. Absence = no built-in row judges it (a
11702
+ POSITIVE fact), NOT "no read is deny-judged" — a deployment's own `READ_DENY_PATTERNS` is
11703
+ deliberately never reported (pattern TEXT is deployment material, not an enum).
11704
+ 🔴 AN EMPTY ARRAY IS NEVER EMITTED ("off" and "on to nothing" are one fact) ⇒ treat `[]` as a bad
11705
+ producer. The tier vocabulary is owned by core — do not narrow it on the consumer side.
11706
+ ⚠️ Deployment semantics flipped in core 7.22.0: `READ_DENY_BUILTIN_TIERS` UNSET = OFF.
11707
+ autoConsolidation:
11708
+ allOf: [{ $ref: '#/components/schemas/WiringManifestAutoConsolidation' }]
11709
+ description: >
11710
+ core 7.21.0 (#761), server >= 7.82.0 — OPERATOR face only (stripped on every tenant stream): is
11711
+ automatic memory consolidation ARMED.
11712
+ 🔴 PRESENT IFF armed, and the only present shape is `{ onRecommendation: true }` (core's construct
11713
+ gate refuses an armed-but-unrunnable state). ABSENT = NOT ARMED (a positive fact) — never read it
11714
+ as `false`, never as "unknown". Armed means a terminal harvest over the threshold auto-runs one
11715
+ host consolidation: memory content auto-egresses to the configured model, which is why it sits in
11716
+ the operator family beside `governance`.
11717
+ ⚠️ The tenant-readable twin of the same fact is `GET /v1/sessions/{sessionId}/memory-status`'s
11718
+ `autoConsolidationArmed` (always present, a real boolean). One fact, one source, two faces.
11133
11719
  governance:
11134
11720
  type: object
11135
11721
  additionalProperties: false
@@ -12140,6 +12726,41 @@ components:
12140
12726
  frame and the inbox row; the `card_json` card deliberately does NOT carry it (it travels with `origin`,
12141
12727
  which is not on the card). Absence = `origin` is not that word — NOT an assertion that the rule store
12142
12728
  is healthy.
12729
+ readRootCandidate:
12730
+ type: object
12731
+ required: [dir, clearsThisAsk]
12732
+ additionalProperties: false
12733
+ properties:
12734
+ dir:
12735
+ type: string
12736
+ minLength: 1
12737
+ maxLength: 1024
12738
+ description: >-
12739
+ The ABSOLUTE, lexically-folded directory to add to this session's read roots, spelled by the read
12740
+ boundary itself — THE STRING SHOWN IS THE STRING TO ADD. A shell may render a shortened / `~` form,
12741
+ but must echo THIS string back verbatim. The server carries it whole or drops the whole key
12742
+ (a malformed shape, or a `dir` over 1024 UTF-16 code units ⇒ the key is ABSENT — never truncated,
12743
+ because a truncated directory is a DIFFERENT directory and adding it would not clear this ask;
12744
+ see the STRING-LENGTH UNIT convention in `info.description` for this `maxLength`'s counting unit).
12745
+ clearsThisAsk:
12746
+ type: boolean
12747
+ enum: [true]
12748
+ description: >-
12749
+ Shape, not value — the domain is the literal `true` only, so this seat cannot be quietly widened
12750
+ into a "might help" hint.
12751
+ description: >-
12752
+ server >= 7.78.1 (core 7.19.0), ADDITIVE, LIVE `tool_approval` FRAME ONLY — the clearing directory for
12753
+ an OUT-OF-ROOT READ ask: add `dir` to this session's read roots (the shell's existing `/add-dir`) and
12754
+ the same call stops asking.
12755
+ 🔴 READ PRESENCE ONLY, NEVER ABSENCE. Absence is NOT "nothing can be done": it also covers sensitive-path
12756
+ deny rows (pattern-judged — no root changes them), commands the read boundary cannot parse (heredoc /
12757
+ newline / command substitution / an argument used as the program), unexpanded globs, recursive walks,
12758
+ and every ask NOT raised by the read boundary — every ask on server <= 7.78.0 has this shape too.
12759
+ ⚠️ THIS FACE ONLY: `card_json`, the §2 inbox row, the `/v1/approvals` rich row and the park face carry
12760
+ NO twin of this seat in this version — a PARKED out-of-root read ask still renders that single card.
12761
+ ⚠️ Known shapes (upstream-registered, so a shell must NOT render this option as a guarantee):
12762
+ `cd X && cat Y` yields X's PARENT directory; and on a deployment that normalizes declared read roots
12763
+ (e.g. `/tmp/x` stored as `/private/tmp/x`) the same call may ask once more after the root is added.
12143
12764
  ApprovalRequestFrame:
12144
12765
  type: object
12145
12766
  description: >
@@ -14022,6 +14643,16 @@ components:
14022
14643
  WiringManifest:
14023
14644
  type: object
14024
14645
  description: >
14646
+ 🔴 ONE RULE FOR EVERY "EFFECTIVE HALF ONLY" KEY (sdk 9.7.0, after a codex adversarial-review [high]):
14647
+ this schema's only consumer today is `GET /v1/diagnostics/wiring`'s `static` half, and every key marked
14648
+ "effective half only" (`leg`, `ask.effective`, `configFingerprint`, `tools`, `hooks`, `lsp`,
14649
+ `modelGate`, `autoMode`, `writeProtection`, `readDeny`, `autoConsolidation`) is ALWAYS ABSENT on that
14650
+ wire ⇒ THEIR ABSENCE HERE CARRIES NO INFORMATION. The per-key absence readings ("absent = not armed",
14651
+ "absent = a third state", …) hold for the LIVE FRAME (`Event_wiring_manifest`) and must not be carried
14652
+ over to this response. For the facts behind those keys: a tenant reads
14653
+ `GET /v1/sessions/{sessionId}/memory-status` (`autoConsolidationArmed`); an operator subscribes to that
14654
+ leg's live stream (the governance segment and these three sections exist only on a projection that
14655
+ KNOWS the caller's identity).
14025
14656
  core's assembly self-evidence manifest (core >=5.14.0 `WiringManifest`). TWO HALVES, ONE shape (a
14026
14657
  consumer must line the two faces up, so they are not split into separate types) — but which keys
14027
14658
  belong to which half is DETERMINATE, not "usually": `leg`, `ask.effective` and `configFingerprint`
@@ -14165,6 +14796,78 @@ components:
14165
14796
  core 7.18.0 (#789), server >= 7.78.0 — the LSP seam. EFFECTIVE half only; present iff a manager
14166
14797
  seat resolved. `mounted` and `lane` are INDEPENDENT facts — never fold them into one.
14167
14798
  Full per-key contract: see Event_wiring_manifest.lsp.
14799
+ writeProtection:
14800
+ allOf: [{ $ref: '#/components/schemas/WiringManifestWriteProtection' }]
14801
+ description: >
14802
+ core 7.20.1 (#853), server >= 7.80.1 — TENANT-visible: which target reading this leg's
14803
+ write-protection judge got. EFFECTIVE HALF ONLY ⇒ ALWAYS ABSENT ON THIS ENDPOINT, and absence
14804
+ HERE carries no information (see this schema's own description for the one rule that governs
14805
+ every effective-half-only key). The three-state reading ("absence = no judge at all, never fold
14806
+ it to `spelling-only`") is the LIVE FRAME's reading — do not apply it to this response.
14807
+ Full per-key contract: see Event_wiring_manifest.writeProtection.
14808
+ readDeny:
14809
+ allOf: [{ $ref: '#/components/schemas/WiringManifestReadDeny' }]
14810
+ description: >
14811
+ core 7.22.0 (#889), server >= 7.85.0 — OPERATOR face only, effective half only, AND the server
14812
+ explicitly rules it off this endpoint ⇒ always absent here; absence HERE says nothing about the
14813
+ deployment's tiers. The present-iff / never-empty-array reading belongs to the operator LIVE
14814
+ projection. Full per-key contract: see Event_wiring_manifest.readDeny.
14815
+ autoConsolidation:
14816
+ allOf: [{ $ref: '#/components/schemas/WiringManifestAutoConsolidation' }]
14817
+ description: >
14818
+ core 7.21.0 (#761), server >= 7.82.0 — OPERATOR face only, EFFECTIVE HALF ONLY ⇒ ALWAYS ABSENT ON
14819
+ THIS ENDPOINT. 🔴 DO NOT read absence here as "not armed": that renders a deployment which IS
14820
+ auto-egressing its whole memory store as one that is not. The present-iff-armed reading belongs to
14821
+ the operator LIVE projection; for "is this deployment armed", read
14822
+ `GET /v1/sessions/{sessionId}/memory-status`'s `autoConsolidationArmed` (always present, a real
14823
+ boolean). Full per-key contract: see Event_wiring_manifest.autoConsolidation.
14824
+ WiringManifestWriteProtection:
14825
+ # sdk 9.7.0:与 `WiringManifestMcpEntry` / `WiringManifestLspSeam` 同一条裁定 —— 同一段形被 live 帧与
14826
+ # 静态诊断面两处引用,内联就是两份会各自漂的镜像,所以具名单源。
14827
+ type: object
14828
+ additionalProperties: false
14829
+ description: >
14830
+ The `wiring_manifest.writeProtection` section (core 7.20.1 #853 / clay C-R55 丙) — which TARGET
14831
+ READING this leg's write-protection judge got. `"target"` = the judge holds the execution environment
14832
+ and re-judges on the RESOLVED target when the spelling missed (a write that reaches a protected row
14833
+ through an alias or a symlink is cleared); `"spelling-only"` = no environment, the spelling IS the
14834
+ whole verdict. ABSENCE (the section itself) is a THIRD state — no judge was compiled at all.
14835
+ required: [targetView]
14836
+ properties:
14837
+ targetView: { type: string, description: "core's two-word closed set (`spelling-only` / `target`); read as open — the vocabulary owner is the engine." }
14838
+
14839
+ WiringManifestReadDeny:
14840
+ type: object
14841
+ additionalProperties: false
14842
+ description: >
14843
+ The `wiring_manifest.readDeny` section (core 7.22.0 #889 / C-R60) — the BUILT-IN sensitive-path
14844
+ read-deny tiers that MATERIALLY activate rows on this leg, in core's own tier order (NOT the tiers the
14845
+ operator typed: one whose every row the exclude list removed is not named). OPERATOR audience.
14846
+ required: [builtinTiers]
14847
+ properties:
14848
+ builtinTiers:
14849
+ type: array
14850
+ minItems: 1
14851
+ items: { type: string }
14852
+ description: >
14853
+ Tier names, core-owned vocabulary (today `credentials` / `shell-history` / `browser` / `wallet` /
14854
+ `agent-config`) — passed through verbatim, never narrowed here. AN EMPTY ARRAY IS NEVER EMITTED.
14855
+
14856
+ WiringManifestAutoConsolidation:
14857
+ type: object
14858
+ additionalProperties: false
14859
+ description: >
14860
+ The `wiring_manifest.autoConsolidation` section (core 7.21.0 #761) — PRESENT IFF automatic memory
14861
+ consolidation is EFFECTIVELY armed on this deployment. OPERATOR audience: armed means memory content
14862
+ auto-egresses to the configured consolidation model after a qualifying terminal harvest, and it is not
14863
+ observable to the caller at all (the run happens host-side, fire-and-forget, after the harvest).
14864
+ required: [onRecommendation]
14865
+ properties:
14866
+ onRecommendation:
14867
+ type: boolean
14868
+ enum: [true]
14869
+ description: "core's ONLY present shape — the construct gate refuses an armed-but-unrunnable state, so this is a SHAPE, not a value."
14870
+
14168
14871
  WiringManifestHookEntry:
14169
14872
  # sdk 9.5.0:与 `WiringManifestMcpEntry` 同一条裁定 —— 同一份行形被 live 帧与静态诊断面两处引用,
14170
14873
  # 内联就是两份会各自漂的镜像,所以一开始就具名。