refactor: OpenTelemetry-aligned rename (agent_turns→traces, llm_calls→spans)#174
Conversation
…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).
There was a problem hiding this comment.
I have enough. Let me write the review.
Summary
PR #174 is a four-phase OpenTelemetry-aligned rename: DB tables (agent_turns→traces, llm_calls→spans, call_ids→span_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:342 —
AgentTurnDetail.call_ids: string[]is now stale. The RustTraceDetail(server/h-storage/src/query.rs:523) was renamed tospan_ids: Vec<String>, so serde now emits"span_ids"in the JSON. The TS interface still declarescall_ids, so at runtimeturn.call_idsisundefined. This breaksagent-breakdown.tsx:41-42(turn.call_ids.length→TypeError: Cannot read properties of undefined (reading 'length')) on every turn-detail render. Either rename the TS field tospan_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 toCREATE TABLE IF NOT EXISTS traces/spans. IfALTER TABLE agent_turns RENAME TO tracesfails (concurrent writer, disk issue, etc.),CREATE TABLE IF NOT EXISTS tracesthen succeeds on a fresh empty table — orphaning the legacyagent_turnsdata behind the old name and silently switching all reads/writes to the empty table. The Phase 4/5/6/7ADD COLUMNmigrations 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 (returnAppError) so a botched migration surfaces loudly instead of stranding data. -
server/h-common/src/config.rs:1037-1044 — The doc comment on
ConfigIssue::TracesRetentionExceedsSpansstill references the old table names (agent_turns,llm_calls) and old field names (calls_days,turns_days) mid-paragraph, while the variant and fields aretraces/spans. Inconsistent with the rename; update the prose for clarity. -
server/h-storage/src/classify.rs:160 — The comment was renamed from
tool_call_idstotool_span_ids, but the actual OpenAI/vLLM wire field istool_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.tscall_ids→span_idsrename intentionally deferred to a follow-up, or is this an oversight? If intentional, the breakage inagent-breakdown.tsxsuggests 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
ALTERfailures 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,TraceSpanItemfield names all match their TS counterparts inconsole/src/types/api.ts— the rename was type-name-only on the TS side, wire-compatible. The one exception isTraceDetail.span_idsvsAgentTurnDetail.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 underdocs/superpowers/andCHANGELOG.md(intentional). - Console route registration:
console/src/app.tsxSPA routes/agent-turnsand/llm-callsare 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-264stampsDeprecation: true+Link: </api/traces>; rel="successor-version"on alias responses viamap_response— applies to both success and error responses (middleware runs after the handler). Regression test atduckdb_routes.rs:555asserts both header + status 200. - Serde alias on RetentionConfig:
server/h-common/src/config.rs:604,608—alias = "calls"onspans,alias = "turns"ontraces. Existing config files with old keys still load. Note: serialize emits new names only (one-way migration), which is the intended direction. - ClickHouse
span_idscolumn/struct alignment:services.rs:483SQL aliasspan_ids AS span_idsmatchesTurnEndpointRow.span_idsfield at services.rs:135. No deserialization drift. - DuckDB
kindbackfill:ALTER TABLE spans ADD COLUMN IF NOT EXISTS kind VARCHAR DEFAULT 'llm'— DuckDB backfills existing rows with the default; migration test atmigrations.rs:721-724assertskind='llm'on the pre-existing row.
🤖 Reviewed by the review bot • workflow run
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
agent_turns→traces,llm_calls→spans; columntraces.call_ids→span_ids; a new forward-lookingspans.kindcolumn(always
'llm'today) leaves room for wire-visible tool spans. Both DuckDBand ClickHouse auto-migrate in place on
init()— an idempotentdetect-then-rename runs before
CREATE TABLE IF NOT EXISTS, so existingdatabases upgrade with zero data loss (no file deletion needed).
RENAMEhasno
IF EXISTS, so each is guarded by a table/column existence check.AgentTurn→Trace,Turn*/Call*DTOs →Trace*/Span*) and everyStorageBackendmethod (write_calls→write_spans,query_turns→query_traces, …) renamed across the workspace.The
h-turncrate dir name is kept (not a contract)./api/traces*+/api/spans*(the child/api/traces/{id}/spansreplaces/calls). The pre-rename/api/agent-turns*and
/api/llm-calls*keep working as deprecated aliases carrying an RFC8594
Deprecationheader, so an existing external consumer is not broken. Thefirst-party console moves to the canonical paths.
calls/turns→spans/traces, with serdealiases so existing config files keep loading.
Compatibility
Old DB files, old config keys, and old API routes all keep working.
sessionstays
session(the/api/agent-sessions*routes are unchanged).Tests
rename,
span_idsJSON preserved byte-for-byte,kindbackfilled to'llm',and zero row loss.
Deprecation+Linkheaders.cargo test --workspace(+--features fault-injection) green; criterionbenches compile.