Metadata-Version: 2.5
Name: sessionmail
Version: 0.1.0
Summary: sessionmail - mail for coding agents: message, pager and memory over one maildir. Name reserved; the tool follows.
Project-URL: Homepage, https://github.com/mbailey/session
Author: Mike Bailey
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Classifier: Development Status :: 1 - Planning
Requires-Python: >=3.11
Requires-Dist: click>=8.1
Requires-Dist: markdown>=3.6
Requires-Dist: mcp>=2.2.0
Requires-Dist: pymdown-extensions>=10.0
Requires-Dist: sessiontools>=0.0.4
Description-Content-Type: text/markdown

# sessionmail

Mail for coding agents: `message` moves messages, `pager` alerts, `memory`
is the filesystem view - three commands over one maildir.

## Quick start

```
claude plugin marketplace add mbailey/sessionmail
claude plugin install sessionmail
uv tool install sessionmail          # the commands the plugin calls
pager listen                         # blocks until mail arrives, then says so
```

Both of the first two lines need the repo and the release to be public:
see **Status** before you copy them.

The last line is the point: an agent that runs `pager listen` in the
background can be reached while it is idle, instead of finding out on its
next manual check.

## Installation

**Two installs, and you need both.** The plugin brings the skills, the two
hooks and the MCP servers; the package brings `message`, `pager` and
`memory`, which is what all of those call.

```
claude plugin install sessionmail    # skills, hooks, MCP entries
uv tool install sessionmail          # the three commands on PATH
```

With only the plugin, the hooks exit quietly and the MCP servers do not
start: you get skills describing commands the machine does not have. The
hook says so once per session rather than leaving you to guess.

**The notmuch hook is installed by hand.** `hooks/notmuch-post-new` tags
mail at index time (`+agent`, `+memory`) and re-derives `superseded` from
each message's `Supersedes:` header, so a correction survives rebuilding
the index. It is NOT installed by the plugin or the package. On a host it
runs as both `post-new` and `post-insert`, and needs two indexed headers:

```
ln -s <repo>/hooks/notmuch-post-new ~/.mail/.notmuch/hooks/post-new
ln -s <repo>/hooks/notmuch-post-new ~/.mail/.notmuch/hooks/post-insert
notmuch config set index.header.Supersedes Supersedes
notmuch config set index.header.XSupersedes X-Supersedes
```

Headers added to an existing index need a one-off `notmuch reindex '*'`.

**An optional command after each index run.** The hook can run one command
of yours after its tagging, for anything that should follow new mail (a
dashboard redraw, say). It is off unless a notmuch config key names it:

```
notmuch config set sessionmail.post_new_command /abs/path/to/command   # on
notmuch config set sessionmail.post_new_command                        # off
```

The value is an absolute path to an executable, run with no arguments and no
shell; a command that needs arguments gets a wrapper script. It never blocks
or fails indexing: the hook spawns a detached runner and exits 0 at once.
Runs are coalesced, one at a time: mail arriving during a run gives exactly
one more run. The command gets cwd `/`, its own process group, stdin
`/dev/null`, and only `HOME`, `PATH=/usr/bin:/bin:/usr/sbin:/sbin`,
`NOTMUCH_CONFIG` and `SESSIONMAIL_IN_POST_NEW_COMMAND=1`; it is killed after
300 s. A `notmuch new` it runs itself is tagged as usual and does not run it
again (a command that clears its environment must pass `--no-hooks`
instead). It must not send mail on every run: sent mail comes back as an
arrival. Its output and a `start`/`end` line per run go to
`~/.sessionmail/post-new/log` (rolled to `log.1` past 1 MiB). With the key
unset, nothing is created and nothing is spawned.

## Usage

```
message send NAME -s SUBJECT -f BODY.md   # and list, tree, read, mark
pager listen                              # block until mail; page; exit
pager status                              # what would page you now
memory ls / | memory cat PATH             # the box as a filesystem
```

Each command's own `--help` is the reference; `<command> <verb> --help` is
the page for a verb.

**Where mail comes from.** sessionmail reads and writes maildirs; it does
not require an MTA. On a host with postfix, mail arrives the ordinary way.
Without one, anything that can write a maildir is a delivery agent - your
own MTA, a script, or a chat bridge that drops each message into `new/`
(one door in, an outbox out). The pager watches the directory, whatever
filled it.

## How `pager listen` works

`pager listen --help` says when it pages, what it prints and how to stop.
This is how, for whoever has to reason about it. Paths below are under
`<home>/sessions/<session>/`, written `<session>/`; `<home>` is
`SESSIONMAIL_HOME`, default `~/.sessionmail`.

- **One box.** It watches this session's box (the session id is a box);
  with no session id, the maildir `$MAIL` names, which must hold `new/`. It
  reads no subscriptions: what else reaches the box is postfix's doing, set
  by `message subscription add BOX`.
- **Birth.** A session's box is created on its first listen when it is
  missing, and its alias line is minted in the same step, bounded in time.
  The banner says the doorbell's state: whether mail sent to the session
  will reach the box. That first listen also subscribes the session to
  each box in `SESSIONMAIL_SUBSCRIBE`, once. A `$MAIL` box is never
  created: a human's mailbox is theirs to make.
