Skip to content

feat: land the launcher and live-coverage arc — output collector, dogfooded fixtures, and the 89f1943 pin - #58

Merged
Blind-Striker merged 64 commits into
masterfrom
codex/independent-live-coverage
Sep 7, 2026
Merged

Blind-Striker merged 64 commits into
masterfrom
codex/independent-live-coverage

Conversation

@Blind-Striker

@Blind-Striker Blind-Striker commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

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 ProjectWorktreesClient is gone; the plugin types were reshaped upstream into PluginInfo with an IPluginState union; 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 through OpenCodeServerOptions.Output, which stays readable after a failed start. OpenCodeServer.ProcessId now 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 than ObjectDisposedException; 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 OpenCodeServer instead 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 on net472 Windows through taskkill /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 across net8.0, net9.0, net10.0 and net472. generate --verify, refresh-spec --verify and --help pass 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.subscribe and none on v2.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.

@Blind-Striker Blind-Striker changed the title test: add normal PTY live lifecycle coverage test: expand typed live lifecycle coverage Sep 6, 2026
@Blind-Striker Blind-Striker changed the title test: expand typed live lifecycle coverage test: expand live coverage and preserve owned failure diagnostics Sep 6, 2026
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 Blind-Striker self-assigned this Sep 7, 2026
@Blind-Striker Blind-Striker changed the title test: expand live coverage and preserve owned failure diagnostics feat: land the launcher and live-coverage arc — output collector, dogfooded fixtures, and the 89f1943 pin Sep 7, 2026
@Blind-Striker
Blind-Striker marked this pull request as ready for review September 7, 2026 21:52
@Blind-Striker
Blind-Striker merged commit bb335ba into master Sep 7, 2026
4 checks passed
@Blind-Striker
Blind-Striker deleted the codex/independent-live-coverage 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.

1 participant