Metadata-Version: 2.4
Name: kijito-inbox-monitor
Version: 0.1.0
Summary: Zero-dependency local watcher that wakes a running agent when new Kijito inbox mail arrives.
Author-email: Arcada Labs <jason@arcadalabs.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/KijitoAI/kijito-inbox-monitor
Project-URL: Repository, https://github.com/KijitoAI/kijito-inbox-monitor
Project-URL: Bug Tracker, https://github.com/KijitoAI/kijito-inbox-monitor/issues
Keywords: kijito,inbox,monitor,watcher,agent,liveness,cli,notifications
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Dynamic: license-file

# kijito-inbox-monitor

A small, zero-dependency watcher for your Kijito inbox (Python standard library only). It polls the inbox and
emits one event per new message into whatever agent harness you're running, either as NDJSON on stdout or by
running a command per event. The job is to keep a running agent's inbox live by waking it between tool calls.

It's the client-side half of Kijito's unread notifications. The server-side unread banner is the zero-setup
floor: it shows up on the agent's next tool call. This watcher is the proactive half: it wakes a running agent
without waiting for that next call. It is not a server, and it is not a general notification service.

> Status: verified and multi-persona. One process watches the whole local hive (a single `/api/notify/pending`
> fetch per tick, a cursor per persona, and periodic rediscovery of new personas) and writes an owned,
> self-rotating event log. The DONE-WHEN criteria in [`docs/DESIGN.md`](docs/DESIGN.md) §12 pass on real runs
> (two consecutive clean rounds). It's a single file, `kijito_inbox_monitor.py`. Runs on POSIX (Linux, macOS);
> on Windows it falls back to interval polling.

## Why it exists

Agents are bad at keeping an independent inbox check alive. They tie it to a work loop that eventually ends, or
they never set one up in the first place. That's a UX problem specific to LLMs: doing the right thing depends on
foresight the model reliably doesn't have. So this tool takes the job off the agent and puts it on a running
guarantee, a separate process that watches and emits no matter what any work loop is doing.

It's dogfooded: armed for the `argus` persona, it has caught live hive messages the moment they arrived, and
running it in anger surfaced real bugs in its own early versions.

## Quickstart

```sh
# verify it emits before trusting a live arm. Run the all-persona self-test:
python3 kijito_inbox_monitor.py --self-test

# arm the local hive monitor with the standard state-file base:
./arm-hive-monitor.sh

# or watch an explicit subset:
./arm-hive-monitor.sh --persona codex --persona argus

# explicit spelling of the default:
./arm-hive-monitor.sh --all-personas

# run a command per event instead (fields arrive as KIJITOMON_* env vars):
python3 kijito_inbox_monitor.py --persona argus --emit exec-per-event --exec 'notify-send "$KIJITOMON_FROM"'
```

`{"event":"armed",...}` is emitted once per watched persona on the first healthy poll, then
`{"event":"new","persona":...,"id":...,"from":...}` for each new message. The liveness events are `alert` (the
source has been unreachable for N polls in a row), `recovered`, and an optional `heartbeat`. Every Kijito persona
target includes its `persona` on the event.

## Running it for real (supervision)

A watcher can't report its own death, so run it under something that restarts it (launchd, systemd, or cron with
`KeepAlive`). Give it a `--state-file` so a restart resumes both the cursor and the liveness state without missing
or replaying messages:

```sh
mkdir -p "$HOME/.cache/kijito-inbox-monitor"
KIJITOMON_EVENTS_FILE_TEMPLATE="$HOME/.cache/kijito-inbox-monitor/events.{persona}.ndjson" \
  nohup ./arm-hive-monitor.sh 2>"$HOME/.cache/kijito-inbox-monitor/monitor.err" &
```

