--- name: omp-extension-event-probe description: "Prove which lifecycle events the omp harness actually dispatches to extensions, with a throwaway timestamping probe plugin, instead of inferring delivery from side effects." --- # Prove harness→extension event delivery with a probe plugin Use when an extension handler seems never to fire, or when you must know *when* an event arrives relative to another (mid-run vs settle). Never infer dispatch from the feature's side effects: async work in the handler (LLM rewrites, synthesis, network) lags seconds and destroys the timing signal. ## Shape (no build step needed) `/tmp/probe/package.json` — the installer validates the declared entry on disk: ```json { "name": "event-probe", "version": "0.0.0", "type": "module", "exports": "./index.js", "pi": { "extensions": ["./index.js"] } } ``` `/tmp/probe/index.js` — append arrival times, not counts: ```js import { appendFileSync } from "node:fs"; const LOG = "/tmp/probe.log"; const stamp = (line) => appendFileSync(LOG, `${new Date().toISOString().slice(11, 23)} ${line}\n`); export default function probe(pi) { pi.on("agent_start", () => stamp("agent_start")); pi.on("turn_end", (e) => stamp(`turn_end turn=${e.turnIndex} toolResults=${(e.toolResults ?? []).length}`)); pi.on("agent_end", (e) => stamp(`agent_end messages=${(e.messages ?? []).length} willContinue=${String(e.willContinue)}`)); } ``` ## Run 1. `cd ~/.omp/plugins && omp plugin link /tmp/probe` (link, never edit `node_modules`), then `omp plugin doctor` → expect a `✔ plugin:event-probe` line. 2. Launch a **fresh** TUI session in a pty (`omp --cwd /tmp/probe-run`, `pty: true`). Extensions load at startup only — an already-running session never sees the probe. 3. PTY gotcha: input sent before the splash clears is swallowed. Read the screen first, then submit text and finish with a separate write containing a bare `\r` (LF alone left the text sitting in the composer). 4. Drive a task with ≥2 tool rounds so turns and settles separate in time (sequential reads work and need no approval; ask for steps "strictly one after the other" or the model parallelises them into one turn). 5. Wait for a terminal line with an async bounded loop (`grep -q agent_end`, `sleep 1` inside one backgrounded bash call) — never poll across turns. ## Read `turn_end` timestamps strictly earlier than `agent_end` ⇒ per-turn delivery works. `willContinue=true` on `agent_end` marks a non-terminal settle (harness maps `isTerminal === false` to it) — handlers that `return` on it never speak. ## Teardown (mandatory) `omp plugin uninstall event-probe`; `omp plugin doctor`; delete `/tmp/probe*`. The link writes into `~/.omp/plugins/package.json` (tracked, live tier) — uninstall restores it; confirm with `grep` for the probe name returning nothing. ## Ground truth read for free Event union + payloads: `~/.omp/plugins/node_modules/@oh-my-pi/pi-coding-agent/dist/types/extensibility/shared-events.d.ts`; the session→extension mapping lives in `src/extensibility/extensions/lifecycle-mirror.ts` (`extensionEventFromSessionEvent`). Extension-level `turn_end` carries `turnIndex`/`message`/`toolResults` but **no `willContinue`** — that field exists only on `agent_end`, so "is this the last turn" cannot be decided from `turn_end`.