Skip to content

perf(agent-sessions): detect agent traces on a filtered span index, not a raw scan - #692

Open
JeremyFunk wants to merge 5 commits into
mainfrom
feat/ai-trace-index
Open

perf(agent-sessions): detect agent traces on a filtered span index, not a raw scan#692
JeremyFunk wants to merge 5 commits into
mainfrom
feat/ai-trace-index

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Why

The Agent Sessions list times out (the gateway kills it at 15s) on 12–24h windows, and the goal is 30-day scans. The cost is entirely in detection: agent traces are found by mapContains(SpanAttributes, 'maple_ai.vendor.id') over raw traces, and that predicate cannot be indexed at production shape — GenAI spans are ~0.01% of rows but arrive continuously, roughly one per index granule, so the mapKeys(SpanAttributes) bloom prunes nothing and the scan decompresses the fat Map column for every span in the window (~70M/day). Measured in production 2026-08-29: ~3.6s for a one-hour window; a full day dies at the kill. Windows before GenAI stamping existed return instantly — the bloom only wins when the key is absent from whole granules — which is how this stayed cheap until the spans arrived in volume.

What

ai_trace_index — a filtered projection (the error_events shape from docs/warehouse-rollups.md): only vendor-stamped spans, maple_ai.* identity and failure attributes pre-extracted to plain columns, (OrgId, Timestamp, TraceId) sort key, 30-day TTL. ~10k narrow rows/day at current volume.

  • datasources.ts + materializations.ts: the table and ai_trace_index_mv. The write filter is rendered from the same MAPLE_AI_* domain constants the read side now imports, so the two cannot drift silently.
  • ClickHouse migration 0023 (requiredForIngest: false — nothing writes the table directly) and local-store schema v13 with the v12 -> v13 edge (purely additive: the v13 bootstrap creates both objects, no data moves).
  • Read path: aiSessionListQuery's detection subquery and aiSessionFacetsQuery read the index. The per-trace fan-out still reads trace_detail_spans; the per-session window/spans reads are untouched — they prune by the session-id value, which the mapValues bloom still serves.
  • MCP describe_warehouse_tables notes steering ad-hoc SQL onto the index instead of the raw-traces mapContains scan.

What this does not do (deliberate, follow-ups)

  • No backfill: the MV fills forward, so list/facets under-report windows predating the deploy until the 30-day TTL ages the gap out (agent tracing shipped ~2026-08-20). A chunked BackfillSpec from traces is the follow-up if that month matters.
  • Fan-out unchanged: the second measured cost is per-partition — the same 90 trace ids cost 1.4s over a 2-day window and time out over 28 days — so very wide windows still pay for the trace_detail_spans fan-out. The follow-up selects the LIMIT 50 sessions from the index first and fans out only their traces with bounds derived from those sessions, which collapses the fan-out window regardless of the scan range.

Verification

  • packages/domain (622), packages/query-engine (1354), packages/query-engine-integrations (314, baseline re-recorded), apps/cli (517) all green; clickhouse:schema:check + tinybird:manifest:check clean.
  • ClickHouse e2e (CLICKHOUSE_E2E=1, server 26.2): full src/services/warehouse sweep green — the analyzer gate runs the rewritten queries against a database built by replaying the real migration chain through 0023.
  • Deploy note: Tinybird CD and the worker deploy race on main-push, so the two rewritten reads can error for the minutes until the datasource exists — the same window every MV-consuming change has.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Devin Review

…ot a raw scan

The Agent Sessions list and facets detect agent traces by
mapContains(SpanAttributes, 'maple_ai.vendor.id') over raw traces, and that
shape cannot be indexed: GenAI spans are ~0.01% of rows but arrive
continuously — roughly one per index granule at production volume — so the
mapKeys bloom index prunes nothing and the scan decompresses the fat Map
column for every span in the window. Measured against production on
2026-08-29: ~3.6s for one hour, dead at the gateway's 15s kill by a day,
while the page offers 30.

This adds `ai_trace_index` (migration 0023, local schema v13): a filtered
projection in the `error_events` shape holding only the vendor-stamped spans,
with the maple_ai.* identity and the failure attributes pre-extracted to
plain columns — ~10k narrow rows per day against 70M raw spans. The list's
detection subquery and the facets query now read it; the per-trace fan-out
still reads trace_detail_spans, and the per-session window/spans reads are
untouched (they prune by the session-id VALUE, which the mapValues bloom
still serves).

The index fills forward from its deploy: windows predating it under-report
on these two surfaces until the raw tables' 30-day TTL ages the gap out.
The backfill (a chunked BackfillSpec from traces) and the fan-out
restructure that collapses the remaining wide-window cost are follow-ups.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

const sessionTraceIds = from(Traces)
// No vendor-presence predicate: `ai_trace_index_mv` admits only spans with a
// non-empty vendor id, so membership in the table IS the detection predicate.
const sessionTraceIds = from(AiTraceIndex)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Older clusters lose agent sessions

When ai_trace_index is absent, aiSessionListQuery fails instead of using raw traces. BYO organizations can lose the list and facets indefinitely.

Prompt for agents
Add a raw-traces fallback for Agent Sessions list and facet reads when ai_trace_index is missing. Migration 0023 is requiredForIngest: false, and apps/api/src/workflows/ClickHouseSchemaApplyWorkflow.run.ts treats performance migrations as best-effort; BYO ClickHouse clusters can therefore lack the table indefinitely. The list and facets handlers in apps/api/src/routes/internal/ai-sessions.http.ts currently execute only the indexed queries. Follow the existing missing-table detection and fallback pattern in apps/api/src/services/warehouse/missing-table.ts and apps/api/src/routes/internal/query-engine.http.ts. Preserve the previous raw traces predicates and identical result semantics for both aiSessionListQuery and aiSessionFacetsQuery.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…t is absent

Adversarial-review fixes for the ai_trace_index PR — the two serious gaps
plus the paper cuts found alongside them:

- The repo's documented classic MV failure (a write filter and a read guard
  that agree with each other and disagree with reality → a 0-row table no
  test can see) had no guard here: every new test asserted SQL text or
  schema shape. New ClickHouse e2e suite inserts vendor-stamped spans into
  `traces` on a real-migration database and asserts the MV materialized
  them — per column, because a TO-table view inserts by name and a mistyped
  alias silently fills '' — then runs the compiled list query end to end.
- A BYO-ClickHouse org only gains the table when an admin applies migration
  0023, and the apply workflow treats requiredForIngest:false migrations as
  best-effort — so the list/facets reads could 502 indefinitely where they
  previously worked. They now degrade to an empty page via the existing
  missing-table pattern (isMissingAiTraceIndex + Effect.catchIf), the same
  face the fill-forward index shows for pre-deploy windows.
- Register the ai_trace_index pair in the materialized-projection-order gate.
- Fix a vacuous assertion (the no-filters test still checked for the old
  SpanAttributes spelling of the vendor filter, which can never appear).
- Correct two comments that overstated the code: the index's failure columns
  are written for the planned limit-first restructure, nothing reads them
  yet; and aiSessionWindowQuery is a value-pruned raw-traces read, not "the
  detection scan".
- Refresh the stale MV/datasource counts in docs/warehouse-rollups.md.
Round-2 adversarial review of the degrade found the one thing wrong with
it: it was silent. An empty 200 from a cluster that lacks ai_trace_index is
indistinguishable from "no agent traces" — the exact 0-row blind spot the
materialization e2e was added to prevent, reintroduced at the HTTP layer,
and reachable in production if one Tinybird CD leg lags the worker deploy.
The degrade now logs a warning (with the org) and stamps the handler span
with the same `query.rollup.fallback` flag `makeRollupFallback` uses, so
the condition is visible without changing the response contract.

Locked in with route tests: empty 200 for the missing-index error on both
the list and the facets, and — the half that keeps the predicate honest —
a 502 preserved for every other WarehouseConfigError (bad DSN and friends).

The e2e also grew the two assertions the reviewer showed were reasoned
rather than proven: rows decode through the query's own row schema exactly
as production does, and OrgId/Timestamp are asserted per column with a
foreign-org vendor span that must materialize under its own OrgId and never
surface in the org-scoped list.
…parately

Round-3 audit: the degrade fixture satisfied BOTH disjuncts of
isMissingTable at once, so neither was individually pinned — and the
message-regex half is the only one the managed Tinybird fleet exercises
(its "Resource … not found" carries no clickhouseType). The list test now
fails on the gateway shape, the facets test on the direct-ClickHouse
UNKNOWN_TABLE shape, so "simplifying" either half of the predicate breaks
a test. Also reattach the group's doc comment (the helper's JSDoc had
displaced it) and fold a duplicate import.
Review fallout on `ai_trace_index`, all of it subtraction.

Six columns, not ten. `VendorVersion`, `StatusCode`, `ErrorType` and
`ResponseStatus` had no reader: detection selects the trace-id set, the
facets group on `SessionId` and count `VendorId`/`ServiceName`, and every
other fact about an agent span — its status, its failure attributes, its
vendor version — is read per-trace off `trace_detail_spans`, which the
fan-out already touches. They were carried for a restructure that has not
happened, justified by the cost of a second backfill of a table no
customer has read yet. Both halves of that argument are hypothetical, so
the columns go and the table is exactly its two callers' shape.

The missing-index degrade goes with them. It turned an absent table into
an empty 200, and `isMissingTable` matches any `UNKNOWN_TABLE` on the
ClickHouse path without consulting the table name — so a cluster missing
`trace_detail_spans`, which `aiSessionListQuery` also reads, would have
rendered "no agent sessions" forever behind a warning naming the wrong
table. The test pinned that shape rather than catching it: its message
names no table at all, and neither degrade test asserted the warning or
the span flag the route's own doc called the difference between loud and
silent — the harness disables the logger, so they could not. Deleting the
path restores the pre-existing 502, which says the schema is not applied
and is the answer an operator can act on. `ai-sessions.http.ts`,
`ai-sessions.http.test.ts` and `missing-table.ts` are untouched by this
branch again.

The remaining edits are comments that stopped being true. The MV no
longer claims `hasVendorId` as its read-side twin — this branch deleted
it, and membership is the guard now. The e2e header no longer says a
mistyped alias fills its column with '': a `TO`-table view maps by name,
so an unmatched alias is a hard THERE_IS_NO_COLUMN at CREATE. The catalog
note drops a hardcoded deploy date that is wrong for every BYO cluster,
and points at `trace_detail_spans` for the attributes this table no
longer carries.

Regenerated the schema, manifest, insert mappings and the v13 local
snapshot; the local identity moves af29f0e6 -> b0cb5bfd.
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