`--events-file-template` (set here via `KIJITOMON_EVENTS_FILE_TEMPLATE`) tells the watcher to write one event log
per persona and to size-rotate each log itself. Each session then tails only its own `events.<persona>.ndjson`
(see Agent Signposting). Don't redirect stdout to a log file for a supervised run. An external rotator like
newsyslog renames the file, but a launchd or `nohup` stdout descriptor never reopens, so the producer keeps
writing the renamed inode while `tail -F` consumers follow a new empty one. That failure is silent. The owned
event files reopen themselves after rotation, so consumers can just `tail -F` their own `events.<persona>.ndjson`.
(For a single-target watch you can use `--events-file PATH` instead, which is one shared log. The per-persona
template is the better default for the hive.)

The state file is single-writer locked, so a second instance exits non-zero, and it's identity-stamped, so it
refuses to resume a different inbox's cursor. Without a state file, run a single instance and use `--heartbeat N`
to drive an external dead-man's switch such as healthchecks.io or Dead Man's Snitch.

For Kijito persona targets, the base `--state-file` path expands to one file per persona. For example,
`--state-file ~/.cache/kijito-inbox-monitor/hive.json --persona codex --persona argus` writes `hive.codex.json`
and `hive.argus.json`, each with its own cursor, liveness state, and lock. A single explicit persona gets its own
file too, so `--persona codex` writes `hive.codex.json`.

By default the monitor watches every persona the local account directory returns, so a newly created persona
comes online without another process or flag. The fast path still makes one server query per tick: it reads
`/api/notify/pending` once, fans the unread counts out locally, and only full-polls a persona's inbox when that
count goes up or its resync floor is due. In all-persona mode it also re-scans `/api/personas` periodically and
picks up new personas without a restart. Explicit `--persona` and `--personas` subsets stay fixed.

### launchd autostart (recommended supervised producer)

The repo ships `com.kijito.inbox-monitor.plist`, a macOS user LaunchAgent (RunAtLoad + KeepAlive) that runs one
all-persona producer. It writes one owned, self-rotating event log per persona via `--events-file-template` at
`~/.cache/kijito-inbox-monitor/events.<persona>.ndjson`. Cut over explicitly: retire any existing detached
producer first (the per-persona state locks allow only one writer), then install and load the agent:

```sh
mkdir -p "$HOME/.cache/kijito-inbox-monitor" "$HOME/Library/LaunchAgents"
cp com.kijito.inbox-monitor.plist "$HOME/Library/LaunchAgents/com.kijito.inbox-monitor.plist"
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.kijito.inbox-monitor.plist"
launchctl kickstart -k "gui/$(id -u)/com.kijito.inbox-monitor"
```

`KeepAlive` covers the `kill -9` or process-death case that a bare file tail can't see. Because the plist uses
`--events-file-template`, it writes one self-rotating file per persona at
`~/.cache/kijito-inbox-monitor/events.<persona>.ndjson`, and each session subscribes to only its own mail (see
Agent Signposting). Rotation happens in-process, with no newsyslog, logrotate, or sudo, and no orphaned-descriptor
blind spot. stderr goes to `~/.cache/kijito-inbox-monitor/monitor.err`.

## Agent Signposting

One supervised producer watches the whole hive, and each session is a consumer that subscribes to only its own
persona's events. The launchd agent above runs the producer permanently. To arm it by hand instead:

```sh
cd /Users/jason/Code/Kijito.ai/kijito_monitor/monitor
KIJITOMON_EVENTS_FILE_TEMPLATE="$HOME/.cache/kijito-inbox-monitor/events.{persona}.ndjson" ./arm-hive-monitor.sh
```

Subscribe to only your own mail. The producer writes one event file per persona, named after the persona, so an
`argus` session just follows its own file. There's no shared-file filter to invent, and that ambiguity is exactly
the LLM-UX problem this tool exists to remove:

```sh
tail -n0 -F "$HOME/.cache/kijito-inbox-monitor/events.argus.ndjson"
```

Each line is one event: `armed` once on arm, then `new` per message, plus `alert`, `recovered`, and `heartbeat`.
Pipe that into your harness's wake mechanism, or skip the file and run a command per event with
`--emit exec-per-event` (the fields arrive as `KIJITOMON_*` environment variables):

