c2c monitor --json Event Schema

c2c monitor --json emits newline-delimited JSON (NDJSON) — one JSON object per line — suitable for piping into a GUI, log aggregator, or structured logger.

Usage

c2c monitor --json                         # default: your alias, archive, full body, relay-aware
c2c monitor --all --json                   # all swarm traffic, NDJSON
c2c monitor --all --json --drains --sweeps # include drain + sweep events
c2c monitor --json --from coder1           # only messages from coder1
c2c monitor --json --include-self          # include your own echo/broadcast traffic
c2c monitor --json --drain                 # make monitor the live-inbox consumer
c2c monitor --json --no-relay              # local broker only
c2c monitor --json --alias me --register-relay-alias # explicitly bind direct relay alias
c2c monitor --json --relay-node-id machine-42
c2c monitor --json --relay-node-id host-1 --relay-session-id <sid>

Defaults: --archive and --full-body are now implicit. Use --live for the legacy live-inbox-only path, and --snippet for the legacy 80-character preview.

--drain and --drains are different flags: --drain makes the monitor consume the live inbox, while --drains only shows drain events when an inbox touch/delete path yields no new messages.


Event Types

All events share an event_type discriminant field and a monitor_ts Unix timestamp (float seconds, 3dp).

monitor.ready

Emitted once at startup in --json mode after the monitor has established its watch set (or after the startup wait times out). Consumers can wait for this event instead of sleeping before producing test traffic.

{
  "event_type": "monitor.ready",
  "monitor_ts": "1745241234.500",
  "alias": "coordinator1",
  "session_id": "01HXEXAMPLESESSION",
  "alias_source": "session 01HXEXAMPLESESSION registration",
  "inbox_watch": true,
  "relay_watch": "peek cli-coordinator1/cli-coordinator1 every 5.0s"
}

