Skip to content

feat(metrics): catalog and track metrics from every harness, and bridge omp - #109

Merged
jwaldrip merged 6 commits into
mainfrom
feat/cross-harness-metrics
Aug 12, 2026
Merged

jwaldrip merged 6 commits into
mainfrom
feat/cross-harness-metrics

Conversation

@jwaldrip

@jwaldrip jwaldrip commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

han could only catalog Claude Code. The audit found this is not a labeling gap. Every bridge already stamped a correct harness id on its events; the ingestion path was broken in four independent places, so no bridged harness session has ever reached the database.

  1. Every bridge called han coordinator ensure --background --watch-path <dir>. --watch-path was never a registered option, so Commander exited 1 on every call. Reproduced: error: unknown option '--watch-path'.
  2. The coordinator watched ~/.claude/projects plus DB-registered config_dirs only. Bridge dirs were never watched.
  3. registerConfigDir throws coordinator-internal operation, so han coordinator register could not register them either.
  4. The indexer merged a *-han.jsonl only into a sibling native transcript and returned Ok(None) otherwise. Bridges never produce a sibling, and the full scan explicitly skipped standalone han-event files.

The payoff is measurable. Running the new discovery against this machine found four harness roots quietly accumulating unindexed sessions:

discovered harness roots: ~/.han/codex, ~/.han/omp, ~/.han/gemini-cli, ~/.han/kiro, ~/.han/opencode
codex: 3 orphaned session files
kiro: 3 orphaned session files
gemini-cli: 1 orphaned session file
opencode: 1 orphaned session file

All five bridge installation docs claimed these sessions appear in the Browse UI. That has been false since the bridges shipped.

What changed

Ingestion. A han events file standing alone is now indexed as a session in its own right. The transcript parser is skipped for those files: parsing han events as transcript lines produced unknown-typed rows whose ids collided with the han event rows, which then silently failed to insert, so the events were lost even once the file was reached. native_sibling() is the single place that decides.

Attribution. sessions.harness, derived from the path rather than file contents, since a session's harness cannot legitimately vary line to line. Existing rows are backfilled to claude-code so a filter on the column and a read through the API agree. Exposed as Session.harness: String!, filterable and orderable.

Discovery. The coordinator watches and scans ~/.han/<harness>/projects, enumerated from disk. Requiring registration would mean a bridge that never called it stayed invisible, which is the failure mode being fixed.

Cost, for the first time. The han-event insert path passed None for all four token columns, so cost metrics were structurally impossible for any harness without a native transcript. A new token_usage event lands those counts in the columns every existing aggregate already reads.

An omp bridge. omp records per-message tokens and dollar cost in its own session file, so the bridge reads them rather than estimating. It subscribes to tool_execution_start/end rather than tool_call/tool_result on purpose: omp fail-closes when a tool_call handler throws, and telemetry must not be able to block a user's tool.

Five bridges repaired. Broken coordinator call removed, provider renamed to harness with one shared seven-id union, and three session-id correlation bugs fixed.

Honest scope

Only Claude Code and omp report tokens and cost. The other five record sessions, tool calls, and file changes with empty token columns. Closing that is a per-bridge change and needs nothing further from the indexer. The docs say so rather than implying coverage.

One caveat: harness roots are enumerated when the watcher starts, so the first session from a newly installed bridge appears at the next coordinator start or scan rather than live. Every session after that is live.

Verification

Gate Result
cargo test --workspace 0 failed suites
cargo check --workspace --all-targets 0 errors
bun test (packages/han) 2204 pass, 0 fail
tsc --noEmit, biome lint clean
All six bridges build OK
omp bridge tests 18 pass
plugin generate-hooks --all --check 88 checked, up to date
markdownlint (docs, blueprint, bridge READMEs) 0 errors

Four new integration tests in han-indexer/tests/bridged_harness_test.rs cover standalone ingestion, token-column population, per-harness attribution, and the negative case that a han events file beside a transcript must not become a second session.

Beyond tests, the omp bridge was reconciled against a real 4.6 MB omp session, 559 assistant messages, matching omp's own totals to the cent, then exercised inside omp 17.2.15. A live check wrote a bridge events file to the real ~/.han/omp/projects and read the session back with harness = "omp" and populated token columns.

blueprints/cross-harness-metrics.md records the ingestion path and each of the four breaks so they are not reintroduced.

