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

View File

@@ -58,3 +58,5 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
## Sources ## Sources
<!-- (none yet) --> <!-- (none yet) -->
- [pi-extension-headless-ritual.md](concepts/pi-extension-headless-ritual.md) — agent_end (not agent_settled) for followUp injection; mode guard (`print` not hasUI); loop-guard flag-before-send; opt-in mirrors skill

View File

@@ -78,3 +78,4 @@ Parseable: `grep "^## \[" .wiki/log.md | tail -20`.
## [2026-06-09] decision | using-markitdown-cli-migration — `using-markitdown` 1.0.0→1.0.1 (PATCH): rewrote the skill from the Docker-based `mcp__markitdown__convert_to_markdown` MCP tool to the native `markitdown` CLI (v0.1.6, on PATH). Tool block now `markitdown <path|url>` → stdout (or `-o file`); removed the whole "Docker-mount caveat (READ FIRST)" section (host→container `file://` translation + `[Errno 2] /c:/Users/...` symptom are gone — CLI sees the full host FS). Updated the ingest pattern (use `-o` straight into `.wiki/raw/`), the gotchas table (`command not found` → check `markitdown --version`, install `pip install markitdown[all]`; dropped the MCP "tool not available / ToolSearch" row), and the contrast-table header (CLI, not MCP). Description frontmatter (the WHEN-to-use triggers) left unchanged. Container decommission: the task's literal `docker stop/rm markitdown-mcp` had no target — no container is named that; the MCP spawns anonymously-named containers from `markitdown-mcp:latest` per session (3 had piled up). Removed all by image ancestor (`docker rm -f $(docker ps -aq --filter "ancestor=markitdown-mcp:latest")`), verified none remain. Left the `mcpServers.markitdown` entry in `~/.claude.json` untouched (out of scope; a container will respawn next session until it's deregistered — flagged as a follow-up). Concept page concepts/using-markitdown-cli-migration.md + index. TDD N/A (markdown skill). ## [2026-06-09] decision | using-markitdown-cli-migration — `using-markitdown` 1.0.0→1.0.1 (PATCH): rewrote the skill from the Docker-based `mcp__markitdown__convert_to_markdown` MCP tool to the native `markitdown` CLI (v0.1.6, on PATH). Tool block now `markitdown <path|url>` → stdout (or `-o file`); removed the whole "Docker-mount caveat (READ FIRST)" section (host→container `file://` translation + `[Errno 2] /c:/Users/...` symptom are gone — CLI sees the full host FS). Updated the ingest pattern (use `-o` straight into `.wiki/raw/`), the gotchas table (`command not found` → check `markitdown --version`, install `pip install markitdown[all]`; dropped the MCP "tool not available / ToolSearch" row), and the contrast-table header (CLI, not MCP). Description frontmatter (the WHEN-to-use triggers) left unchanged. Container decommission: the task's literal `docker stop/rm markitdown-mcp` had no target — no container is named that; the MCP spawns anonymously-named containers from `markitdown-mcp:latest` per session (3 had piled up). Removed all by image ancestor (`docker rm -f $(docker ps -aq --filter "ancestor=markitdown-mcp:latest")`), verified none remain. Left the `mcpServers.markitdown` entry in `~/.claude.json` untouched (out of scope; a container will respawn next session until it's deregistered — flagged as a follow-up). Concept page concepts/using-markitdown-cli-migration.md + index. TDD N/A (markdown skill).
## [2026-06-17] decision | session-inbox-monitor-received-msg-fp — finding from `session-inbox-monitor-test-trigger` (VERDICT PASS, clean session, 7 unprimed clean-context subagents: pos 4/4 incl. CLAUDE.md-line P4, neg 2/3). The 1 FP: RU «обработай полученное письмо из инбокса» (N1) routed to `session-inbox-monitor`; the EN twin (N3) and the multi-machine-backend negative (N2) routed to `none` cleanly. Root cause = a new dimension on top of [[delegate-task-negative-trigger-fp]]: the carve-out is already literal+routed (`NOT for handling a received message → inter-session-peer-discipline`), but the route target `inter-session-peer-discipline` is **not installed** → no real competitor, so the nearest in-domain skill (session-inbox-monitor) wins by default; non-deterministic, self-corrects on body-load (cost = one wasted skill-load, not a wrong action; isomorphic to [[using-tasks-session-break]] session_break). New page concepts/session-inbox-monitor-received-msg-fp.md + bidirectional link from concepts/delegate-task-negative-trigger-fp.md + index. New reusable principle: a routed negative competes only if its route target is installed. Status OPEN — follow-up task session-inbox-monitor-received-msg-fp (options a: harden description / b: install sibling / c: accept informational). Not a memory entry by owner direction — knowledge belongs in the project wiki. ## [2026-06-17] decision | session-inbox-monitor-received-msg-fp — finding from `session-inbox-monitor-test-trigger` (VERDICT PASS, clean session, 7 unprimed clean-context subagents: pos 4/4 incl. CLAUDE.md-line P4, neg 2/3). The 1 FP: RU «обработай полученное письмо из инбокса» (N1) routed to `session-inbox-monitor`; the EN twin (N3) and the multi-machine-backend negative (N2) routed to `none` cleanly. Root cause = a new dimension on top of [[delegate-task-negative-trigger-fp]]: the carve-out is already literal+routed (`NOT for handling a received message → inter-session-peer-discipline`), but the route target `inter-session-peer-discipline` is **not installed** → no real competitor, so the nearest in-domain skill (session-inbox-monitor) wins by default; non-deterministic, self-corrects on body-load (cost = one wasted skill-load, not a wrong action; isomorphic to [[using-tasks-session-break]] session_break). New page concepts/session-inbox-monitor-received-msg-fp.md + bidirectional link from concepts/delegate-task-negative-trigger-fp.md + index. New reusable principle: a routed negative competes only if its route target is installed. Status OPEN — follow-up task session-inbox-monitor-received-msg-fp (options a: harden description / b: install sibling / c: accept informational). Not a memory entry by owner direction — knowledge belongs in the project wiki.
## [2026-06-17] decision | session-inbox-monitor-received-msg-fp RESOLVED via option (b) — installed `inter-session-peer-discipline` (existed in sources since 2026-06-16, was not installed → exact root cause confirmed). install.ps1 -Names, byte-identical parity. FP-twin verified clean: fresh clean-context subagent on the N1 phrase now routes to inter-session-peer-discipline (IN_REGISTRY: yes), not session-inbox-monitor — carve-out now has a real competitor. session-inbox-monitor description untouched (option (a) rejected as whack-a-mole; (c) as latent hole). Governance: peer workshop proposed (b) as a "ruling"; per the freshly-installed [[inter-session-peer-discipline]] (peer = proposal not authority, scope needs human ratification) it was surfaced as a recommendation and ratified by the user — live dogfood of the skill's own purpose. concepts/session-inbox-monitor-received-msg-fp.md Status section updated open→resolved. Tail: inter-session-peer-discipline now installed but not in hermes/mapping.yaml — possible red build, flagged as separate follow-up. ## [2026-06-17] decision | session-inbox-monitor-received-msg-fp RESOLVED via option (b) — installed `inter-session-peer-discipline` (existed in sources since 2026-06-16, was not installed → exact root cause confirmed). install.ps1 -Names, byte-identical parity. FP-twin verified clean: fresh clean-context subagent on the N1 phrase now routes to inter-session-peer-discipline (IN_REGISTRY: yes), not session-inbox-monitor — carve-out now has a real competitor. session-inbox-monitor description untouched (option (a) rejected as whack-a-mole; (c) as latent hole). Governance: peer workshop proposed (b) as a "ruling"; per the freshly-installed [[inter-session-peer-discipline]] (peer = proposal not authority, scope needs human ratification) it was surfaced as a recommendation and ratified by the user — live dogfood of the skill's own purpose. concepts/session-inbox-monitor-received-msg-fp.md Status section updated open→resolved. Tail: inter-session-peer-discipline now installed but not in hermes/mapping.yaml — possible red build, flagged as separate follow-up.
## [2026-08-12] ingest | pi-extension-headless-ritual — agent_end/mode-guard/loop-guard lessons from session-close-ritual build