refactor(agentex): remove the legacy Postgres spans API, table and UI reader - #430
mohammadatallah-scale wants to merge 9 commits into
Conversation
… reader Agent spans live in Scale GenAI Platform through the SDK's SGP tracing processor, so the Postgres-backed /spans routes, ORM model, repository, use case, schemas and tests go, and a migration drops the spans table. The UI traces sidebar was the last reader. It now fetches the task's trace from the platform through a scoped same-origin BFF route, the same way feedback already reaches the platform. The regenerated OpenAPI spec drops the spans surface, so the next SDK generation removes the client resource. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs.
|
| ❗ Reference/NotFound: Missing reference: `#/components/schemas/Span` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /spans` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `patch /spans/{span_id}` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans/{span_id}` |
⚠️ agentex-sdk-typescript studio · code · diff
Your SDK build had at least one "error" diagnostic, which is a regression from the base state.
generate ❗(prev:generate ⚠️) →build ⏭️→lint ⏭️→test ⏭️New diagnostics (5 error)
❗ Reference/NotFound: Missing reference: `#/components/schemas/Span` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /spans` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `patch /spans/{span_id}` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans/{span_id}`
⚠️ agentex-sdk-python studio · conflict
Your SDK build had at least one new error diagnostic, which is a regression from the base state.
New diagnostics (5 error)
❗ Reference/NotFound: Missing reference: `#/components/schemas/Span` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /spans` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `patch /spans/{span_id}` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /spans/{span_id}`
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-09-14 19:09:04 UTC
…latform search with the browser The traces query keyed on the task id alone, so a deep link that switched the account while keeping the task could show the other account's cached spans. The BFF route also let a cancelled browser request keep the platform search running, unlike the Agentex proxy next to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
P3 PR body - "SDKs older than 0.13" is wrong, the default processor left the SDK in 0.11.1. Left as is: the two applied migrations whose docstrings still name the deleted runbook, since applied migrations are not edited. |
…pt short pages The platform defaults an omitted search window to the last 90 days and refuses a page that exceeds its byte budget unless the caller opts into short pages, so an old or a heavy task read as empty or as an error. The sidebar now passes the task's creation time, which the proxy turns into a 90-day window, and opts into short pages. The header no longer loads spans to learn the trace id, which is the task id by construction. The truncation notice names the Investigate link only when it renders, and CI now runs the UI unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The raw fetch behind the traces sidebar returned a 401 to the user when the access token had expired, where the SDK client refreshes the session and retries. The hook now does the same, only when login is enabled. The migration docstring also stops claiming that nothing writes the table and that the drop always fits the statement timeout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
andrewvo-scale
left a comment
There was a problem hiding this comment.
Blocking: the new creation-anchored 90-day query in agentex-ui/app/api/traces/[traceId]/spans/route.ts silently excludes spans emitted after roughly day 90. Agentex explicitly supports Temporal workflows that run for days, weeks, months, or indefinitely, and later turns keep the same task/trace ID. Because has_more is computed only inside this bounded request, the sidebar can present an apparently complete but stale trace with no warning. Please cover successive windows of at most 90 days, or deliberately choose and surface a truncated window so omitted history is explicit.
xsfa
left a comment
There was a problem hiding this comment.
P2 (non-blocking) — the creation-anchored 90-day window is silent, and the fix can lean on the platform's own truncation contract. This corroborates the open change request on this head. agentex-ui/app/api/traces/[traceId]/spans/route.ts:16-28 pins to_ts = min(now, created_at - 5min + 90d - 60s), so a task older than ~90 days loses every later span (re-ran the arithmetic: 91 days old drops the last day, 200 days old drops the last 110). has_more is computed inside the requested window, hooks/use-spans.ts:42-45,112 keep only items/has_more, and components/traces-sidebar/traces-sidebar.tsx:89-95 renders a notice only on hasMore, so nothing signals the cut. Two facts for the fix: the v5 search API clamps an over-wide window and reports window_truncated / effective_from_ts / effective_to_ts on SpanSearchPage rather than refusing it (the route.ts:14 comment and the resolved self-review thread assume a rejection), so sending to_ts = now (or omitting it) and passing those fields through gives the sidebar an explicit "spans before X not shown" notice without a multi-window walk; and route.test.ts:83-97 pins the capped arithmetic (from = 2026-01-01, asserts toTs - fromTs === WINDOW_MS), so it moves with the design and needs a case with from older than 90 days asserting the truncation signal.
P2 — the empty-state copy renders while the task is still loading. traces-sidebar.tsx:32 leaves createdAt undefined until useTask resolves, use-spans.ts:114 disables the query on that, and a disabled TanStack v5 query reports isLoading = false, so line 83 shows "No spans found for this task" until the task round-trip completes (probe on this head: useSpans('task-1', undefined) -> isLoading: false, spans: 0; base fetched immediately and showed "Loading traces..."). Treat "task unknown" as loading (pending = isLoading || (taskID !== null && createdAt === undefined)) and pin it; the existing "waits until the task creation time is known" test passes either way.
P2 — the platform search fires on every task view with the sidebar collapsed. components/agentex-ui-root.tsx:28,148 mounts TracesSidebar closed by default, useSpans at traces-sidebar.tsx:33 has no isOpen term, and with task-header.tsx:40 now traceId = taskId the hidden panel is the query's only consumer. Base was equally eager but hit the local table; this is now a POST /v5/spans/search hydrating up to 100 spans' input/output/metadata per (task, account) per page load. Gate enabled on isOpen; the cache key can stay so the result is reused once opened.
P2 — the drop also locks tasks, and the irreversible drop has no operator procedure. DROP TABLE spans (2026_09_11_1239_drop_spans_78384970fed5.py:39) cascades to fk_spans_task_id_tasks, whose referential-action triggers live on tasks; PostgreSQL RemoveTriggerById opens that relation with AccessExclusiveLock (REL_17_STABLE src/backend/commands/trigger.c), held to commit, so the migration waits up to lock_timeout=3s on tasks, queues every task read and write behind it, and retries on the next start if a longer tasks transaction wins. The docstring (lines 13-20) covers the spans lock only. Separately, the migration runs unattended on pod start while the PR body says "export the table if its history matters" and this PR deletes the only spans runbook: a short pre-release procedure (confirm no live writers, pg_dump -t spans if wanted, then deploy) or a rename-now / drop-next-release two-step would make a missed export recoverable. Name the tasks lock in the docstring either way.
Reviewed exact head caeb229 on base 53f82a2: full 26-file diff (git diff --find-renames 53f82a21...caeb229c) plus CLAUDE.md, CONTRIBUTING.md and .cursor/rules/* at both SHAs; on a git archive caeb229c agentex-ui tree, vitest run of route.test.ts + use-spans.test.tsx (17/17) and the full agentex-ui suite (114/114); red/green: trace_ids -> trace_id fails 1 route test, a changed BFF path fails 2 hook tests; renderHook(useSpans('task-1', undefined)) loading probe; window arithmetic re-run for 91/200-day-old tasks; python3 agentex/scripts/ci_tools/migration_lint.py on the new migration (no findings) and an Alembic graph walk (single head 78384970fed5 revising c4e8b2a7f91d); git grep at head for SpanORM|span_repository|spans_use_case|agentexClient.spans (none outside two applied-migration docstrings); PostgreSQL REL_17_STABLE tablecmds.c/dependency.c/trigger.c for the FK-trigger lock chain.
Agentex UI - Lint & Typecheck, wherenpm run test:runnow runs, is not a required status check (the ruleset requires only the PR-title lint andCI Status Check, andci-statusdoes notneedit), so the new UI tests are advisory until the job is folded intoci.ymlor the ruleset.- The Stainless preview on this head reports
Reference/NotFound #/components/schemas/Spanand fourEndpoint/NotFoundfor/spans: the hosted project config still declares the resource, so its removal belongs in the landing sequence (scale-agentex-python#517, this PR, then the config) before an SDK release is cut. - Agents on SDK <= 0.11.0 send spans through the async queue, which catches and logs the 404 per drained batch, so "loses their traces" undersells the error-log volume; one line in the body or docstring sets that expectation.
The history stays recoverable until an operator confirms it is not needed, and a rename locks only spans: dropping cascaded to the foreign-key triggers on tasks, which took an exclusive lock there too. A later revision drops spans_legacy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n notice A window capped at 90 days after the task's creation silently dropped every later span of a long-running task. The search now starts at creation and leaves the end open, the platform clamps a wider window to its newest 90 days and reports it, and the sidebar names the instant before which spans are not shown. The sidebar reads as loading until the task is known, and it only searches the platform while it is open. The UI typecheck, lint and tests move into the required CI status check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@andrewvo-scale @xsfa Landed in 0b37028 and c577637. P2 90-day window: the search now starts at the task's creation and leaves the end open, so a long-running task's later turns are read. The platform clamps a wider window to its newest 90 days and reports it, and the sidebar now says "Spans before are not shown" from that signal, with route and hook tests on a 200-day-old task. |
Addressed and pushed. Dismissing the stale review so the PR stops reading as blocked. A fresh approval is still required to merge.
|
Blocking: this migration and current |
…d run the UI job on workflow changes The rename only fires when the old table exists and the new name is free, in both directions, so a re-run after a partial apply does not fail on the taken name. The change filters now include the workflow file, so a PR that edits the CI jobs runs them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
main added a migration after this branch was cut, so the merged tree had two Alembic heads and every server failed to start. The rename now revises that migration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
xsfa
left a comment
There was a problem hiding this comment.
Approving exact head c96aea5 on base b0e3443. Independently verified: git diff --find-renames b0e34432...c96aea5c (27 files, no renames) plus CLAUDE.md, CONTRIBUTING.md and .cursor/rules/* at both SHAs (the three CLAUDE.md hunks only drop references to artifacts this PR deletes); the Alembic graph rebuilt from every versions/*.py is linear with one head (78384970fed5 -> b7d3e1f4a2c6); python3 agentex/scripts/ci_tools/migration_lint.py on the new migration (no findings); on a git archive c96aea5c agentex-ui tree, vitest run of route.test.ts + use-spans.test.tsx (20/20) and the full agentex-ui suite (117/117), with red/green mutations for each PR-body claim (trace_ids -> trace_id, BFF path, dropped truncation flag, ignored open gate each fail); git grep at head for SpanORM|span_repository|spans_use_case|agentexClient.spans|AgentexTracingProcessor (none outside applied-migration docstrings); openapi.yaml is removal-only with no dangling Span refs; the v5 search contract the BFF depends on (allow_short_pages, trace_ids body, start_timestamp sort, window_truncated/effective_from_ts) confirmed at scaleapi master after #161225; BFF credential handling probed with the real route.ts + bff.ts (client authorization dropped, _jwt stripped, upstream set-cookie/location not forwarded, hostile traceId JSON-escaped into the body only).
Non-blocking:
- Rolling deploy: pods still on the pre-#430 image answer
/spanswith 500 (relation "spans" does not exist) from the rename commit until they terminate, not the 404 the docstring (lines 10-12) and body describe; pre-0.11.1 SDK clients retry 5xx twice, so each deploy hosting old agents gets a bounded 500 burst. One docstring line sets that expectation, or the rename can ride the release after the route removal. _rename_if_only_source_existsguards onto_regclass('public.{source}')(lines 47-48) whileALTER TABLE {source} RENAME(line 49) resolves throughsearch_path; on a non-publicsearch_paththe guard is NULL and 78384970fed5 records itself applied without renaming. An unqualifiedto_regclass('{source}'), orcurrent_schema()as c4e8b2a7f91d uses, keeps both halves consistent.spansKeys.byTaskIdis now['spans','task',taskId,accountId,createdAt], butuse-task-messages.ts:168,:256anduse-task-subscription.ts:106still invalidate['spans', taskId], which prefix-matches nothing (already true at base), so with the globalstaleTime: Infinitythe sidebar never refetches when the agent finishes. A sharedspansKeys.byTask(taskId)prefix plus a test that the invalidation reaches the query closes it.
|
Closing. The Agentex team owns this removal and will sequence it against the customers still on the legacy tracing path. The branch stays as a working reference: routes and table retired, sidebar reading from the platform, span model owned by the library. |
Important
Pending Mohammad's self-review. This note is removed by a human, not automation.
The problem. Agentex keeps its own Postgres spans store and API. Scale GenAI Platform has held agent spans since the SDK's SGP processor became the default, so only the UI sidebar still read this copy.
The fix.
Removed from the Agentex API and database:
POST /spansGET /spansGET /spans/{span_id}PATCH /spans/{span_id}spansspans_legacy, dropped by a later revision once its history is confirmed unneededHow the traces sidebar gets its data now:
GET /api/traces/{taskId}/spans, a Next.js route that attaches the platform credentials server-sidePOST /v5/spans/searchwith the task id as the trace id, first 100 spans in start order, window from the task's creationBlocked on scaleapi/scale-agentex-python#517 first. Agents on SDKs older than 0.11.1 write here by default, so a deployment hosting them loses their traces (a logged 404 per batch) until they move to SGP.
Test plan
The PR appears safe to merge.
What we checked:
idfilter remains and is added to the same query as the metadata condition.c4e8b2a7f91dtob7d3e1f4a2c6to78384970fed5. The index also runs concurrently inside an autocommit block.Summary
Diagram
sequenceDiagram participant U as User participant UI as Traces sidebar participant BFF as Next.js trace route participant S as User session participant P as Platform spans API participant DB as PostgreSQL Note over DB: Alembic renames spans to spans_legacy U->>UI: Open a task's traces UI->>UI: Read task creation time and selected account UI->>BFF: "GET /api/traces/{taskId}/spans?from={createdAt}" BFF->>S: Read user credentials S-->>BFF: Access token or identity cookie BFF->>P: POST /v5/spans/search with task trace ID Note over BFF,P: Search starts at task creation minus five minutes P-->>BFF: First 100 spans and window flags BFF-->>UI: Stream JSON response UI-->>U: Show spans and any cutoff notice opt User closes or changes the task UI-xBFF: Cancel request BFF-xP: Forward cancellation BFF-->>UI: 499 endReviews (5) · Last reviewed commit: "fix(agentex): revise the spans rename fr..."