han could only catalog Claude Code. Bridged harnesses wrote correctly formed
event files to ~/.han/<harness>/projects/** and nothing ever read them back,
because the indexer only merged a *-han.jsonl into a sibling native transcript
and returned Ok(None) when that sibling was absent. Bridges never produce one,
so no OpenCode, Gemini CLI, Kiro, Codex, or Antigravity session has ever
appeared in the database.

A han events file standing alone is now indexed as a session in its own right.
The transcript parser is skipped for those files: parsing han events as
transcript lines produced unknown-typed rows whose ids collided with the han
event rows, which then silently failed to insert, so the events were lost even
once the file was reached.

Sessions carry a harness, derived from the path rather than from file contents,
since a session's harness cannot legitimately vary line to line. Existing rows
are backfilled to claude-code so a filter on the column and a read through the
API agree; leaving them NULL would have made harness = 'claude-code' silently
skip every session recorded before today.

The coordinator now watches and scans ~/.han/<harness>/projects, discovered
from disk. Requiring a registration call would mean a bridge that never made it
stayed invisible, which is the failure mode being fixed.

Adds a token_usage event, the first token-shaped event han has had. The
han-event insert path passed None for all four token columns, so cost and token
metrics were structurally impossible for any harness without a native
transcript. Those counts now land in the same columns every existing aggregate
already reads.
@railway-app

railway-app Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the han-pr-109 environment in han-team-platform

Service Status Web Updated (UTC)
api ✅ Success (View Logs) Aug 12, 2026 at 11:02 am
website ⏭️ Skipped (View Logs) Aug 12, 2026 at 11:00 am
3 services not affected by this PR
  • postgres
  • redis
  • website

@railway-app
railway-app Bot temporarily deployed to han-team-platform / han-pr-109 August 12, 2026 10:16 Destroyed
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review of #109 (commit 6270b4da)

Reviewed the actual diff (a100ad5e...fac1f36d) rather than relying on the PR description alone. The landed portion (harness column + migration, path-based harness inference, standalone *-han.jsonl indexing, bridge-root auto-discovery for the watcher/full-scan, token-usage lifting) is well-scoped and backed by good tests (bridged_harness_test.rs, updated sessions/mod.rs filter tests). No CRITICAL issues found in the code that's actually here. A few things worth addressing before this comes out of Draft.

MODERATE

  1. Only 2 of the 4 described breaks are fixed here. The PR body lists four independent breaks. This diff fixes #2 (watcher never watched bridge dirs — now bridge_harness_roots()/add_extra_watch_paths cover it) and feat: add buki-react-native-web plugin #4 (indexer dropped standalone -han.jsonl files — now indexed via index_project_directory/handle_file_event). It does not touch Add Claude Code GitHub Workflow #1 (han coordinator ensure --background --watch-path <dir> — --watch-path still isn't a registered Commander option in packages/han/lib/commands/coordinator/index.ts, so that call still exits 1) or React Native Web Buki #3 (registerConfigDir in packages/han/lib/grpc/data-access.ts:1166 still throw new Error('registerConfigDir: coordinator-internal operation')). Since the watcher/full-scan now auto-discover ~/.han/<harness>/projects from disk, Add Claude Code GitHub Workflow #1/React Native Web Buki #3 may turn out to be unnecessary for ingestion to work — but that's worth confirming and stating explicitly, since right now every bridge's own startup command is still failing every time it runs.

  2. read_han_events has no incremental offset, and it's now the primary ingestion path for entire bridged sessions. index_session_file only advances last_indexed_line from the main-transcript read loop (guarded by is_han_events_file); for a standalone events file that loop never runs, so last_indexed_line never advances and read_han_events (processor.rs ~947) re-reads the whole *-han.jsonl file from line 0 on every single indexing pass (each file-watcher trigger, each full scan). insert_batch's ON CONFLICT DO NOTHING keeps this correct, but it's O(n) re-parse + re-attempted-insert of the entire session history per new event. This was a pre-existing pattern for Claude Code's sidecar han-events file (small, occasional hook events), but this PR repurposes the same file to be a bridged harness's entire transcript (including a token_usage event per turn), so the cost profile changes materially. Worth tracking a real incremental offset for events-only sessions before this is used at scale.

  3. TokenUsageEvent.data.model / .provider / .cost_usd are defined but silently dropped. han_event_token_usage() (processor.rs ~684) only lifts input_tokens/output_tokens/cache_read_tokens/cache_creation_tokens; model, provider, and cost_usd are never read anywhere, and there's no model/cost column on messages to put them in. Given the whole point of this feature (per the target-market/pricing docs) is per-model cost attribution and dashboard cost analysis, bridged sessions will show token counts but have no way to be priced or broken down by model — silently, with no TODO/error. Worth an explicit note (or a follow-up issue) so this doesn't get assumed as "done" once tokens start showing up in the DB.

MINOR

  1. harness_from_path (processor.rs ~630) walks the path looking for a projects segment; if a bridge's encoded project slug were ever literally "projects" (not just contains it — test_harness_ignores_project_named_projects only covers the near-miss -Users-me-projects case), the function would match the slug directory as the sentinel, then find the real projects/harness segments don't line up with .han, and silently fall back to claude-code instead of the real harness. In practice this seems unreachable since project slugs are always dash-prefixed by the path encoder, but a one-line comment or test for the exact-match case would make that assumption explicit rather than implicit.

  2. The new harness field on BaseEvent (packages/han/lib/events/types.ts) and written by EventLogger.createBaseEvent() (logger.ts) is never read by the Rust indexer — harness attribution is derived entirely from file path (harness_from_path), not from this per-event field. Not harmful, but currently dead weight; worth either wiring it in as a cross-check/fallback or dropping it until it's used.

  3. packages/browse-client/schema.graphql wasn't regenerated (bun run schema:pull) to include Session.harness, SessionFilter.harness, SessionOrderBy.harness. No CI check currently enforces schema freshness, so this won't break the build, but flagging so it isn't forgotten before the "GraphQL surface" follow-up work mentioned in the PR description.

  4. crud::sessions::upsert's doc comment says passing harness: None "leaves an existing value alone rather than clearing it" — true for the ON CONFLICT update path, but for a genuinely new session (INSERT, not conflict), harness: None still inserts a NULL row. Both current call sites always pass Some(...), so this isn't live today, but the doc comment could be read as covering the insert case too; worth a one-line clarification for future callers.

Nits observed, not blocking

  • entities/sessions.rs's comment "NULL means the row predates harness tracking" is slightly stale immediately after the migration runs (the migration backfills every existing NULL to claude-code, so by the time app code runs there should be no NULL rows left except via the insert-path gap in Handle gitignore with the ignore library #7 above). Not incorrect, just worth double-checking the wording still matches intent.

Overall: the core ingestion fix (bridge sessions reaching the DB at all) looks correct and is well tested. The main things I'd want resolved or explicitly scoped-out before merge are #1 (confirm whether the --watch-path/registerConfigDir breaks still need fixing given the disk-discovery approach), #2 (incremental read for events-only sessions), and #3 (cost/model attribution gap), since those directly affect whether the feature delivers on its stated goal of comparable cross-harness metrics.

test_read_real_database read $HOME/.han/han.db directly, so it failed the
moment a migration added a column, since a developer's database sits at
whatever state their last coordinator run left it in. That says nothing about
whether the entities can read real data, which is the only thing the test is
for.

It now copies the database to a temp dir and migrates the copy. The test keeps
its value, stops reporting schema lag as a product failure, and no longer opens
a database it does not own.
All five bridge installation pages claimed the coordinator indexed their
sessions into SQLite and served them through the Browse UI. That was false from
the day the bridges shipped, and this branch is what makes it true, so the
pages now describe the real mechanism: harness roots discovered from disk, a
standalone events file indexed as its own session, attributed to its harness.

Adds the harness dimension to the metrics docs with the field, the filter, and
worked queries, and a per-harness matrix of what each one actually reports.
Only Claude Code and omp produce token and cost figures today; the other five
record sessions, tool calls and file changes with empty token columns. Saying
han tracks cost across harnesses would repeat the class of claim this change
exists to delete.

blueprints/cross-harness-metrics.md records the ingestion path, the single
decision point that separates a standalone events file from one beside a
transcript, and each of the four breaks repaired, so they are not reintroduced.
omp had no bridge, so none of its sessions were catalogued. This one ships as
an omp extension and writes han events to ~/.han/omp/projects, which the
coordinator now discovers and indexes.

It is the first bridge to report real cost. omp records per-message token
counts and dollar cost in its own session file, so the bridge reads them there
rather than estimating: input, output, cacheRead and cacheWrite map onto the
token_usage contract, cost.total onto cost_usd, and a message with no model is
dropped rather than given an invented one. Emitted totals were reconciled
against a real 4.6 MB omp session, 559 assistant messages, matching to the cent,
and the extension was then exercised inside omp 17.2.15.

token_usage uuids are a hash of the omp entry id rather than random, because
omp rewrites session files in full on migration, fork and move, which forces a
re-scan. messages::insert_batch conflicts do nothing on id, so a re-emitted
event collapses onto the existing row instead of inflating token columns.

It deliberately subscribes to tool_execution_start and tool_execution_end
rather than tool_call and tool_result: omp fail-closes when a tool_call handler
throws, so a telemetry bug there would block a user's tool. Telemetry must not
be able to stop work.
…butable

Every bridge called `han coordinator ensure --background --watch-path <dir>`.
`--watch-path` was never a registered option, so Commander exited 1 on every
call and the coordinator was never started by any bridge. Reproduced verbatim as
`error: unknown option '--watch-path'`. The flag is gone; the coordinator now
discovers harness roots from disk.

The event envelope field is `harness` rather than `provider`, and every bridge
carries the same seven-id union instead of five partial unions where each bridge
knew only its own name plus opencode and claude-code.

Three session-id defects that broke correlation:
- kiro minted a random UUID in agent-spawn instead of resolving from the payload,
  so a session's first event landed under a different id than every later one.
- opencode generated a UUID at plugin load while OpenCode's real session id was
  available on every event and used elsewhere in the same file. Loggers are now
  built lazily per host session.
- gemini-cli re-derived its id independently at five call sites, so one process
  could fragment a session five ways. Consolidated to one resolver. The
  cross-process case is unsolved and stays unsolved: nothing documents
  GEMINI_SESSION_ID as a variable Gemini CLI actually sets, and inventing a
  replacement would be worse than a known gap.

Also fixes an unguarded optional dereference in kiro's discovery that gemini-cli
already guarded, and corrects five READMEs that documented the old field name
and the indexing that never happened.
Reading token counts off omp's session JSONL looks like avoidable on-disk
coupling until you check the alternative: after_provider_response fires before
the response stream body is consumed, so final output token counts do not exist
yet, and the only usage consumer on that path handles provider rate-limit
headers rather than per-message accounting. Recorded in both the blueprint and
the omp install page so nobody simplifies it into silently incomplete numbers.

Also documents session_switch, which the bridge wires because omp retargets the
session file mid-process; without it every tool call and token after a switch
would be billed to the session you just left.
@jwaldrip
jwaldrip marked this pull request as ready for review August 12, 2026 11:01
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Verification Review of #109 (commit 871ce478)

No code changes since the last review (0 lines changed) and no author responses on the prior findings, so all original concerns remain as stated:

  1. ⏳ STILL PENDING — Only 2 of 4 described breaks fixed (--watch-path CLI option and registerConfigDir still unimplemented); unclear if still needed given disk-discovery auto-fixes ingestion.
  2. ⏳ STILL PENDING — read_han_events has no incremental offset; re-reads entire bridged session transcript on every indexing pass.
  3. ⏳ STILL PENDING — model/provider/cost_usd from TokenUsageEvent are silently dropped, blocking per-model cost attribution for bridged sessions.
  4. ⏳ STILL PENDING (minor) — harness_from_path edge case for a project slug literally named "projects" untested.
  5. ⏳ STILL PENDING (minor) — BaseEvent.harness field written but never read by the indexer.
  6. ⏳ STILL PENDING (minor) — browse-client/schema.graphql not regenerated for new harness fields.
  7. ⏳ STILL PENDING (minor) — upsert doc comment about harness: None doesn't cover the insert path.

Verdict: No new issues raised. Since this PR is in Draft and the moderate items (#1-#3) directly affect whether the feature delivers on its stated goal, I'd hold approval until those are resolved or explicitly scoped out — but nothing here has gotten worse or blocks incrementally landing this as draft work.

@railway-app
railway-app Bot temporarily deployed to han-team-platform / han-pr-109 August 12, 2026 11:02 Destroyed
@jwaldrip
jwaldrip merged commit a7a02cc into main Aug 12, 2026
14 checks passed
@jwaldrip
jwaldrip deleted the feat/cross-harness-metrics branch August 12, 2026 11:07

This branch was successfully deployed

No deployments
han-team-platform / han-pr-109 — d899c53d Deployed Aug 12, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant