Skip to main content

Zero-dependency local watcher that wakes a running agent when new Kijito inbox mail arrives.

Project description

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.

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:

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kijito_inbox_monitor-0.1.0.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kijito_inbox_monitor-0.1.0-py3-none-any.whl (23.5 kB view details)

Uploaded Python 3

File details

Details for the file kijito_inbox_monitor-0.1.0.tar.gz.

File metadata

  • Download URL: kijito_inbox_monitor-0.1.0.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for kijito_inbox_monitor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f373daeda72689556c2fbf69326f727e3ab4fa254f137d7401542223d8821453
MD5 0ede49ec7a1ab8e623c056b32ec8e267
BLAKE2b-256 5515f93c52d7c78320994ef38642ab6dc720e5df7c665b6886bd9f0458a56985

See more details on using hashes here.

Provenance

The following attestation bundles were made for kijito_inbox_monitor-0.1.0.tar.gz:

Publisher: publish-pypi.yml on KijitoAI/kijito-inbox-monitor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kijito_inbox_monitor-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for kijito_inbox_monitor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 632fff0ca9666b0bb559e5bb5e0fd0ef7f6f73727a939f4888b04c4f086ab566
MD5 6b3a0c390ca18cc046b673be8cfe94e2
BLAKE2b-256 06aca410ddc54260b81bb3525bf55b591b9ae90bb92736c8f982664c6c6fb6a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for kijito_inbox_monitor-0.1.0-py3-none-any.whl:

Publisher: publish-pypi.yml on KijitoAI/kijito-inbox-monitor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page