Skip to main content

API Enrichment Audit — First-Class Agent State

Goal: Make the GC API rich enough that any dashboard (Mission Control, custom UIs, monitoring tools) can build a complete agent monitoring experience from GC alone — without needing to scrape OS process tables or talk to provider-specific APIs like YepAnywhere. Principle: The agent abstraction owns the data. Provider and session details stay hidden behind the abstraction. If a dashboard needs to know something about an agent, GC should expose it as a first-class field on the agent, not force the consumer to reverse-engineer it from PIDs and cwds.

Current agent response (GET /v0/agents, /v0/agent/{name})

{
  "name": "rig/agent-1",
  "running": true,
  "suspended": false,
  "rig": "rig",
  "pool": "rig/agent",
  "session": {
    "name": "city--rig--agent-1",
    "last_activity": "2026-03-06T...",
    "attached": false
  },
  "active_bead": "abc123"
}
This is structurally correct but data-poor. A dashboard builder has to make N+1 calls (fetch agent list, then fetch each bead, then peek each session) to build a useful display. The agent abstraction should carry enough state that a single GET /v0/agents call gives you everything you need.

Gaps — organized by what the agent abstraction should own

Gap 1: Agent identity metadata

The agent knows its name and rig, but not its provider or what it’s running. This is static config data that should be on every agent response. Add to agentResponse:
FieldTypeSourceNotes
providerstringconfig.Agent.Provider"claude", "codex", "gemini", etc.
display_namestringProviderSpec.DisplayName"Claude Code", "Codex CLI", etc.
Why: Every dashboard wants to show what kind of agent this is. Today you’d have to cross-reference the agent name against the config to find the provider. The API should just tell you. Effort: Trivial — the config is already loaded; add two fields to the response builder in handleAgentList.

Gap 2: Agent activity state (beyond running/not-running)

running: true is a binary. Dashboards need a richer state model to show what the agent is actually doing. Add to agentResponse:
FieldTypeSourceNotes
statestringDerived (see below)Enum: "idle", "working", "waiting", "stopped", "suspended", "quarantined"
Derivation logic (in API handler, not Go business logic — pure data mapping):
if suspended        → "suspended"
if quarantined      → "quarantined"
if !running         → "stopped"
if active_bead != "" {
  if last_activity recent (< threshold) → "working"
  else                                  → "waiting"
} else              → "idle"
The threshold for “working” vs “waiting” can be a reasonable default (10min) or configurable. This replaces the crude running boolean with a human-meaningful state without adding decision logic to Go — it’s a pure data derivation from fields we already have. Effort: Small — all inputs already exist in the handler.

Gap 3: Process-level metadata

Dashboards want PID, memory usage, and uptime per agent. The tmux provider already has GetPanePID() and can query /proc/{pid}/status for RSS. This data belongs on the agent response, not discovered by the consumer via ps. Add to agentResponse:
FieldTypeSourceNotes
process*processInfoSession providernull when not running
"process": {
  "pid": 12345,
  "rss_mb": 280,
  "elapsed_sec": 3600
}
New session.Provider method:
// ProcessInfo returns OS-level process metadata for the named session.
// Returns nil if the session isn't running or info is unavailable.
ProcessInfo(name string) *ProcessInfo

type ProcessInfo struct {
    PID        int
    RSSBytes   int64
    ElapsedSec int
}
The tmux provider implements this via GetPanePID + reading /proc/{pid}/stat (or ps -p {pid} -o rss=,etimes=). Non-tmux providers return nil. Effort: Medium — new Provider interface method, tmux implementation, wire into API handler. The building blocks exist; this is plumbing.

Gap 4: Active work context

active_bead: "abc123" is an opaque ID. Dashboards have to fetch the bead separately to learn what the agent is working on. Add to agentResponse:
FieldTypeSourceNotes
active_work*workContextBead store lookupnull when no active bead
"active_work": {
  "bead_id": "abc123",
  "title": "implement user auth",
  "type": "task",
  "started_at": "2026-03-06T..."
}
Why: The agent handler already calls findActiveBead() which iterates bead stores. It currently returns only the ID. Extend it to return title, type, and created_at from the same bead it already found. Effort: Trivial — the bead is already loaded; return more fields from it.

Gap 5: Last output / peek preview

