Skip to content

Manage native completion hooks for Pi, Oh My Pi, and OpenCode launches (S3c) - #728

Merged
onevcat merged 8 commits into
mainfrom
feat/agent-signal-hooks-s3c
Aug 27, 2026
Merged

Manage native completion hooks for Pi, Oh My Pi, and OpenCode launches (S3c)#728
onevcat merged 8 commits into
mainfrom
feat/agent-signal-hooks-s3c

Conversation

@onevcat

@onevcat onevcat commented Aug 26, 2026

Copy link
Copy Markdown
Owner

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.md

What changes for users

  • Agent Profile launches of Pi, Oh My Pi, and OpenCode now report session-start / turn-ended / needs-input through exact channels (hook_pi, hook_omp, hook_opencode), so prowl agents wait is deterministic for them like for the other five runtimes. Manually typed runtimes stay heuristic.
  • Nothing is written to runtime homes, global config, or repositories. Pi (-e) and Oh My Pi (--hook) load a read-only extension shipped in the app through an additive flag; OpenCode receives a launch-scoped OPENCODE_CONFIG_CONTENT whose plugin list is appended to whatever the Profile or your shell already exports (config layers concatenate plugin lists).
  • Launches that cannot carry a hook run unchanged with one managed_hook_degraded warning: --pure / OPENCODE_PURE, malformed content, an ambiguous project positional, an unusable shell probe, or a missing bundled file.

Design

  • The bundled extensions are dumb relays: they forward the runtime's native event names (agent_settled, session_switch, session.idle, …) inside the existing Claude-shaped envelope to the hidden agents _hook bridge, so the shared decoder and the S3a trust model are unchanged and signals.last.native_event stays honest. AgentNativeHookDecoder.nativeEvents(for:) is now the single source for every runtime's table; the adapters declare it.
  • Oh My Pi maps session_stop (documented main-session only) rather than agent_end, which fires once per in-process task sub-agent; /new rotates through session_switch; tool_approval_requested is 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.
  • OpenCode is non-announcing like Codex (its session is created lazily and /new / resume emit nothing): the first session.idle verifies the channel. permission.asked / question.asked are needs-input, with permission.asked removed from --auto launches, where OpenCode auto-replies in the same millisecond (the Copilot/Qoder false positive again). The plugin filters sub-agent sessions by parentID, and OpenCodeSessionStore ignores parent_id rows so the detector never displaces the pane's session.
  • New ShellEnvironmentProbe resolves several variables from the login shell in one spawn (set / empty / unset, multi-line safe); the Droid probe now delegates to it. AgentHookPreparedInvocation.environmentValues carries hook environment variables the same way hook argv values ride PROWL_LAUNCH_HOOK_* carriers.
  • The Active Agents "exact" badge previously attached to S3c was dropped without commitment (owner decision).

Verification

  • make check, make build-cli, make test-cli-integration (97), make build-app, make test.
  • Focused suites: AgentS3cHookPayloadTests, AgentS3cHookRenderingTests, plus additions to AgentObservationTests, AgentProfileHookCarrierTests, AgentSessionProfileTests, AgentsCommandParsingTests.
  • Live gate in an isolated Debug instance (CFFIXED_USER_HOME, dedicated socket), Pi 0.84.3 / Oh My Pi 18.0.6 / OpenCode 1.18.23 upgraded first: all three reach verified_live and resolve agents wait --until idle on turn-ended; OMP approval and OpenCode question dialogs resolve --until blocked on the exact channel; Pi/OMP /new rotate 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

https://claude.ai/code/session_016P9EWPCSZBP3HoaEqvBdUL

…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
@onevcat onevcat closed this Aug 26, 2026
@onevcat onevcat reopened this Aug 26, 2026
@onevcat onevcat closed this Aug 26, 2026
@onevcat onevcat reopened this Aug 26, 2026
@onevcat onevcat closed this Aug 26, 2026
@onevcat onevcat reopened this Aug 26, 2026
@onevcat
onevcat merged commit d8eea12 into main Aug 27, 2026
2 of 3 checks passed
@onevcat
onevcat deleted the feat/agent-signal-hooks-s3c branch August 27, 2026 01:44
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