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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user