Skip to content

refactor: OpenTelemetry-aligned rename (agent_turns→traces, llm_calls→spans)#174

Merged
vaderyang merged 5 commits into
mainfrom
feat/otel-rename
Jun 23, 2026
Merged

refactor: OpenTelemetry-aligned rename (agent_turns→traces, llm_calls→spans)#174
vaderyang merged 5 commits into
mainfrom
feat/otel-rename

Conversation

@vaderyang

Copy link
Copy Markdown
Collaborator

Aligns Heron's storage entities and HTTP API with the industry-standard
OpenTelemetry Session → Trace → Span vocabulary. Pure rename + backward
compatibility — no behavior change.

What changed

  • Storage (DB): tables agent_turnstraces, llm_callsspans; column
    traces.call_idsspan_ids; a new forward-looking spans.kind column
    (always 'llm' today) leaves room for wire-visible tool spans. Both DuckDB
    and ClickHouse auto-migrate in place on init() — an idempotent
    detect-then-rename runs before CREATE TABLE IF NOT EXISTS, so existing
    databases upgrade with zero data loss (no file deletion needed). RENAME has
    no IF EXISTS, so each is guarded by a table/column existence check.
  • Rust: domain/query types (AgentTurnTrace, Turn*/Call* DTOs →
    Trace*/Span*) and every StorageBackend method (write_calls
    write_spans, query_turnsquery_traces, …) renamed across the workspace.
    The h-turn crate dir name is kept (not a contract).
  • HTTP API: canonical /api/traces* + /api/spans* (the child
    /api/traces/{id}/spans replaces /calls). The pre-rename /api/agent-turns*
    and /api/llm-calls* keep working as deprecated aliases carrying an RFC
    8594 Deprecation header, so an existing external consumer is not broken. The
    first-party console moves to the canonical paths.
  • Config: retention keys calls/turnsspans/traces, with serde
    aliases so existing config files keep loading.
  • Docs: schema design doc, configure guide, sample TOML, CHANGELOG, CLAUDE.md.

Compatibility

Old DB files, old config keys, and old API routes all keep working. session
stays session (the /api/agent-sessions* routes are unchanged).

Tests

  • New migration test: a full pre-rename DB → migrate → asserts the table/column
    rename, span_ids JSON preserved byte-for-byte, kind backfilled to 'llm',
    and zero row loss.
  • New route test: the deprecated aliases still return 200 and carry the
    Deprecation + Link headers.
  • cargo test --workspace (+ --features fault-injection) green; criterion
    benches compile.

Vader Yang added 4 commits June 22, 2026 11:45
…lign, DB layer)

Phase A of the OpenTelemetry-aligned rename. Storage layer only — SQL/DDL and
migration code; Rust domain/query types are unchanged in this commit.

- DuckDB + ClickHouse: rename tables agent_turns→traces, llm_calls→spans and
  column traces.call_ids→span_ids; add a forward-looking spans.kind column
  (always 'llm' today) for future wire-visible tool spans.
- Idempotent in-place migration on init(): detect-then-rename guards (RENAME has
  no IF EXISTS) run before CREATE so a migrated DB no-ops the CREATE IF NOT
  EXISTS; kind added nullable-with-default per the DuckDB ADD COLUMN rule; CH
  uses system.tables/columns existence checks + ALTER ... ADD COLUMN IF NOT
  EXISTS.
- ClickHouse write Row (TurnRow) field call_ids→span_ids so the RowBinary insert
  names the renamed column; read structs stay positional.
- migrations.rs: post-init assertions read the new names; add a full pre-rename
  → migrate test asserting table/column rename, span_ids JSON preserved
  byte-for-byte, kind backfilled to 'llm', and zero row loss.
- Update pipeline_e2e and backfill-tokens SQL to the new names.

Tests: h-storage-duckdb 62 + 8 migration (incl. fault-injection) green;
pipeline_e2e 2/2 green; clickhouse crate compiles.
…el align, Rust layer)

Phase B of the OpenTelemetry-aligned rename. Pure internal rename — zero
behavior change; whole workspace + tests compile and pass.

- Domain: AgentTurn→Trace, TurnKey→TraceKey, TurnStatus→TraceStatus,
  ActiveTurnRegistry→ActiveTraceRegistry, new_active_turn_registry→…trace…,
  field call_ids→span_ids.
- Query DTOs: Turn*→Trace* (TurnListItem/TurnsQuery/TurnsPage/TurnDetail/
  TurnCallItem→TraceSpanItem, SessionTurn*→SessionTrace*), Call*→Span*
  (CallListItem/CallsQuery/CallsPage/CallDetail).
