Skip to content

refactor(driver): unify the pi/omp session scaffolding and the exit-label map - #510

Merged
schickling-assistant merged 4 commits into
mainfrom
schickling-assistant/2026-09-07-pi-family-session
Sep 7, 2026
Merged

refactor(driver): unify the pi/omp session scaffolding and the exit-label map#510
schickling-assistant merged 4 commits into
mainfrom
schickling-assistant/2026-09-07-pi-family-session

Conversation

@schickling-assistant

Copy link
Copy Markdown
Contributor

Tier-1 item T1-DUP from #504, held back from #499#502 because the dedupe carries deliberate
behaviour changes and folding those into a "no semantic change" PR is how they ship unreviewed.
Each behaviour change is its own commit, named below with its pin.

Four commits, in this order:

  1. fix(omp) — the session-end diagnostic moves off the agent's terminal (behaviour change).
  2. refactor(driver) — the pi/omp launch body becomes one HarnessKind fork (no behaviour change).
  3. refactor(driver) — one exit-label map (behaviour change: one wire-visible label).
  4. refactor(driver) — the byte-identical adapter copies fold (no behaviour change).

The fix leads deliberately. omp's eprintln! lives inside the function commit 2 deletes, so had it
landed last the unify itself would have silently carried it — the exact failure mode this split
exists to prevent.


Deliberate behaviour changes

(a) The (None, None) exit label flips from "exited" to "exit unknown" for pi and omp

Wire-visible: it is the exit field of the harness-state record. Five copies of the
(status, signal) -> label map existed and disagreed on exactly this one arm — pi/omp said
"exited", provider_session, opencode_session and codex said "exit unknown". The unified
label is "exit unknown": "exited" claims an ordinary end for a status that answers neither
question, which is the one thing that arm cannot know.

Pin: provider_session::tests::the_exit_label_map_covers_every_arm — a
(status, signal) -> label table over every arm, (None, None) included. This pin did not exist
before. Note the correction to the report in #504: the claim that the unification is "pinned by
provider_session.rs:337" is false — that assertion is fed by
SessionObserver::launch_error()'s hardcoded "exit unknown" literal and never calls the map.

Reachability, verified in this tree, not assumed. The arm is unreachable for a child this
process reaped. Child::wait/try_wait call waitpid with neither WUNTRACED nor
WCONTINUED, so every status they return satisfies WIFEXITED or WIFSIGNALED, and code() or
signal() answers. The only construction is ExitStatus::from_raw(0x7f) — the stopped wait status
— and grep -rn "from_raw" src/ crates/ finds exactly four ExitStatus::from_raw call sites in
the whole tree (two in provider_session, two in pi_family_session), every one of them inside a
#[cfg(test)] mod tests. Every other from_raw* hit is FromRawFd.

Every ExitStatus reaching the pi/omp label comes from ProviderOutcome, whose two variants are
built only from child.try_wait() and child.wait() in provider_session::run_provider_observed
and stop_provider_group.

No reader parses the label back. Every consumer of the exit field either displays it or asks
is_some():

reader what it does with exit
agents::ObservedJson (src/agents.rs:179) observed.exit.as_deref() straight into JSON output
src/main.rs:1131 .exit.as_deref().map(str::to_owned) for display
harness_state coalescing (:420, :433) exit.is_some() and whole-field equality
harness_state claim eligibility (:891) record.exit.is_some() — "is this a real terminal record"

No match on the string, no prefix test, no split. grep -rn '"exited"' across src/, tests/,
crates/, hooks/ and components/ finds no consumer of the harness-state literal: every other
"exited" in the tree is the unrelated PTY task lifecycle word (runtime.state), which this diff
does not touch.

(b) omp's session-end failure moves off the agent's stderr into the tracing log

provider_session spawns the provider with stderr(Stdio::inherit()), so omp's
eprintln!("st2 omp driver: recording session end failed: …") painted into the interactive
harness's own terminal — inside the agent's UI, where a human is reading a session. pi's twin
already used tracing::warn!; the merged item keeps the log.

Pin: none, and that is stated rather than papered over. No test asserts the wrapper's stderr,
and adding a process-level stderr-capture test for one warning line would be a heavier fixture than
the change deserves. It is review-pinned only, and it is its own commit precisely so a reviewer sees
it as a change of destination rather than as three lines inside a 700-line refactor.


Doc-rationale merges

