docs(wiki): ingest pi-extension-headless-ritual concept (agent_end/mode-guard/loop-guard lessons)

This commit is contained in:
2026-08-12 23:06:05 +03:00
parent 348f6f3108
commit ced67d3eb8
3 changed files with 66 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
---
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` (`.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` (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: `.common/lib/pi-extensions/session-close-ritual.ts` (+ `.test.mjs`, 12 blocks)
- Skill: `session-handoff` v0.5.0 — «Headless (pi)» section
- pi docs: `extensions.md` — lifecycle diagram, `sendUserMessage` (deliverAs/triggerTurn), mode table