Files
cleveragents-core/.opencode/telemetry
drew eb01eb0172 feat(controller): dual-mode launcher (fork/prod) + DB-mode validator
Adds the operator surface for switching the controller pipeline between
the personal fork (drew/cleveragents-core) and the canonical repo
(cleveragents/cleveragents-core) via a MODE env + --prod CLI flag,
backed by safety primitives that make a wrong-mode launch loud rather
than silent.

run-controller-state-machine-pipeline.sh: --prod flag and MODE env
(primary home: .devcontainer/.env) select fork vs prod. After resolving
MODE, the launcher auto-sources the matching overlay file
(.devcontainer/.env.{fork,prod}) and asserts MODE didn't drift during
the source step. The drift assertion uses a readonly snapshot under an
obscure variable name so a stray ``MODE=fork`` in .env.prod aborts the
launch with a clear bash error rather than silently demoting the run.
CONTROLLER_RUN_DIR_ROOT now overrides the trial /tmp path so prod can
use a persistent /var/lib/cleveragents/run dir.

tools/launch_prod.sh (new): sibling to launch_fork.sh with the opposite
safety primitive — affirmative GET /repos/{owner}/{repo} that asserts
the target is non-fork, exists, isn't archived, and the bot has push.
On any failure, no env is exported. Honors HAL_* aliases for parity
with launch_fork.sh and prints a hard-to-miss PROD-MODE banner.

tools/controller/deploy/validate_db_mode.py (new): stamps a _mode_marker
table on each SQLite db (controller DB + telemetry cache) on first use,
asserts a match on every subsequent launch, and moves mismatched files
aside as <name>.<prior-mode>.bak.<ts> — never deletes. The --adopt flag
lets an operator grandfather in already-good pre-marker data without
losing history. Wired into the launcher's startup sequence before
OpenCode and the master start.

tools/_cache_path.py (new): single source of truth for the per-(owner,
repo) Forgejo cache file convention. .opencode/telemetry/server.py and
the launcher both delegate here so the dual-source-truth drift risk is
eliminated. tools/_pipeline_cache.py and tools/controller/db/models.py
documented as not owning the _mode_marker table so future migrations
leave it alone.

.opencode/telemetry/server.py: hosts the llm_activity scraper as a
background subprocess thread (60s cadence, --since-hours 1 in steady
state, full backfill on first tick). Re-homes the cost-telemetry data
path after the pr_state_warmer was retired by the controller migration
— without this the Cost tab freezes when the warmer's loop is gone.
Subprocess (not in-process) for isolation; failures swallowed.

opencode.json: local-claude provider's baseURL now reads
{env:LOCAL_PROXY_URL} instead of the literal http://127.0.0.1:3456/v1,
matching the apiKey pattern already in use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:25:24 -04:00
..

Pipeline Telemetry Console

A small, dependency-free HTTP console that consolidates everything you need to monitor the auto-agents pipeline into a single browser tab. Stdlib-only Python — no pip install required.

Why

Pipeline state lives in five silos today: the SQLite cache (merge_cycle / conflict_drive_cycles / llm_activity / pulls / commits), the running daemon processes, per-driver heartbeat files, the OpenCode HTTP server, and the live Forgejo API. Tailing five terminals + ad-hoc sqlite3 + curl is fine for the operator who built the pipeline; it does not scale to "another dev wants to see what's happening". This console is the consolidated view.

Run

# Always source the launcher first so the console reads the right
# cache and hits the right Forgejo target.
source tools/launch_fork.sh

# Default: bind 127.0.0.1:8765
python3 .opencode/telemetry/server.py

# Custom host/port
python3 .opencode/telemetry/server.py --host 127.0.0.1 --port 8765
# or via env
TELEMETRY_HOST=127.0.0.1 TELEMETRY_PORT=8765 \
  python3 .opencode/telemetry/server.py

# Browse:
#   http://127.0.0.1:8765/

The console never writes to Forgejo, OpenCode, or the SQLite cache. It is read-only by design.

Tabs