- StorageBackend trait methods: write_calls→write_spans, query_calls→
  query_spans, query_call_by_id→query_span_by_id, query_calls_by_ids→
  query_spans_by_ids, write_turns→write_traces, query_turns→query_traces,
  query_turn_by_id→query_trace_by_id, query_turn_calls→query_trace_spans,
  query_session_turns→query_session_traces, update_turn_metadata→…trace…,
  checkpoint_turns_writer→checkpoint_traces_writer — and every impl + mock.
- Crate dir name h-turn kept (not a contract). Route module files
  (agent_turns.rs/llm_calls.rs) renamed in Phase C with the route paths.
- Migration SQL keeps the legacy `call_ids` token (RENAME COLUMN source name);
  schema.rs + migrations.rs excluded from the field rename.

Tests: cargo test --workspace green.
…aliases

Phase C of the OTel rename. Public API surface aligns to trace/span while the
old paths keep working for the external consumer.

- Route modules renamed: routes/agent_turns.rs→traces.rs, llm_calls.rs→spans.rs.
- Canonical routes: /api/traces, /api/traces/summary|activity|{id}, the child
  /api/traces/{id}/spans (was /calls), /api/traces/{id}/proxy-view; /api/spans,
  /api/spans/{id}.
- Deprecated aliases → identical handlers on their own sub-routers, stamped with
  an RFC 8594 `Deprecation: true` + `Link: …; rel="successor-version"` header:
  /api/agent-turns*, /api/agent-turns/{id}/calls, /api/llm-calls*.
- Route param structs TurnsParams→TracesParams, CallsParams→SpansParams.
- Console: first-party fetch hooks point at the canonical paths
  (/api/traces*, /api/spans*); SPA routes / component names unchanged.
- Tests: existing list tests hit /api/spans; new regression test asserts the
  aliases still 200 and carry the Deprecation + Link headers. h-api green.

Note: /api/agent-sessions* intentionally unchanged (session stays "session").
…he OTel rename

Phase D of the OTel rename — config keys + documentation.

- RetentionConfig keys calls→spans, turns→traces, with serde aliases so existing
  config files keep loading. Cascades through RetentionPolicy
  (calls_before→spans_before, turns_before→traces_before), RetentionReport
  (calls_deleted→spans_deleted, turns_deleted→traces_deleted), the
  validate() constraint (ConfigIssue::TracesRetentionExceedsSpans), and the
  sweeper. Queue-capacity keys (storage_calls/storage_turns) left as-is.
- docs/design/07-schema.md: entities renamed to traces/spans/span_ids, new
  spans.kind column, and an "OpenTelemetry-aligned rename" upgrade note
  describing the in-place auto-migration + route/config back-compat.
- docs/configure.md, config/default.toml, config/demo.toml: retention example
  uses spans/traces and documents the deprecated calls/turns aliases.
- CHANGELOG: Unreleased entry. CLAUDE.md: storage entity description updated.

Tests: cargo test --workspace green (incl. retention validate + alias load).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have enough. Let me write the review.

Summary

PR #174 is a four-phase OpenTelemetry-aligned rename: DB tables (agent_turnstraces, llm_callsspans, call_idsspan_ids, + new spans.kind), Rust domain/query types and StorageBackend trait methods, HTTP routes (/api/traces*, /api/spans* with deprecated aliases), and config keys. The storage-layer migration is idempotent and tested; the trait rename is mechanical and complete across the workspace. However, there is one Blocking schema-drift bug: the Rust TraceDetail now serializes span_ids, but the console's AgentTurnDetail TS interface and agent-breakdown.tsx still read call_ids, so the turn-detail page will render undefined for the call count. There's also a migration-safety concern worth flagging. Recommendation: REQUEST_CHANGES.

Blocking

  • console/src/types/api.ts:342AgentTurnDetail.call_ids: string[] is now stale. The Rust TraceDetail (server/h-storage/src/query.rs:523) was renamed to span_ids: Vec<String>, so serde now emits "span_ids" in the JSON. The TS interface still declares call_ids, so at runtime turn.call_ids is undefined. This breaks agent-breakdown.tsx:41-42 (turn.call_ids.lengthTypeError: Cannot read properties of undefined (reading 'length')) on every turn-detail render. Either rename the TS field to span_ids (and update the consumer), or add #[serde(alias = "call_ids")] on the Rust side for one release.

