fix(session-inbox-monitor): scope pi extension to own project inbox only

Per vitya's rule an agent may only read its own directory's inbox. The
first global-scan version (all ~/projects inboxes + PI_INBOX_ROOTS) was
scrapped same day; rewrote as session-scoped: watches only
<ctx.cwd>/.claude-inbox/, decoy test proves other inboxes untouched.
This commit is contained in:
2026-08-10 22:49:53 +03:00
parent a0491e00fd
commit 07e782040d
3 changed files with 35 additions and 30 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,30 +1,32 @@
# session-inbox-monitor-pi-extension — working context
**Status:** 🟢 done (shipped 2026-08-10) — pi-native inbox delivery shipped for ALL pi sessions, see STATUS.md block.
**Status:** 🟢 done (shipped 2026-08-10, scoped-fix same day) — pi-native inbox delivery shipped for ALL pi sessions, see STATUS.md block.
**Owner:** vitya (pi interactive session, .admin)
**Notify:** OpeItcLoc03/claude-skills
Pi (pi-coding-agent) не покрывался скилом: Monitor tool — CC-only, хуки CC-only.
Собран pi-native аналог: глобальное расширение, доставляющее сообщения из ЛЮБОГО
`.claude-inbox/` в текущую pi-сессию независимо от cwd (требование vitya: «мне
похуй, какой агент в какой папке сидит — система сообщений должна работать»).
Собран pi-native аналог.
## Решения
1. **Глобально, не cwd-scoped.** Скан всех инбоксов: прямые дети `~/projects`
(все 20 инбоксов живут на глубине 1, проверено find). Оверрайд —
`PI_INBOX_ROOTS` (pathsep, `;` на Windows).
1. **Session-scoped, только свой инбокс.** Расширение смотрит только
`<ctx.cwd>/.claude-inbox/` — инбокс СВОЕГО проекта. Чужие инбоксы НЕ читает
(правило vitya: «ты другие инбоксы читать не имеешь права. Только в своей
директории»). Установлено глобально (`~/.pi/agent/extensions/`) — каждый pi
имеет capability, но каждый трогает только свой. ПЕРВАЯ версия (глобальный
скан всех инбоксов + `PI_INBOX_ROOTS`) — отменена в тот же день: противоречит
правилу. Переписана на session-scoped, decoy-тест (чужой инбокс не тронут) PASS.
2. **Два пути доставки** (контракт как у CC-хуков):
- PUSH: один глобальный poll-интервал 15s (интерактив только).
- PUSH: poll-интервал 15s по своему инбоксу (интерактив только).
- PULL: `agent_settled` sweep — pi-эквивалент Stop-хука.
- Dedup per-process по `inbox|filename`; move в `.read/` — кросс-процессный
- Dedup per-process по filename; move в `.read/` — кросс-процессный
guard: кто первый swept — тот и забрал (CC hook или pi), второй пропускает.
3. **Headless (`pi -p`, `ctx.hasUI === false`) — НИКАКОЙ доставки.** Ни watcher,
ни sweep. Сообщения ждут интерактивную сессию. Зеркалит CC-headless (там
внешний Notify/ntfy, не инжект в ран) + не угоняет one-shot прогоны и не
съедает сообщения без обработки.
4. **Доставка:** `pi.sendUserMessage(body, { deliverAs: "followUp", triggerTurn:
true })` — заголовок `[inbox] <project>/<name>` + тело инлайн, потом move в
true })` — заголовок `[inbox] <имя-файла>` + тело инлайн, потом move в
`.read/`. Пустые файлы (частичная запись) пропускаются и ретраятся.
5. **Source of truth:** `.common/lib/pi-extensions/inbox-monitor.ts` (Node
built-ins only, без npm deps — авто-дискавери без package.json). Деплой:
@@ -34,8 +36,8 @@ Pi (pi-coding-agent) не покрывался скилом: Monitor tool — CC
## Verified
- Функциональный тест 3 блоков (интерактив-глобал, headless-no-delivery,
partial-write-retry) — ALL TESTS PASSED.
- Функциональный тест 3 блоков (interactive-own-inbox + decoy-чужой-инбокс,
headless-no-delivery, no-inbox-silent) — ALL TESTS PASSED.
- Нет зависания процесса: headless не стартует интервал; интерактив чистит
интервал в `session_shutdown` (timeout-прогон EXITED CLEANLY).
- Деплой: diff source↔`~/.pi/agent/extensions/` = 0 (byte-identical).
@@ -45,6 +47,9 @@ Pi (pi-coding-agent) не покрывался скилом: Monitor tool — CC
- **Кросс-харнессный двойной пикап:** CC и pi оба свипят — `.read/` move делает
first-wins, не double-processing. Два pi-процесса на одной машине гонятся как
две CC-сессии (known limitation, см. SKILL.md).
- **Межпроектные сообщения:** письмо, брошенное в инбокс проекта X, доставится
только pi/CC-сессии проекта X. Если активной сессии X нет — письмо ждёт в
инбоксе (это by design, не баг).
- **Проверка живого pi-лоада** (что авто-дискавери подхватил расширение в
реальном TUI) — за `/reload` при следующей интерактивной pi-сессии.
- **Headless-доставка не реализована** (сознательно): при необходимости —

View File

@@ -12,8 +12,8 @@ description: >
«настрой авто-монитор инбокса», «raise inbox monitor», «auto-arm inbox
watcher». Headless (`claude -p`): does NOT raise — Monitor doesn't work
there; rely on the Stop-hook inbox pickup + Notify/ntfy. Pi (pi-coding-agent)
sessions: a global extension delivers any `.claude-inbox/` message to the
current session regardless of cwd (see the Pi section). NOT for how to
sessions: a global extension delivers the session's own project inbox
messages (own directory only, see the Pi section). NOT for how to
handle a received message (→ inter-session-peer-discipline) nor the
multi-machine inbox backend (→ cross-machine-inbox design).
---
@@ -129,26 +129,26 @@ for free on session end. Headless sessions skip it and rely on the pull-model
- **No repo writes.** The hook and its `~/.claude/settings.json` registration are
machine-local; only this skill (docs) and `.claude-inbox/` activity are in play.
## Pi (pi-coding-agent) support — global extension
## Pi (pi-coding-agent) support — session-scoped global extension
Same contract, pi-native, and **not cwd-scoped**: which folder the pi session
sits in is irrelevant — every `.claude-inbox/` under the configured roots is
watched and swept; a message pages the *current* pi session with the project
name in the header (`[inbox] <project>/<name>` + body inline, then moved to
`.read/`).
Same contract, pi-native, and **session-scoped**: the extension watches ONLY the
current session's project inbox (`<ctx.cwd>/.claude-inbox/`) — it never reads
other projects' inboxes (vitya's rule: an agent may only read its own
directory's inbox). Installed globally so *every* pi session has the
capability, but each session only ever touches its own project's inbox.
- **Source of truth:** `.common/lib/pi-extensions/inbox-monitor.ts` (Node
built-ins only, no npm deps). Deploy: copy to
`~/.pi/agent/extensions/inbox-monitor.ts` (global → every pi, every
directory), hot-reload with `/reload`. Test:
`node --experimental-strip-types .common/lib/pi-extensions/inbox-monitor.test.mjs`.
- **Roots:** direct children of `~/projects` by default (all inboxes live there,
depth 1); override with `PI_INBOX_ROOTS` (pathsep-separated, `;` on Windows).
- **PUSH:** one 15s global poll while a session is live (interactive only).
**PULL:** `agent_settled` sweep — the pi equivalent of the CC Stop-hook
pickup. Both share one dedup set per process; the `.read/` move is the
cross-process guard — first sweeper (CC hook or pi) claims the message, the
other skips it.
`node --experimental-strip-types .common/lib/pi-extensions/inbox-monitor.test.mjs`
(incl. decoy check — another project's inbox is never touched).
- **Opt-in per project** — same as CC: `.claude-inbox/` dir exists OR CLAUDE.md
line `inbox monitor: raise on start`.
- **PUSH:** 15s poll of the session's own inbox (interactive only). **PULL:**
`agent_settled` sweep — the pi equivalent of the CC Stop-hook pickup. Both
share one dedup set per process; the `.read/` move is the cross-process
guard — first sweeper (CC hook or pi) claims the message, the other skips it.
- **Headless (`pi -p`, `ctx.hasUI === false`):** NO delivery — no watcher, no
sweep. Messages sit in the inbox until an interactive session picks them up.
Mirrors CC headless (external Notify there, nothing in-run) and avoids