Several of these pairs carried different rationales for identical code. The rationale is the part
that was genuinely not duplicated, so the merged item carries both halves:

  1. OFFLINE_DEFAULTS — pi's ("a managed agent that update-checks or self-updates at boot makes
    its own launch latency depend on the network, and lets a release change a running fleet") plus
    omp's open question ("whether they suppress the update banner in interactive boots is still open,
    DQ-OMP-5; shipping them is harmless either way"). Both survive on the shared const.
  2. publish_provider_auth's None / Support::Unknown — Claude's ("a hook payload carries no
    Claude version — session id, transcript path, cwd, prompt id, permission mode, agent identity and
    effort, and nothing else (2.1.259) — and st2 gates no Claude version") plus the pi family's ("the
    WRAPPER, not the channel, owns the version gate and refuses the launch on an unadmitted MINOR,
    OMP-R05, so a running channel has no version fact of its own to publish"). Both are now stated on
    the one function, because they answer the same question for different reasons.
  3. publish_provider_auth's "fresh publisher per edge" — Claude's ("each hook invocation is its
    own process, so the stage set starts empty and the on-disk fallback is what lets a later Stop
    clear a rejection an earlier StopFailure wrote") plus pi_channel's ("a channel that restarted
    mid-session inherits the predecessor's record rather than silently starting clean").
  4. with_channel_extension — pi's doc is a strict superset of omp's: it carries the $ST_HOOKS
    token failure mode and omp's "a rendered machine-local path would pin one host's layout into a
    catalog". Both clauses are on the merged item; nothing was dropped.
  5. record_session_end — pi's doc adds why the Writer is constructed at the terminal edge (it
    re-reads what the channel last wrote and continues its transition counter); omp's is contained in
    it. Kept in full, generalized from "pi" to "the harness channel".
  6. describe_exit — gains the reachability rationale above, so the next reader does not have to
    re-derive why the arm says "unknown".

What stays per-harness, and why

  • The channel env-name constants do not move. pi_channel.rs reads both modules' CHANNEL_* by
    path (PI_KIND / OMP_KIND in pi_channel), and the whole point of two sets is that an omp seat
    can never adopt a stray pi configuration. HarnessKind references them; it does not own them. New
    pin: pi_family_session::tests::the_two_harnesses_export_disjoint_channel_variable_names.
  • omp's version gate rides the descriptor as verify_version: Option<fn(&str) -> Result<()>>
    and is called exactly where omp called it: after the empty-argv check and before
    harness_state::claim, so an unadmitted minor still fails without claiming ownership of the seat.
    pi's slot is None rather than a function that always succeeds — a gate that cannot refuse is not
    a gate. New pin: omp_session::tests::the_version_gate_is_wired_into_the_shared_launch_fork.
  • provider_auth_edge stays forked. The two decode disjoint inputs — a hook event name plus a
    JSON payload versus a typed TurnResult — and shared only the output enum, which this PR unifies.

Adapters folded, each verified identical first

item copies verdict
exit label 5 folded onto provider_session::describe_exit (see (a))
with_channel_extension 2 byte-identical → shared, extension name parameterized
offline_defaults + its const 2 byte-identical → shared
channel_env 2 identical modulo the per-harness const names → shared, names from the descriptor
record_session_end 2 identical modulo label and warning sink → shared
channel_content 2 byte-identical (diff clean) → native_channel
write_json 2 byte-identical (diff clean) → native_channel
publish_provider_auth 2 identical modulo a hardcoded Driverdriver_diagnostic, driver as an argument
enum ProviderAuthEdge 2 byte-identical definitions (diff clean) → driver_diagnostic, which already owns Driver/Stage/Reason/Source/Support
opencode's completed 1 folds onto completed_provider("opencode", …): "{provider} provider exited with {exit}" reproduces "opencode provider exited with {exit}" byte for byte

channel_content and write_json land in a new crate-internal native_channel module rather than
in either channel: what a delivered inbox message looks like to the model, and how a frame is
terminated on the wire, are st2's decisions rather than each channel's. The NDJSON framing also
gains the pin it never had — one newline-terminated line per value, including a body carrying its
own newline.

Not done, with the evidence

Every item below was re-checked against this tree, not taken on the report's word.

  • compaction_trigger unification — skipped. claude_session::compaction_trigger
    (src/claude_session.rs:355-357) decodes {manual, auto} and everything else to Unknown;
    pi_channel (:564-568) decodes five words. Unifying would widen Claude's decode vocabulary to
    {manual, auto, threshold, overflow, idle}, and the narrow set is a documented decision pinned by
    an_unrecognized_trigger_word_decodes_as_unknown_not_as_a_definite_one, which asserts Claude
    decodes "idle" as Unknown. That test would have to be deleted to land the "dedupe".
  • Extracting the shared --version banner capture — skipped. harness_version's module doc
    states the divergence is intentional: "omp gates the launch, opencode degrades to no native
    delivery, and codex refuses semantic-version reasoning outright." Only the parsing is shared, and
    it already is. Extraction would also change one of opencode's error strings.
  • Folding codex's completed_tui — skipped. Its message is
    "controlled Codex TUI exited with {status}" (src/codex_app_server.rs:2897), which
    completed_provider cannot reproduce: an operator-visible string change for zero structural gain.
  • stop_provider_group — skipped, genuinely non-extractable. provider_session's takes an
    observer and writes the terminal record on two paths opencode's has no equivalent for (the
    graceful reap and the pre-SIGKILL cover); opencode's takes only the child, because its observation
    ownership lives in a different type.
  • pi/omp's own anyhow::ensure!(exit.success(), "{label} provider exited with {exit}") — left
    inline rather than routed through completed_provider. It is byte-identical and would fold
    cleanly, but it is not on the sheet, and this PR's job is the enumerated set.

