Files
skills/.wiki/concepts/pi-extension-headless-ritual.md
vitya 7d08c5beba docs(wiki): pi-extension-headless-ritual — source path → pi-extensions repo (#1034)
Legacy .common/lib/pi-extensions removed; source of truth is the repo.
2026-08-24 15:49:57 +03:00

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 (репо OpeItcLoc03/pi-extensions, 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: ~/projects/pi-extensions/extensions/session-close-ritual.ts (+ scripts/session-close-ritual.test.mjs, 12 blocks)
  • Skill: session-handoff v0.5.0 — «Headless (pi)» section
  • pi docs: extensions.md — lifecycle diagram, sendUserMessage (deliverAs/triggerTurn), mode table