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

View File

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