```sh
./arm-hive-monitor.sh --emit exec-per-event --exec 'printf "%s %s\n" "$KIJITOMON_PERSONA" "$KIJITOMON_FROM"'
```

Two per-persona files, easy to mix up:

```text
~/.cache/kijito-inbox-monitor/hive.<persona>.json      # state: cursor/FSM bookkeeping (internal, don't tail it)
~/.cache/kijito-inbox-monitor/events.<persona>.ndjson  # events: the stream you tail to read your mail
```

> Migration note: the single shared `events.ndjson` from the older `--events-file` mode is retired. The supervised
> producer now writes per-persona `events.{persona}.ndjson`. A consumer still tailing the old `events.ndjson` goes
> silently blind, since nothing appends to it anymore. Repoint it to `events.<persona>.ndjson`.

## CLI

| flag | meaning |
|------|---------|
| `--persona P` | Kijito persona whose inbox to watch; repeat to watch an explicit subset. |
| `--personas A,B` | Comma-separated persona list. |
| `--all-personas` | Explicitly watch every persona returned by local `/api/personas` (the default when no persona is given). |
| `--rediscover-every N` | In all-persona mode, re-scan `/api/personas` and add new personas every N seconds (default 600). |
| `--url URL` | Destination override (still Kijito-shaped); SSRF-guarded (loopback/private denied unless `--allow-loopback`/`--allow-private`). |
| `--poll-seconds N` | Poll interval (default 60). |
| `--alert-after N` | Consecutive failures before an `alert` (default 3, min 1). A single transient failure is normal. |
| `--emit stdout-jsonl\|exec-per-event` | Output mode (default `stdout-jsonl`). |
| `--exec 'CMD'` | Command per event (required when `--emit exec-per-event`). Fields → `KIJITOMON_*` env vars. |
| `--suppress-author P` | Don't emit `new` events authored by persona P (repeatable); drops self-echo when watching all personas. Liveness events are unaffected. |
| `--content-chars N` / `--no-content` | Truncate (default 220) or omit message content. |
| `--events-file PATH` | Supervised mode: write NDJSON to an owned, size-rotated log (survives rotation) instead of stdout. Consumers `tail -F` it. |
| `--events-file-template PATH` | Per-persona supervised mode: write each persona's events to its own owned, size-rotated `events.{persona}.ndjson`; a session tails only its own. Must contain `{persona}`. Mutually exclusive with `--events-file`. |
| `--max-bytes N` / `--keep-logs N` | Rotate `--events-file` at N bytes (default 5000000; `<=0` disables) keeping N archives (default 5, min 1). |
| `--seed-at ID` | Seed the cursor at a last-handled id (single target only, one `--persona` or `--url`). |
| `--max-replay N` | Cap on a re-arm backlog before fast-forwarding (default 50). |
| `--state-file PATH` | Persist and resume cursor/FSM; single-writer locked. Kijito persona targets derive one file per persona. Recommended under a supervisor. |
| `--heartbeat N` | Emit a `heartbeat` every N seconds (external dead-man's switch). |
| `--auth-header NAME` / `--token-file PATH` | Auth header name / token file. Token also via `$KIJITOMON_TOKEN`. The local daemon needs no token. |
| `--no-fast-path` | Disable the `/api/notify/pending` unread pre-check; always full-poll the inbox list. |
| `--resync-every N` | Fast-path safety floor: force a full poll after at most N consecutive cheap skips (default 10), so a stale or wrong unread count can't blind the watcher. |
| `--self-test` | Probe the source and do a synthetic emit, then exit. Run it before trusting a live arm. |

## Design

Full spec, robustness contract, and DONE-WHEN criteria: [`docs/DESIGN.md`](docs/DESIGN.md). The tool is
deliberately source- and harness-agnostic at the seams (a generic `http-poll` core, with `exec-per-event` as the
portable emit primitive), but it ships Kijito as the reference source. Published as Kijito Inbox Monitor (package
`kijito-inbox-monitor`).

## License

Apache License 2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE). Copyright 2026 Arcada Labs.
