Skip to content

feat: land the M2 second breadth batch — session lifecycle, the Shells family, and the location design - #38

Merged
Blind-Striker merged 13 commits into
masterfrom
feature/m2-second-breadth-batch
Aug 16, 2026
Merged

Blind-Striker merged 13 commits into
masterfrom
feature/m2-second-breadth-batch

Conversation

@Blind-Striker

Copy link
Copy Markdown
Collaborator

Executes the plan in docs/superpowers/plans/2026-08-15-m2-second-breadth-batch.md — the design-prover batch: every mechanism the M3 Arc 2 seals (Q93/Q94) introduced is exercised by a real operation. Closes #28.

Machinery

  • 204 no-content envelopes. The binder admits exactly one success of 200 or a content-free 204; the envelope plan records its success status; responses emit payload-free records and adapters refuse a 204 that arrives with a body.
  • {location, data} envelope profiles, object and list variants. The required location sibling must reference a named component schema (promoted-inline refused, mirroring the cursor-list item wall) and surfaces as a guarded Location property beside the payload.
  • DeepObject location query channel (Q94). One admitted deep-object shape — the optional nullable {directory?, workspace?} selector, matched structurally — typed by the hand-written LocationSelector spine; every other deep-object stays refused.
  • Merged body+query request models (Q93). A body+query mix binds into one uniform *Request exactly when every query property is the location selector; the model gains [JsonIgnore] query-side properties and the route builder types itself with the body model. Every other mix keeps the deliberate wall.
  • Ambient location headers from the options snapshot, with the directory percent-encoded and the workspace verbatim — mirroring the server, which decodes only the former.
  • PATCH and DELETE verbs. PATCH rides the internal OpenCodeHttpMethod spine because HttpMethod.Patch does not exist downlevel.
  • M2: curated mutually-exclusive query parameters — order+cursor refusal at the route boundary (V10) #28: the mutuallyExclusiveQueries curation section — fail-closed validated, pinned to the bound query surface, refused at the route boundary before any request is built.

Surface

Seven operations join the four already callable: session.remove, session.rename, and the Shells family (list/create/get/remove/timeout). The verb vocabulary gained remove/rename/timeout, RequestTypeName folds Get like the response name, reachability recollects after alias collapse so Shell.Info's promoted inline children stay bound, the alias comparer resolves promoted references structurally, and Shell.Info1 collapses through a second schemaAliases row. Extensions registers the family the roster test demanded. v2.shell.output is deferred — its inline data object and integer cursor query parameters each need a mechanism no other admitted operation needs.

Verification

All six gates ALL-GREEN at every commit; 1042 tests. Demonstrated live against opencode2 serve v0.0.0-next-17403: shell create → get → timeout → remove and session rename → remove, 204s typed, the ambient location echoed back.

Review, and what it caught

A three-dimension adversarially-verified review ran on the branch. Runtime-correctness surfaced a genuine wire defect that the earlier demo had missed because its path contained no special characters:

The ambient directory header rode the wire raw, but the server percent-decodes it. Measured against a live server whose cwd differed from every probe target, a directory literally named loc%20test returns HTTP 500 with an empty body raw — the server decodes %20 into a space and resolves a directory that does not exist — and 200 with the correct location when escaped. Fixed in 431293b.

That finding triggered a full upstream tour (research log Q95), which also settled three mechanics the documentation had stated too loosely and now states correctly: precedence resolves per field, not per location object; the middleware is attached per group, so the ambient channel is inert on session-scoped and unmiddlewared operations; and the headers are absent from the schema because they were never expressible in it — OpenApi.fromApi derives parameters from declared endpoint schemas, which is why Authorization is equally invisible while remaining mandatory.

The remaining findings — member ordering in two files, and a curated row that could go dormant on an operation with no query surface — are fixed in e9a61f3 and c521105.

Follow-up

#37 anchors the ambient channel's future at M5, decision-first: packaging unblocking freezes the public surface, making it the last free moment to drop OpenCodeClientOptions.Location or fold it into the query channel.

