Design Note: Observability
June 16, 2026 · View on GitHub
- Status: Draft
- Tracking issue: to be filed
- Author: @shangdinggu
- Last updated: 2026-05-08
- Tracks roadmap:
0002-daemon-foundation-roadmap.md(Phase 5 — operability) - Builds on: every prior kernel RFC (0003 / 0005 / 0006 / 0007 / 0008 / 0009 / 0010 / 0011)
This RFC closes the kernel's "functional" gap and opens its "operability" one. Through Phase 4 the kernel can do everything an agent OS needs to do. Through Phase 5 it becomes debuggable, measurable, and resilient under failure — the qualifications demanded by the word "production-grade".
Four primitives ship in this RFC:
- Introspection — read-only RPC views that combine state
across all kernel tables:
kernel.observe.proc(pid),kernel.observe.summary(). - Trace — walk the
causation_idchain insideagent_eventsto reconstruct the directed graph of agent activity:kernel.observe.trace(event_id, depth). - Metrics — Prometheus exposition format text that the daemon's
existing
/metricsendpoint surfaces alongside its current payload. No new HTTP endpoint; just additional content. - Chaos primitives — a small
kernel/chaos.pymodule for tests, withChaosMonkey.kill_random_agent,simulate_disk_full, etc. Real chaos suite expansion (network partition, time skew, process tree fault) is a v1.1 deliverable; this RFC ships the plumbing.
This RFC ships purely additive code. No schema bump — every view is computed on demand from existing tables.
1. Goals & non-goals
Goals:
- One agent, full picture.
kernel.observe.proc(pid)returns one JSON dict combining process row + capability + ledger entries- mailbox stats + scheduler queue items + fs object count + recent events. The supervisor or a human operator can debug an agent without joining six tables.
- System rollup.
kernel.observe.summary()returns kernel uptime, schema version, total agents per state, total events, queue depth, mailbox queue total, fs object count. - Causation graph.
kernel.observe.trace(event_id, depth=10)walkscausation_idupstream and returns the chain. Useful for "why did this agent do this?". - Prometheus alongside existing metrics. When the kernel is
active,
/metricsreturns existing daemon metrics + kernel metrics. Format conforms to Prometheus text exposition v0.0.4. - Chaos for tests. A small library with deterministic-seeded helpers for fault injection in tests. No production code path uses it.
Non-goals (v1):
- Tracing across daemons. Single-daemon.
correlation_idis the cross-daemon hook for a future cluster RFC. - Distributed tracing (OTel). Out of scope. Future RFC may add an OpenTelemetry exporter; the chain we expose here is OpenTelemetry- compatible in shape.
- Persistent metric history. Prometheus is the right place to store historical metrics; the kernel only exposes the current snapshot.
- Live event subscription. RFC 0001's
/eventsSSE channel already covers this. RFC 0012 adds read-style observe APIs, not push. - Audit query language.
kernel.events.tailis the read API for events; observe.trace just walks causation. A SQL-ish DSL is overkill.
2. Data model
No new dataclasses; all responses are dicts conforming to:
ProcView (kernel.observe.proc result):
{
"process": AgentProcess.to_dict() | None,
"capability": Capability.to_dict() | None,
"ledger": [LedgerEntry.to_dict()],
"mailbox": {
"exists": bool,
"queue_size": int | null,
"pending": int | null,
"subscriptions": [str],
},
"scheduler": {
"queued": int,
"dispatched": int,
"completed": int,
"expired": int,
"cancelled": int,
},
"fs": { "object_count": int, "total_bytes": int },
"registry": { "names": [str] },
"recent_events": [Event.to_dict()], # last 20 events for this pid
}
SummaryView (kernel.observe.summary result):
{
"kernel_version": str,
"schema_version": int,
"started_at": float, # ObservabilityStore instantiation time
"uptime_s": float,
"agents": {
"READY": int, "RUNNING": int, "WAITING": int,
"SUSPENDED": int, "DEAD": int,
"total": int,
},
"events": { "total": int, "max_event_id": int },
"scheduler": { "queued": int, "dispatched": int, ... },
"mailbox": { "mailboxes": int, "pending_messages": int },
"ledger": { "agents_with_budgets": int, "breached": int },
"fs": { "objects": int, "total_bytes": int },
"registry": { "entries": int },
}
TraceResult (kernel.observe.trace result):
{
"events": [Event.to_dict()], # ordered downstream → upstream
"depth": int, # actual depth walked (≤ requested)
"truncated": bool, # True if depth limit hit
}
3. Operations
proc(pid) -> ProcView
One read across all tables filtered by pid. Cheap; uses indexed queries on each.
summary() -> SummaryView
System rollup. Aggregates COUNT() against each table. The kernel holds no in-memory cached counters; every call re-queries. SQLite WAL keeps reads cheap; if this becomes a bottleneck (unlikely at v1's scale), a future RFC can add cached counters with periodic sync.
trace(event_id, depth=10) -> TraceResult
Starts at event_id, walks causation_id upstream up to depth
hops, returns the chain. Halts on:
- causation_id is NULL (root cause)
- depth limit hit (sets
truncated=True) - referenced event missing (corrupt log; sets
truncated=True)
depth is capped at 100.
prometheus_text() -> str
Returns Prometheus text exposition format string. Includes (at minimum):
# HELP cheetahclaws_kernel_schema_version Kernel schema version
# TYPE cheetahclaws_kernel_schema_version gauge
cheetahclaws_kernel_schema_version 5
# HELP cheetahclaws_kernel_uptime_seconds Seconds since kernel registered
# TYPE cheetahclaws_kernel_uptime_seconds counter
cheetahclaws_kernel_uptime_seconds 12345.6
# HELP cheetahclaws_kernel_agents Number of agents by state
# TYPE cheetahclaws_kernel_agents gauge
cheetahclaws_kernel_agents{state="READY"} 3
cheetahclaws_kernel_agents{state="RUNNING"} 1
... (and DEAD)
# HELP cheetahclaws_kernel_events_total Lifetime event count
# TYPE cheetahclaws_kernel_events_total counter
cheetahclaws_kernel_events_total 9876
# HELP cheetahclaws_kernel_scheduler_queue Items in scheduler queue by state
# TYPE cheetahclaws_kernel_scheduler_queue gauge
cheetahclaws_kernel_scheduler_queue{state="queued"} 5
cheetahclaws_kernel_scheduler_queue{state="dispatched"} 2
# HELP cheetahclaws_kernel_ledger_breached Agents with at least one breached dim
# TYPE cheetahclaws_kernel_ledger_breached gauge
cheetahclaws_kernel_ledger_breached 0
# HELP cheetahclaws_kernel_mailbox_pending Pending messages across all inboxes
# TYPE cheetahclaws_kernel_mailbox_pending gauge
cheetahclaws_kernel_mailbox_pending 12
# HELP cheetahclaws_kernel_fs_objects Total AgentFS objects
# TYPE cheetahclaws_kernel_fs_objects gauge
cheetahclaws_kernel_fs_objects 84
# HELP cheetahclaws_kernel_fs_bytes Total AgentFS bytes stored
# TYPE cheetahclaws_kernel_fs_bytes gauge
cheetahclaws_kernel_fs_bytes 1048576
Format compliance:
# HELPand# TYPElines per metric.- Label values double-quoted, escaped.
- Newline-separated.
- Final newline mandatory.
The daemon's /metrics endpoint integration is described in §5.
4. RPC surface
kernel.observe.proc
params: { pid }
result: ProcView
kernel.observe.summary
params: {}
result: SummaryView
kernel.observe.trace
params: { event_id, depth?=10 }
result: TraceResult
kernel.observe.prometheus
params: {}
result: { text: str } # Prometheus exposition string
No new error codes — observe methods just propagate
InvalidPayload for bad params (already -32602) and
UnknownPid (already -32102) where applicable.
5. Daemon /metrics integration
The daemon's existing /metrics endpoint goes through
health.payload_for(path, config). To preserve byte-for-byte
backwards compatibility, the kernel does NOT modify
/metrics directly. Instead:
kernel.observe.prometheusis the canonical kernel-metrics RPC.- A future, optional patch can wire kernel metrics into the daemon's
/metricspayload by readingdaemon_state.observability_storeif present. This RFC does not commit to that wiring; it ships the RPC and leaves the merge as a follow-up.
This separation means /metrics remains a stable JSON contract for
clients that already scrape it, while Prometheus users opt into
kernel metrics through the RPC channel (or via a future text passthrough).
6. Chaos primitives
A new module kernel/chaos.py, intended for use only by
tests. Production code paths must not import from it.
class ChaosMonkey:
def __init__(self, *, seed: int | None = None): ...
def kill_random_agent(self, kernel_store) -> int | None:
"""Pick a non-DEAD agent at random; transition it to DEAD with
exit_kind='crashed'. Returns the pid killed, or None if no
live agents exist. Deterministic given the same seed."""
def fill_mailbox(self, mailbox_store, pid: int) -> int:
"""Send messages to a pid's mailbox until it's full. Returns
count of successful sends."""
def simulate_disk_full(self) -> contextmanager:
"""Context manager that monkey-patches sqlite3 to raise
sqlite3.OperationalError('disk full') on next write."""
def lose_event(self, kernel_store, event_id: int) -> bool:
"""Manually delete an event row. Tests robustness against
log corruption (which should never happen, but the kernel
should degrade gracefully)."""
The module is a few hundred lines at most and ships with one smoke test:
tests/test_kernel_chaos_smoke.py — Spawn a daemon, register kernel,
create 5 agents, run 3 chaos operations, verify the daemon doesn't
crash and kernel.observe.summary() still returns sensible numbers.
7. Backwards compatibility
- No schema change.
- No existing module modified.
- The daemon's
/metricsJSON payload is unchanged.
8. Open questions
- Should
summary()include event-rate (events/sec)? Requires tracking a windowed counter in memory. Useful but adds state. Lean: ship without rate; add in v1.1 if Prometheus users ask. - Should
proc(pid)include sandbox info? The kernel doesn't own sandbox state — that lives in the supervisor's process tree. Lean: leave out; supervisor's own observability covers it. - Should
prometheus_textbe split per-agent labels? A 100-agent system would emit 700+ time series. Cardinality matters. Lean: no per-pid labels at v1; aggregate only. Per-pid is opt-in via a separatekernel.observe.proc_prometheus(pid)if anyone asks.
9. Acceptance criteria
A PR claiming this RFC must:
kernel.observe.proc(pid)returns a single dict with all sub-fields, even when the pid has no rows in some tables (those sub-fields are empty/None, not missing).kernel.observe.summary()reports correct counts that match a manual SUM over each table.kernel.observe.trace(event_id)walks causation_id correctly (verified by enqueueing a known causation chain and asserting the walk).kernel.observe.prometheus()produces text that parses with theprometheus-clientparser if available, OR passes a basic regex contract test.ChaosMonkey.kill_random_agentis deterministic given a seed.- The smoke test demonstrates a daemon survives 3 chaos operations without crashing.
- No file outside
kernel/,tests/,docs/RFC/modified.