feat(skills): mail on Mappa — inter-session-messaging v2.0.0 + session-inbox-monitor v1.0.0

- inter-session-messaging v2.0.0: channel switched from file inbox
  (.agents/inbox/) to Mappa inbox.send/inbox.monitor/entity_get; letters are
  entities i:N, delivery is a lease carve-out; address book + project-exists
  check via admin_status; replies via entity_get(id).meta.from; subject
  carries [event: ...] instead of frontmatter.
- session-inbox-monitor v1.0.0: monitor now polls GET /inbox?project=<cwd>
  (HTTP, dedup by letter id, no .read/ move); hook inbox-monitor.ps1 rewritten
  to inject the mappa poll command (sweep sentinel + project dir).
- delegate-task v0.5.1: covering letter goes through inbox_send, not file path.

Part of #983 (mappa-skills).
This commit is contained in:
2026-08-24 15:19:27 +03:00
parent 1b00c37546
commit 146fbdb107
8 changed files with 208 additions and 247 deletions

View File

@@ -1,35 +1,38 @@
---
name: session-inbox-monitor
author: ours
version: 0.4.1
version: 1.0.0
description: >
Raises a persistent Monitor (Monitor tool, NOT background Bash) on the
project's `.agents/inbox/` at the start of an interactive session, so
Raises a persistent Monitor on the project's Mappa inbox (poll
`mcp__mappa__inbox_monitor` / HTTP `GET /inbox?project=<cwd>`), so
inter-session messages page the session in real time; the monitor dies on
session end on its own. A paired SessionStart hook injects the
raise-instruction and first sweeps orphaned monitors of this inbox (a
`/clear` leaves them running → re-raise would stack duplicates). Triggers:
session end on its own. Pi (pi-coding-agent) sessions: a global extension
polls the same HTTP endpoint, session-scoped (own project only). Triggers:
AGENTS.md line `inbox monitor: raise on start`, or «подними монитор почты»,
«настрой авто-монитор инбокса», «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 the session's own project inbox
messages (own directory only, see the Pi section). NOT for how to
handle a received message (→ inter-session-messaging) nor the
multi-machine inbox backend (→ cross-machine-inbox design).
watcher». Headless (`claude -p` / `pi -p`): does NOT raise — rely on the
Stop-hook / agent_settled pull pickup. File channel `.agents/inbox/` is
gone (flip, решение 15) — letters live in Mappa as entities `i:N`, delivery
is a carve-out (no lease, решение 19). NOT for how to handle a received
message (→ inter-session-messaging).
---
# session-inbox-monitor
Auto-raises a session-length Monitor on `.agents/inbox/` at interactive-session
start (via a paired SessionStart hook that injects the instruction and sweeps
orphans), so inter-session messages page the session in real time. Tears down
for free on session end. Headless sessions skip it and rely on the pull-model
(Stop-hook pickup + Notify).
Auto-raises a session-length Monitor on the project's **Mappa inbox** at
interactive-session start, so inter-session messages page the session in real
time. Tears down for free on session end. Headless sessions skip it and rely
on the pull-model (Stop-hook / agent_settled pickup).
Канал — Mappa, НЕ файлы: письма — сущности `inbox` (`i:N`) в сервисе, читаются
через `mcp__mappa__inbox_monitor(project, limit)` (или HTTP `GET
/inbox?project=<имя-папки>&limit=N`). Доставка/чтение — карв-аут (решение 19):
не блокируются лизом, работают всегда. Файловые `.agents/inbox/` и `.read/`
выпилены.
## When to use
- **Automatic (the common path).** The paired SessionStart hook injects an
- **Automatic (the common path).** A paired SessionStart hook injects an
instruction at the start of every interactive session of an opted-in project.
You act on that injection — raise the monitor as your first action — without a
user phrase.
@@ -37,142 +40,85 @@ for free on session end. Headless sessions skip it and rely on the pull-model
монитор почты», «настрой авто-монитор инбокса», «raise inbox monitor»,
«auto-arm inbox watcher».
- **NOT for** handling the content of a received message (→
`inter-session-messaging`), nor the multi-machine delivery backend (→
`cross-machine-inbox`). This skill is only the monitor's *lifecycle* on one
machine.
`inter-session-messaging`). This skill is only the monitor's *lifecycle*.
## Inputs
- `<project>/.agents/inbox/` — the watched directory. Direct-child `*.md` files
are inbox messages (the Stop-hook moves them to `.read/` once handled).
- The SessionStart hook supplies the **exact Monitor command** to run, with the
sweep sentinel (`CLAUDE_INBOX_MONITOR`) and the absolute inbox path baked in.
Use it verbatim — do not hand-author a different poll command, or the sweep
won't recognise the process it spawns.
- `<project>` — своё имя папки (cwd basename), адрес в Mappa.
- Письма: `mcp__mappa__inbox_monitor(project=<своя папка>, limit)` →
`{rows: [{id, slug, body}]}` (последние N). `from`/`subject` — в meta, через
`entity_get(id)` при ответе.
- Мопitor-команда при CC — HTTP-опрос `GET /inbox?project=...` (без токена на
текущем проде; если `MAPPA_API_TOKEN` задан — `x-api-token` header).
## Steps
1. **Mode check.** If this is a headless / non-interactive run (`claude -p`),
**STOP — do not raise a monitor.** The Stop-hook inbox pickup plus `Notify:`/
ntfy cover delivery there; a Monitor can't idle-watch in headless and is
killed ~5s after the run. There is no hook-level headless signal, so this is
your judgement call from the run context.
2. **Raise exactly one persistent Monitor** using the command the hook injected:
the **Monitor tool** with `persistent: true`, `description: "inbox watcher"`.
The hook already swept any orphan before injecting, so you start from a clean
slate — raise one, not more.
1. **Mode check.** Headless / non-interactive (`claude -p`, `pi -p`,
`ctx.hasUI === false`): **STOP — do not raise a monitor.** The pull pickup
(CC Stop-hook / pi `agent_settled`) plus `Notify:` cover delivery there; a
Monitor can't idle-watch in headless.
2. **Raise exactly one persistent Monitor** on the Mappa inbox:
`mcp__mappa__inbox_monitor(project=<cwd-имя-папки>, limit=50)`, poll
interval ~15s, `description: "inbox watcher"`. Dedup by письменному id:
новое письмо — это id, которого ещё не было в предыдущих ответах.
3. **Do not sweep yourself.** Killing orphans is the hook's job (it runs before
you, at SessionStart, when no other session activity is live).
4. **On an event** (`New inter-session message in inbox: <name>`), read
`.agents/inbox/` and handle the message per `inter-session-messaging`.
The Stop-hook also force-delivers any inbox messages at end of turn as a
backstop, so nothing is lost if the monitor missed a beat.
you, at SessionStart).
4. **On an event** (новый id в ответе inbox_monitor), прочитай письмо (body) и
обработай per `inter-session-messaging`. Для ответа — `entity_get(id)` →
`meta.from`. Письма не перемещаются (нет `.read/`) — обработанные остаются
в списке; дедуп по id в памяти монитора.
5. **Teardown is automatic.** The Monitor dies at session end. Do **not** add a
SessionEnd teardown — and note `/clear` does not fire SessionEnd anyway
(that's why the sweep lives in SessionStart, not SessionEnd).
## Deployment (machine-local)
- Hook script: `skills/session-inbox-monitor/hooks/inbox-monitor.ps1` (versioned
here) → deploy to `~/.claude/hooks/inbox-monitor.ps1`.
- Register in `~/.claude/settings.json` under `hooks.SessionStart` (no matcher →
fires on startup/resume/clear/compact), e.g.:
```json
{ "hooks": [ { "type": "command",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"C:\\Users\\<you>\\.claude\\hooks\\inbox-monitor.ps1\"",
"timeout": 15, "statusMessage": "inbox-monitor" } ] }
```
- Twin pattern: `poller-interactive-lock-writer` (`interactive-lock.ps1`).
- Opt-in per project: the hook fires only when the project has a `.agents/inbox/`
directory **or** an AGENTS.md line `inbox monitor: raise on start`.
## Failure modes
- **No inbox, no opt-in line** → the hook injects nothing; no monitor. Expected
for projects that don't use inter-session messaging.
- **Two live interactive sessions on the same project** → the second session's
SessionStart sweep kills the first session's monitor (the match is
per-inbox-path, not per-session). Known limitation; the deliberate invariant is
"exactly one monitor per inbox per machine." If the first session is still
active, its next Stop-hook turn still delivers inbox mail — only the real-time
paging is lost until it re-raises. See `inter-session-messaging`.
- **Sweep over-match** → any *live* process whose command line contains both the
sentinel `CLAUDE_INBOX_MONITOR` and the inbox path is killed. At a real
SessionStart no agent/tool processes are running yet, so only the orphaned
monitor matches. Don't echo or run a command carrying that sentinel+path during
a session's startup.
- **Headless didn't skip** → a monitor raised in headless is a harmless no-op,
killed ~5s after the run ends. The default errs toward raising because a
false-skip in an interactive session would silently lose the feature.
- **Monitor auto-stopped** → the harness stops monitors that emit too many
events. The injected poll command de-dups by filename (pages once per message,
not every 15s) to stay under that bar.
- **Mojibake on force-delivery** → the Stop-hook (`stop-dispatcher.ps1`) injects
message bodies to stdout; WinPS 5.1 must set `[Console]::OutputEncoding =
[System.Text.Encoding]::UTF8` or non-ASCII (Cyrillic) bodies arrive mangled
(it emits in the OEM code page under a harness-spawned redirected pipe). Inbox
messages must be written as **no-BOM UTF-8, LF** — the Write tool does this;
PowerShell writers must use
`[IO.File]::WriteAllText($p,$t,[Text.UTF8Encoding]::new($false))`, NOT
`Set-Content`/`Out-File -Encoding utf8` (which adds a BOM under 5.1). Same
WinPS-5.1 encoding class as the hook-source em-dash gotcha. Fixed + in-situ
verified 2026-06-17. The SessionStart injector (`inbox-monitor.ps1`) carries
the **same `[Console]::OutputEncoding` UTF-8 guard** as a forward-protection
(v0.2.2): it interpolates the inbox path into the injected JSON, so a non-ASCII
path or `additionalContext` would otherwise mangle the same way — the guard is
preventive (today's `$ctx` is ASCII) but cheaper than an "ASCII-only" invariant.
## Side effects
- Spawns one Monitor (and its backing Git-Bash poll process) per interactive
session; both die at session end.
- Force-kills orphaned monitor processes of this inbox at every SessionStart.
- **No repo writes.** The hook and its `~/.claude/settings.json` registration are
machine-local; only this skill (docs) and `.agents/inbox/` activity are in play.
SessionEnd teardown.
## Pi (pi-coding-agent) support — session-scoped global extension
Same contract, pi-native, and **session-scoped**: the extension watches ONLY the
current session's project inbox (`<ctx.cwd>/.agents/inbox/`) — it never reads
Same contract, pi-native, session-scoped: the extension polls ONLY the current
session's project inbox (`GET /inbox?project=<basename cwd>`); 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.
project's inbox). Installed globally so *every* pi session has the capability,
but each session only ever touches its own project.
- **Source of truth:** `~/projects/pi-extensions/extensions/inbox-monitor.ts`
(репо `OpeItcLoc03/pi-extensions`, Gitea — дом pi-расширений). Deploy:
`just install` в клоне репо — копирует с затираанием в
`~/.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`
(incl. decoy check — another project's inbox is never touched).
- **Opt-in per project** — same as CC: `.agents/inbox/` dir exists OR AGENTS.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.
directory), hot-reload with `/reload`.
- **Opt-in per project:** AGENTS.md / CLAUDE.md line
`inbox monitor: raise on start`. (Файловой директории `.agents/inbox/` больше
нет — триггер только строка.)
- **PUSH:** ~15s poll of the session's own Mappa inbox (interactive only).
**PULL:** `agent_settled` sweep — same poll, backstop. Both share one dedup
set per process (by letter id).
- **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
hijacking one-shot scripted runs or consuming messages nobody processes.
sweep. Messages sit in Mappa until an interactive session picks them up.
- **Delivery:** `pi.sendUserMessage(body, { deliverAs: "followUp", triggerTurn:
true })` — paged into the transcript as a user message; the agent handles it
per `inter-session-messaging`. Partial writes (empty file) are skipped
and retried next poll.
- **Failure mode — cross-harness double-pickup:** CC and pi both sweep; the
`.read/` move makes it first-wins, not double-processing. Two live pi sessions
in one process tree share the dedup set; two pi *processes* on one machine
race like two CC sessions (known limitation, see Failure modes above).
per `inter-session-messaging`.
## Failure modes
- **Нет opt-in строки** → монитор не поднимается; ожидаемо.
- **Сервис mappa недоступен** → poll-запрос падает; монитор ретраит следующий
тик. Письма в сервисе не теряются (они — сущности), доставятся когда сервис
вернётся. НЕ дублируй в файлы — фолбэк-канал выпилен.
- **Two live interactive sessions on the same project** → оба поллят один
инбокс; дедуп по id делает доставку first-wins (кто первый прочитал id —
тот и доставил; второй пропускает). Real-time paging теряет только тот, кто
опоздал; письма не теряются (pull-свип в конце хода).
- **Monitor auto-stopped** → harness останавливает мониторы с переизбытком
событий; дедуп по id держит частоту пейджинга под баром.
- **Обработанное письмо вернулось в списке** — не баг: в Mappa нет `.read/`,
письма не перемещаются. Сверяйся с уже виденными id, не перечитывай.
## What NOT to do
- **Don't watch the file inbox** (`.agents/inbox/`) — канал выпилен; пиши и
читай через Mappa.
- **Don't watch the inbox with a background Bash** (`run_in_background`) — it
leaks across `/clear` and accumulates zombies. Use the Monitor tool.
- **Don't add a SessionEnd teardown hook** — the Monitor self-terminates, and
`/clear` never fires SessionEnd.
- **Don't raise more than one monitor.** The hook guarantees a clean slate before
you raise.
- **Don't add a SessionEnd teardown hook** — the Monitor self-terminates.
- **Don't raise more than one monitor.** The hook guarantees a clean slate
before you raise.
- **Don't handle message content here** — that's `inter-session-messaging`.
- **Don't rely on this in headless** — use the pull model (Stop-hook + Notify).
Active headless polling, if ever needed, is a separate cron Routine, not this
skill.
- **Don't rely on this in headless** — use the pull model (Stop-hook +
Notify).