Repository navigation
feat: land the launcher and live-coverage arc — output collector, dogfooded fixtures, and the 89f1943 pin - #58
Merged
Conversation
The two retained unavailable tests called the unregistered rpc id in every fixture mode. On an operator-supplied external server that id could already be registered, so the tests would invoke a foreign RPC contract before asserting. Route both channels through the shared mode preparation: owned mode keeps the unregistered id; external mode proves the repository-owned plugin id absent after activation and a successful plugin list, then calls only that id, preserving the NoThrow and throwing unavailable assertions.
The repository-owned plugin's emit method publishes a declared event; the SDK's event bus must deliver it typed as EventRpc with the exact prefixed type, the caller's nonce, and the plugin's registration location. The subscription is attached (server.connected observed) before the emit call, so the proof rests on a causal barrier rather than on replay. Owned mode only; an external endpoint has no owned plugin and fails fast.
Cancelling a call, or the progress window expiring, while the response body was still being materialized could surface the runtime's own fault instead of the documented outcome. The buffering policy interrupts a stalled read by disposing the content; on .NET 10 HttpContent buffers into a pooled stream that Dispose returns, and its ReadAsStreamAsync path then builds the read stream over a null array (ArgumentNullException) when disposal lands after the buffer is assigned and before the stream is created. That exception is not one the read phase classifies, so it escaped as-is. Once the policy has interrupted the read itself, any fault the BCL raises afterwards is a consequence of that interruption: map it as caller cancellation when the caller's token is cancelled and as a transport failure when the window expired. Map's non-cancellation tail becomes MapFault and is shared. Two deterministic regression tests reproduce the runtime's exact exception through a content whose materialization blocks until disposal.
… collector An optional OpenCodeServerOutput collector, supplied through OpenCodeServerOptions.Output, retains a bounded tail of the child's stdout and stderr (the newest 500 lines and at most 1,048,576 characters per stream, the readiness line included) for pull snapshots that report whether either stream lost content. The collector binds to exactly one start before the child is spawned, invokes no caller code on the process readers, stays readable after a failed start, needs no disposal, and is closed after the existing bounded diagnostic drain on disposal so a snapshot taken then is final. Callers that supply no collector see no change. The generator's reserved spine names learn the two new public types, the public API baseline records them, and client-runtime carries the sealed paragraph. Real-process tests cover both streams and empty lines, output after a failed start, single binding before spawn, and finalization on the forced, graceful, and already-exited disposal arms; unit tests cover the bounds and flags.
ProcessId read the live Process handle, so a caller writing diagnostics after DisposeAsync got "No process is associated with this object". The identity is captured while the handle is live and stays readable afterwards.
…dapter Both server fixtures now start their child through the SDK's own launcher with an output collector, so every live test exercises the product launcher and a launcher regression surfaces in the whole live lane. The test-only CliWrap adapter, its log tail, its tests, and the CliWrap dependency are removed; the persistent simulation launch recipe becomes a shared helper. Output-dependent proofs move from push waits to the collector's final snapshot: the persistent simulation host's three milestones are proven by a dedicated real lifecycle (stdout carries only readiness; starting, ready, stdin-closed reach stderr in order with their severity) and rechecked by the simulated fixture at teardown; the fixture failure proofs wait for the owned server's actual disposal to settle before delivering a controlled teardown timeout, so the output they read back is the launcher's final collection. Adapter-specific deadline proofs (forced-exit verdict, startup teardown timeout) retire with their owner; the product launcher's startup failures are covered by its own lifecycle tests, and the fixture still retains logs under the results root when the start fails.
… final snapshot Over a chatty session the collector's bound evicts the early starting/ready lines, so the shared fixture's teardown proves the one milestone the final snapshot can still hold; the dedicated PersistentSimulationHostTests lifecycle proves all three on a short-lived host.
Merges codex/launcher-reap-tests (f7e64b5, 240b3a4, 2de3fb7) as reviewed: the child-tree peer, its authenticated root/child control leases, and the startup-tree scenario. The test project's embedded-resource list already covered the peer scripts. The accepted completion contract follows in the next commit.
A whole-tree kill is asynchronous: the direct process exiting does not guarantee that every descendant has finished exiting at that instant, which the Linux legs of the parked proofs showed. The startup-tree proofs keep the immediate direct-child exit assertion and separately observe the already identified grandchild terminate inside a ten-second bound of its own, with both authenticated control leases still held; a descendant that survives the bound fails the test with its process evidence. This is bounded descendant termination evidence, never all-descendants-reaped-at-return evidence; the launcher's runtime behavior is unchanged, and the client-runtime canon states the boundary.
The readiness bound and the ten-second grace live in one policy owner, the simulated launch recipe builds the launcher options itself, the dedicated persistent-host lifecycle owns its server for disposal, and the fixtures name the margin their teardown bound keeps above the launcher's worst case.
…m boundary The three failed-start proofs share one assertion helper stating the accepted contract; the scenario's cleanup message no longer calls a bounded observation immediate; the stdin-lease proof says why it disposes twice; and the roadmap records that the downlevel non-Windows tree-kill arm is untested and that Linux/macOS termination is proven only by the three-OS run.
…t repository Over the simulated server and an owned local repository: the root row lists with no strategy; a linked Git worktree is created under an owned parent with a fixed absent name and proven by the parent's seeded marker rather than the spelling of a temporary path; the inventory shows root plus the linked row with strategy git; refresh answers 204 and preserves the inventory; a dirty worktree refuses non-forced removal with the typed WorktreeError naming force, and forced removal empties the inventory back to the root; an unavailable strategy answers the exact 400 message and creates no checkout. Cleanup removes the linked worktree through the SDK at its known owned destination and then disposes the workspace under finite budgets that preserve the primary failure.
…me the worktree proofs once The create-and-prove block and the WorktreeError requirement become one helper each, the fixed child name is a constant, and the live proofs observe the child absent before the create rather than inferring it.
The handshake requires the six tool capabilities; the composer gains the canonical toolCall model item and the tool.attach/update/finish/fail requests, writing supplied JSON elements rather than encoded strings; the controller routes llm.request, tool.invocation, and tool.cancel to separate bounded FIFOs, exposes attach/update/finish/fail and the two tool waits, and a model request now carries its cloned body so a proof can require the real content the model loop resumed with. The simulation seed gains the c16 agent whose builtin read is gated by a real permission prompt. Existing simulated workflows are unchanged in behavior.
… end Three live proofs over the real server in simulation mode, each advancing only from an observed positive fact. The permission chain runs one session through three model turns: the model calls the owned drive_echo tool, the controller answers progress and a structured result, and the next model request is required to carry that real tool result; that turn calls the builtin read tool, whose own permission assert parks execution until the SDK replies Once, and the following model request is required to carry the real read result before the final text and the terminal success. Per-call order is asserted as a subsequence, so unrelated events may interleave. The lifecycle proofs cover the two remaining protocol arms. A failed tool reaches the SDK as a correlated failure whose real message the model loop resumes with. An interrupted session cancels the held invocation with the exact tool.cancel identity, fails the tool as aborted, settles as a user interruption, and refuses a late finish with the backend's own message. The scenario owns its workspace, registration, session and single event reader, and its cleanup interrupts an unfinished session, requires the exact cancellation for a tool it still holds, waits idle, resets the registration, removes the session and settles the reader under independent budgets. The event probe records what became of its reader so a barrier that expires names the reason.
…ario's client The lifecycle proofs now assert the tool invocation's owning session, so a stale invocation left in the controller's shared queue cannot satisfy either arm. The scenario owns the run wrapper, so the tests no longer carry cleanup plumbing, and it disposes the client it created. The event probe races the connected frame against its own reader, so a subscription that ends or faults before its first event surfaces as that rather than as a later barrier expiry, and it records any other fault for the same reason. Progress joins the drive call's asserted order, the permission indexes correlate by request id, cleanup's cancellation bound sits inside its budget so the controller's diagnostic is the one reported, settling releases only the invocation it names, and the chat route has one home.
The Windows leg now runs 5,006 tests; the three modern targets Linux and macOS run carry 3,934 of them.
CI downloads ripgrep 15.1.0 with pinned digests so the offline simulated server never has to reach the network for it, but the authority for that number is upstream's own constant, which no watched source covered. A refresh that moves it would have reproduced cleanly and broken the filesystem live tests instead. Prepared and applied at the accepted pin, so the snapshot document is byte-identical and only the watch list and receipt grow.
ProcessId is documented and changelogged as readable after disposal because fixture owners write their diagnostics at teardown, but nothing asserted it; a regression would have surfaced as an exception inside a teardown rather than as a named failure. The tool scenario's reader now gets a budget that fits inside the scenario's own, so its cleanup diagnostic is the one a stuck teardown reports, and the owned-reader arbitration states its winner.
The contributor prerequisite list gains the dependency the README already documents and CI already provisions. The roadmap carries its latest substantive date and records the second launcher caveat, that a Unix grandchild's exit is visible only once an adopting parent reaps it. The changelog says the retired worktree handle is a nightly compile break and names the destination directory that became optional with the strategy.
SessionMoveEvents, ScriptedTurnEvents and RpcObservedEvents each ran their own loop over the event bus to settle a single correlated event, differing only in that event's predicate and its message. SessionEventProbe already holds one reader for a whole scenario and answers typed barriers from what it retained, so the three are gone and their live consumers wait on it. The probe now races its reader on every wait. A subscription that faults or ends mid-turn reports its own cause instead of expiring the caller's barrier on an event that can never arrive, and a caller cancellation keeps its identity rather than being reported as a timeout, which is the distinction the retired helpers made through the reader's own classifier. An optional anchor restores the other ordering they enforced: the scripted turn's success must be the one that followed the reply it scripted, not an earlier execution's. The barrier bound moves onto the probe that owns it, behind a factory that replaces six copies of the linked-source ritual. OwnedEventReader.Start and the "event observation" deadline it drove have no consumer once the probe hands its loop over through Own, so they go with the classifier only they used. The retired unit proofs move onto the probe, keeping the bounded transcript and the preserved reader failure and gaining the retained-then-arriving barrier that nothing covered before.
A successful SSE body stays live until caller cancellation, server completion, or failure, so a peer that disappears without closing hangs a consumer that supplied no cancellation of its own. Ordinary resets, server exits, and killed processes already surface at once, and a locally launched server closes its socket when it dies, so the gap is real only for remote, proxied, or sleep-and-resume endpoints. A consumer cannot bound it from outside the SDK: the server's keepalive comments carry no event, so nothing above the framer can tell an idle session from a dead connection. The bound also cannot be a property of every SSE body, because upstream writes a keepalive every fifteen seconds on v2.event.subscribe and none on v2.session.log, whose follow mode is silent by design while a session is idle. It is therefore queued behind M6's public network-timeout knob, which the milestone now names, so the bound arrives configurable rather than as a behavior no caller can widen.
…d proof The collector's exited-child proof launches a server that ends itself on a half-second timer, then takes a handle on it to wait for that exit. Taking the handle is itself racing the timer: on a loaded runner with four target frameworks in flight the child is already gone, and Process.GetProcessById answers ArgumentException rather than a handle, failing the arrangement for reaching the exact state it was arranging. Three-OS CI caught it on Windows net8.0 and net9.0 while net10.0, net472, Linux and macOS passed. The wait now treats an absent process as the state it waits for and only awaits a handle it actually obtained, which is the contract IsProcessRunning in the same class already relies on. The proof itself is unchanged: the child is gone before disposal, and disposal still finalizes both retained streams.
Blind-Striker
marked this pull request as ready for review
September 7, 2026 21:52
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.
Sixty-four commits over
d3c2517, landing the A/B/C arc: the refreshed upstream pin, the launcher's public output surface, and live coverage that exercises the SDK against a real opencode server rather than a mock.Public surface. Three compile breaks reach nightly consumers, each detailed in the change log: worktree operations resolve their project through an optional location query and
ProjectWorktreesClientis gone; the plugin types were reshaped upstream intoPluginInfowith anIPluginStateunion; and the RPC and plugin operations arrived under curated names. New surface:OpenCodeServerOutput, a caller-owned bounded collector for the launched child's stdout and stderr, supplied throughOpenCodeServerOptions.Output, which stays readable after a failed start.OpenCodeServer.ProcessIdnow stays readable after disposal, because owners write their diagnostics at teardown.Two SDK defects the live suite found. A body read interrupted by its own progress window surfaced as an unclassified fault on .NET 10, where
HttpContent's buffering path fails with whatever its torn-down state yields rather thanObjectDisposedException; it is now classified as the interruption it is. The launched server's process id was read from a disposed handle while teardown diagnostics were being written.Launcher. Test fixtures now start their server through the SDK's own
OpenCodeServerinstead of the CliWrap adapter, which is retired, so a launcher regression fails the live suite instead of hiding behind separate process plumbing. Descendant termination is bounded and proven: the direct child exits at once and the grandchild inside a ten-second bound, on the modern target frameworks across all three operating systems and onnet472Windows throughtaskkill /T.Live coverage. Sessions drive a real model loop through a simulated provider under WebSocket control, covering a model-originated tool's progress, completion, failure and interruption, the builtin read tool's permission gate, worktree lifecycle, session move, logs and replay, and the RPC family's live event. Every owned resource settles under an independent finite budget that preserves the primary failure.
Evidence. Full Release suite on
540f460: 5014 tests, 0 failed, 0 skipped, nine assemblies acrossnet8.0,net9.0,net10.0andnet472.generate --verify,refresh-spec --verifyand--helppass on the same tree. Three-platform CI passed on Windows, Linux and macOS: run 34163034864.One gap is recorded rather than closed. A half-open event stream is not detected: a successful SSE body stays live until caller cancellation, server completion, or failure, so a peer that disappears without closing hangs a consumer that supplied no cancellation of its own. The bound has to be per operation, because upstream writes a keepalive on
v2.event.subscribeand none onv2.session.log, so it is queued behind M6's public network-timeout knob. The roadmap's Known Gaps carries the detail.Publication remains a maintainer decision.