--- title: pi-extension headless ritual — lifecycle + mode lessons type: concept created: 2026-08-12 --- # pi-extension headless ritual (agent_end, mode guard, loop-guard) Durable lessons from building `session-close-ritual` (консолидирован в `extensions/mappa.ts` репо `OpeItcLoc03/pi-extensions`, task:1486; исторически — отдельный файл `session-close-ritual.ts`), the headless injector for the session-handoff closing ritual. All three points were live-verified, not docs-read-only. ## 1. `agent_settled` is TOO LATE for followUp injection `agent_settled` fires when pi "will not continue running automatically" — the process is tearing down (no retry/compaction/follow-up left). A `sendUserMessage` with `deliverAs: "followUp"` queued there is never processed; the run ends, and the extension handler even hits a stale-ctx error during teardown. **Use `agent_end`** — it fires right after the agent run ends, while queued follow-ups are still delivered (`followUp` waits for the agent to finish, then delivers; `triggerTurn: true` starts a new turn when idle). Verified against `agent-session.js:779-780` ("agent loop drains both queues before emitting agent_end") + live runs. ## 2. `ctx.hasUI === false` is NOT headless-only — guard by `mode` `hasUI` is `false` in BOTH `-p` (print) and `--mode json`. An unsolicited injected user-message into an event-stream consumer (JSON mode) is a protocol surprise. RPC mode has `hasUI === true` (so a hasUI-guard accidentally allows rpc while missing json). **Guard: `ctx.mode === "print"`** for pi -p / scripted runs. Excludes tui/json/rpc in one condition. ## 3. Loop-guard pattern: flag-before-send, per-session-per-cwd `agent_end` fires again after the injected ritual turn (the agent made tool calls, then the run ends) — without a guard: agent_end → ritual → agent_end → ritual → … loop. - Set the flag **synchronously BEFORE** `sendUserMessage` (no `await` between check and set → no race; `emit()` is serial). - Per-session-per-cwd `Map`, reset on `session_start`. - On send-failure: keep the flag (at-most-once wins over retry — a missed ritual is cheaper than double-inject). This is a deliberate asymmetry vs `inbox-monitor` (which unmarks and retries). - `injectRitual`'s send is wrapped in try/catch: the real `sendUserMessage` is a sync wrapper (`assertActive()` throws on shutdown race). ## 4. Opt-in mirrors the skill, not the extension The extension checks the same opt-in as the skill it serves: the project `CLAUDE.md` contains the skill's trigger line (`session handoff: read on start, write on end`) AND `.tasks/` exists AND `.git` exists. No opt-in → silent. Cache per-cwd; staleness within a long session is accepted (same as `inbox-monitor`). ## References - Source: `~/projects/pi-extensions/extensions/mappa.ts` (секция close-ritual; консолидация 6 расширений, task:1486 — исторически `session-close-ritual.ts` + `scripts/session-close-ritual.test.mjs`, 12 blocks, ныне тесты на mappa.ts) - Skill: `session-handoff` v0.5.0 — «Headless (pi)» section - pi docs: `extensions.md` — lifecycle diagram, `sendUserMessage` (deliverAs/triggerTurn), mode table