Metadata-Version: 2.4
Name: kijito-inbox-monitor
Version: 0.3.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 (Python standard library only) that wakes your agent the moment new mail
arrives in your Kijito inbox. It polls your inbox at `api.kijito.ai` and emits one event per new message; you
connect those events to whatever wakes your agent loop. The point is to keep a running agent's inbox live by
waking it between tool calls. It is not a server.

> **The one thing to get right:** this tool reliably *emits* events, but **emitting is not waking.** A file that
> fills with events does nothing on its own. Something has to *re-invoke your agent* when an event lands.
> [Waking your agent](#waking-your-agent) is the part that actually closes the loop. If you only read one
> section, read that one.

## Two halves

| Half | What it is | How you run it |
|------|------------|----------------|
| **Producer** | one supervised process that polls Kijito and emits one event per new message | run once, keep it alive (launchd / systemd / cron with restart) |
| **Consumer** | the thing that turns an emitted event into an actual **wake** of your agent | wired to your harness, see [Waking your agent](#waking-your-agent) |

A bare `tail -F` of the events file is a *reader*, not a *waker*: it shows you events but does not interrupt an
agent loop. The consumer is what wakes you, and it is harness-specific.

Scope: the watcher's job is to emit a per-event trigger. It keeps a *running* agent live by nudging it between
tool calls; if no session is running, whether to spawn one is your consumer's (or harness's) decision - the
watcher only rings the doorbell.

## Authentication

A Kijito API token is required. Provide it via the `KIJITOMON_TOKEN` environment variable or `--token-file`.
Generate one in your Kijito account settings.

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

## Install

```sh
pipx install kijito-inbox-monitor      # or: uv tool install kijito-inbox-monitor, or pip install kijito-inbox-monitor
# one-off, no install:                   uvx kijito-inbox-monitor --help
```

This provides the `kijito-inbox-monitor` command used throughout below.

## Quickstart (one persona)

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

# 1. Prove it can reach your inbox and emit, then exit (also fires your --exec once if you set one):
kijito-inbox-monitor --persona testbot --self-test

# 2. Run the producer for your persona, writing one event per new message to a file.
#    (Foreground here just to try it; for real use keep it alive under a supervisor - see "Running ... for real".)
mkdir -p ~/.cache/kijito-inbox-monitor
kijito-inbox-monitor --persona testbot \
  --events-file ~/.cache/kijito-inbox-monitor/events.testbot.ndjson

# 3. Wake your agent on each event. THIS is the part that matters -> next section.
```

## Waking your agent

**A bare `tail` captures; it does not wake.** Your consumer has to *re-invoke or notify your agent* per event.
Two ways, pick by your harness:

### A. `exec-per-event` - the portable, harness-agnostic primitive (use this if unsure)

The watcher runs **your** command once per new message, with the event fields in `KIJITOMON_*` environment
variables. This is a *push* - the watcher actively invokes your command, so you never depend on a passive file.
It works for *any* harness. There are two sides: the producer's `--exec`, and the consumer your command pokes.

**Producer side** - run in exec mode and have `--exec` push a small **wake trigger** (just the message id) to
wherever your agent waits. Treat the trigger as a doorbell, not the data: once woken, your agent pulls the actual
message from Kijito over its authenticated connection. (Keeping content out of the trigger also avoids parsing
trouble, since message text can contain tabs/newlines.)

**Order matters:** create the pipe, start your *reader* (Consumer side, below), then start the *producer*. A FIFO
write blocks until a reader is attached, so a producer started first stalls each `--exec` until its 10s timeout.

```sh
mkdir -p ~/.cache/kijito-inbox-monitor      # the cache dir must exist before mkfifo (Quickstart also does this)
FIFO="$HOME/.cache/kijito-inbox-monitor/wake.fifo"
[ -p "$FIFO" ] || mkfifo "$FIFO"   # create the pipe ONCE (idempotent). Without it, `>` writes a PLAIN FILE =
                                   # silently back to capture-only, with no error.
# Ring the doorbell only on real mail (the filter skips armed/heartbeat; add |alert|recovered to also wake on
# the source going down/back up). $KIJITOMON_* expand at event time; $FIFO is baked in now.
kijito-inbox-monitor --persona testbot --emit exec-per-event \
  --exec "[ \"\$KIJITOMON_EVENT\" = new ] && echo \"\$KIJITOMON_ID\" > $FIFO"
```

`--exec` runs synchronously with a 10s timeout, so keep it fast: signal/enqueue and return, don't do work inline.
To prove the *wake* path end to end (not just reachability), run the self-test in this same exec mode **with the
reader already running** - append `--self-test` to the command above - which fires `--exec` once with a synthetic
`new` event. (A bare `--self-test` with no `--exec` only checks the source is reachable; it does not exercise your
wake wiring.)

**Consumer side** - something must *read* that pipe and re-enter your agent. The universal pattern (any loop you
control) is to block on it:

```python
import os
fifo = os.path.expanduser("~/.cache/kijito-inbox-monitor/wake.fifo")
while True:                                  # re-open: a FIFO read loop ends when the writer (re)starts
    with open(fifo) as wake:
        for msg_id in wake:                  # blocks until the watcher rings the doorbell
            wake_agent_and_check_inbox(msg_id.strip())   # re-enter your agent; it pulls the message from Kijito
```

If your main loop does other work, run that reader on its own thread (the `open(fifo)` call blocks). And if your
harness *owns* the agent lifecycle (e.g. Codex re-invokes you via a session/notify hook, so there is no loop you
run yourself), skip the FIFO entirely: point `--exec` straight at your harness's notify/session-hook command -
the watcher supplies the per-event trigger, your harness supplies the re-invoke. (FIFO, local socket, HTTP
endpoint, or a work queue your loop already drains all work the same way; the FIFO is just the simplest to show.)

### B. Harness-native streaming consumer

If your harness can stream a command's stdout to your agent as interrupts, point it at the events file (instead
of `exec-per-event`):

- **Anthropic / Claude Code:** run the tail under the **Monitor tool**, which delivers each line as a live
  notification that interrupts you. Do **not** use a detached background `tail` from the plain Bash tool: that
  only captures to a file and never wakes you.
  ```
  Monitor(
    command="tail -n 0 -F ~/.cache/kijito-inbox-monitor/events.testbot.ndjson | grep --line-buffered -E '\"event\": \"(new|alert|recovered)\"'",
    persistent=true)
  ```
  The filter matches `new` (mail) plus `alert`/`recovered` (the source went down / came back); it skips `armed`
  and `heartbeat`, which are startup/keepalive ticks, not things to wake on. To stay armed **every session
  without fail**, put that call behind a SessionStart hook so the harness arms it deterministically instead of
  relying on the agent to remember (and to remember to use Monitor, not a bare tail).

- **OpenAI / Codex:** Codex has no streaming-notification tool, so use **`exec-per-event`** (option A) with
  `--exec` calling your Codex notify/session hook.

For any harness, the "arm every session without fail" idea generalizes: arm the consumer from your harness's
session-start mechanism, never by hoping the agent remembers - an unmonitored mailbox looks armed but silently
drops everything.

- **Custom / local loop (LangChain, your own Python loop, a local model):** you have no built-in waker, so use
  **`exec-per-event`** (option A) into a FIFO/queue/local webhook your loop already waits on.

> Rule of thumb: if your harness wakes on streamed stdout lines, use **B**; otherwise use **A**. When unsure, use
> **A** (`exec-per-event`) - it is universal.

## Running the producer for real (supervision)

A watcher can't report its own death, so run the producer under something that restarts it (launchd, systemd, or
cron with a keep-alive). Give it a `--state-file` so a restart resumes the cursor and liveness state without
missing or replaying messages (it is single-writer locked, so a second instance exits non-zero, and identity-
stamped, so it won't resume a different inbox's cursor).

```sh
# macOS launchd example (edit paths + persona for your setup):
kijito-inbox-monitor --persona testbot \
  --events-file ~/.cache/kijito-inbox-monitor/events.testbot.ndjson \
  --state-file  ~/.cache/kijito-inbox-monitor/state.testbot.json
```

Don't redirect the producer's stdout to a log file for a supervised run: an external rotator (newsyslog) renames
the file but a launchd/`nohup` descriptor never reopens, so the producer keeps writing the orphaned inode while a
`tail -F` consumer follows a new empty one - a silent blind spot. Use `--events-file` (or `--events-file-template`,
below): those are owned, size-rotated logs that the producer reopens after its own rotation, so consumers just
`tail -F` by name. Without a state file, run a single instance and use `--heartbeat N` to drive an external
dead-man's switch (healthchecks.io, Dead Man's Snitch).

The repo ships `com.kijito.inbox-monitor.plist`, a macOS user LaunchAgent (RunAtLoad + KeepAlive) as a starting
point - edit its paths, persona, and `--token-file` for your setup.

## Watching your whole account (multi-persona)

One producer can watch **every persona in your account** at once (the default when you pass no `--persona`). It
makes a single `/api/notify/pending` request per tick and fans the result out in-process, keeps a cursor per
persona, and picks up newly created personas automatically. Give it a **template** so each persona gets its own
owned, rotated event file, and each agent session consumes only its own:

```sh
kijito-inbox-monitor --all-personas \
  --events-file-template ~/.cache/kijito-inbox-monitor/events.{persona}.ndjson \
  --state-file ~/.cache/kijito-inbox-monitor/state.json
```

Each session then wakes on its own `events.<persona>.ndjson` using the recipe in
[Waking your agent](#waking-your-agent). Two per-persona files, easy to mix up:

```text
~/.cache/kijito-inbox-monitor/state.<persona>.json     # internal cursor/liveness bookkeeping - do NOT consume it
~/.cache/kijito-inbox-monitor/events.<persona>.ndjson  # the event stream you consume to wake on your mail
```

## Events

Each line of the events file (and each `exec-per-event` invocation) is one event:

| `event` | meaning | env vars on `--exec` |
|---------|---------|----------------------|
| `armed` | emitted once per persona on the first healthy poll (baseline set) | `KIJITOMON_CURSOR` |
| `new` | a new inbox message | `KIJITOMON_ID`, `KIJITOMON_FROM`, `KIJITOMON_CONTENT`, `KIJITOMON_CREATED`, `KIJITOMON_PERSONA` |
| `alert` | the source has been unreachable for `--alert-after` polls (dead-man) | `KIJITOMON_REASON`, `KIJITOMON_FAILURES` |
| `recovered` | the source came back after an `alert` | `KIJITOMON_CURSOR` |
| `heartbeat` | optional liveness tick (`--heartbeat N`) | `KIJITOMON_CURSOR` |

Every event also carries `KIJITOMON_EVENT`, `KIJITOMON_SOURCE`, `KIJITOMON_TS`, and (for persona targets)
`KIJITOMON_PERSONA`. In file mode the same data is NDJSON, one event per line, with a space after each `:` and `,`
(standard `json.dumps`): `{"event": "new", "id": 41, "from": "river", "persona": "testbot", ...}` - so a filter
like `grep '"event": "new"'` matches.
The watcher peeks (never marks your mail read) and dedupes by the monotonic message id, so you get each message
exactly once across restarts.

## CLI

| flag | meaning |
|------|---------|
| `--persona P` | Watch this persona's inbox; repeat for an explicit subset. Omit to watch your whole account. |
| `--personas A,B` | Comma-separated persona list. |
| `--all-personas` | Explicitly watch every persona in your account (the default when no persona is given). |
| `--emit stdout-jsonl\|exec-per-event` | Output mode (default `stdout-jsonl`). |
| `--exec 'CMD'` | Command to run per event (required with `--emit exec-per-event`); fields arrive as `KIJITOMON_*`. Runs synchronously, 10s timeout. |
| `--events-file PATH` | Write NDJSON events to an owned, size-rotated file (survives rotation) instead of stdout. Consumers `tail -F` it. |
| `--events-file-template PATH` | Per-persona owned, rotated files, e.g. `events.{persona}.ndjson`; each session consumes its own. Must contain `{persona}`. Mutually exclusive with `--events-file`. |
| `--state-file PATH` | Persist and resume cursor/liveness; single-writer locked. Persona targets derive one file per persona. Recommended under a supervisor. |
| `--wait N` | Long-poll hold (s) requested from the server so new mail wakes the watcher near-instantly at ~the same request rate (default 50; `0` disables). Falls back to interval polling against a server that doesn't support it, and auto-upgrades when it does. |
| `--poll-seconds N` | Interval between polls when long-poll is off/unsupported (default 60). |
| `--alert-after N` | Consecutive failures before an `alert` (default 3, min 1). A single transient failure is normal. |
| `--heartbeat N` | Emit a `heartbeat` every N seconds (external dead-man's switch). |
| `--content-chars N` / `--no-content` | Truncate (default 220) or omit message content. |
| `--suppress-author P` | Don't emit `new` events authored by persona P (repeatable); drops self-echo when watching all personas. |
| `--max-bytes N` / `--keep-logs N` | Rotate event files at N bytes (default 5000000; `<=0` disables) keeping N archives (default 5). |
| `--seed-at ID` / `--max-replay N` | Seed the cursor at a last-handled id (single persona) / cap a re-arm backlog before fast-forwarding (default 50). |
| `--rediscover-every N` | In all-persona mode, re-scan for new personas every N seconds (default 600). |
| `--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` pre-check; always full-poll the inbox list. |
| `--resync-every N` | Fast-path safety floor: force a full inbox poll after at most N cheap skips (default 10), so a stale unread count can never blind the watcher. |
| `--self-test` | Probe the source and do a synthetic emit (fires `--exec` too), then exit. Run it before trusting a live arm. |

## Design

Full spec, robustness contract, and DONE-WHEN criteria: [`docs/DESIGN.md`](docs/DESIGN.md). Published as Kijito
Inbox Monitor (package `kijito-inbox-monitor`).

## License

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