Manage native completion hooks for Pi, Oh My Pi, and OpenCode launches (S3c) - #728
Merged
Conversation
…tation Record the 2026-08-26 measurements of Pi 0.84.3, Oh My Pi 18.0.6, and OpenCode 1.18.23 (extension injection, lifecycle sequences, session identity, sub-agent shape, trust, cwd forms, `--pure`/`--auto` traps) and the resulting S3c plan: extensions relay native event names inside the Claude-shaped envelope, OMP maps `session_stop`, OpenCode is non-announcing with a sub-agent filter, and the Active Agents exact badge is dropped without commitment. Mark S3b merged (#725) in the release plan, the wave-1 plan, the S3b record, and the 064 plan. Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
…ches
Close S3 wave 1 with the last three tier-A runtimes. Each ships a small
bundled extension (`Resources/agent-hooks/{pi,omp,opencode}/prowl-hooks.ts`)
that relays the runtime's own event names through the hidden
`agents _hook` bridge inside the Claude-shaped envelope, so the shared
decoder and the S3a trust model stay unchanged. `AgentNativeHookDecoder.
nativeEvents(for:)` is now the single source for every runtime's table
and the adapters declare it.
Pi (`-e`) and Oh My Pi (`--hook`) receive the extension through an
additive flag inserted before the prompt; Oh My Pi's `--cwd` is the
registered launch directory. Oh My Pi maps `session_stop` (main-session
only; `agent_end` fires per in-process sub-agent), `session_switch`
rotates, and `tool_approval_requested` is needs-input. The extension
recognises sub-agent sessions by their nested session file and forwards
only their approvals under the main session.
OpenCode receives its plugin through a launch-scoped
`OPENCODE_CONFIG_CONTENT` carried like the hook token: the plugin URL is
appended to the effective content (Profile override, else the login
shell via the new `ShellEnvironmentProbe`, which Droid's probe now uses
too), preserving every other key. The runtime is non-announcing like
Codex — the first `session.idle` verifies — and `permission.asked` /
`question.asked` are needs-input, with `permission.asked` dropped under
`--auto` where OpenCode auto-replies in the same millisecond. `--pure`,
`OPENCODE_PURE`, malformed content, an ambiguous project positional, or
an unusable probe degrade the launch. The plugin drops sub-agent
sessions by `parentID`, and `OpenCodeSessionStore` ignores `parent_id`
rows so the detector never displaces the pane's session.
Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
Pin the measured native event tables and their agreement with the adapters, decode the relayed envelope per runtime and reject the excluded events, render the additive extension flags and the OpenCode content merge (existing content, `--pure`, `OPENCODE_PURE`, project positional and `run --dir`, override vs shell precedence, `--auto` trimming, missing resources), exercise the generic shell probe through the real `/bin/sh` with set / empty / unset / multi-line values, keep hook environment values on child-only carriers, rotate Oh My Pi through `session_switch` while rejecting delayed events, verify OpenCode's non-announcing rotation, exclude sub-agent rows from the OpenCode session store, and parse the stdin relay in the CLI. Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
… S3c Extend the agent-detection, agent-profiles, and CLI docs, the `agents-signal` contract, and the `prowl-cli` skill with the Pi, Oh My Pi, and OpenCode channels, their enablement, and the cases that launch unchanged. Add the S3c action record with the isolated-instance live gate for all eight runtimes, the Oh My Pi sub-agent session finding, and the display-sleep cause behind the intermittent Profile `CREATE_FAILED`. Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
… OpenCode launch inputs Review of #728 found three real gaps, each pinned by a test first: - The Pi/Oh My Pi extensions recognised sub-agents by the `<timestamp>_<uuid>.jsonl` session file name, which also matched a session started with a custom `--session-id` and silently dropped every event of it. A stateful replacement ("the first announcer is the pane's session") passed its harness and failed live: the runtime loads a fresh extension instance for every sub-agent session, so module state never reaches the sub-agent's handlers. The rule is now stateless and structural — a sub-agent's session file is nested inside the parent's session directory, so a session whose file has a session-directory ancestor is a sub-agent and that directory names the pane's session; the id itself is never interpreted. `scripts/test_agent_hooks.py` runs the real extensions through Node against a capture CLI and is part of `make check`; the fix is re-verified live for an OMP sub-agent turn and a Pi session with a custom id. - OpenCode's TUI `--replay-limit <N>` was missing from the value-option table, so `7` would have been registered as the project directory and every hook rejected on the cwd guard without a warning. The table now matches the 1.18.23 `--help`, and because OpenCode refuses to start in a directory that does not exist, a positional that is not an existing directory is treated as an unknown option's value and the launch directory stays inherited. - `ShellEnvironmentProbe` declared a 256 KiB output bound but ran `CodexShellProbeProcess` at its 16 KiB default, degrading a launch whose exported `OPENCODE_CONFIG_CONTENT` was a few pages long. The production runner now carries the probe's bound (and accepts an injected environment so the test never mutates the host's), covered at the process level and end to end. Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
The bundled extensions spawned one bridge process per event without waiting for it, so adjacent lifecycle events — Pi's `agent_settled` and `session_shutdown` are milliseconds apart at exit, and a `/new` is a `session_shutdown` immediately followed by a `session_start` — could reach Prowl out of order, where a late session start clears the terminal evidence a wait relies on and cancels an incomplete dispatch candidate. The native-hook runtimes run their hook commands sequentially; the extensions now do the same per instance: a promise queue starts the next bridge only after the previous one closed, the runtime callback never waits on it, and a bridge that hangs is killed after 5 s so later events keep flowing. The Node harness now fires every step back to back and asserts strict order, including a 36-event burst that reordered with the old relay; it passes serially and under parallel load. `make test-scripts` joins the CI test tasks so the harness actually runs there. Re-verified live for Pi, Oh My Pi, and OpenCode, including `/new` rotation. Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
…gnore reloads
A per-instance delivery queue still raced across instances. Pi's
`/reload` has the old extension instance report
`session_shutdown{reason:"reload"}` and, four milliseconds later, a fresh
instance report `session_start{reason:"reload"}` for the same session id
(measured), so a late shutdown could become the live session's exact
`session-end` and complete `agents wait --until exit` while the runtime
is alive.
Reload-reason lifecycle events are no longer forwarded — the session
neither ends nor changes (Oh My Pi has no `/reload`; the text is treated
as input) — and the delivery queue now lives on `globalThis`, which every
module instance in the process shares (measured), so sub-agent instances
and reloads keep one order. The Node harness loads extra instances
through a cache-busting import and pins both the reload sequence and
cross-instance ordering; both tests fail against the previous relay.
Re-verified live: after `/reload`, `agents wait --until exit` times out
instead of completing, and the next turn resolves on `hook_pi`.
Claude-Session: https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL
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.
Summary
S3c closes S3 wave 1 (docs-ai 064): Prowl-managed native completion hooks now cover all eight tier-A runtimes. This PR adds Pi, Oh My Pi, and OpenCode on top of the S3a foundation (#721/#723) and the S3b adapters (#725), and closes the tier-A docs.
Plan:
docs-ai/064-agent-completion-signals/010-s3c-plan.md· Record:011-s3c-action.mdWhat changes for users
session-start/turn-ended/needs-inputthrough exact channels (hook_pi,hook_omp,hook_opencode), soprowl agents waitis deterministic for them like for the other five runtimes. Manually typed runtimes stay heuristic.-e) and Oh My Pi (--hook) load a read-only extension shipped in the app through an additive flag; OpenCode receives a launch-scopedOPENCODE_CONFIG_CONTENTwhosepluginlist is appended to whatever the Profile or your shell already exports (config layers concatenate plugin lists).managed_hook_degradedwarning:--pure/OPENCODE_PURE, malformed content, an ambiguous project positional, an unusable shell probe, or a missing bundled file.Design
agent_settled,session_switch,session.idle, …) inside the existing Claude-shaped envelope to the hiddenagents _hookbridge, so the shared decoder and the S3a trust model are unchanged andsignals.last.native_eventstays honest.AgentNativeHookDecoder.nativeEvents(for:)is now the single source for every runtime's table; the adapters declare it.session_stop(documented main-session only) rather thanagent_end, which fires once per in-processtasksub-agent;/newrotates throughsession_switch;tool_approval_requestedis needs-input. Sub-agent sessions have their own ids with a nested session file — the extension drops their lifecycle events and forwards only their approvals under the main session./new/ resume emit nothing): the firstsession.idleverifies the channel.permission.asked/question.askedare needs-input, withpermission.askedremoved from--autolaunches, where OpenCode auto-replies in the same millisecond (the Copilot/Qoder false positive again). The plugin filters sub-agent sessions byparentID, andOpenCodeSessionStoreignoresparent_idrows so the detector never displaces the pane's session.ShellEnvironmentProberesolves several variables from the login shell in one spawn (set / empty / unset, multi-line safe); the Droid probe now delegates to it.AgentHookPreparedInvocation.environmentValuescarries hook environment variables the same way hook argv values ridePROWL_LAUNCH_HOOK_*carriers.Verification
make check,make build-cli,make test-cli-integration(97),make build-app,make test.AgentS3cHookPayloadTests,AgentS3cHookRenderingTests, plus additions toAgentObservationTests,AgentProfileHookCarrierTests,AgentSessionProfileTests,AgentsCommandParsingTests.CFFIXED_USER_HOME, dedicated socket), Pi 0.84.3 / Oh My Pi 18.0.6 / OpenCode 1.18.23 upgraded first: all three reachverified_liveand resolveagents wait --until idleonturn-ended; OMP approval and OpenCode question dialogs resolve--until blockedon the exact channel; Pi/OMP/newrotate the channel; OpenCode and OMP sub-agent turns resolve only with the parent's final answer on screen; Claude / Codex / Copilot / Droid / Qoder regression on the same build passes.Findings worth a follow-up
CREATE_FAILEDin isolated instances (and the flakydeferredProfileAppliesFontSizeAdjustmentAfterSurfaceCreation()test) is display sleep: deferred Ghostty surface creation fails withCVDisplayLinkCreateWithCGDisplays … invalid display count (0)while the display is off. Documented in the record; candidate issue.https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL