You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What exactly does the versioned telemetry snapshot returned by agentctl telemetry --json contain, and how is it versioned?
Fields: control state (reuse of the status payload or a reference to it), current-attempt telemetry (token categories, main/subagent context, peak context, budget, soft threshold, effective soft threshold after the handoff reserve, hard ceiling, turns, cache hit rate), recent log events, and limits (per the research and live-check tickets).
Provenance: each figure carries measured / estimated / configured (research outcome); the context window is configuration, provider rate-limit headroom is unavailable unless the live checks say otherwise.
Identity and freshness: instance identity for the fleet view (repository, agent version, pinned SDK version, process started_at), plus generated_at and a monotonic sequence so the dashboard can detect stale data.
Between attempts: the ledger exists only during an attempt. What is shown when no attempt is active, e.g. a summary of the last finished attempt (outcome, totals) from the attempt archive?
Recent log events: which events, how many, held in which in-memory buffer, and always passed through Redactor (they leave the process now).
Versioning: schema version field, compatibility rules (unknown fields ignored, missing = unavailable), null/unavailable semantics, and a size bound that stays well below the control socket's 1 MiB reply limit.
Sources: which in-process source feeds each field (TokenLedger, checkpoint, control status, attempt archive).
The UI prototype is input: fields it does not need are candidates to drop from v1.
Requirement from the resolved ticket "Decide instance discovery and the dashboard's Docker access" (#226): the snapshot must identify its own target repository (TARGET_REPO), because the dashboard performs no separate status check before reading and displays the repository the snapshot reports. Instance identity on the dashboard side is the container name, so the snapshot does not need its own instance id for v1.
Decided in a grilling session; the operator asked to take the recommended answer on every question. Facts were checked against the code at 8b69ac3 (file references below).
The per-figure marker the prototype called "provenance" is named basis in the contract: "provenance" is already taken by provenance verification of the pinned runtime.
Shape (schema version 1)
{
"schema_version": 1,
"generated_at": "…Z", // when this reply was serialised"sequence": 412, // +1 per published telemetry update; resets with the process"identity": {
"repository": "owner/name", // TARGET_REPO (requirement from #226)"agent_version": "0.4.4",
"sdk_version": "0.2.163", // from provenance evidence"cli_version": "2.1.286",
"process_started_at": "…Z"// new: recorded at startup
},
"control": { … }, // the `agentctl status` payload, same builder, plus additions below"current_attempt": { … } |null, // null = no attempt is active (not applicable, not "unavailable")"last_finished": { … } |null, // null = no finished attempt known"recent_events": [ … ],
"truncated": false
}
Figure object. Every numeric figure in current_attempt / last_finished is {"value": <number|null>, "basis": "measured|estimated|configured|sdk_assumed|unavailable"}; unavailable always has value: null and may carry a short reason. Plain fields (ids, timestamps, enums, booleans) are bare values.
control
Reuses build_status (control.py:2053) verbatim, so status and telemetry cannot drift: agent_version, repository, intake, recovering, active_attempt, pending_command, pending_next_issue, stop_plan, recovery (incl. free-text next_action, shown ready to copy), handoff (phase, deadlines), consecutive_errors (count / max), commands.
Additions: active_attempt.issue_title (new checkpoint field written at claim; today the title lives only on the claim object, lifecycle.py:2530).
Present from claim until the attempt is finalised; after the model execution ends (pushing, publishing, handoff publication) the figures stay frozen at their final values and observed_at stops advancing.
Field
Content
Basis
Source
attempt_id, issue_number, observed_at
checkpoint started_at, issue, time of the last telemetry publication
—
checkpoint, executor
budget.used
budget_tokens
measured if estimated part is 0, else estimated
TokenLedger
budget.measured, budget.estimated
split of used (drives the hatched bar)
measured / estimated
TokenLedger
budget.hard_ceiling
MAX_BUDGET_TOKENS
configured
config
budget.soft_threshold
{value, rule: percentage|reserve, percentage, reserve_turns, crossed, crossed_at_tokens, crossed_at}; value is the effective threshold after the handoff reserve
configured under percentage; under reserve inherits main context's basis
executor _check_limits (model_execution.py:1925)
budget.hard_ceiling_reached
bool
—
executor
tokens_by_category
input_tokens, cache_read_input_tokens, cache_creation_input_tokens, output_tokens (measured part only; the estimated remainder is budget.estimated)
measured
measured_by_category
turns
main-thread response count, with max (MAX_TURNS, configured)
measured
response_count("main")
continuation
{index, max}; index computed once at claim and stored in the checkpoint (today a tracker lookup, lifecycle.py:2306)
configured for max
checkpoint, config
cache.main_hit_rate
with counted responses; unavailable below PROMPT_CACHE_MIN_MEASURED_RESPONSES
measured
main_cache_stats
context.main, context.main_peak
latest and peak main-thread context
per ledger reading
context_tokens("main"), peak_main_context_tokens
context.window
the window the CLI actually uses
sdk_assumed unless CLAUDE_CODE_MAX_CONTEXT_TOKENS is set, then configured
get_context_usage().rawMaxTokens, called on the attempt's event loop at session start and after each compaction (cost verified in #229)
Held in memory by the lifecycle, updated at finalisation, and seeded at startup from disk (latest completed_attempts.json entry + token_budget from that attempt's attempt.json), so it survives restarts.
Agent-side change: the published PR number/URL is written into the attempt's attempt.json at publication (today it is not persisted anywhere).
Shown alongside current_attempt too (the drawer always has a "last finished" line).
recent_events
New in-memory ring buffer of the last 50 events, fed by JsonEventLogger.emitafter redaction, so every entry has already passed through Redactor.
Entry: {timestamp, level, event, issue_number, phase, text}; text is detail truncated to 300 characters.
Excluded high-volume events already represented as figures or too noisy: limits_checked and tool_call.
Newest last. The buffer is process-local; it starts empty after a restart.
Versioning and compatibility
schema_version is an integer major. Additive changes (new fields, new enum values for basis, phase, events) do not bump it; removing, renaming or changing the meaning of a field does.
Dashboard rules: ignore unknown fields; an unknown basis value is rendered as estimated; a missing field means unavailable; a null section (current_attempt, last_finished) means not applicable; a schema_version higher than supported shows the instance as "unsupported schema vN" using only the stable fields.
Stable across all versions: schema_version, generated_at, identity.repository, identity.agent_version.
Serialised snapshot capped at 256 KiB (a quarter of the 1 MiB reply limit, control_server.py:56). Over the cap the agent trims recent_events (oldest first), then context.subagents, and sets truncated: true. Typical size is a few tens of KiB.
Freshness
generated_at (serve time) and sequence + current_attempt.observed_at (publication time) let the dashboard tell "agent answered" from "telemetry moving". A restart is visible as a new process_started_at (sequence resets).
Activity
lbacik commentedon Oct 14, 2026
Requirement from the resolved ticket "Decide instance discovery and the dashboard's Docker access" (#226): the snapshot must identify its own target repository (
TARGET_REPO), because the dashboard performs no separate status check before reading and displays the repository the snapshot reports. Instance identity on the dashboard side is the container name, so the snapshot does not need its own instance id for v1.lbacik commentedon Oct 14, 2026
Resolution
Decided in a grilling session; the operator asked to take the recommended answer on every question. Facts were checked against the code at
8b69ac3(file references below).Terminology
CONTEXT.mdon branchdocs/fleet-dashboard-glossary(commit 17fec34).basisin the contract: "provenance" is already taken by provenance verification of the pinned runtime.Shape (schema version 1)
{ "schema_version": 1, "generated_at": "…Z", // when this reply was serialised "sequence": 412, // +1 per published telemetry update; resets with the process "identity": { "repository": "owner/name", // TARGET_REPO (requirement from #226) "agent_version": "0.4.4", "sdk_version": "0.2.163", // from provenance evidence "cli_version": "2.1.286", "process_started_at": "…Z" // new: recorded at startup }, "control": { … }, // the `agentctl status` payload, same builder, plus additions below "current_attempt": { … } | null, // null = no attempt is active (not applicable, not "unavailable") "last_finished": { … } | null, // null = no finished attempt known "recent_events": [ … ], "truncated": false }Figure object. Every numeric figure in
current_attempt/last_finishedis{"value": <number|null>, "basis": "measured|estimated|configured|sdk_assumed|unavailable"};unavailablealways hasvalue: nulland may carry a shortreason. Plain fields (ids, timestamps, enums, booleans) are bare values.controlbuild_status(control.py:2053) verbatim, sostatusandtelemetrycannot drift:agent_version,repository,intake,recovering,active_attempt,pending_command,pending_next_issue,stop_plan,recovery(incl. free-textnext_action, shown ready to copy),handoff(phase, deadlines),consecutive_errors(count / max),commands.active_attempt.issue_title(new checkpoint field written at claim; today the title lives only on the claim object,lifecycle.py:2530).recovery.publicationis filled by live GitHub lookups (lifecycle.py:2260-2295); in the snapshot it carries the last known values andcomment_confirmed: null/pull_request: nullmean "not known yet".current_attemptPresent from claim until the attempt is finalised; after the model execution ends (pushing, publishing, handoff publication) the figures stay frozen at their final values and
observed_atstops advancing.attempt_id,issue_number,observed_atstarted_at, issue, time of the last telemetry publicationbudget.usedbudget_tokensmeasuredifestimatedpart is 0, elseestimatedTokenLedgerbudget.measured,budget.estimatedused(drives the hatched bar)measured/estimatedTokenLedgerbudget.hard_ceilingMAX_BUDGET_TOKENSconfiguredbudget.soft_threshold{value, rule: percentage|reserve, percentage, reserve_turns, crossed, crossed_at_tokens, crossed_at};valueis the effective threshold after the handoff reserveconfiguredunderpercentage; underreserveinherits main context's basis_check_limits(model_execution.py:1925)budget.hard_ceiling_reachedtokens_by_categoryinput_tokens,cache_read_input_tokens,cache_creation_input_tokens,output_tokens(measured part only; the estimated remainder isbudget.estimated)measuredmeasured_by_categoryturnsmax(MAX_TURNS,configured)measuredresponse_count("main")continuation{index, max}; index computed once at claim and stored in the checkpoint (today a tracker lookup,lifecycle.py:2306)configuredformaxcache.main_hit_ratecountedresponses;unavailablebelowPROMPT_CACHE_MIN_MEASURED_RESPONSESmeasuredmain_cache_statscontext.main,context.main_peakcontext_tokens("main"),peak_main_context_tokenscontext.windowsdk_assumedunlessCLAUDE_CODE_MAX_CONTEXT_TOKENSis set, thenconfiguredget_context_usage().rawMaxTokens, called on the attempt's event loop at session start and after each compaction (cost verified in #229)context.auto_compact_thresholdget_context_usage()context.compactions{count, last: {at, trigger, pre_tokens, post_tokens}}measuredcompact_boundary/compact_metadatacontext.subagents{id, type, context, running}; running ones first, at most 20 entriesestimatedthread_contexts()+ subagent start/finish tracking (new)provider_rate_limit{value: null, basis: "unavailable", reason: "not exposed through the gateway"}in v1unavailableelapsed_seconds,timeout_secondsmeasured/configuredMODEL_TIMEOUTCut from v1 (prototype decision): USD cost (
total_cost_usdis not sent).Between attempts:
last_finished{attempt_id, issue_number, issue_title, outcome, finished_at, budget_used, turns, pull_request: {number, url} | null}.completed_attempts.jsonentry +token_budgetfrom that attempt'sattempt.json), so it survives restarts.attempt.jsonat publication (today it is not persisted anywhere).current_attempttoo (the drawer always has a "last finished" line).recent_eventsJsonEventLogger.emitafter redaction, so every entry has already passed throughRedactor.{timestamp, level, event, issue_number, phase, text};textisdetailtruncated to 300 characters.limits_checkedandtool_call.Versioning and compatibility
schema_versionis an integer major. Additive changes (new fields, new enum values forbasis,phase, events) do not bump it; removing, renaming or changing the meaning of a field does.basisvalue is rendered asestimated; a missing field means unavailable; a null section (current_attempt,last_finished) means not applicable; aschema_versionhigher than supported shows the instance as "unsupported schema vN" using only the stable fields.schema_version,generated_at,identity.repository,identity.agent_version.agentctl telemetryfailing; its error shape is Decide the agentctl telemetry command: socket operation, snapshot publication, failure isolation #225's.Size bound
control_server.py:56). Over the cap the agent trimsrecent_events(oldest first), thencontext.subagents, and setstruncated: true. Typical size is a few tens of KiB.Freshness
generated_at(serve time) andsequence+current_attempt.observed_at(publication time) let the dashboard tell "agent answered" from "telemetry moving". A restart is visible as a newprocess_started_at(sequence resets).Agent-side changes surfaced (for the later split)
telemetrysocket operation andagentctl telemetry --jsonreturning this shape (mechanics: Decide the agentctl telemetry command: socket operation, snapshot publication, failure isolation #225).issue_titleand the automatic-continuation index at claim.crossed_attimestamp, compactions, subagent running state, window viaget_context_usage()).last_finished, seeded from disk; PR number/URL persisted inattempt.json.JsonEventLogger.Records