CheckStatusShape admits exactly one success of 200 or a content-free 204; the
envelope plan records its success status and carries null payload names for the
new NoContent kind; envelopes emit payload-free records, adapters emit the
declared-status arm and refuse a 204 arriving with a body via the new
NonEmptyNoContentFailure on the adapter spine. Fixture and snapshots grow the
delete-shaped no-content operation.
The {location,data} wrapper — classified at ingestion since the census — now
binds fail-closed in object and list variants: the wrapper joins the excluded
response roots, the required location sibling must reference a named component
schema (promoted inline refused, mirroring the cursor-list item wall), and the
envelope plan records the sibling type. Envelopes surface a guarded Location
property beside the payload, DTOs read both members in one pass, adapters
project through the generalized sibling helper, and 'Location' joins the
reserved payload names.
The binder admits exactly one deep-object shape — the optional nullable
{directory?, workspace?} selector, matched structurally — as the new Location
query kind typed by the hand-written LocationSelector spine record; every other
deep-object stays refused. QueryStringBuilder.AddLocation writes the
location[directory]/location[workspace] keys with literal brackets matching the
first-party client's serialization, route builders emit the call, and query
records surface the selector property. PublicApi gains the spine type.
…pshot (Q94)

OpenCodeClientOptions gains the LocationSelector ambient default; the pipeline
snapshots it at construction and rides the x-opencode-directory/x-opencode-workspace
middleware channel verbatim — the server resolves any explicit per-request query
location first, so no client-side merge exists. The selector refuses blank
members at init on both channels, and the shared recorder captures request
headers.
A body+query operation now binds when every query property is the location
selector: the query plan rides the request body model (RidesRequestBody), the
body model gains JsonIgnore query-side properties attached at composition, the
route builder types itself with the body model and the operation method's one
request parameter feeds both the route and the JSON path. Every other mix keeps
the deliberate refusal, and standalone query records are untouched.
… Shells family

Seven operations land on the sealed design-prover surface: RemoveSessionAsync
and RenameSessionAsync complete the session lifecycle over the 204 machinery,
and the Shells family (list/create/get/remove/timeout) exercises every Arc 2
seal — deepObject location selectors, the merged body+query ShellCreateRequest
and ShellTimeoutRequest, location-echo envelopes, and the first PATCH/DELETE
verbs (PATCH rides the internal OpenCodeHttpMethod spine, absent downlevel).
The verb vocabulary gains remove/rename/timeout, RequestTypeName folds Get like
the response name, reachability recollects post-alias so Shell.Info's promoted
inline children stay bound, the alias comparer resolves promoted references
structurally, Shell.Info1 collapses through the second schemaAliases row, the
Shells IDE0130 arbitration glob extends per its own rule, Extensions registers
the family the roster test demanded, and the contract matrix covers the new
surface. shell.output defers to a later batch pending two mechanisms of its
own.
…dary

The fifth curation section declares two query parameters one operation's server
refuses to combine — spec prose the binder cannot derive. The validator owns
the row shape (pairs only, reason required, selected-profile membership), the
binder pins each name to the bound query surface and records the pair on the
plan, the route builder throws ArgumentException before any request is built,
and message.list's order+cursor pair rides the first row with a no-send
contract test. Closes #28.
The server decodes the directory header (decodeURIComponent with a raw
fallback) while reading the workspace header verbatim, so the escaping mirrors
that asymmetry exactly. Measured against a live server whose cwd differed from
both probe targets: a directory literally named 'loc%20test' returns HTTP 500
with an empty body when the header rides raw — the server decodes '%20' into a
space and resolves a directory that does not exist — and returns 200 with the
correct location when the value is escaped. Escaping also keeps a non-ASCII
path sendable at all, since header values cannot carry it raw.
…ntation

Session 26 (Q95) records what the upstream sweep and the live probes found: the
header channel is not deprecated — it was never expressible in a schema derived
from declared endpoint parameters, the same reason Authorization is absent —
precedence resolves per field rather than per location object, and the
middleware is attached per group so the ambient channel is inert on
session-scoped and unmiddlewared operations. The options documentation now
states both limits instead of promising a blanket override; doc 15 gains the
mechanics; the ambient channel's future is anchored at M5 as #37, where
packaging unblocking makes it the last free moment to change the surface.
…alidator

QueryStringBuilder's private block reads in first-use order again — Append,
reached from AddText, precedes the bracketed variant AddLocation reaches. In
CurationValidator the new mutual-exclusion validator had landed between the
alias walls' documentation and the method it describes, leaving one method with
two summaries and the other with none; it now sits last among the privates,
matching its position in the Validate call sequence.
…ith no query surface

The row wall lived behind the query-request binder, which returns early when an
operation declares no query parameters — so a row naming such an operation went
dormant instead of refusing, unlike the property-override and schema-alias
drift contracts. The wall now answers for that case too, which is the shape a
spec refresh would produce by dropping an operation's query parameters.
@Blind-Striker
Blind-Striker merged commit f6a569a into master Aug 16, 2026
3 checks passed
@Blind-Striker
Blind-Striker deleted the feature/m2-second-breadth-batch branch September 14, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M2: curated mutually-exclusive query parameters — order+cursor refusal at the route boundary (V10)

1 participant