npm.io
0.1.0 • Published 4d agoCLI

kijito-inbox-monitor

Licence
Apache-2.0
Version
0.1.0
Deps
0
Size
25 kB
Vulns
0
Weekly
0

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 §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

# 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:

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.

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:

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:

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:

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):

./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:

~/.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. 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 and NOTICE. Copyright 2026 Arcada Labs.

Keywords