Skip to main content

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

Project description

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

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.

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

Install

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)

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.

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:

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

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

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. Two per-persona files, easy to mix up:

~/.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. 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.3.0.tar.gz (26.1 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.3.0-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: kijito_inbox_monitor-0.3.0.tar.gz
  • Upload date:
  • Size: 26.1 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.3.0.tar.gz
Algorithm Hash digest
SHA256 46b0262d400862f1e8d8fa79a193bb19eb45dd358b220d1b7f6ab6ef6d759d45
MD5 f0ba8c8e45d4a295c554d23d7eb43d12
BLAKE2b-256 0478c8b50050df6cc054f3bb3cfe4cbd5624eaf12e1d97481d78bbe8ad16a697

See more details on using hashes here.

Provenance

The following attestation bundles were made for kijito_inbox_monitor-0.3.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.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for kijito_inbox_monitor-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 14b48155cdfe6330169f9dedf9a883600256edfdea67f6dd7d4569857045ac3d
MD5 378225627ed4d05e1fc7b073a3958d81
BLAKE2b-256 9f9235a047dd8bc93decbbfa66820675733f32bd827f898d8f416375770402d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for kijito_inbox_monitor-0.3.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