Verification

  • CARGO_TARGET_DIR=/tmp/st2-lane-cargo CARGO_BUILD_JOBS=8 nix develop --command cargo check --all-targets
    — clean, and no warning in any file this diff touches. (The tree's pre-existing warnings in
    resource_profile_supervisor, ding/mod.rs, tests/resync.rs and codex_app_server's
    private_interfaces are unchanged.)
  • cargo test --lib -- pi_session:: omp_session:: pi_family_session:: provider_session:: opencode_session:: codex_app_server:: pi_channel:: claude_session:: claude_mcp:: native_channel:: driver_diagnostic::168 passed, 0 failed (run three times).
  • cargo test --test driver_expansion --test harness_state_teardown --test up_once_exit
    13 + 3 + 4 passed, 0 failed. harness_state_teardown is the one that exercises a real wrapper's
    terminal record end to end (exit = "signal 9", "stopped", "signal 15").
  • Size: git diff origin/main --numstat over omp_session.rs, pi_session.rs, pi_channel.rs,
    claude_mcp.rs, claude_session.rs, opencode_session.rs, codex_app_server.rs is
    +80 / −670 (net −590). The whole-diff net is +39, because the shared modules carry the
    merged body, both halves of every merged rationale, and five new pins (~100 lines of tests that
    did not exist). Stated plainly: this removes a duplicated body, not line count.

Independent review

A reviewer with a clean context was asked to falsify the exclusivity claim above. Verdict: the two
stated behaviour changes are the only ones
— no third change, no bug introduced by the fold. It
re-derived the unify by normalizing both original launch bodies and diffing them (operationally
identical apart from the harness word, omp's version gate, doc wording, the stderr sink and a type
alias), confirmed the version gate still runs before harness_state::claim, confirmed all five
exit-label copies are folded with every non-(None, None) arm spelled identically, re-ran the
reachability greps, and checked each adapter fold against BOTH originals.

Two corrections it made to this description, neither changing the verdict:

  • The reader table above is incomplete. The full set of exit-field readers is
    agents.rs:179, main.rs:1131, and harness_state.rs:420 (is_some), :433 (whole-field
    equality), :474 (pass-through), :614 (is_none, combined with reason == "superseded"),
    :646 (pass-through), :891 (is_some). None parses, prefixes or splits the string, so the
    conclusion holds.
  • A readability hazard, not a defect: pi_channel already has private consts named PI_KIND /
    OMP_KIND of type ChannelKind, and the new HarnessKind consts reuse those identifiers in
    different modules. Legal and non-colliding, but two same-named consts of different types now live
    in the family.
Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.43sz6ujq
session dev3.43sz6ujq
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.1.7
agent_runtime OMP 18.1.7
tooling_profile dotfiles@39a19af

… terminal

`provider_session` spawns the provider with `stderr(Stdio::inherit())`, so this
`eprintln!` painted "recording session end failed" straight into the
interactive harness's own terminal — inside the agent's UI, where a human is
reading a session, not a log. pi's twin already used `tracing::warn!`.

It lands first because the pi/omp unify that follows deletes this line: with the
two copies merged there is exactly one sink, and this is the one they should
share. No test asserts the wrapper's stderr, so this is review-pinned only.

agent-identity: dev3.direct.omp.43sz6ujq
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.7
agent-runtime: OMP 18.1.7
tooling-profile: dotfiles@39a19af
pi and omp are the same harness family launched the same way: an extension
loaded into the interactive process that reaches st2 by spawning
`st2 driver <harness>-channel`. `omp_session.rs` carried a forked copy of the
whole launch body, and the two copies had already drifted in doc rationale and
in style without any behaviour differing.