Suggestions

  • server/h-storage-duckdb/src/schema.rs:197-214 — The Phase 8 rename migrations log failures as non-fatal tracing::warn! and then proceed to CREATE TABLE IF NOT EXISTS traces/spans. If ALTER TABLE agent_turns RENAME TO traces fails (concurrent writer, disk issue, etc.), CREATE TABLE IF NOT EXISTS traces then succeeds on a fresh empty table — orphaning the legacy agent_turns data behind the old name and silently switching all reads/writes to the empty table. The Phase 4/5/6/7 ADD COLUMN migrations that follow tolerate this because they target the (now-empty) renamed table, but the data loss is the issue. Consider making the rename failures fatal (return AppError) so a botched migration surfaces loudly instead of stranding data.

  • server/h-common/src/config.rs:1037-1044 — The doc comment on ConfigIssue::TracesRetentionExceedsSpans still references the old table names (agent_turns, llm_calls) and old field names (calls_days, turns_days) mid-paragraph, while the variant and fields are traces/spans. Inconsistent with the rename; update the prose for clarity.

  • server/h-storage/src/classify.rs:160 — The comment was renamed from tool_call_ids to tool_span_ids, but the actual OpenAI/vLLM wire field is tool_call_id (per OpenAI's API spec — assistant.tool_calls[].id). The string match ("chatcmpl-tool-") is still correct, but the comment now misnames the wire field. Revert the comment rename here — the OTel rename applies to our storage vocabulary, not to upstream wire-protocol field names.

Questions

  • The PR commit messages describe a 4-phase rollout (DB → Rust types → API → config/docs). Was the console/src/types/api.ts call_idsspan_ids rename intentionally deferred to a follow-up, or is this an oversight? If intentional, the breakage in agent-breakdown.tsx suggests the follow-up needs to land in the same release.

  • Why does the DuckDB Phase 8 migration swallow rename errors as non-fatal warnings (schema.rs:200, 206, 212) when every other migration in this file also treats ALTER failures as non-fatal but for additive columns — where a failure just means a column is missing and the next init retries? For a rename, a non-fatal failure is structurally different: the data ends up in the wrong table and subsequent reads silently return empty. Is there a specific failure mode you're defending against here?

Verified

  • Schema drift (Rust↔TS field names): SpanListItem, SpanDetail, TraceListItem, SessionTraceItem, TraceSpanItem field names all match their TS counterparts in console/src/types/api.ts — the rename was type-name-only on the TS side, wire-compatible. The one exception is TraceDetail.span_ids vs AgentTurnDetail.call_ids (Blocking above).
  • Caller compatibility for renamed trait methods: Grepped the whole repo for write_calls|query_calls|query_call_by_id|query_calls_by_ids|write_turns|query_turns|query_turn_by_id|query_turn_calls|query_session_turns|update_turn_metadata|checkpoint_turns_writer — no live source code references remain; only historical plan/spec docs under docs/superpowers/ and CHANGELOG.md (intentional).
  • Console route registration: console/src/app.tsx SPA routes /agent-turns and /llm-calls are browser routes (not API paths) and are intentionally unchanged.
  • TanStack queryKey staleness: use-agent-turns.ts, use-llm-calls.ts, use-agent-overview.ts, use-llm-call-detail.ts — all queryKeys include every varying input; the "agent-turns" / "llm-calls" key prefixes are cosmetic cache namespace tags and don't need to match the URL path.
  • Deprecated alias headers: server/h-api/src/lib.rs:244-264 stamps Deprecation: true + Link: </api/traces>; rel="successor-version" on alias responses via map_response — applies to both success and error responses (middleware runs after the handler). Regression test at duckdb_routes.rs:555 asserts both header + status 200.
  • Serde alias on RetentionConfig: server/h-common/src/config.rs:604,608alias = "calls" on spans, alias = "turns" on traces. Existing config files with old keys still load. Note: serialize emits new names only (one-way migration), which is the intended direction.
  • ClickHouse span_ids column/struct alignment: services.rs:483 SQL alias span_ids AS span_ids matches TurnEndpointRow.span_ids field at services.rs:135. No deserialization drift.
  • DuckDB kind backfill: ALTER TABLE spans ADD COLUMN IF NOT EXISTS kind VARCHAR DEFAULT 'llm' — DuckDB backfills existing rows with the default; migration test at migrations.rs:721-724 asserts kind='llm' on the pre-existing row.

🤖 Reviewed by the review botworkflow run

@vaderyang
vaderyang merged commit 5452291 into main Jun 23, 2026
2 checks passed
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