Dashboards want a quick preview of what the agent is doing without a separate peek call. MC uses this for question detection and status display. Add to agentResponse:
FieldTypeSourceNotes
last_outputstringsession.Peek(name, 5)Last ~5 lines, truncated. Empty when not running.
Concern: Peek is not free (tmux capture-pane). For the agent list endpoint, this could be expensive with many agents. Two options:
  • Option A: Only include when ?peek=true query param is set. Default list call stays fast; detail call includes it.
  • Option B: Always include on single-agent GET /v0/agent/{name}, never on list endpoint.
Recommend Option A for flexibility. Effort: Small — Peek already works; add optional inclusion in list handler.

Gap 6: Rig/project enrichment

Rigs are the GC equivalent of “projects” but lack activity metadata. Dashboards want to know when a rig was last active and its git state. Add to rigResponse:
FieldTypeSourceNotes
last_activitystringMax of agent last_activity times for rigISO8601 or empty
agent_countintCount of agents assigned to this rigIncludes pool expansion
running_countintCount of running agents in this rig
Git status — new optional sub-object, populated when ?git=true:
FieldTypeSourceNotes
git*gitStatusgit -C {path} ...null unless requested
"git": {
  "branch": "main",
  "clean": false,
  "ahead": 2,
  "behind": 0,
  "changed_files": 3
}
Effort: Medium — agent counts are cheap (already computed). Git status requires shelling out to git, so it must be opt-in (?git=true) and have a short timeout.

Gap 7: City-level overview stats

The status endpoint is minimal. Dashboards want a single call that gives the full picture. Enrich GET /v0/status:
FieldTypeSourceNotes
versionstringBuild-time constantGC binary version
uptime_secinttime.Since(startTime)Controller uptime
agentsobjectCounts{ "total": N, "running": N, "suspended": N, "quarantined": N }
rigsobjectCounts{ "total": N, "suspended": N }
workobjectBead store summary{ "in_progress": N, "ready": N, "open": N }
mailobjectMail store summary{ "unread": N, "total": N }
Effort: Small-medium — all data sources exist; this is aggregation.

Gap 8: Health endpoint enrichment

GET /health returns {"status": "ok"}. This is fine for liveness probes but useless for dashboards. Enrich GET /health:
{
  "status": "ok",
  "version": "0.11.0",
  "city": "bright-lights",
  "uptime_sec": 86400
}
Keep it lightweight — no expensive queries. This is a probe endpoint that also gives enough context for dashboard connection verification. Effort: Trivial — add three string/int fields.

Summary: implementation order

#GapFieldsEffortPriority
1Agent identityprovider, display_nameTrivialP0
2Agent state enumstateSmallP0
3Process metadataprocess.{pid, rss_mb, elapsed_sec}MediumP0
4Active work contextactive_work.{bead_id, title, type, started_at}TrivialP0
5Peek previewlast_output (opt-in)SmallP1
6Rig enrichmentlast_activity, agent_count, running_count, gitMediumP1
7Status overviewAggregate counts + version + uptimeSmallP1
8Health enrichmentversion, city, uptime_secTrivialP2
P0 = needed for any useful dashboard integration (Gaps 1-4) P1 = makes dashboards significantly better (Gaps 5-7, 9) P2 = nice-to-have polish (Gap 8)

Gap 9: Session log viewer (model, context usage, conversation)

GC already has internal/sessionlog (merged to main as 1a7ae398) — a Go package that reads Claude Code’s JSONL session files, resolves the DAG to the active conversation branch, and provides compact-boundary pagination. This is the “container log” observation layer. It currently supports:
  • DAG resolution (uuid/parentUuid chain walking, tip selection)
  • Compact boundary handling (logicalParentUuid bridging)
  • Pagination (slice at compact boundaries for incremental loading)
  • Tool pairing (orphaned tool_use detection)
  • Session discovery (find most recent JSONL by working directory slug)
What it does NOT yet extract (but can, from the same JSONL data):
  • Model name — stored in message.model on assistant entries (e.g., "claude-opus-4-5-20251101"). Just needs a helper that scans for the first assistant entry with a non-synthetic model field.
  • Context usage % — computed from the last assistant message’s message.usage fields (input_tokens + cache_read_input_tokens + cache_creation_input_tokens), adjusted by compaction overhead (compactMetadata.preTokens), divided by a model context window lookup.
