research/qevents-payloads.md
qevents on the wire: the payload reference
Read: 2026-11-11, from qcontrol origin/main 9e6cd55a0 (crates/qevents/src, extracted read-only), for c3 Phase 1 (design c138). Line numbers are as of that commit. (inferred) marks the reader's inference; everything else is read from code.
Used by: the synthetic stream scripts (crates/fixtures/src/streams.rs) and Guide page 3 (the data modelling).
Anything marked (inferred) is my reading, not stated in code or doc comments.
Notation
| Mark | Meaning |
|---|---|
f: T | required, always serialized |
f?: T | Option + skip_serializing_if = is_none: key omitted when absent |
f: T | null | Option without skip: key always present, value may be null |
f?: [T] | Vec + skip_serializing_if = Vec::is_empty: omitted when empty |
f?: {..} | serde_json::Map + skip_serializing_if = Map::is_empty: omitted when empty |
any | arbitrary JSON (serde_json::Value or RawJson) |
object | JsonObject = serde_json::Map<String, Value> (agent.rs:16) |
Integers: u64/i64/u32/u16/u8/i32/usize are JSON numbers; f64 is a JSON number.
Decoding: serde derive treats a missing Option field as None, so T | null fields also decode when the
key is missing.
1. Envelope
1.1 Shape (public.rs)
// EventRecord (public.rs:31), #[serde(untagged)]: Entity(EntityRecord) | Host(HostRecord)
// EntityRecord (public.rs:40): one OS process lifetime
{
"timestamp": "2026-06-02T12:34:57.123456789Z", // Timestamp, required
"severity": "info", // always serialized; #[serde(default = "Severity::default_value")] -> "info" when missing on decode
"entity_id": "pid:4321:start:1750000000", // String, required
"type": "agent.tool_call", // from #[serde(flatten)] event: EntityEvent
"payload": { ... } // "
}
// HostRecord (public.rs:54): host inventory, NOT scoped to one process
{ "timestamp": "...", "severity": "info", "type": "installation.discovered", "payload": { ... } }
serde attributes that shape the wire:
| Where | Attribute | Effect |
|---|---|---|
EventRecord public.rs:30 | untagged | no wrapper key; decode tries Entity first, so a record without entity_id falls to Host |
EntityRecord.event public.rs:48, HostRecord.event public.rs:60 | flatten | the family's type and payload keys sit at the top level beside timestamp |
EntityRecord.severity, HostRecord.severity public.rs:44,58 | default = "Severity::default_value" | decode only: missing becomes info. Always serialized |
EntityEvent public.rs:66 | untagged, order: Process, Mcp, Io, Netstat, Llm, Agent, Diagnostic, Plugin | Plugin is last "so first-party families win untagged deserialization" (public.rs:77) |
HostEvent public.rs:94 | untagged: Installation, Process | |
every family enum (ProcessEvent public.rs:84, InstallationEvent :102, ProcessInventoryEvent :118, McpEvent mcp.rs:142, IoEvent io.rs:60, NetstatEvent netstat.rs:119, LlmEvent llm.rs:23, AgentEvent agent.rs:27, RunDiagnosticEvent run_diagnostic.rs:22) | tag = "type", content = "payload" + rename = "<family>.<kind>" per variant | adjacently tagged: {"type": "...", "payload": {...}} |
CustomPluginEvent plugin.rs:21 | a struct, not an enum: event_type renamed to type, custom deserialize_with | type must equal "plugin.event" on decode |
Key order as written by serde_json::to_string: timestamp, severity, entity_id, type, payload (struct field
order, flatten emitted in place) (inferred from the field order; JSON consumers should not depend on it).
1.2 timestamp (timestamp.rs:16)
Timestamp(String), #[serde(transparent)]: a bare JSON string. Built only via From<DateTime<Utc>>
(timestamp.rs:25-28): to_rfc3339_opts(SecondsFormat::Nanos, true), so producer output always has
9 fractional digits and a Z suffix, e.g. 2026-06-02T12:34:56.123456789Z. Decode does not
validate (any string is accepted; tests feed 2026-06-02T12:34:57Z). JSON schema: {"type":"string","format":"date-time"}.
The same type is used for process.*.payload.started_at and the run context's started_at.
1.3 entity_id (entity.rs:11)
pub fn process_entity_id(pid: u32, start_unix_secs: u64) -> String { format!("pid:{pid}:start:{start_unix_secs}") }
// "pid:4321:start:1750000000"
- pid + OS-reported process start time in whole Unix seconds, so PID reuse does not merge lifetimes (entity.rs:1-5).
- Monitor
process.started/process.stopped: built inentity_id_from_started_at(public.rs:344-348), which parsespayload.started_atas RFC3339 and uses.timestamp().max(0)(fraction truncated, negative clamped to 0). A badstarted_atgivesErr("invalid process started_at: ...")fromTryFrom<ScanEvent>. - Captured events (
EventRecord::entity, public.rs:125) takeentity_idas the caller passes it. The doc requires the "same pinned start-time read used by the process monitor" (entity.rs:9-10). (inferred) Producers callprocess_entity_id. process.snapshotis a host record: each entry carries its ownentity_idinsidepayload.processes[](scan.rs:81).- README calls it opaque. Treat it as an opaque join key, even though the format is fixed today.
1.4 severity (severity.rs:22)
enum Severity { Debug, Info, Warn, Error }, rename_all = "lowercase": wire values "debug" | "info" | "warn" | "error".
Ordered debug < info < warn < error (derives Ord). Serde decode accepts only those four strings. The "warning" alias
exists only in FromStr (severity.rs:62), for CLI parsing. Severity::default_value() = Info (severity.rs:32).
Gating happens at the emit site (severity.rs:6-9, 105-190): SeverityFilter{min} defaults to info, so debug events are not
constructed or emitted unless --log-level/QCONTROL_LOG is debug or trace (trace folds into debug). Downstream consumers do not filter.
Where defaults come from:
EventRecord::entity(ts, id, ev)(public.rs:125) setsseverity = ev.default_severity().EntityEvent::default_severity()(public.rs:224):Infofor Process, Mcp, Netstat, Agent, Plugin. Delegates toIoEvent::default_severity(io.rs:152),LlmEvent::default_severity(llm.rs:55),RunDiagnosticEvent::default_severity(run_diagnostic.rs:46).EventRecord::host(ts, ev)(public.rs:138) hard-codesInfo.TryFrom<ScanEvent> for EventRecord(public.rs:291) keeps the ScanEvent's severity.ScanEvent::new(scan.rs:393) usesScanEventKind::default_severity()(scan.rs:464):installation.tap_error=Error, everything elseInfo.ScanEventdoc (scan.rs:20-21) says "tap skips arewarn", but no variant defaults to warn. (inferred) A producer sets it viaScanEvent::new_with_severity(scan.rs:398), presumably oninstallation.tap_result.RunRecord::event(run.rs:140) usesRunEvent::default_severity()(run.rs:230). Same values, plusrun.started=Info(run.rs:250).
1.5 Type index: record kind and default severity
type | record | default severity | payload struct |
|---|---|---|---|
process.started | entity | info | ProcessStarted scan.rs:94 |
process.stopped | entity | info | ProcessStopped scan.rs:134 |
process.snapshot | host | info | ProcessSnapshot scan.rs:72 |
installation.discovered | host | info | InstallationRecord scan.rs:157 |
installation.details | host | info | InstallationDetailsRecord scan.rs:205 |
installation.snapshot | host | info | InstallationSnapshot scan.rs:192 |
installation.tap_result | host | info (warn for skips, see 1.4) | InstallationTapResult scan.rs:325 |
installation.tap_error | host | error | InstallationTapError scan.rs:371 |
mcp.request / .response / .error / .notification / .session_close / .diagnostic / .oauth | entity | info (all) | mcp.rs |
file.open / .read / .write / .close | entity | info | io.rs |
exec.spawn / exec.exit | entity | info | io.rs |
connection.open / .update / .close | entity | info | io.rs |
connection.mitm_success | entity | debug | io.rs:468 |
connection.mitm_failure | entity | error | io.rs:487 |
connection.proxy_error | entity | error | io.rs:512 |
connection.intake_fallback | entity | warn | io.rs:532 |
http.request / .response / .exchange_close | entity | info | io.rs |
sse.open / .event / .close | entity | info | io.rs |
websocket.open / .send / .recv / .close | entity | info | io.rs |
websocket.trace | entity | debug | io.rs:948 |
netstat.snapshot | entity | info | netstat.rs:91 |
llm.request / .response / .usage / .rate_limit | entity | info | llm.rs |
llm.provider_matched / .provider_unmatched | entity | debug | llm.rs:83,103 |
agent.message / .tool_call / .tool_decision / .tool_result | entity | info | agent.rs |
run.adapter_error | entity | warn | run_diagnostic.rs:68 |
run.plugin_load_success | entity | debug | run_diagnostic.rs:96 |
run.plugin_load_failure | entity | error | run_diagnostic.rs:110 |
plugin.event | entity | info | plugin.rs:21 |
run.started | not on the monitor stream | info | run.rs:98 (TryFrom<RunEvent> rejects it, public.rs:286) |
run.* diagnostics are "provisional; consumers are instructed not to build integrations around this family yet" (public.rs:74-75).
1.6 Internal run record (not the monitor stream)
qcontrol run NDJSON (RunRecord, run.rs:24) is the internal shape. It has no entity_id and carries a nested run object.
CHANGELOG ("unified monitor consumer stream") says the public stream replaced it with entity_id.
{ "timestamp": "...", "severity": "info",
"run": { "id": "<opaque>", "run_pid": 7, "agent_pid"?: 42, "started_at": "<Timestamp>", "version": "0.1.0",
"plugins": ["mcp","llm-events"] /* always serialized; default [] on decode */, "agent_id"?: "claude-cli", "installation_id"?: "..." },
"type": "...", "payload": {...} } // RunContext run.rs:38; RunEvent untagged run.rs:71 (Started first, Plugin last)
2. Shared sub-objects
// ScanAgent (scan.rs:56)
{ "id": string, "name": string, "vendor": string, "kind": AiSystemKind }
// AiSystemKind (agent_identity.rs:32), snake_case: "cli" | "ide" | "desktop_app" | "vscode_extension" | "other"
// AiMatch (agent_identity.rs:41)
{ "strategy": "executable_name" | "executable_path" | "bundle_path" | "arg_pattern" | "explicit_agent",
"value": string, "confidence": "low" | "medium" | "high" }
// AgentEventContext (agent.rs:68) and LlmEventContext (llm.rs:126): identical on the wire, separate types
{ "session_id": string, "name": string /* agent family e.g. "claude", "codex" */, "raw"?: object }
// mcp SessionContext (mcp.rs:107): always under payload.session
{ "session_id"?: string /* MCP-Session-Id header; absent while pending, e.g. during initialize */,
"host"?: string, "transport": "http" | "stdio",
"protocol_version"?: string, "advertised_server_name"?: string /* serverInfo.name from initialize */ }
// RawJson (mcp.rs:30): serializes as the embedded JSON value (object/array/...), NOT a quoted string
// io Connection (io.rs:317)
{ "id": u64 /* plugin-assigned at open, stable */, "fd": i64 /* in-process proxy fd, not the program's */,
"transport": "tcp" | "unknown", "direction": "outbound" | "inbound",
"src": Endpoint, "dst": Endpoint,
"domain": string | null, "protocol": "http/1" | "http/2" | "unknown" | null,
"tls": { "enabled": bool, "version": string | null, "cipher": string | null } } // ConnectionTls io.rs:595
// Endpoint (io.rs:547)
{ "host": string | null, "port": u16 | null }
// Host (io.rs:558) untagged Ip(IpAddr) | Name(String). Custom Serialize (io.rs:569) writes the text either way.
// Host::parse (io.rs:581) strips one pair of [] for IPv6 literals; non-IP text is kept verbatim.
// HttpExchange (io.rs:613)
{ "id": u64 /* exchange id, same across request/response/close */, "connection_id": u64 | null /* -> Connection.id */,
"fd": i64, "stream_id": u32 | null /* HTTP/2 only */, "version": "http/1.0" | "http/1.1" | "http/2" | "unknown" }
// SseStream (io.rs:806) and WebSocketStream (io.rs:863): same shape
{ "http_exchange_id": u64 /* -> http.*.exchange.id */, "connection_id": u64 | null, "fd": i64, "version": HttpVersion }
// HeaderField (io.rs:788) { "name": string, "value": string }
// HttpRequestRef (io.rs:704) { "method": string, "path": string | null, "raw_target": string | null }
Lenient string enums (io.rs:18-47, custom Deserialize): any unrecognised value decodes to Unknown (serializes as "unknown").
They are io Transport, Protocol, ConnectionUpdateReason, HttpVersion, HttpCloseReason, WebSocketMessageKind and
WebSocketCloseInitiator. WebSocketCloseInitiator also accepts client_to_server/server_to_client. Their schema is
type: string with examples, not enum. Every other enum is strict serde, including Direction, Tool.kind, and all mcp/llm/agent/scan enums.
3. Payloads by family
3.1 process.* (scan.rs; envelope enums public.rs:84,118)
process.started: ProcessStarted scan.rs:94. Entity record.
{ "pid": u32, "installation_id"?: string, "ppid"?: u32, "exe": string,
"argv"?: [string], "cwd"?: string, "agent"?: ScanAgent, "matches"?: [AiMatch],
"started_at": Timestamp /* OS start time */, "rule"?: string /* first matching config rule id */ }
agent is absent for explicitly instrumented generic processes (scan.rs:113-115).
process.stopped: ProcessStopped scan.rs:134. Entity record. Polling sees only that the process is gone, so there is
no exit status (scan.rs:131-132). Join adapter and installation context through entity_id.
{ "pid": u32, "exe": string, "started_at": Timestamp, "duration_ms": u64, "rule"?: string }
process.snapshot: ProcessSnapshot scan.rs:72. Host record. Authoritative running set, emitted at startup and on
explicit refresh (scan.rs:67-70). Consumers replace their state, then apply deltas.
{ "processes": [ { "entity_id": string, /* + every ProcessStarted field, flattened (scan.rs:83) */ } ] } // required; [] valid
3.2 installation.* (scan.rs; all host records; envelope public.rs:102)
installation.discovered: InstallationRecord scan.rs:157. An upsert / current-state record.
{ "id"?: string, // lower-case hex SHA-256 of logical exe path (canonical), filename, ctime, mtime, size (scan.rs:158-170);
// absent only when exe metadata unreadable. == process.started.installation_id == run.installation_id
"agent": ScanAgent, "executable_path": string, "app_path"?: string, "version"?: string, "config_path"?: string,
"tap": { "status": "tapped" | "not_tapped" | "requires_attention", // InstallationTapState scan.rs:298, status scan.rs:314
"rule"?: string, "policies"?: [string] /* Option<Vec>: [] = discovery only; omitted when no rule */ } }
installation.snapshot: InstallationSnapshot scan.rs:192. { "installations": [InstallationRecord] } (required).
installation.details: InstallationDetailsRecord scan.rs:205, with InstallationDetails scan.rs:228 flattened into
the payload (no details key). Emitted after .discovered, only for adapters that support inspection. Join on id. Read from
config files on disk, never by executing the binary. SECRETS-BEARING (scan.rs:236-238).
{ "id"?: string, "agent": ScanAgent,
"version"?: string, "default_model"?: string,
"mcp_servers"?: [ { "name": string, "transport"?: string /* e.g. stdio|http|sse, verbatim */, "command"?: string,
"args"?: [string], "url"?: string,
"env"?: { ... } /* verbatim INCLUDING VALUES */, "headers"?: { ... } /* verbatim INCLUDING VALUES */ } ], // McpServerDetail scan.rs:259
"skills"?: [ { "name": string, "description"?: string } ], // SkillDetail scan.rs:285
"plugins"?: [string], "settings"?: { ... } /* agent-specific config keys as found */, "warnings"?: [string] }
Every field empty and no warnings means "installed but unconfigured". Any warnings entry means the sibling fields are partial (scan.rs:222-226).
installation.tap_result: InstallationTapResult scan.rs:325.
{ "id"?: string, "agent": ScanAgent, "target": string /* tap slot: canonical install path */,
"outcome": "fresh" | "no_op" | "refresh_shim" | "adopt_replaced" | "pending", // TapOutcome scan.rs:351
"tap_type"?: string, "policies"?: [string] }
installation.tap_error: InstallationTapError scan.rs:371. No stable error-kind taxonomy (scan.rs:364-369).
{ "id"?: string, "agent": ScanAgent, "target": string, "tap_type"?: string, "policies"?: [string], "error": string }
3.3 mcp.* (mcp.rs; McpEvent mcp.rs:143, #[non_exhaustive])
Every payload carries session: SessionContext (section 2). request_id is the JSON-RPC id, always stringified.
exchange_id is the observer's HTTP exchange id, pairing requests and replies independently of MCP sessions.
mcp.request: Request mcp.rs:201
{ "exchange_id"?: u64, "session": {...}, "method": string, "request_id": string,
"tool_name"?: string /* tools/call */, "prompt_name"?: string /* prompts/get */,
"resource_uri"?: string /* resources/read|subscribe|unsubscribe */, "params"?: any /* raw JSON-RPC params */ }
mcp.response: Response mcp.rs:234
{ "exchange_id"?: u64, "session": {...}, "method": string | null /* null when uncorrelated */, "request_id": string,
"tool_name"?: string, "prompt_name"?: string, "resource_uri"?: string,
"is_error"?: bool /* tools/call result.isError: tool failed, RPC succeeded */, "result"?: any }
mcp.error: McpError mcp.rs:274
{ "exchange_id"?: u64, "session": {...}, "method": string | null, "request_id": string | null /* null when error has no id */,
"tool_name"?: string, "prompt_name"?: string, "resource_uri"?: string,
"error": { "code": i64, "message": string, "data"?: any } } // JsonRpcError mcp.rs:299
mcp.notification: Notification mcp.rs:316. No request_id.
{ "session": {...}, "method": string, "source": "client" | "server", "resource_uri"?: string, "params"?: any }
mcp.session_close: SessionClose mcp.rs:336
{ "session": {...}, "reason": "transport_close" | "delete" | "timeout" }
mcp.diagnostic: Diagnostic mcp.rs:360. category is an internally tagged enum (tag = "category", snake_case,
mcp.rs:379) flattened into the payload.
{ "session": {...}, "category": <see below>, /* + category fields */ "phase"?: "request" | "response",
"request_id"?: string, "body"?: string /* best-effort UTF-8 */ }
// category variants (mcp.rs:380):
// "transport_failure" + http_status: u16 | "parse_failure" + detail: string | "buffer_limit_exceeded" + bytes: usize
// "malformed_sse_line" | "duplicate_sse_field" + field: string | "invalid_sse_utf8" | "invalid_sse_retry"
// "truncated_sse_data" + bytes: usize | "sse_control_event" + event_type: string
mcp.oauth: OAuth mcp.rs:407
{ "session": {...},
"step": "challenge" | "resource_discovery" | "server_discovery" | "registration" | "token_exchange" | "token_refresh",
"status": u16, "error"?: string, "error_description"?: string, "resource_metadata_url"?: string, "scope"?: string,
"authorization_servers"?: [string], "issuer"?: string, "client_id"?: string }
3.4 file.* / exec.* (io.rs; IoEvent io.rs:61, #[non_exhaustive])
FileInfo (io.rs:219) = { "id": u64 | null /* set after a successful tracked open */, "fd": i64, "path": string /* at open time */ }.
Metadata only, no file bytes (io.rs:217).
| type | struct | payload |
|---|---|---|
file.open | FileOpen io.rs:230 | { file, flags: i64, mode: u64, result: i64 /* fd or negative */ } |
file.read | FileRead io.rs:243 | { file, count: u64 /* requested */, result: i64 } |
file.write | FileWrite io.rs:253 | { file, count: u64, result: i64 } |
file.close | FileClose io.rs:263 | { file, result: i64 } |
exec.spawn | ExecSpawn io.rs:275 | { id: u64, path: string, argv: [string] /* incl argv[0], always present */, cwd: string | null } |
exec.exit | ExecExit io.rs:288 | { id: u64 /* = exec.spawn.id */, path: string, pid: i64, status: ExitStatus } |
ExitStatus (io.rs:301) is internally tagged (tag = "kind", snake_case): { "kind": "exited", "code": i32 } | { "kind": "signaled", "signal": i32 }.
3.5 connection.* (io.rs)
| type | struct | payload |
|---|---|---|
connection.open | ConnectionOpen io.rs:435 | { connection: Connection } |
connection.update | ConnectionUpdate io.rs:445 | { reason: "tls" | "domain" | "protocol" | "unknown", connection } (full snapshot) |
connection.close | ConnectionClose io.rs:454 | { connection, result: i64 /* close() return */ } |
connection.mitm_success | io.rs:468 | { connection, sni?: string, upstream?: string /* host:port */, alpn?: string } |
connection.mitm_failure | io.rs:487 | { connection, sni?: string, upstream?: string, stage: string /* upstream_tls, downstream_tls, alpn_mismatch, ... */, reason: string /* unstable text */ } |
connection.proxy_error | io.rs:512 | { connection, upstream?: string, stage: string /* upstream_connect, runtime_session_open, listener_accept, ... */, reason: string } |
connection.intake_fallback | io.rs:532 | { connection, upstream?: string, from: string /* e.g. tls_mitm */, to: string /* e.g. raw_connect */, reason: string } |
3.6 http.* (io.rs)
http.request: HttpRequest io.rs:683
{ "exchange": HttpExchange,
"connection": { "domain": string | null, "protocol": "http/1" | "http/2" | "unknown" | null,
"tls": { "enabled": bool, "version": string | null } }, // HttpConnection io.rs:663, HttpTls io.rs:674 (no cipher)
"method": string, "scheme": string | null, "authority": string | null /* host[:port] */,
"path": string | null /* decoded */, "raw_target": string | null, "headers": [HeaderField] }
http.response: HttpResponse io.rs:715
{ "exchange": HttpExchange, "request_ref": HttpRequestRef, "status_code": u16, "headers": [HeaderField] }
http.exchange_close: HttpExchangeClose io.rs:772. One per exchange.
{ "exchange": HttpExchange, "request_ref": HttpRequestRef,
"reason": "complete" | "aborted" | "parse_error" | "connection_closed" | "upgraded" | "unknown", // HttpCloseReason io.rs:728
"request_done": bool, "response_done": bool, "status_code": u16 | null }
3.7 sse.* (io.rs). No event data bytes (io.rs:836-838)
| type | struct | payload |
|---|---|---|
sse.open | SseOpen io.rs:820 | { stream: SseStream, content_type: string } |
sse.event | SseEvent io.rs:828 | { stream, event_name: string | null, id: string | null, retry_ms: u64 | null, data_byte_len: u64 } |
sse.close | SseClose io.rs:843 | { stream, reason: HttpCloseReason } |
3.8 websocket.* (io.rs). No message bytes
| type | struct | payload |
|---|---|---|
websocket.open | WebSocketOpen io.rs:877 | { stream: WebSocketStream, subprotocol: string | null, extensions: string | null } |
websocket.send | WebSocketMessage io.rs:890 | { stream, kind: "text" | "binary" | "unknown", byte_len: u64 } (client to server) |
websocket.recv | WebSocketMessage io.rs:890 | same shape (server to client) |
websocket.close | WebSocketClose io.rs:930 | { stream, initiator: "client" | "server" | "unknown", code: u16 | null, reason: string | null, clean: bool } |
websocket.trace: WebSocketTrace io.rs:948. Debug only, metadata only, no headers or bytes (io.rs:942-946). Every optional field is omitted, not null.
{ "stage": string /* observer_attached, frame_parse_failed, ... */, "connection"?: Connection, "stream"?: WebSocketStream,
"direction"?: string /* client_to_server|server_to_client */, "path"?: string, "status_code"?: u16, "byte_len"?: u64,
"buffered_len"?: u64, "opcode"?: u8, "compressed"?: bool, "extensions"?: string,
"outcome"?: string /* attached|opened|skipped|failed|finished */, "reason"?: string }
3.9 netstat.snapshot (netstat.rs; NetstatEvent netstat.rs:120)
NetstatSnapshot netstat.rs:91. One process-wide TCP/UDP sample from inside the wrapped process. This is not a connection.* lifecycle event.
{ "sequence": u64 /* from 1; a gap = a dropped snapshot */, "pid": u32 /* publisher pid */, "duration_ns": u64,
"window": Traffic /* since previous snapshot */, "total": Traffic /* since publisher start */,
"send_bps": u64, "recv_bps": u64,
"connections": [ { // NetstatConnection netstat.rs:61; REQUIRED, [] valid; null or missing fails decode
"protocol": "tcp" | "udp", "local_addr": string | null, "local_port": u16 /* 0 = unknown */,
"remote_addr": string | null, "remote_port": u16, "window": Traffic, "total": Traffic,
"send_bps": u64, "recv_bps": u64 } ] } // rows ordered by window bytes, largest first (netstat.rs:53)
// Traffic = NetstatTraffic netstat.rs:37: { "bytes_sent": u64, "bytes_recv": u64, "send_fail": u64, "recv_fail": u64 }
3.10 llm.* (llm.rs; LlmEvent llm.rs:24)
Every payload except the provider diagnostics has context: LlmEventContext (section 2).
llm.request: Request llm.rs:159
{ "context": {...}, "model": string, "request_id"?: string, "system_instructions"?: string }
llm.response: Response llm.rs:175
{ "context": {...}, "model"?: string, "request_id"?: string,
"phase": "completed" | "error" | "transport_error", // ResponsePhase llm.rs:301
"duration_ms"?: u64, "input_tokens"?: u64, "output_tokens"?: u64 /* excl. reasoning */, "reasoning_tokens"?: u64,
"cache_read_tokens"?: u64, "cache_write_tokens"?: u64, "cost_usd"?: f64, "error"?: string }
llm.usage: Usage llm.rs:214
{ "context": {...}, "model"?: string, "request_id"?: string, "input_tokens"?: u64, "output_tokens"?: u64,
"reasoning_tokens"?: u64, "cache_read_tokens"?: u64, "cache_write_tokens"?: u64, "total_tokens"?: u64,
"model_context_window"?: u64, "raw"?: object /* "safe to surface" */ }
llm.rate_limit: RateLimit llm.rs:251
{ "context": {...}, "request_id"?: string, "scheme": string /* e.g. openai_http, codex_app_server */,
"buckets": { "<bucket name>": { // BTreeMap: required object, keys sorted
"limit"?: f64, "remaining"?: f64, "reset_unix"?: u64, "status"?: string, "utilization"?: f64 /* 0.0..=1.0 */,
"used_percent"?: f64, "window_duration_mins"?: u64 } }, // RateLimitBucket llm.rs:274
"plan_type"?: string, "rate_limit_reached_type"?: string, "raw"?: object }
llm.provider_matched: LlmProviderMatched llm.rs:83. Debug. No context, no session.
{ "provider": string /* anthropic, openai */, "request_kind": string /* = qllm::RequestKind::label, e.g. anthropic_messages */,
"method"?: string, "path"?: string }
llm.provider_unmatched: LlmProviderUnmatched llm.rs:103. Debug. { "method"?: string, "path"?: string, "host"?: string }
3.11 agent.* (agent.rs; AgentEvent agent.rs:28)
Every payload has context: AgentEventContext (section 2). No agent.session_start yet (TODO agent.rs:29).
Tool (agent.rs:109) is internally tagged (tag = "kind", rename_all = "snake_case"), with a strict decode (an unknown kind fails):
{ "kind": "builtin", "name": string } // Bash, exec_command, ...
{ "kind": "mcp", "server": string, "name": string } // server = user's config alias from mcp__<alias>__<tool>, NOT mcp session.advertised_server_name (mcp.rs:124-127)
{ "kind": "skill", "name": string, "path"?: string } // defined but not emitted yet (TODO agent.rs:122)
{ "kind": "unknown" } // tool could not be classified
| type | struct | payload |
|---|---|---|
agent.message | Message agent.rs:80 | { context, role: "user" | "assistant" | "system" | "tool", prompt?: string /* after adapter redaction */, prompt_char_length?: u64 } |
agent.tool_call | ToolCall agent.rs:192 | { context, tool: Tool, call_id?: string, arguments?: object } |
agent.tool_decision | ToolDecision agent.rs:207 | { context, tool, call_id?: string, decision: "approved" | "denied", source: string /* config, user, policy */ } |
agent.tool_result | ToolResult agent.rs:239 | { context, tool, call_id?: string, success: bool, duration_ms?: u64, output?: [ { "type": "text", "text": string } ], structured_output?: any } |
output blocks: ToolResultOutputBlock agent.rs:232, internally tagged tag = "type". Only text exists.
3.12 run.* diagnostics (run_diagnostic.rs; RunDiagnosticEvent run_diagnostic.rs:23). Provisional, entity records
| type | struct | payload |
|---|---|---|
run.adapter_error | AdapterError run_diagnostic.rs:68 | { source: string /* plugin_socket_decode, plugin_socket_read, mitm_revert, ... */, reason: string /* bounded: json_decode, socket_read, socket_accept, ... */, byte_len?: u32, sample_hash?: string /* short non-crypto hex */ } |
run.plugin_load_success | PluginLoadSuccess run_diagnostic.rs:96 | { name: string /* as in QCONTROL_PLUGINS */, path: string } |
run.plugin_load_failure | PluginLoadFailure run_diagnostic.rs:110 | { name, path, reason: string } |
run.started (run stream only, RunStarted run.rs:98):
{ exe, cmd /* argv0 */, args: [string] /* always serialized */, cwd, agent?: { id, name, vendor, kind: AiSystemKind, matches: [ {strategy, value, confidence} ] } }.
RunAgentAttribution is at run.rs:115 and RunAgentMatch at run.rs:130. No pid; the pid is in run.agent_pid.
3.13 plugin.event (plugin.rs)
CustomPluginEvent plugin.rs:21. The fallback for any plugin-socket type outside the reserved prefixes.
{ "type": "plugin.event", // const PLUGIN_EVENT_TYPE plugin.rs:13; decode rejects any other value
"payload": { "plugin_name": string, "event": string /* plugin-defined type, e.g. custom.foo */,
"data": any /* plugin's payload, unmodified; decode also accepts legacy key "payload" (plugin.rs:76) */ } }
CHANGELOG "Pending - Custom plugin data": the rename payload.payload to payload.data is a breaking output change. Serialization emits only data.
Readers should accept both: use data when the key exists, else payload.
4. Correlation keys: where they sit
| Key | Location | Joins |
|---|---|---|
entity_id | envelope (entity records); process.snapshot.payload.processes[].entity_id | everything from one process lifetime |
installation_id | process.started.payload.installation_id; run.installation_id (run stream) | installation.*.payload.id |
id | installation.{discovered,details,tap_result,tap_error}.payload.id | same SHA-256 install identity |
session_id | mcp.*: payload.session.session_id (optional); agent.* / llm.* (except provider_*): payload.context.session_id (required) | conversation or MCP session |
call_id | agent.tool_call / .tool_decision / .tool_result payload.call_id | tool lifecycle |
request_id | mcp.request/.response (string), mcp.error (string or null), mcp.diagnostic (optional); llm.request/.response/.usage/.rate_limit (optional) | JSON-RPC id / provider request |
exchange_id | mcp.request/.response/.error payload.exchange_id (u64, optional) | observer HTTP exchange (inferred): same id space as http.*.exchange.id, not stated |
exchange.id | http.*.payload.exchange.id | sse.*.stream.http_exchange_id, websocket.*.stream.http_exchange_id |
connection.id | connection.*.payload.connection.id | http.*.exchange.connection_id, sse/websocket.*.stream.connection_id |
fd | connection.fd, exchange.fd, stream.fd | proxy-side handle. IoEvent::connection_fd() io.rs:173 unifies these |
file.id | file.*.payload.file.id | open/read/write/close of one file session |
exec id | exec.spawn.payload.id = exec.exit.payload.id | child spawn/exit; exec.exit.pid is the child pid |
sequence, pid | netstat.snapshot.payload | gaps mean dropped samples |
5. Constructors and helpers
EventRecord::entity(ts, entity_id, EntityEvent)public.rs:125 sets severity to the event's default.EventRecord::host(ts, HostEvent)public.rs:138 sets severity to info.EventRecord::{entity_id() -> Option<&str>, event_type() -> &str, timestamp(), severity()}public.rs:146-174.EventRecord::session_id()public.rs:179:mcp.*givespayload.session.session_id(Option),agent.*givespayload.context.session_id(always Some),llm.*gives the context session (None for provider_matched/unmatched). Everything else gives None.EventRecord::installation_id()public.rs:199: onlyprocess.startedgivespayload.installation_id. Everything else, includinginstallation.*, gives None.EntityEvent::{event_type, default_severity}public.rs:211,224. Per-familyevent_type()on every enum.TryFrom<ScanEvent> for EventRecordpublic.rs:291 computes entity_id fromstarted_at.TryFrom<RunEvent> for EntityEventpublic.rs:274 rejects onlyrun.started.process_entity_id(pid, start_unix_secs)entity.rs:11.ScanEvent::{new, new_with_severity, installation_discovered, installation_details, installation_snapshot, tap_result, tap_error, process_snapshot, process_started, process_stopped}scan.rs:393-443.McpEvent::{session(), session_id()}mcp.rs:175,189.AgentEvent::session_id() -> &stragent.rs:53.LlmEvent::session_id() -> Option<&str>llm.rs:66.IoEvent::connection_fd() -> Option<i64>io.rs:173. It is None for file/exec.websocket.traceuses stream.fd, else connection.fd.Tool::builtin(name),Tool::display_name()(name,server/name,skill:name,<unknown>),Tool::parse_wire_name("mcp__srv__tool")agent.rs:150-179.Host::parse(text)io.rs:581.RawJson(String)mcp.rs:30.CustomPluginEvent::{from_parts, plugin_name, typ, data, payload}plugin.rs:31-55.CustomPluginPayload::newplugin.rs:81.RunRecord::{event, event_with_severity, started, from_line},RunContext::new(..).with_agent_pid/with_plugins/with_agent_id/with_installation_idrun.rs:140-213.- Severity:
filter_from_log_level(Option<&str>),SeverityFilter::{new, enabled, min},install_global_filter,global_filterseverity.rs:105-190. LlmEventContextandAgentEventContextconvert both ways withFrom(llm.rs:136-154).
Plugin-socket ingress (plugin_socket.rs; input side, not the public output):
- Lines are
{plugin_name, type, payload}(PluginEvent:62).PluginSocketRecord::{from_line, from_value, from_wire}:151-234 drop empty names or types and null payloads. - Reserved prefixes (:28) are
mcp. file. exec. connection. http. sse. websocket. netstat. agent. internal:(each must decode as a typed event or be dropped) andllm. run. plugin.(always dropped). Any other type is wrapped asplugin.event. internal:qproxy.{mitm_success,mitm_failure,proxy_error,intake_fallback,websocket_trace}(QproxyDiagnosticEvent:275) is accepted only fromplugin_name == "qproxy"and becomes the publicconnection.*/websocket.trace.internal:llm.*is accepted only from"llm-events"(:240). Those becomeqllm::ProviderEvent/LlmDiagnosticEvent(defined in theqllmcrate, not here), which adapters map tollm.*/agent.*.
6. Secret-bearing and content-bearing fields
Documented in code:
installation.detailsis "a secrets-bearing artifact" (scan.rs:236-238).mcp_servers[].envandmcp_servers[].headersare verbatim, values included (scan.rs:275-280; the test usesGITHUB_TOKEN: "ghp_secret", scan.rs:607,636).settingsis agent config keyed as found, which (inferred) can also hold credentials.run.adapter_errordeliberately never echoes failed socket lines. It carriesbyte_len+sample_hashinstead (run_diagnostic.rs:62-66).websocket.traceexcludes headers and bytes "that can contain credentials or user content" (io.rs:942-946).- File, SSE and WebSocket events carry lengths, not bytes (io.rs:217, 836-838, 894-895).
llm.usage.rawandllm.rate_limit.raware described as "safe to surface" (llm.rs:244,267).agent.message.promptis text "after the adapter applies the agent's redaction policy" (agent.rs:85). It is still user content.
Not documented as secret, but content-bearing (inferred):
http.request.headers/http.response.headersare name/value pairs with no redaction stated (Authorization, Cookie).mcp.*params/result/error.dataare verbatim RawJson passthrough.mcp.diagnostic.bodyis a raw body excerpt.agent.tool_call.arguments,agent.tool_result.output/structured_output,llm.request.system_instructions, andcontext.raw(agent/llm).exec.spawn.argvandprocess.started.argv(command lines can carry tokens).mcp.oauth.client_idandscope.- Also for JS/TS consumers: u64 ids (
exchange_id,connection.id,exchange.id, netstat counters) can exceed 2^53.