From ced67d3eb8516604b617a4b763851d359ac90c77 Mon Sep 17 00:00:00 2001 From: vitya Date: Wed, 12 Aug 2026 23:06:05 +0300 Subject: [PATCH] docs(wiki): ingest pi-extension-headless-ritual concept (agent_end/mode-guard/loop-guard lessons) --- .../concepts/pi-extension-headless-ritual.md | 63 +++++++++++++++++++ .wiki/index.md | 2 + .wiki/log.md | 1 + 3 files changed, 66 insertions(+) create mode 100644 .wiki/concepts/pi-extension-headless-ritual.md diff --git a/.wiki/concepts/pi-extension-headless-ritual.md b/.wiki/concepts/pi-extension-headless-ritual.md new file mode 100644 index 0000000..155d135 --- /dev/null +++ b/.wiki/concepts/pi-extension-headless-ritual.md @@ -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 diff --git a/.wiki/index.md b/.wiki/index.md index deb0ef9..d2a413b 100644 --- a/.wiki/index.md +++ b/.wiki/index.md @@ -58,3 +58,5 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever ## Sources +- [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 + diff --git a/.wiki/log.md b/.wiki/log.md index 22bb719..7f253b5 100644 --- a/.wiki/log.md +++ b/.wiki/log.md @@ -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 ` → 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 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