Repository navigation
feat(sdk): M2 first breadth batch — session list/get/create and message list - #26
Merged
Merged
Conversation
… plans The binder opens the M1 query wall: optional nullable query parameters bind into per-operation options plans carrying wire names, PascalCase property names, and shape-derived value kinds (text, positive count, asc/desc order, parent-session filter). A fail-closed profile wall derives an operation from the ListOptions base only when its wire parameters are exactly the cursor-pagination trio; anything unrecognized (required, non-nullable, deep-object, foreign enums, object shapes) refuses generation. Query schemas stay out of the model closure, and options type names join the generated-type collision checks.
POST joins the admitted methods, paired fail-closed with bodies: GET must not
carry one, POST must. A required JSON body referencing an object schema binds
into a request-body plan; the operation parameter is optional exactly when
every body property is optional, so an empty call sends an empty JSON object.
Inline body schemas are named from operation identity through the mechanical
{Subject}{Verb}Request rule instead of their op-rooted graph keys, which
would otherwise surface the protocol prefix ADR-0005 strips.
The envelope plan carries an explicit kind (bare, data, cursor list) instead of a data-envelope flag. Cursor-list responses bind fail-closed: the wrapper must require exactly 'data' and 'cursor', the cursor must be the optional-nullable previous/next object, and items must reference named component schemas. Wrapper roots — component-named ones included — and the cursor object stay out of the model closure: they are envelope structure the response spine renders, never generated models.
A schemaAliases curation section declares one component as a spelling of another — the resolution sealed for upstream's InvalidRequestError1 duplicate, whose identical _tag would otherwise trip the per-status duplicate-tag wall and poison tag dispatch. The collapse is a pure document transform ahead of name resolution, so downstream machinery never sees the duplicate; union branches that fold into the same reference deduplicate mechanically. Every drift direction stays loud: a deleted source or target orphans the row, a dereferenced source goes dormant, and any structural divergence — the tag included — fails the deep-identity wall. The binder's own duplicate-tag refusal stays intact for anomalies without an alias.
v2.message.list belongs to the 'message' wire group but its sealed home is the Sessions client family. Client assembly now clusters by declared client name instead of wire group, and the curation validator demands identical handle declarations across a merged family so a divergent row cannot fork it. A pinned-shape test also documents that 5xx arms ride the existing status-map machinery unchanged.
ExecuteAsync gains a body-carrying overload serializing through source-generated JsonTypeInfo metadata so the AOT commitment holds; both overloads share one core, keeping decoration, buffering, and error-channel policy identical. The body rides ByteArrayContent under a bare application/json content type (JSON is UTF-8 by RFC 8259), the body-less path stays body-free, and the recording test seam now captures request content and content type.
Verb detection is structural: only the final identifier segment can be a verb (create/get/list), so mid-position segments spelling a verb stay in the subject — the C18 fix. Empty subjects fall back to the group, pluralized for list operations under a naive fail-closed rule (words needing -es or -ies refuse until the rule set grows). Response types fold non-Get verbs so get/create envelopes cannot collide; client-placed route members mirror their method names so merged families stay unique, while root members keep their bare-verb shape. The naming walls batch rides along: reserved keywords stop passing identifier validation (C20), the reserved payload set covers object/record-synthesized members (C19), and mechanical payload names pass the same identifier wall as curated ones (C17). The committed two-operation surface is byte-identical under the new rules.
ListOptions carries the sealed cursor-pagination trio as the seam the M3 paginator will consume; ListOrder spells asc/desc without magic strings; ListCursor surfaces the wire cursor on list envelopes; SessionParentFilter makes the root-only sentinel unrepresentable as ordinary data and refuses the literal 'null' spelling. The internal QueryStringBuilder owns the route-boundary conversions: invariant positive counts (non-positive refused, the sealed int? limit contract), wire order spellings, escaped values, omitted unset optionals. PublicApi baseline grows by exactly these four types, identically across TFMs.
…t envelopes The emitters open every M1 wall the binder now crosses. New emitters render public options records (flat, or deriving from the ListOptions base) and the internal envelope DTOs behind P2's single-pass read: every wrapped success body deserializes once through source-generated metadata, retiring the JsonDocument double-parse and the hand-written ReadDataPayload primitive. Route builders compose escaped query suffixes through QueryStringBuilder; cursor-list envelopes carry a guarded IReadOnlyList payload plus the wire cursor; adapters project DTOs directly; operation methods thread body models (an omitted optional body still sends the empty JSON object) and query options, and the per-call OpenCodeRequestOptions parameter is uniformly named requestOptions. Committed two-operation surface regenerated under the new emitters; snapshots and the PublicApi baseline reviewed alongside.
The profile selects v2.session.list/get/create and v2.message.list; the message wire group joins the Sessions client family and the InvalidRequestError1 duplicate collapses through its curated alias. The committed surface gains ListSessionsAsync, GetSessionAsync, CreateSessionAsync, and ListMessagesAsync with their options records, request model, cursor-list envelopes, single-pass DTOs, query-composing routes, and the Session.Info model closure (fork boundary union with its unknown carrier, revert, file diff, time/fork nests) plus the InvalidCursorError and UnknownError types — the first 5xx arm included. Pinned binder expectations and the PublicApi baseline reviewed alongside; packing stays blocked behind the 114 pending operations.
Contract tests cover the four new operations end to end: typed pages with cursor round-trips, escaped query composition, the sealed non-positive limit refusal at the route boundary, both declared 400 tags on session.list, the empty-body and typed-body create paths, every remaining declared error status including the first 5xx arm on both error channels, undeclared-2xx and missing-envelope protocol failures. The matrix caught a real defect: the error-path constructor pushed its forgiven null through the list payload's defensive-copy init accessor, crashing every cursor-list error before the guard could hold — the copy now passes the forgiven null through uncopied, and the envelope snapshot records the shape.
…carriers (#19) Every union now emits a concrete-type converter for its Unknown* carrier: writing replays the preserved document instead of the record shape source-generated metadata would emit, and reading reproduces the base converter's fallback arm, so consumer serialization of a concrete carrier can no longer leak a {marker, Payload} record. Round-trip tests cover representative carriers through both the union base and the concrete type; the benchmark suite gains the ListMessagesAsync cursor-page baseline (58.1 us / 28.24 KB) and records the P2 single-pass win on GetMessageAsync (67.4 -> 56.2 us, allocations flat).
Seven latent walls close ahead of the surfaces that would trip them: numeric literals refuse at binding instead of mapping to string (F08); nested marked unions may spell their branches with oneOf (F09); a tag owned by two closure schemas refuses before it can poison the converter dispatch map (F10); wire parameters landing on emitted signature names (options, requestOptions, request, pipeline, cancellationToken) fail at bind, never as emitted compile errors (F11); an out-of-range curation placement is a loud curation error instead of a silently dropped group (F13); repeated path-template tokens refuse at ingestion since builders substitute each token exactly once (F14); and generated client, response, and options names collide against the model namespace and the hand-written spine, not just each other (F15). The P2 envelope fold landed with the emitter batch. Sandbox now demos create -> list -> get -> messages.
Research log Session 20 records the decisions sealed during execution (options/order/cursor type names, the schemaAliases resolution of the InvalidRequestError1 duplicate with its drift guarantees, the options/requestOptions parameter rule) and the execution notes (concrete verb rules, the P2 numbers, the defect the contract matrix caught). The ROADMAP moves the batch to complete, the plan's checkboxes close, and the consumed 2026-08-14 handover is deleted.
Null stays the unset spelling that reaches the OPENCODE_SERVER_PASSWORD fallback; an explicitly empty or whitespace password fails loudly with ArgumentException at construction instead of silently bypassing the fallback and traveling as a real Basic credential. The server always generates a password, so an empty-credential mode has no upstream meaning to model. Property documentation states the contract.
Research log Session 20 gains the review's three sealed decisions: operation
inputs unify on {Group}{Subject}{Verb}Request across query and body (Q83,
revising Q76's naming half on the ecosystem survey — no surveyed SDK splits
the suffix by verb), the shipped SDK slices by client family with flat
public namespaces and mirrored test layout (Q84, canonical wording now in
coding-style §4 with the IDE0130 arbitration), and the Extensions package
rises in parallel with M2 breadth instead of waiting for M6 (Q85). The #20
and #25 closures are recorded, the ROADMAP carries the alignment-batch
queue, and HANDOFF-2026-08-14-2 hands the execution to the next session.
The next session's handover now carries the whole sequence instead of the alignment batch alone: comprehensive onboarding order, the PR #26 state, the alignment batch landing on the open PR, PR finalization, the external multi-agent review cycle with its triage protocol, merge, and the M3 pickup (plan-first convention, scope pointers, and the sanctioned spec-pin refresh at the boundary).
Every operation input is now {Group}{Subject}{Verb}Request and the Options
family retires: the hand-written ListOptions seam becomes ListRequest (same
Limit/Order/Cursor trio), generated query records derive through
RequestTypeName, and the method and route-builder parameter renames
options -> request (requestOptions unchanged). Internally the plan vocabulary
follows: QueryRequestPlan, QueryPropertyPlan, QueryRequestEmitter,
OperationPlan.QueryRequest.
ReservedParameterNames shrinks to emitted reality (request, requestOptions,
cancellationToken, pipeline); a path parameter named 'options' is admitted
again and pinned by test. An operation declaring both a body and query
parameters now derives the same type name twice and fails the existing
collision wall - the intended fail-closed state until a merged-Request
design is sealed, pinned by a refusal test.
The PublicApi baseline diff is the rename only, reviewed as such.
Perf-neutral: no emitted-shape change beyond names.
The shipped SDK now slices by client family with flat public namespaces: Sessions/ and Health/ hold each family's clients, envelopes, and query requests; Pagination/ holds the ListRequest/ListOrder/ListCursor spine; Models/, Internal/, and the root client with the response/exception spine stay put. Namespaces remain OpenCode.Sdk and OpenCode.Sdk.Models - folders are placement, a namespace is API surface - so the PublicApi baseline is untouched by this change. Emission is name-blind: envelope and query-request files derive their folder from the operation's RouteContainerName, family clients from the new ClientPlan.ContainerName the binder sets from the curated client name; the root client, OpenCodeRoutes, and the registry keep their homes. The writer's allowlist learns the family folders fail-closed through the new GenerationWriteRequest (admitted folders come from the plan's container names, one level deep, never shadowing a static directory); manifest entries validate shape only so a retired family's files stay deletable behind the provenance wall. WriteAsync's six parameters fold into the request record per the signature-hygiene rule. IDE0130 is arbitrated for the SDK's public folders and their test mirrors per the standing per-rule pattern; contract tests and the parent-filter spine test move under Sessions/ with flat test namespaces. Manifest paths churned once; stale root-level copies deleted in the same regen.
OpenCode.Sdk.Extensions gains its first surface: three AddOpenCode overloads mirroring the client constructors - (Uri), (Uri, Action<OpenCodeClientOptions>?), and a caller-owned HttpClient with a required configure action (the endpoint is required on that path). Each registers the root OpenCodeClient as a singleton the container disposes, plus a SessionsClient registration resolved from the root so consumers inject sub-clients directly. Extension methods live in the Microsoft.Extensions.DependencyInjection namespace per the recorded companion-package convention, with the IDE0130 arbitration scoped to that one file. The only new dependency is Microsoft.Extensions.DependencyInjection .Abstractions per the research doc 06 policy; IHttpClientFactory integration stays out of this first cut deliberately. The new OpenCode.Sdk.Extensions.Tests project mirrors the layout and covers each overload's composition, sub-client resolution, the configure action, the caller-owned transport path against a canned health fixture, and the input guards. Registrations stay hand-written; the research log records the generator-emission trigger for when client families multiply.
The external/opencode gitlink still pointed at the 1.x line (v1.18.15) while spec/ has pinned v2-branch commit a6a712a since the M1 retarget - every fresh clone checked out a tree that no longer matches the surface the SDK is built against. The gitlink now records the exact spec-pin commit, so 'git submodule update --init' lands there mechanically; .gitmodules documents the v2 tracking branch, and the SNAPSHOT.md refresh procedure gains the lockstep step so the pointer and the snapshot never diverge again.
.gitignore carried an external/ pattern that never affected the tracked gitlinks but made every 'git add external/opencode' refuse with an ignored-path warning - the submodule pin update tripped over it. The submodules are ordinary tracked entries; they need no ignore rule.
Research doc 15 gains the parameter-placement census at the pin (5a: zero header params; the location deepObject on 61 of 120 ops; all 15 body+query mixes caused by location alone; session.list as the single flat-field exception) and the re-derived auth and location mechanics (optional Basic auth, configurable username via --username / OPENCODE_SERVER_USERNAME defaulting to opencode, client-side password resolution owned by the CLI consumer over OPENCODE_PASSWORD -> OPENCODE_SERVER_PASSWORD, dual-channel location resolution with query > header precedence). Research log Session 22 records the findings and the sealed decision: the location + merged-Request input design runs as a short session opening M3 planning instead of waiting for the collision wall. The vision spec's obsolete header-only Directory story is corrected in place; the ROADMAP M3 milestone carries the design session.
Sealed as the in-session 'Q86' batch (research log Q90). The client now constructs from options alone - OpenCodeClient(OpenCodeClientOptions) and the caller-owned-HttpClient overload; the Uri constructors retire and the endpoint has exactly one home, killing the endpoint-must-stay-unset guard. Options gain the read-only IOpenCodeClientOptions view the pipeline consumes and snapshots at construction (mutating an options object after a client is built never changes that client), plus a configurable Username (upstream: --username / OPENCODE_SERVER_USERNAME, default opencode) beside the optional Password whose null now plainly means anonymous requests - auth is optional on v2 servers. The OPENCODE_SERVER_PASSWORD environment fallback leaves the SDK entirely (upstream layering: the consumer resolves the environment; revises the env half of #20 on Session 22 evidence), and the environment-provider seam retires with it. The self-created transport rides SocketsHttpHandler with a pooled connection lifetime on modern TFMs. Extensions rebuilds on IHttpClientFactory: AddOpenCode(Action<...>) and a config-binding AddOpenCode(IConfiguration) (AOT-annotated) register a transient typed client over a named factory client and return the IHttpClientBuilder, so consumer handlers, resilience, and telemetry chain on without SDK middleware; SessionsClient resolves straight from the container. The sandbox becomes the live showcase: Generic Host, AddOpenCode, a consumer-owned ConsoleTimingHandler on the builder, and direct sub-client resolution. The PublicApi baseline diff carries exactly this reshape.
Research log Session 22 gains the Q90 entry (options-only construction, read-only options view, Username, no SDK environment reads, factory-based AddOpenCode over IHttpClientBuilder, pooled-lifetime owned transport); the ROADMAP status paragraph carries the reshape.
This was referenced Aug 14, 2026
This was referenced Aug 14, 2026
…rge, M3 The PR #26 review cycle ran through adversarial verification (36 findings: 30 confirmed, 4 plausible, 2 refuted, 10 merge blockers). Survivors are preserved durably: issues #27 (M5 naming-wall gaps), #28 (M2 curated mutual-exclusion, decision-first), #29 (M3 success-path allocations riding #23), #30 (M2 cursor PrintMembers), and seven hygiene items on #24. HANDOFF-2026-08-14-3 supersedes and deletes its predecessor: it carries the Q91 BYO-HttpClient question (recorded leaning: internalize behind the Extensions IVT; not sealed - an industry survey runs first, and a first-class-but-pragmatic default-header resolution keeps BYO acceptable), the ten-blocker fix batch with scratchpad-independent summaries and fold-ins, the merge steps, and the M3 pickup whose planning opens with the location + merged-Request design session. The ROADMAP status paragraph reflects the verification outcome and Q91 joins its open questions.
…ays public Seven-agent primary-source survey (doc 16): no surveyed SDK gates transport behind a DI companion, none guards the anonymous default-Authorization leak, and the stock typed-client pattern requires the public constructor. Anonymous mode will fail closed at construction and per send (blocker #1's fix shape); the ROADMAP open question resolves and the stale env-fallback sentence (#25) aligns with Q90.
… clients Anonymous mode cannot be expressed by omission over a caller-owned HttpClient: the BCL copies default headers onto every request that lacks the header, so a foreign credential would ride requests documented as anonymous (review blocker #1; Q91 seal). The pipeline now refuses the conflict fail-closed at construction (ArgumentException) and before every send (the typed transport spine, before any handler runs), by presence rather than parseability; with a password set the per-request header keeps winning by the BCL request-wins merge, now documented on the constructor and options. Direct-constructor and IHttpClientFactory/ConfigureHttpClient regressions pin that no request reaches the handler.
…ma forbids it The binder overwrote the sole nullability signal when widening optional non-collection properties for absence, so a wire null reached typed success instead of the malformed-body wall (review blocker #2). The plan now carries an AllowsWireNull bit capturing schema-admitted null (anyOf-null, plus JsonElement whose unrestricted schema admits everything) separately from the C# absence shape, and the emitter puts a shared AOT-safe rejecting converter on every optional non-collection property whose schema does not admit null: present-and-null throws JsonException, which the response adapters already map to OpenCodeTransportException; absence and true anyOf-null stay legal. Covered at the binder (synthetic + pinned parentID), the emitter snapshot, and the concrete 200 session path.
Public surface first (constructor, factories, ExecuteAsync, Dispose), then private members in first-use order — the repo-wide member-ordering rule.
A 200 page whose data array carried a null element deserialized into a typed success that exploded later with NullReferenceException (review blocker #3). Cursor-list DTOs now ride a shared AOT-safe converter that turns a null array or a null element into a JsonException the adapters map to OpenCodeTransportException, and the public envelope initializer's defensive copy refuses null elements with ArgumentException while keeping the error path's forgiven null behind the getter guard. Both generated cursor-list shapes are covered by contract tests and emitter snapshots.
The parameterless JsonStringEnumConverter admits integers, so a numeric status in a success body surfaced as an undefined enum member instead of a malformed-body transport failure (review blocker #4). Every generated enum now rides a shared strict converter chaining to JsonStringEnumConverter<TEnum>(namingPolicy: null, allowIntegerValues: false); the six enums regenerate, and context plus 200-session-path tests pin numeric-status rejection alongside the known-string mapping. Union carriers (ADR-0009) are untouched.
Alias identity compared primitive nodes by kind alone even though format
picks the emitted CLR type ('format: uri' emits Uri), so a format-drifted
duplicate could collapse silently instead of tripping Q81's fail-closed
drift wall (review blocker #5). Format now compares ordinally ahead of the
node-kind switch, riding every recursive comparison; a format-only alias
divergence refuses.
The collision wall bulk-unioned model names with ReservedSpineTypeNames, silently swallowing duplicates such as a schema named ListCursor whose unqualified emitted references would then resolve to the pagination spine type (review blocker #6). Reserved names now seed the owner set and each model name joins individually; a model/spine duplicate reports a deterministic naming diagnostic and no emit plan escapes.
Registry composition deduplicated model and DTO names with a Distinct, so an operation-derived DTO name matching a component model collapsed two serializer identities and made the emitted DTO recursively typed instead of refusing (review blocker #7). DTO names now validate against the schema registry and against each other before composition, with deterministic naming diagnostics; the Distinct is gone.
An override keyed to an alias source validated cleanly, then vanished after collapse because the type binder looked it up under the target key (review blocker #9). Override schema keys now map through the alias collapse before type binding, and rows landing on the same logical (schema, property) after resolution refuse with a curation diagnostic instead of one silently winning. Regressions cover the reachable-source override and the source-plus-target collision.
Operation-derived Request naming applied only to inline bodies, so a
selected operation with a component-referenced body would surface the raw
component spelling instead of the sealed {Subject}{Verb}Request name (review
blocker #12). The name resolver now takes the selected operations and claims
every selected nominal body root — inline or component — with the
operation-derived name; ownership is selection-scoped so pending operations
cannot rename shared components, two selected claims under different names
refuse, nested dependencies stay shared, and BindRequestBody asserts the
resolved name. Folds #8: operations mixing a request body and query
parameters refuse explicitly in the wire-shape check, independent of any
name collision, until the merged-Request design seals at M3.
Concrete unknown-carrier converters lacked HandleNull, so deserializing a null token bypassed Read and returned null for a non-nullable carrier (review blocker #14); neither converter validated a nested union's fixed outer tag, so a contradicted discriminator rode into the carrier and replayed on serialization (#13, folded). Carriers now emit HandleNull with an explicit null-token rejection, and both the carrier and base converters validate the fixed marker's presence, kind, and exact value while keeping unknown-inner-marker tolerance and raw replay. All eight carriers regenerate; regressions deserialize through the raw type info so the wrapper's null coalesce cannot mask a bypass.
Four small confirmed findings from the PR #26 review land together: the owned transport disposes when the pipeline constructor refuses credentials after creation (#15); the family-folder shadow and duplicate checks compare with the writer's case-insensitive owned-path comparer (#20); QueryStringBuilder escapes parameter names exactly like values (#21); and the User-Agent product token resolves once into a shared immutable instance instead of re-reflecting per client (#30 — no performance claim made, the reflection removal is the recorded obvious-waste rule). The Username documentation drops its pinned-server claim: the pinned server accepts only 'opencode'; --username/OPENCODE_SERVER_USERNAME is upstream direction, and the option is the gateway seam (#23; doc 15 and the Q89 finding corrected).
Comments in code artifacts explain the status quo on the spot — no doc or ADR citations, no decision dates or attributions, no milestone scoping or future promises, no review-finding ids. A repository-wide sweep rewords or drops every such fragment across the SDK, the generator, the tests, and the build infrastructure, and reunites a doc comment that had drifted away from ReservedPayloadNames.
…ing BaseAddress Second-review runtime findings: an HttpClient timeout escaped as TaskCanceledException masquerading as caller cancellation — cancellation whose token the caller never canceled now wraps into OpenCodeTransportException on both the send and body-read paths, while genuine token cancellation still passes through untouched. An injected client's BaseAddress, which the pipeline never consults, now refuses at construction and before every send instead of being silently ignored — the same loud posture as the anonymous Authorization guard. The non-positive Limit refusal becomes ArgumentOutOfRangeException carrying the route builder's own 'request' parameter name instead of a helper-internal one.
The handle factory and handle constructor now emit the same route-value guard as the route builders — non-whitespace and never a dot segment — so an id the route would refuse can no longer construct a handle and detonate later under a foreign parameter name; the guard pair lives once in EmissionSyntax. Unknown-carrier constructors refuse an unparsed default(JsonElement) payload with an argument exception instead of letting Clone surface a bare InvalidOperationException. The family-folder shadow wall now also owns Pagination, the hand-written spine folder its own doc comment promised to protect.
This was referenced Aug 15, 2026
M2: contract-test infrastructure consolidation — shared scaffold, single Support classes (R2-15)
#34
Closed
…queue, perf pass [skip ci]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
M2's first breadth batch:
v2.session.list,v2.session.get,v2.session.create, andv2.message.listbecome callable through their final generated surface, deliberately opening the three machines M1 walled off — query-parameter binding, request bodies (first non-GET verb), and cursor-paged list envelopes. Architecture unchanged: SpecIR + curation → plans → name-blind emitters → committed source delegating once into the hand-writtenPipeline, which grew exactly one capability (JSON bodies through the source-generated context).Executed per
docs/superpowers/plans/2026-08-14-m2-first-breadth-batch.md; decisions in research log Sessions 19–22 (Q76–Q90). Every commit is a red-test-first increment behind all six gates (830 tests at head, alignment batch and the construction/options/DI reshape included).Surface
MessageListRequest : ListRequeston exact trio match;SessionListRequestflat with 9 typed properties); sealedint?limit refuses non-positives at the route boundary.IReadOnlyList<T>payload + sharedListCursor; single-pass deserialization through internal envelope DTOs (the P2 fold —ReadDataPayload's double parse retired).Session.Infomodel closure (fork-boundary marked union with unknown carrier, revert, file diff, time/fork nests),InvalidCursorError,UnknownError, and the first 5xx status-map arm.InvalidRequestError1duplicate collapses through the newschemaAliasescuration (deep-identity wall; every upstream drift direction stays loud).List*/Create*verb rules with the C18 structural fix; C17/C19/C20 naming walls; the M2: generator fail-closed walls batch (F08 F09 F10 F11 F13 F14 F15 + P2 fold) #21 fail-closed walls batch (F08–F15); concreteUnknown*carrier converters (M2: Unknown* concrete-type serialization does not reproduce the raw document (F07) #19).request(operation input) andrequestOptions(per-call) — the Q83 rename landed in the alignment batch below.Alignment batch (Q83–Q85)
The three sealed alignment decisions (research log Session 20) landed on this branch after the breadth batch, one commit each:
*Requestrename (Q83,c26653a). Every operation input is{Group}{Subject}{Verb}Requestand the Options family retires: the hand-writtenListOptionsseam becomesListRequest(same Limit/Order/Cursor trio — the M3 paginator seam keeps its shape), generated query records derive throughRequestTypeName, and the method/route-builder parameter renamesoptions→request. An operation declaring both a body and query parameters now derives the same type name twice and fails the existing collision wall — the intended fail-closed state until a merged-Request design is sealed, pinned by a refusal test. The PublicApi diff of this commit is the rename only.251b483).Sessions/,Health/, andPagination/folders with flat public namespaces — folders are placement, a namespace is API surface, so the PublicApi baseline did not move at all. Emission is name-blind (per-operationRouteContainerName; the binder-setClientPlan.ContainerNamefor family clients); the writer's allowlist learns family folders fail-closed from the plan's container names through the newGenerationWriteRequest; IDE0130 is arbitrated for the SDK's public folders and their test mirrors only.1680d7b), reshaped by Q90 below. New mirroredOpenCode.Sdk.Extensions.Testsproject; the first-cut constructor-mirror shape was grilled and rebuilt the same day.a0c83a9) — sealed after a maintainer grill plus upstream re-research at the pin (research log Session 22, Q87–Q90). Construction is options-only (OpenCodeClient(OpenCodeClientOptions)+ BYO-HttpClient; the Uri ctors retire — one home for the endpoint). Options stay convention-mutable but consumption is immutable: the pipeline reads the newIOpenCodeClientOptionsview and snapshots at construction (pinned by test).Usernamejoins (upstream--username/OPENCODE_SERVER_USERNAME, defaultopencode);Passwordnull = anonymous (v2 auth is optional); theOPENCODE_SERVER_PASSWORDfallback leaves the SDK entirely — consumers resolve env, per upstream's own layering (revises the env half of M2 decision: OpenCodeClientOptions.Password semantics — unset vs empty vs whitespace (C16) #20).AddOpenCoderebuilds onIHttpClientFactory:Action<>+ AOT-annotatedIConfigurationoverloads, transient typed client, explicit sub-client registrations, returnsIHttpClientBuilderfor handler/resilience/telemetry chaining. Owned transport gainsSocketsHttpHandler+PooledConnectionLifetimeon modern TFMs. The sandbox is now the Generic Host showcase of all of it.Numbers
P2 single-pass envelopes:
GetMessageAsync67.4 → 56.2 μs, allocations flat (27.4 KB). New baseline:ListMessagesAsync58.1 μs / 28.24 KB.GetHealthAsyncunchanged (1.6 μs / 2.08 KB). The alignment batch is perf-neutral (names and file placement only; no emitted-shape change).Live demo (opencode2 serve v0.0.0-next-17403, 2026-08-14)
Review guidance
tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txtis the primary review surface: the hand-written spine types (ListRequest,ListOrder,ListCursor,SessionParentFilter,IOpenCodeClientOptions), the options-only constructor pair, the uniformrequest/requestOptionsparameters, the four operations with their request/response types, theSession.Infomodel graph, and[JsonConverter]attributes on the eightUnknown*carriers. The Extensions package has no baseline lock yet — reviewOpenCodeServiceCollectionExtensionsdirectly (two public methods returningIHttpClientBuilder).tools/curation.jsongained themessagegroup row (merged into the Sessions client family) and theInvalidRequestError1alias — curation changes are API review per ADR-0008.Sessions.ListSessions,Sessions.GetSession) — the mechanical resolution that keeps merged families collision-free; root members keep the bare-verb shape (Health.Get).Verified-review fix batch (2026-08-15)
The adversarially verified external review (36 findings: 30 confirmed / 4 plausible / 2 refuted) closed out on this branch — every merge blocker plus the small confirmed fold-ins landed red-test-first, one green commit per fix, all six gates before each. Tests: 830 → 904.
(HttpClient, options)constructor stays public — no surveyed SDK gates transport behind a DI companion, and the stock typed-client pattern requires the public constructor.b099aaa— anonymous mode refuses a defaultAuthorizationon an injected client, at construction and before every send (presence-based); with a password set the SDK's per-request header wins, now documented on the constructor and options.7ccb717— the plan carriesAllowsWireNull; optional non-collection properties whose schema forbids null reject explicit wire null through shared AOT-safe converters (JsonException →OpenCodeTransportException); absence and true anyOf-null stay legal. The PublicApi baseline diff is exactly the 44 new converter attributes.5d65711— cursor-list pages reject a null array and null elements on the DTO path; the public initializer's defensive copy refuses null elements.1b22c49— generated enums ride a strict string-enum converter (allowIntegerValues: false); numeric wire values are malformed bodies.ae73e8a—Formatjoins schema-alias deep identity (the Q81 drift wall).64a94dc— model names colliding with hand-written spine types refuse with explicit naming diagnostics.9dd4a37— envelope DTO names collision-check against model names; the registry's collision-hidingDistinctis gone.1c8b190— property-override keys canonicalize through the alias map; rows collapsing onto one logical property refuse.e7015df— selected component request bodies get operation-derived*Requestnames through a selection-scoped ownership map; body+query operations refuse explicitly until the merged-Request design seals at M3.691efc8— unknown-carrier converters gainHandleNullwith explicit null rejection, and both converters enforce a nested union's fixed outer marker; unknown-inner-marker tolerance and raw replay stay.52c9696— feat(generator): compile the M1 model closure #15 owned-transport dispose on refused construction, M2 decision: OpenCodeClientOptions.Password semantics — unset vs empty vs whitespace (C16) #20 case-insensitive family-folder checks, M2: generator fail-closed walls batch (F08 F09 F10 F11 F13 F14 F15 + P2 fold) #21 query-name escaping, M3: union deserialization single-pass redesign + streaming adapter boundary (P1 P5 P3) — benchmark-gated #23 Username documentation correction, Response envelopes omit Cursor and Location from PrintMembers (V27) #30 one-time User-Agent resolution. (M2 decision: DynamicProxyGenAssembly2 InternalsVisibleTo — keep as recorded mechanism or replace the seam (N1) #25 landed with the Q91 docs commita782d34; the Pipeline member-order refactor isf74be1f.)Second review pass (same day)
A second full-diff review produced 15 verified findings. The six small runtime/emitter fixes landed here (
03c0aa1,b628c0f): timeouts wrap intoOpenCodeTransportExceptioninstead of masquerading as caller cancellation, a conflicting injectedBaseAddressrefuses loudly like the Authorization guard, handle construction applies the same route-value guard as the route builders, unknown-carrier constructors refuse unparsed payloads, the non-positive-limit refusal carries the caller-visible parameter identity, and the family-folder wall ownsPagination. Every surviving finding is milestone-anchored with an explicit trigger: #31 (M2, DI lifetime shape, decision-first), #32 (M3, EscapeDataString TFM policy), #33 (M3, carrier hand-construction semantics), #34 (M2, contract-test consolidation), plus enrichments on #27 (five new wall gaps) and #23 (the list re-copy mechanism).Closes #19
Closes #21
Closes #22