2.9 KiB
title, type, created
| title | type | created |
|---|---|---|
| pi-extension headless ritual — lifecycle + mode lessons | concept | 2026-08-12 |
pi-extension headless ritual (agent_end, mode guard, loop-guard)
Durable lessons from building session-close-ritual (.common/lib/pi-extensions/),
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(noawaitbetween check and set → no race;emit()is serial). - Per-session-per-cwd
Map, reset onsession_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 realsendUserMessageis 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:
.common/lib/pi-extensions/session-close-ritual.ts(+.test.mjs, 12 blocks) - Skill:
session-handoffv0.5.0 — «Headless (pi)» section - pi docs:
extensions.md— lifecycle diagram,sendUserMessage(deliverAs/triggerTurn), mode table