--- name: omp-session-journal-cursor-access description: "Read a live omp session's transcript mid-run from an extension (cursor = journal entry id via ctx.sessionManager), with the entry-type noise list, append lag, and the ctx.setInterval metronome." --- # Reading the live session journal from an extension Use when an extension must summarise/narrate/index **everything said since some point** rather than reacting to one event payload, or when you need a durable "where I left off" pointer into a session. Verified 2026-10-30 with a throwaway probe under `omp -p` (v18.12.5), 1 s ticks over a two-tool-round run. ## The accessor (no plugin install needed) `ctx.sessionManager` is on every `ExtensionContext` (`ReadonlySessionManager`, `@oh-my-pi/pi-coding-agent/dist/types/extensibility/extensions/types.d.ts:485`): ``` getEntries() getBranch() getEntry(id) getLeafId() getLeafEntry() SessionMessageEntry = { id, type: "message", message: AgentMessage } ``` - **The cursor is an entry id.** Persist `getLeafId()` (or the id of the last entry you actually consumed); "everything since" = entries after that id. Advance is O(1). - **`getEntries()` grows mid-run** — proven by a timer, with no handler help: `entries=3 → 6 → 7 → 9 → 11` while `idle=false`, leaf ids rotating. - **Append lags `message_end` by ~0.5–1 s.** So "the live edge" always under-reads by a second. Consume only up to the last id you actually read; the sub-second tail rides into the next window. Never advance past what you read. - **`ctx.setInterval(cb, ms)` / `ctx.setTimeout`** (`types.d.ts:555-572`): throws contained, `unref`'d, auto-cleared on `session_shutdown`. Use these, never raw `setInterval`. `ctx.isIdle()` is the cheap "is this session even working" guard. ## The journal is not the conversation (allowlist, never denylist) `agent_end` reported `msgs=6` while the journal held `11` entries. Bookkeeping types in the union: `custom` (zero content, ~3 per tool round), `model_usage`, `thinking_level_change`, `mode_change`, `model_change`, `service_tier_change`, `label`, `title_change`, `ttsr_injection`, `session_init`, `credential_pin`, `reset_boundary`. Speech-relevant: `message` (roles `user` / `assistant` / `toolResult`) and, as context, `compaction` / `branch_summary`. New types appear on the harness's own schedule → an allowlist keeps them silent until someone decides they are speech. Re-export the per-message readers instead of re-parsing content: an assistant message's narration-flagged `thinking` part is **answer text**, unflagged `thinking` is deliberation (`narration: true` discriminator). ## Robustness rules that came out of the failure modes - Empty window ⇒ leave the cursor where it was (nothing new must not read as news). - Cursor id no longer in the journal (compaction, branch rewrite) ⇒ treat as "from the top of what is left", never as "nothing" — a lost pointer must not mute a session forever. `session_compact` and `session_branch` are dispatched events if you want to re-anchor eagerly. ## Probe recipe (proves any of the above in ~60 s, touches nothing live) Project-local extensions load without installing: write `/.omp/extensions/probe.js` (default-export `function (pi)`), log to an absolute path, then ```bash : > /tmp/probe.log timeout 220 omp -p --thinking off --cwd \ "Run these bash commands strictly one after the other: first: sleep 7; echo FIRST. then: sleep 7; echo SECOND. Then reply: FIRST SECOND" ``` Register the tick inside `session_start` (`ctx.setInterval(() => stamp(info(ctx)), 1000)`), and give every entry you expect to exclude a speech-shaped payload with a sentinel word — an absence assertion with an empty producer proves nothing. Do NOT pass `--no-session`: the journal is the thing under test. Teardown: nothing to undo (no plugin link, no `~/.omp/plugins/package.json` write) — delete the run dir.