YepAnywhere computes context % like this (from reader.ts):
1. Look up context window size by model name (hardcoded table:
   claude → 200K, gemini → 1M, codex/gpt-5 → 258K, gpt-4 → 128K)
2. Compute compaction overhead:
   overhead = last compact_boundary.preTokens - last pre-compaction assistant usage
3. Find last assistant message with non-zero usage
4. totalInput = input_tokens + cache_read + cache_creation + overhead
5. percentage = round(totalInput / contextWindowSize * 100)
Our sessionlog package already parses CompactMeta.PreTokens and has the full active branch. Adding model + context usage extraction is straightforward — the JSONL has all the data, we just need to decode two more fields from message. Add to agent API — two layers: Layer A: Agent-level summary fields (on agentResponse):
FieldTypeSourceNotes
modelstringsessionlog extraction"claude-opus-4-5-20251101" or empty
context_pct*intsessionlog extraction0-100, null if unavailable
context_window*intModel lookup tableToken count, null if unknown
These are populated by reading the agent’s most recent session JSONL file. Discovery: the agent’s working directory maps to a Claude projects slug under ~/.claude/projects/. The sessionlog package already has discovery logic for this. Layer B: Full session log endpoint:
GET /v0/agent/{name}/log
GET /v0/agent/{name}/log?tail=1    (last compaction segment only)
GET /v0/agent/{name}/log?before={uuid}  (pagination cursor)
Returns the resolved conversation branch with pagination. This is sessionlog.ReadFile() / ReadFileOlder() exposed over HTTP. Provider- agnostic in concept (any provider that writes structured logs could be supported), Claude-specific in initial implementation. Context window lookup table — a Go map mirroring YA’s:
var modelContextWindows = map[string]int{
    "opus":   200_000,
    "sonnet": 200_000,
    "haiku":  200_000,
    "gemini": 1_000_000,
    "gpt-5":  258_000,
    "codex":  258_000,
    "gpt-4":  128_000,
    "gpt-4o": 128_000,
}
Parse model ID → extract family → lookup. Same regex approach as YA. This table is provider-aware but lives in the sessionlog package, not in the agent abstraction — the API handler just calls sessionlog.ExtractContextUsage(session) and surfaces the result. Is this a ZFC violation? No. The context window table is a fact table (like a timezone database), not a decision tree. It maps model IDs to known token limits. The Go code doesn’t decide anything based on context % — it just reports the number. Dashboards decide what to do with it. Effort: Medium — extend sessionlog with model/usage extraction (small), add context window lookup table (small), add /v0/agent/{name}/log endpoint (medium), wire summary fields into agent response (small).

Summary: updated implementation order

#GapFieldsEffortPriority
1Agent identityprovider, display_nameTrivialP0
2Agent state enumstateSmallP0
3Process metadataprocess.{pid, rss_mb, elapsed_sec}MediumP0
4Active work contextactive_work.{bead_id, title, type, started_at}TrivialP0
5Peek previewlast_output (opt-in)SmallP1
6Rig enrichmentlast_activity, agent_count, running_count, gitMediumP1
7Status overviewAggregate counts + version + uptimeSmallP1
8Health enrichmentversion, city, uptime_secTrivialP2
9Session log + model/contextmodel, context_pct, /v0/agent/{name}/logMediumP1

What this does NOT include (and why)

  • AI-generated summaries. This is a consumer-layer feature. MC generates summaries by calling Claude on session data. GC could store summaries as bead metadata, but generating them is not an SDK concern.
  • Stale/orphan process detection. Once GC owns process metadata (Gap 3), a dashboard can compare GC’s agent list against its own OS process scan. But GC shouldn’t scan for orphans itself — it knows exactly which agents it manages. “Stale” is an MC concept for processes outside any orchestrator’s control.
  • System stats (RAM, CPU, disk). OS-level monitoring is not GC’s job. A separate system monitoring service/API is the right home for this. MC gets this via free, os.loadavg(), df and should continue to.

Compatibility note

All new fields use omitempty. Existing consumers see no breaking changes. New fields appear only when populated. The ?peek=true and ?git=true query params are additive — default behavior is unchanged.
Last modified on March 19, 2026