docs(wiki): ingest pi-extension-headless-ritual concept (agent_end/mode-guard/loop-guard lessons)
This commit is contained in:
63
.wiki/concepts/pi-extension-headless-ritual.md
Normal file
63
.wiki/concepts/pi-extension-headless-ritual.md
Normal 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
|
||||
Reference in New Issue
Block a user