- **The ride queue.** Every arrival in the box's `new/` is hardlinked
  into `<session>/ride/new/`. The ride-along (the PostToolBatch hook)
  scoops that queue on each tool call, so a busy session reads its mail
  without being paged.
- **When it pages.** Idle (not busy for `SESSIONMAIL_IDLE_SECONDS`, 1, by
  Claude Code's own status file, or `session get idle`): at once. Busy:
  when the oldest link has sat in the queue longer than
  `SESSIONMAIL_PAGE_AGE_SECONDS` (60) from its delivery time, because
  nobody scooped it. Then it prints the page and exits; the caller runs it
  again to keep listening. `SESSIONMAIL_PAGE_LIST=on` (or `--list`) adds
  one line per message: box, sender, subject, never a body. The flag wins:
  `--no-list` beats `SESSIONMAIL_PAGE_LIST=on`.
- **Marks.** It claims nothing and moves nothing. It marks each message it
  pages with `seen` (reason `pager wake`), so the same message does not
  page this session again inside the reminder window
  (`SESSIONMAIL_REMIND_MINUTES`, 60). Another session's marks never
  silence this one.
- **Stamps.** A running listener writes its pid to
  `<session>/stamps/listening`; that is what `pager status` and the hooks
  read as "listening", and what `pager stop` signals.
- **Keyed on the session.** The queue, the stamps and the marks key on
  the session id, so a session with no seat gets the same page. With
  neither a session id nor a seat there is no queue; it still listens,
  and the banner says so.

### As a Claude Code plugin monitor: `pager listen --monitor` (SM-678)

A hidden flag, for one caller: a plugin's `monitors/monitors.json`. Claude
Code runs a monitor's command for the whole session and makes each stdout
line a notification; it does not restart one that exits. So `--monitor`
never exits on a page:

- ONE line per page on stdout, flushed, and nothing else there: the count,
  the address each mail was sent to, what woke it - no sender, no subject,
  no re-arm. At most 480 characters (Claude Code cuts at 500).
- Lines at least `SESSIONMAIL_MONITOR_SPACING_SECONDS` apart (2.5): Claude
  Code drops output past a bucket of 10 refilled 1 per 2 s, and a dropped
  page is a wake that never happens.
- After a page it keeps listening. The `seen` marks, the carry stamp and
  the woke records are written as for any page, BEFORE the line, so the
  wake turn finds the carry stamp. A mail paged and never scooped pages
  again after the reminder window, as a re-arm would.
- The "listening" confirm mail is sent once per monitor process, never per
  page.
- SIGTERM, SIGINT or SIGHUP end it with exit 0 and clear its `listening`
  stamp. When it cannot look it prints `pager: DEAF - ...` and exits 2.
- A monitor that finds another listener on its session waits, and takes
  over when that one ends.
- It refuses `--timeout`, `BOX` and `--quiet`.
- **`pager off` / `pager stop` ends a monitor for good.** Nothing restarts
  it; listen again with the bare `pager listen`.

## Not running under Claude Code?

`pager` learns whether you are busy from Claude Code's own per-session
sidecar (`~/.claude/sessions/<pid>.json`), and failing that from `session
get idle`. A client with neither - a bare shell, your own script, a bot -
never looks idle, so waits written for an agent that is mid-tool-call apply
to it:

- a page waits the running page age (60 s) instead of ringing on arrival;
- and if your client names a seat (`CLAUDE_CODE_AGENT`), a second message
  from the same address inside the coalescing window (300 s) does not ring
  again - a running agent would have scooped it on its next tool call.

Nothing is lost; it is late, or it waits for your next `pager listen`. Turn
both waits off in that client's environment:

```
export SESSIONMAIL_PAGE_AGE_SECONDS=0   # ring on arrival
export SESSIONMAIL_BURST_SECONDS=0      # ring for every message
```

`pager listen` says the same thing in its banner when nothing can tell it
whether you are busy.

## Development

```
make install     # uv tool install -e . - editable, so an edit is live
make             # check, then test
make progress    # how much of the CLI is built, counted by running it
```

`openspec/` is the record of what this package is meant to do: read it
before you design or build. `openspec/specs/` is ratified, `changes/` is
proposed and ruled by nobody yet.

## Status

**`sessionmail` 0.0.1 on PyPI is a name reservation, not this code** - a
1.3 KB placeholder with no dependencies, published 2026-09-08. Until 0.1.0
ships, install from source instead of the `uv tool install sessionmail`
line above:

```
uv tool install git+https://github.com/mbailey/sessionmail   # from the repo
make install                                                 # from a clone
```

`message`, `pager` and `memory` all run; `openspec/specs/` says what each
verb is meant to do and `make progress` counts how much of it is built.

## The plugin: three skills, one per command

`skills/message`, `skills/pager`, `skills/memory` - each seed is one
sentence of what and when; each body points at that command's `--help`
and repeats nothing; each `references/` is a flat slipbox of items under
40 lines / 400 tokens. This README is the overview of how the three go
together (Mike, voice 12:32 Wed 2026-09-09: three skills "to give some
clear separation", flexible, to be reviewed).

`slipbox/` ships one note, `pager.md`; the other notes the specs cite by
name live in a private slipbox and are not part of this tree.
