Metadata-Version: 2.4
Name: kijito-inbox-monitor
Version: 0.2.0
Summary: Zero-dependency 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 your inbox at
`api.kijito.ai` 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 your whole Kijito account (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.

## Authentication

A Kijito API token is required (the API is authenticated). Provide it with the `KIJITOMON_TOKEN` environment
variable or a `--token-file`. Get a token from your Kijito account.

```sh
export KIJITOMON_TOKEN="<your-kijito-api-token>"
```

## 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
export KIJITOMON_TOKEN="<your-kijito-api-token>"

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

# arm the account 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 event includes
its `persona`.

## 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 an account with several personas.)

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 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 in your account, 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 in-process, 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 your account 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`. Edit the plist to set your own paths and to point
`--token-file` at a file holding your token (mode `600`). 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 your whole account, 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
export KIJITOMON_TOKEN="<your-kijito-api-token>"
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 in your account (the default when no persona is given). |
| `--rediscover-every N` | In all-persona mode, re-scan your account and add new personas every N seconds (default 600). |
| `--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 `--persona` target only). |
| `--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. 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 (default `Authorization: Bearer`) / token file. Token also via `$KIJITOMON_TOKEN`. A token is required. |
| `--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
harness-agnostic at the emit seam (`exec-per-event` is the portable primitive) and watches your Kijito inbox at
`api.kijito.ai`. Published as Kijito Inbox Monitor (package `kijito-inbox-monitor`).

## License

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