Tab What it shows Refresh Source
Overview Daemon up/down + heartbeat freshness + last 5 cycles per driver 5 s /proc walk · file mtime · cache
Drivers Full merge_cycle and conflict_drive_cycles tables (last 50) + 24 h outcome breakdown 15 s cache
PRs Open PRs by auto/* label · live Forgejo query · click-through to web UI 30 s Forgejo API
Velocity Window stats (24 h / 48 h / 7 d / 30 d) · merged / opened / commits 5 min cache
LLM Active OpenCode sessions · message counts · created/updated ages 10 s OpenCode API
Cost Token usage and estimated cost by model · selectable window 60 s llm_activity table + prices.json
About Environment audit · data-source map · operator notes static /api/meta

Inactive tabs don't poll — switching tabs cancels the previous tab's interval. Server load on a long-lived browser session stays flat.

Connection-status indicator

The pulsing dot in the top-left turns:

  • Blue (pulsing) — fetching, normal state
  • Green — last fetch succeeded
  • Red (fast pulse) — last fetch failed (API unreachable, JSON parse error, server returned non-2xx). Hover to see when it last succeeded; click any tab to retry.

Repo target

The header banner shows <owner>/<repo> and a cache OK / cache MISSING indicator. If you sourced tools/launch_fork.sh, expect to see drew/cleveragents-core. If you forgot to source it, you'll see cleveragents/cleveragents-core (canonical) — restart the console after sourcing the right shell to switch.

Cost tracking — instrumentation status

The Cost tab reads aggregated token usage from the llm_activity(model, tokens_in, tokens_out, cached_tokens) table. The table exists with the right schema; the drivers do not yet write to it. The tab will display an Instrumentation pending notice while this is the case, and populate automatically once the writers land.

The token → USD conversion uses a per-model price table in .opencode/telemetry/prices.json (override) or the seeded defaults in server.py (_DEFAULT_PRICES). Prices are listed as dollars per million tokens for in / out / cached_in. To override:

// .opencode/telemetry/prices.json
{
  "anthropic/claude-opus-4-6": { "in": 15.0, "out": 75.0, "cached_in": 1.5 },
  "CleverThis-15/Qwen3-6-35B-A3B-GGUF-UD-Q3-K-XL": { "in": 0.5, "out": 1.0 }
}

Models without a price entry are surfaced with an unpriced pill so operators know to add an entry rather than treat the row as $0.

Security posture

  • Default bind: 127.0.0.1 (loopback only). No remote-by-default.
  • No authentication in v1. The loopback bind is the security model.
  • The console does not expose secretsGITEA_TOKEN, FORGEJO_PAT, etc. are read into the server process and used to authenticate outbound API calls; their values never appear in any endpoint response. The /api/meta endpoint includes a boolean has_forgejo_token so you can confirm presence without leaking the value.
  • Exposing on a public interface requires a reverse proxy with auth in front of it. Setting TELEMETRY_HOST=0.0.0.0 is a deliberate decision; the server logs the bind interface at startup so this is visible.

Architecture

.opencode/telemetry/
├── server.py     # stdlib HTTPServer, all /api/* + static
├── index.html    # tabbed SPA, no build step
├── app.js        # vanilla JS, per-tab fetch loops
├── style.css     # minimal, prefers-color-scheme aware
├── prices.json   # (optional) token-cost overrides
└── README.md     # this file

Endpoints under /api/* are documented in server.py's module docstring. Each is a thin SQLite query, urllib.request call, or /proc walk — no business logic, easy to extend.

Add a new metric / tab

  1. Add a query function in server.py next to the existing _api_* helpers.
  2. Wire it into _API_DISPATCH with a unique /api/... path.
  3. Add a <section class="pane" data-pane="<name>"> to index.html and a <button class="tab" data-tab="<name>"> in the nav.
  4. Add a PANES['<name>'] = { intervalMs, refresh } entry in app.js.

The whole thing fits in four files; you can read it end-to-end in under ten minutes. By design.

Limitations / planned

  • No charts in v1. Window stats are shown as numbers; the canvas rendered by tools/reports.sh keeps the historical-trend story (weekly throughput, monthly volume, daily series). The console focuses on live/short-window pipeline state, not historical analytics. If we want charts here later, a single <canvas> + vanilla JS plot is the right way to add them — staying dependency-free.
  • No SSE / WebSocket. v1 is plain polling. SSE would cut the no-data-changed re-render cost; not worth the complexity until observed.
  • No request log persistence. Server only logs warnings; if you want a full request log, run with --log-level DEBUG.
  • HEAD is not implemented. Returns 501. Browsers and the SPA only use GET so this hasn't bitten anyone; trivial to add when needed.

Troubleshooting

Symptom Fix
Header shows cleveragents/cleveragents-core but you wanted the fork You forgot source tools/launch_fork.sh. Stop the console, source it, restart.
cache MISSING pill in header The cache file has not been created yet for this (owner, repo). Run any cache-syncing tool (e.g. python3 tools/sync-forgejo-cache.py) once.
Cost tab shows unpriced for the model your driver actually uses Add an entry to prices.json (or the _DEFAULT_PRICES dict in server.py).
LLM tab is empty even though OpenCode is running The console queries GET /session against OPENCODE_URL (default http://127.0.0.1:4096). Set OPENCODE_URL if the server is on a different port.
PRs tab says forgejo unreachable or no token FORGEJO_PAT (or GITEA_TOKEN) is empty in the console's environment. Source launch_fork.sh and restart.