| name | inbox-listener |
| description | Use when arming an event listener for the canonical mesh — when the user says 'arm this listener', 'subscribe to ntfy topic', 'wake me when X arrives', or when responding to a system-reminder from listener-install-pickup. The new canonical flow is `empirica listener on/arm/off` — three single-purpose tool calls that auto-resolve defaults, short-circuit when a persistent OS service is already subscribed, and emit structured next_step JSON the AI can mechanically chain. The older curl-based pattern lives as the 'legacy / custom topics' fallback at the bottom. |
| version | 2.0.0 |
Inbox-Listener Wiring
Empirica's loop-cron skill wires periodic background work into the
registry. This skill does the same for event-driven background work:
held HTTP stream (ntfy), Monitor wake on event, listener stays armed.
Canonical flow (Phase 1 of prop_oxrhoehv4 shipped 2026-05-21):
three single-purpose verbs (on/arm/off) that emit structured
next_step JSON. The AI chains 3 mechanical tool calls per direction;
the CLI handles defaults, persistent-service detection, and state-file
management.
When to Use
- User asks to "arm the listener" or "subscribe to ntfy"
- A system-reminder from
listener-install-pickup lands in your conversation
- SessionStart hook (
session-monitor-arm.py) emits Monitor-arming
instructions in additionalContext (it now delegates to
empirica listener on, so the output is consistent with what
this skill teaches)
If you're scheduling periodic work (cron-mode loop), use /loop-cron
instead. This skill is for "wake when something arrives."
Canonical 3-step arm flow
Three tool calls. CLI handles the rest.
1. empirica listener on --output json
Auto-resolves: --ai-id from .empirica/project.yaml (or pass explicitly),
--name defaults to <ai_id>-inbox, topic defaults to canonical
ntfy:orchestration-events?tags=<ai_id>.
Two possible response shapes:
(a) Persistent OS service already running — short-circuit:
{
"ok": true,
"status": "persistent_service_active",
"next_step": null,
"message": "No in-session Monitor needed — wake events arrive via the system service."
}
You're done. The systemd-user / launchd service already holds the
ntfy stream; wake events arrive through the normal channel.
(b) No persistent service — arm in-session:
{
"ok": true,
"ai_id": "<id>",
"name": "<id>-inbox",
"status": "awaiting_arm",
"next_step": {
"tool": "Monitor",
"args": {
"description": "Cortex orchestration push listener for <id>",
"command": "empirica loop listen --instance <id>",
"persistent": true,
"timeout_ms": 3600000
},
"after_arm": "empirica listener arm <monitor_task_id> --name <id>-inbox"
}
}
2. Arm Monitor with the emitted args
result = Monitor(
description="Cortex orchestration push listener for <id>",
command="empirica loop listen --instance <id>",
persistent=True,
timeout_ms=3600000,
)
3. empirica listener arm <monitor_task_id>
Replaces the monitor_task_id: null placeholder in
listener_active_<instance>_<name>.json with the real id. Now off
knows what to TaskStop later.
Canonical 3-step off flow
1. empirica listener off --output json
Reads the state file, emits:
{
"ok": true,
"monitor_task_id": "tk_xxx",
"next_step": {
"tool": "TaskStop",
"args": {"task_id": "tk_xxx"},
"after_stop": "empirica listener unregister <name>"
}
}
2. TaskStop(task_id)
Disarms the Monitor.
3. empirica listener unregister <name>
Clears the registry entry + sidecar state files. Listener is fully off.
Reaction protocol — what happens when events arrive
Once armed (or with persistent service running), each ECO-decided
proposal event arrives as one <task-notification> JSON line into
your chat. The reaction protocol is owned by /cortex-mailbox-poll
(load that skill before your first transaction if you're a session
that receives mesh events — session-monitor-arm.py reminds you in
its additionalContext).
Architecture (concise)
external publisher (Cortex)
│ HTTP POST with X-Tags including the target ai_id
▼
ntfy server (orchestration-events topic)
│ held HTTP stream
▼
┌──────────────────────────────────────────────────┐
│ EITHER: │
│ (a) persistent OS service holds the stream │
│ (empirica-listener-<ai_id>.service / │
│ com.empirica.listener.<ai_id>.plist) │
│ OR: │
│ (b) in-session `empirica loop listen` subprocess│
│ held via Monitor with persistent: true │
└──────────────────────────────────────────────────┘
│ emits one JSON line per ECO-decided proposal event
▼
<task-notification> wakes the running Claude
│
▼
/cortex-mailbox-poll reaction protocol acts on the event
Idle cost: zero. The held stream consumes no Claude tokens until a
real event arrives.
Wake latency: sub-second from publish to wake handler (network
round-trip + Monitor poll cycle).
SessionStart hook integration
The plugin's session-monitor-arm.py hook delegates to
empirica listener on --output json and renders the JSON response as
markdown in additionalContext. So:
- Fresh SessionStart with persistent service running → hook emits the
"persistent service running, no Monitor needed" block. You read it,
nothing to arm.
- Fresh SessionStart without persistent service → hook emits the
Monitor-arming block. You arm the Monitor + run
empirica listener arm <task_id>.
Either way, the source of truth for what to do is
empirica listener on. The hook is just the automation surface.
Legacy: per-listener custom topics (curl-based)
For listeners on non-canonical topics (e.g. a custom ntfy channel
for an external publisher unrelated to the Cortex mesh), the older
curl-based pattern remains supported via the 9 power-user verbs
(register / pause / resume / record-wake / fire / install-request / list / status / unregister). The Cockpit→Claude install-request
flow still queues a pending install via UserPromptSubmit; this skill
historically taught the manual curl + Monitor + record-wake protocol
for that case.
If you receive a system-reminder from listener-install-pickup with
a custom topic, follow the embedded prompt template — it carries the
specifics (NAME, TOPIC, ON_WAKE, INSTANCE) needed to construct the
curl + Monitor pair manually. For the canonical mesh topic, prefer
empirica listener on instead.
State files
| Path | Purpose | Owner |
|---|
~/.empirica/listeners_<instance>.json | Declarative registry (name/topic/description per listener) | ListenerRegistry |
~/.empirica/listener_paused_<instance>_<name> | Pause sidecar (empty file when paused) | set_listener_paused |
~/.empirica/listener_active_<instance>_<name>.json | Runtime metadata (Monitor task id, curl pid, armed_at) | listener on/arm writes; listener off reads |
on writes the active file with a placeholder monitor_task_id: null;
arm <task_id> replaces the placeholder; off reads it to emit the
TaskStop next_step; unregister deletes it.
Related
/cortex-mailbox-poll — the receive-side reaction protocol for
events arriving via the listener.
/cortex-mailbox-send — the send-side primitive for emitting your
own proposals.
empirica/core/loop_scheduler/persistent_listener.py — the
systemd-user / launchd persistent service shipped in 7eac3c838.
empirica/cli/command_handlers/cockpit_commands.py —
handle_listener_on/arm/off_command (lines ~1530-1735), the
canonical CLI handlers this skill teaches.