Repository navigation
factory r3: telemetry core (P0 retention, claim_mismatch, PR facts, versions, spans, cost ledger, heal, read routes, export) - #139
Merged
Conversation
Lifecycle events (stage, pr, gate, decision, control, demo, task, ...) are kept indefinitely; only message.*, notice.* and preview.health (14 d) and heartbeat.* (2 d) are pruned. register_pr judges the claim against the refreshed gates first and stores claim_mismatch (plus server_ready) in the pr.claim event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t ledger, heal, read routes, export)
- PR facts: apply_refresh stores GitHub's createdAt as opened_at and sets
first_ready_at once.
- telemetry/versions.py: the platform version (image tag, server sha, each
factory bundle's content sha, OMNIGENT_FACTORY_* knobs with secrets
redacted) is upserted after the bundle seed, deduped by content hash.
Members carry omnigent.factory.version and omnigent.factory.bundle from
spawn (GET /v1/factory/agent/version), restart and relaunch.
- telemetry/spans.py: an idempotent span writer (dedupe_key, set-once attrs,
forced closes) reusable by later writers; recorder.py turns stored events
into project, stage, ready_wait, decision, task, env, preview, demo,
incident, intervention and proposal spans.
- telemetry/pr_rounds.py: a hook in prs.apply_refresh writes pr, one
pr_round per head and gate spans per head and gate with first_pass, from
the commit, marker and check history each refresh already fetches.
- The supervisor's child close puts the sub-agent's session usage on its
task span; spans carry version_id and the root's def_version.
- telemetry/cost.py: _record_daily_cost books every turn into
factory_cost_daily by factory, project ('' for leads), role, agent, model
and day, attributed through a 60 s cache over the session's or root's
labels.
- telemetry/heal.py: the nightly prune loop first rebuilds and closes spans
from the PR, decision, demo and project tables, sessions and events; it is
idempotent and its first run backfills.
- GET /v1/factory/factories/{fid}/spans, /prs/{id}/timeline,
/factories/{fid}/cost?group_by= and /factories/{fid}/export (NDJSON or
CSV, streamed, hidden projects dropped, no transcript content).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root's version label stands in only before this server recorded one, so a restarted root does not pin every later span to its old version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… version labels) - PR rounds: marker, evidence and demo shas are validated (7-40 hex) and only add verdicts; a round opens only for a sha seen as the PR's head (headRefOid, gate.changed), so a pasted sha256 can no longer overflow head_sha/dedupe_key on PostgreSQL or add a round, and short shas resolve to the round they abbreviate. - Heal: each section, PR and replayed event heals on its own (HealReport lists skipped parts), it re-reads each PR row before healing it, and a span a live writer closed after heal read the log keeps that close. - Export: decision.withdrawn, needs_human* and label.changed summaries are dropped, free-text label values too; CSV cells that could run as a formula are prefixed with '; sessions outside the factory need READ in the export and the span/timeline routes, as in the activity feed. - Version labels on the root lead at factory start and on peer restarts. - Span writes clip role/agent_name/actor to their columns; the ledger role is clipped to 16; one formula for a task span's start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LittleChimera
force-pushed
the
round3/r3-05
branch
from
October 27, 2026 23:57
9f71828 to
9c17f96
Compare
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes #
Round-3 work package R3-05 (telemetry core). Spec: ROUND3-SPEC §14.1–§14.4 and the export part of §14.7. Builds on R3-03 (#137), whose tables and store methods it fills.
Summary
Stop losing data, and record durable, versioned facts that every metric and the loop engine can build on. Everything new lives in
omnigent/server/factory/telemetry/; the hot files get small hooks.P0: stop losing data (first commit)
background/prune.pykeeps every lifecycle event. Onlymessage.*,notice.*andpreview.health(after 14 days) andheartbeat.*(after 2 days) are pruned. The 2026-10-26 run's events would otherwise age out on 2026-11-09.register_prjudges a claim against the refreshed gates first, then storesclaim_mismatch(andserver_ready) in thepr.claimevent.PR facts.
prs.apply_refreshstores GitHub'screatedAtasopened_atand setsfirst_ready_atonce;ready_atstill means "latest".Versions (
telemetry/versions.py)factory_versions: image tag, server sha, each factory bundle's content sha, and theOMNIGENT_FACTORY_*knobs. Knob names that look like secrets are redacted, and credentials are stripped from URLs. Rows are deduped by content hash.omnigent.factory.versionandomnigent.factory.bundle=<agent_id>/<sha12>:_spawn_memberasks the newGET /v1/factory/agent/version;control.execute_control.Spans (
telemetry/spans.py,recorder.py,pr_rounds.py)write_spanis the reusable writer for the R3-09 to R3-12 spans. It is idempotent ondedupe_key: a repeated write merges into the existing row. It handles set-once attrs, forced closes, reopen, fill-missing fields and named overwrites.events.add_listenerwrites these spans:project, plusstageepisodes;ready_wait,decision,task;envandpreviewstate episodes;demo,incident(needs_human,issue.*), zero-lengthintervention(controls) andproposal.apply_refreshturns the marker, commit and check history the poller already fetches into PR spans:prspan;pr_roundper head;gateper head per gate (checks, review, verify, demo) withattrs.first_pass, which is set once.taskspan. The span covers the whole session, so this is the sum of every run's delta.version_idand the root'sdef_version.Cost ledger (
telemetry/cost.py)_record_daily_costbooks every turn intofactory_cost_daily. Each row has the display cost, tokens and turns, keyed by factory, project (''for leads), role, agent, model and UTC day.orchestration.pycall sites now pass the turn's tokens and model.Heal (
telemetry/heal.py)factory_projects,factory_prs,factory_decisions,factory_demos, the sessions (sub-agents whosetask.donewas lost) and the remaining events.first_ready_at.Read routes (
api/telemetry_read.py)GET /v1/factory/factories/{fid}/spans?kind=&project=&pr_id=&task_id=&since=&until=&limit=GET /v1/factory/prs/{id}/timelineGET /v1/factory/factories/{fid}/cost?group_by=day|project|role|agent|model(comma-combinable)GET /v1/factory/factories/{fid}/export?tables=spans,cost,decisions,prs,demos,events&format=ndjson|csvalert.failedevents;Review fixes (hardening)
headRefOid, agate.changedevent, or an existing round), and short shas resolve to the round they abbreviate. A pasted sha256 in a marker can no longer overflowhead_sha(40) ordedupe_key(96) on PostgreSQL, and intermediate-commit or typo shas no longer add rounds.HealReport.partialand skips only that part. Heal re-reads each PR row before healing it, and a span that a live writer closed after heal read the log keeps that close (no reopen until the next night).decision.withdrawn,needs_human*andlabel.changedsummaries are dropped, and so are free-text label values (only state, health, heartbeat and supervisor values are kept). CSV cells starting with= + - @(or tab/CR) get a'prefix. Sessions outside the factory now need READ in the export and the span/timeline routes, as in the cockpit's activity feed./v1/factory/messagesrestamp.role,agent_nameandactorto their columns, the ledger role is clipped to 16, and one formula computes a task span's start.Test Plan
uv run --frozen pytest -q tests/server/factory tests/runner tests/examples tests/policies tests/test_factory_gates.py: all pass locally. Also ran the session-usage and cost suites (tests/server/routes/test_native_usage_attribution.py,test_usage_report.py,test_session_updates_ws.py,test_sessions_runner_relay.py,tests/runtime/policies/test_user_daily_cost_routing.py,tests/server/integration/test_sessions_endpoints.py,tests/server/test_app.py).test_prune.py: on a seeded database (SQL and memory), a month-old lifecycle survives and the chatter is pruned.test_agent_state_routes.py:pr.claimcarriesclaim_mismatch.test_telemetry_spans.pyreplaysfixtures/live_run_2026_10_26.json, the live run as an event log, through the real poller and event log:first_pass=false); review asked for changes on head b; head c passed every gate first time.ready_waitepisodes.test_telemetry_heal.py:task.doneis closed from the session;test_telemetry_cost.py: an IC turn goes to its project and role, a lead turn to project''; also unpriced turns, the TTL cache, and the real native-usage path.test_telemetry_versions.py: content-hash dedupe, knob redaction, member labels, the spawn route, restart stamping.test_telemetry_routes.py: filters, the timeline, cost grouping. The export excludes transcript content and hides projects the viewer cannot read (NDJSON and CSV).tests/runner/test_factory_spawn.py: spawn stamps the version and bundle labels.report.partial); a close that landed after heal read the log is kept; export drops withdrawal reasons, blockers and free-text labels; CSV cells cannot run as formulas; outsider sessions need READ; the root lead and peer restarts get version labels; long role labels are clipped.fz1a2b3c4d5e); this PR adds no migration.Live acceptance (on omnigent.caffeinelabs.io after the image bump, temp-main factory):
spans?kind=pr_roundshows one row per head per PR, and its gate rows carryfirst_pass.cost?group_by=project,role,daysums to within 2% of the cockpit'ssubtree_cost, for turns after the deploy.Demo
N/A. Server-only: no UI in this package. The routes feed the round-3 Insights and Timeline pages.
Type of change
Test coverage
Coverage notes
The live-run replay runs the real PR poller (fake GitHub), the real
events.emitand the realapply_refreshon one fake clock; the heal and route tests reuse it. Live acceptance on the deployed temp-main factory follows the image bump.Notes for reviewers:
OMNIGENT_IMAGE_TAGandOMNIGENT_SERVER_SHA, which the deployment does not set yet. Until it does, versions differ by bundles and knobs only.turnscounts usage reports that added cost or tokens, not prompt turns.🤖 Generated with Claude Code