The fork now lives where the family's channel side already puts it: a
`HarnessKind` descriptor plus a shared `run_for`, mirroring `pi_channel`'s
`ChannelKind` — the same fork solved the same way twice rather than two
different ways.

What stays per-harness is what genuinely is: each module's doc, its extension
asset, its channel variable NAMES (two disjoint sets so an omp seat can never
adopt a stray pi configuration — `pi_channel` also reads them by path), omp's
measured-build fixtures, and omp's launch-time version gate. That gate rides on
the descriptor as `verify_version` and runs exactly where omp ran it: after the
empty-argv check and before `harness_state::claim`, so an unadmitted minor still
fails without claiming ownership of the seat.

Where the two copies carried different rationales for identical code, the merged
item carries both halves: the offline defaults keep pi's "a supervised seat must
not update-check at boot" and omp's open DQ-OMP-5 note.

No behaviour change: every string, ordering and write is preserved — the exit
label still spells `(None, None)` as "exited" here, and the session-end warning
already went to the tracing log on both sides as of the preceding commit.

agent-identity: dev3.direct.omp.43sz6ujq
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.7
agent-runtime: OMP 18.1.7
tooling-profile: dotfiles@39a19af
Five copies of `(status, signal) -> label` existed; the pi/omp unify folded two
of them. The remaining copies disagreed on exactly one arm: pi/omp spelled
`(None, None)` as "exited", everyone else as "exit unknown".
`provider_session::describe_exit` becomes the one map, and codex's
`describe_tui_exit` keeps only what is genuinely its own — flattening a status
that may never have been observable at all.

Behaviour change, deliberate: the pi/omp record's `exit` field spells
`(None, None)` as "exit unknown" instead of "exited". "exited" claimed an
ordinary end for a status that answers neither question, which is the one thing
that arm cannot know. Two facts de-risk it. The arm is unreachable for a child
this process reaped: `Child::wait`/`try_wait` call `waitpid` with neither
`WUNTRACED` nor `WCONTINUED`, so every status they return satisfies `WIFEXITED`
or `WIFSIGNALED`; only `ExitStatus::from_raw(0x7f)` constructs it, in a test.
And no reader parses the label back — `agents::ObservedJson` and `main.rs` pass
it to display, `harness_state` only asks `exit.is_some()`.

The pin the report claimed existed did not: `provider_session.rs`'s assertion is
fed by `SessionObserver::launch_error()`'s hardcoded literal and never reaches
this map. `the_exit_label_map_covers_every_arm` is that missing pin — the whole
table, `(None, None)` included.

agent-identity: dev3.direct.omp.43sz6ujq
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.7
agent-runtime: OMP 18.1.7
tooling-profile: dotfiles@39a19af
Five class-(a) duplications, each verified identical before folding:

- `channel_content` and `write_json` (claude_mcp, pi_channel) were
  byte-identical. Both are stdio-native-channel framing decisions st2 owns
  rather than each channel, so they move to `native_channel`. The NDJSON
  framing gains the pin it never had: one newline-terminated line per value,
  including a body that carries its own newline.
- `ProviderAuthEdge` had two byte-identical definitions; one moves into
  `driver_diagnostic`, which already owns `Driver`/`Stage`/`Reason`/`Source`/
  `Support`.
- `publish_provider_auth` was identical modulo a hardcoded `Driver`, so it
  joins the enum and takes the driver as an argument, as pi_channel's copy
  already did.
- opencode's `completed` folds onto `completed_provider("opencode", …)`:
  "{provider} provider exited with {exit}" reproduces its message byte for
  byte.

Where the two copies carried different rationales, the merged item carries
both: `publish_provider_auth`'s `None`/`Support::Unknown` now states both why a
Claude hook payload has no version to publish AND why a pi-family channel has
none (the wrapper owns the gate, OMP-R05).

`provider_auth_edge` stays forked in both modules on purpose: the two decode
disjoint inputs — a hook event name plus JSON payload versus a typed
`TurnResult` — and share only the output enum this commit unifies.

agent-identity: dev3.direct.omp.43sz6ujq
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.7
agent-runtime: OMP 18.1.7
tooling-profile: dotfiles@39a19af
@schickling-assistant
schickling-assistant marked this pull request as ready for review September 7, 2026 12:06
@schickling-assistant
schickling-assistant merged commit ea37d2d into main Sep 7, 2026
4 of 5 checks passed
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