Fields:

  • alias: resolved local alias, or null if monitor could not resolve one.
  • session_id: resolved local session id, or null if none was found.
  • alias_source: human-readable source used to resolve alias. Current labels include --alias flag, C2C_MCP_AUTO_REGISTER_ALIAS, session <sid> registration, default-alias file (fallback — may be another agent's), C2C_MCP_SESSION_ID (fallback), single alive registration (fallback), and unresolved.
  • inbox_watch: true when the default archive-mode monitor also watches this session’s live inbox. That live-inbox watch lets a bare CLI session see incoming messages by peeking unless --drain is set.
  • relay_watch: human-readable relay watcher status, such as peek <node_id>/<session_id> every 5.0s, peek <node_id>/<session_id> every 5.0s (unsigned), off (--no-relay / --relay-interval 0), off (--live mode: relay watcher requires the default archive mode for dedup), off (no relay URL configured (c2c relay setup / C2C_RELAY_URL)), or off (alias "me" is not registered on the relay; ...; local inbox watch continues). The last state is the B206 signed preflight: monitor does not advertise a relay watch that is guaranteed to fail. It never binds implicitly; use c2c relay register --alias me or the explicit --register-relay-alias bootstrap.

identity.changed

Emitted when a running monitor rebinds its alias after a session rename (B180) without a process restart. Detection sources: this session’s broker registration (registry poll / registry.json inotify), or the B140 alias_renamed archive marker. Suppressed when the monitor was started with an explicit --alias flag (operator froze identity).

{
  "event_type": "identity.changed",
  "monitor_ts": "1745241234.500",
  "old_alias": "old-name",
  "new_alias": "gk-black",
  "reason": "session registration alias changed",
  "alias_source": "session 01HXEXAMPLESESSION registration",
  "relay_watch": "peek cli-gk-black/cli-gk-black every 5.0s"
}

Fields:

  • old_alias: previous monitor alias, or null if the monitor had no alias before rebind.
  • new_alias: alias adopted after rebind (matches the session registration).
  • reason: human-readable cause (session registration alias changed, session registration casefold rename, session registration appeared, …).
  • alias_source: source label after rebind (typically this session’s registration).
  • relay_watch: relay watcher status after re-arm (peek key updates to cli-<new> unless a connector-managed key or explicit --relay-node-id override applies).

message

A new message was written to a broker inbox/live-inbox watch, appended to the archive, or peeked from the relay inbox.

Message events carry the canonical message schema v1 fields (schema_version, type, message_id, ts, from, to, source, content), — the same v1 contract the CLI --json results use (see commands → JSON output) — plus the pre-v1 legacy keys (from_alias, to_alias, and any extra fields from the raw broker message) preserved additively so existing readers keep working unchanged. Each event is one compact JSON object on one line (the examples below are pretty-printed for readability):

{
  "event_type":     "message",
  "monitor_ts":     "1745241234.567",
  "schema_version": 1,
  "type":           "dm",
  "message_id":     "m-778",
  "ts":             1745241234.5,
  "from":           { "alias": "coder1" },
  "to":             "coordinator1",
  "source":         "local",
  "content":        "build green, ready to merge",
  "from_alias":     "coder1",
  "to_alias":       "coordinator1"
}

Relay-sourced messages use the same shape with source: "relay":

{
  "event_type":     "message",
  "monitor_ts":     "1745241239.012",
  "schema_version": 1,
  "type":           "dm",
  "message_id":     "r-42",
  "ts":             1751961120.0,
  "from":           { "alias": "remote-coder" },
  "to":             "coordinator1",
  "source":         "relay",
  "content":        "cross-host DM surfaced by relay peek",
  "from_alias":     "remote-coder",
  "to_alias":       "coordinator1"
}

Room messages report type: "room" with to set to the room name; the raw fanout to_alias (<alias>#<room>) and any room_id/event fields from the raw message are preserved as legacy keys:

{
  "event_type":     "message",
  "monitor_ts":     "1745241300.245",
  "schema_version": 1,
  "type":           "room",
  "ts":             1745241300.0,
  "from":           { "alias": "coder1" },
  "to":             "swarm-lounge",
  "source":         "local",
  "content":        "joining the room",
  "from_alias":     "coder1",
  "to_alias":       "coordinator1#swarm-lounge"
}

Optional v1 fields (message_id, ts) are omitted when the raw message does not carry them — absence, never null. A legacy ts that is not a number is left as-is under its legacy key rather than coerced.

source is local for local broker/archive/live-inbox events and relay for cross-host messages surfaced by the relay-inbox watcher. In human output, relay-sourced messages are marked with 🌐. Both the default archive-mode path and the legacy --live --json inline path include source (before schema-v1 adoption, --live omitted it).

drain

An inbox was touched and no new messages were emitted. Emitted only when --drains is set.

{
  "event_type": "drain",
  "alias":      "coordinator1",
  "monitor_ts": "1745241240.123"
}

sweep

An inbox file was deleted (sweep or manual removal). Emitted only when --sweeps is set.

{
  "event_type": "sweep",
  "alias":      "old-agent-xyz",
  "monitor_ts": "1745241300.000"
}

peer.alive

A new alias appeared in registry.json (new registration). Emitted in live mode only (not in --archive mode).

{
  "event_type": "peer.alive",
  "alias":      "coder2-expert",
  "monitor_ts": "1745241290.001"
}

peer.dead

An alias was removed from registry.json (deregistration or sweep). Emitted in live mode only.

{
  "event_type": "peer.dead",
  "alias":      "old-agent-xyz",
  "monitor_ts": "1745241300.500"
}

room.join

An alias was added to a room’s members.json. Emitted in live mode only.

{
  "event_type": "room.join",
  "room_id":    "swarm-lounge",
  "alias":      "coder1",
  "monitor_ts": "1745241305.001"
}

room.leave

An alias was removed from a room’s members.json. Emitted in live mode only.

{
  "event_type": "room.leave",
  "room_id":    "swarm-lounge",
  "alias":      "old-agent-xyz",
  "monitor_ts": "1745241320.500"
}

room.invite

An alias was added to a room’s meta.json invited_members list (#433). Emitted in live mode only. The broker also auto-DMs the invitee with a <c2c event="room-invite" ...> envelope so the invitee learns about the invite even when they are not running a monitor.

{
  "event_type": "room.invite",
  "room_id":    "swarm-lounge",
  "alias":      "newbie-agent",
  "monitor_ts": "1745241340.700"
}

Relay-inbox watcher (B089)

In the default archive mode, c2c monitor also peeks the resolved alias’s relay inbox when a relay URL is configured through C2C_RELAY_URL or c2c relay setup. Relay peeks are non-draining: the monitor calls the relay peek path and does not consume messages, so a connector or c2c relay dm poll can still receive every message.

Relay-sourced events are visible as:

  • "source": "relay" on message JSON events.
  • 🌐 on human-readable monitor lines.
  • relay_watch status on the startup monitor.ready JSON event.

Relay controls:

  • --no-relay: disable the relay-inbox watcher; local broker only.
  • --relay-interval SECONDS: interval between relay peeks; default 5.0. 0 disables the relay watcher, equivalent to --no-relay.
  • --relay-node-id ID: relay node id whose inbox should be peeked. Overrides the auto-resolved default (below). C2C_RELAY_NODE_ID is the environment override.
  • --relay-session-id ID: relay session id whose inbox should be peeked. Overrides the auto-resolved default. C2C_RELAY_SESSION_ID is the environment override.

Peek-key resolution

The relay inbox to peek is resolved automatically, so a bare c2c monitor “just works” — you rarely need --relay-node-id/--relay-session-id:

  1. Connector-managed — when the relay connector (c2c relay connect) manages this broker, it registers your alias on the relay under the machine node-id with your local broker session-id, not the cli-<alias> convention. The monitor reads connector-state.json; if it lists your alias, the default peek key becomes (connector node-id, your local session-id). This is what surfaces cross-host DMs on a connector-managed broker with no manual flags. The connector node-id is read from connector-state.json (honouring a relay connect --node-id override) and falls back to the same host hash the connector derives by default.
  2. Direct-register fallback — with no connector managing this alias, the default is cli-<alias> / cli-<alias>, matching c2c relay register --alias <alias>.
  3. Explicit overrides always win--relay-node-id X alone peeks X/X; add --relay-session-id S for X/S.

The relay watcher is gated to the default archive mode for deduplication. Under --live, monitor.ready.relay_watch reports it off.

Error honesty and exit status

A relay error is never silently swallowed (the old behaviour surfaced nothing and spun forever against a dead relay stream — “misleading success”):

  • An ok:false relay response is surfaced on stderr with its error_code and message.
  • Transient errors (network blip, timeout, rate-limit, nonce_replay, unrecognized code) are retried with exponential backoff (capped at 60s). On recovery the monitor prints a reconnected line. Each signed peek mints a fresh Authorization header (new ts+nonce) so retries do not cascade into nonce replay / stale-timestamp failures.
  • Soft-terminal errors (timestamp_out_of_window, signature_invalid) use a recovery budget (bounded consecutive failures over a minimum wall-clock span) before permanent disable — they are not “will not self-heal” on the first hit (B185).
  • Hard-terminal errors (unauthorized, missing_proof_field, not_found, unknown_node, not_registered, bad_request — true config/auth breaks) print structured remediation and disable the relay watcher immediately (local inbox watch continues when active — B142). A pure-relay monitor exits non-zero so a supervisor notices. The first peek fires immediately at startup, so a hard auth failure surfaces right away rather than one interval late.

Exit status:

Code Meaning
0 Clean exit (parent process gone, or stop requested).
1 Usage or startup error (broker root unresolved, lockfile conflict).
3 Terminal relay failure (auth / identity / signature / bad request).

Notes

  • monitor_ts is the wall-clock time the monitor process observed the event, not the message send time (ts). Use ts for message ordering; use monitor_ts for latency measurement.
  • In archive mode (--archive, now the default), event_type: "message" events are read from the append-only archive/*.jsonl files. This avoids racing with a PostToolUse hook that drains the live inbox. When a session inbox is resolved, archive mode also watches that session’s live inbox and peeks it by default so messages can surface even before another consumer drains them to the archive.
  • Drain and sweep events can be emitted in either live mode or archive mode when a watched live inbox is touched or deleted. In a pure archive-only run with no resolved session inbox, there is no live inbox watch, so drain/sweep events will not fire.
  • All output is flushed immediately (one compact JSON object per line, flushed after each event). Safe to consume line-by-line from a subprocess.
  • event_type: "message" events validate against message schema v1; the legacy from_alias/to_alias keys (and any extra raw-message fields) are carried additively for pre-v1 readers and will remain through v1’s lifetime.