feat(history): consolidate durable costs and asset previews - #78
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Manual branch preview is ready: https://console-psra6vzuc-livepeer-foundation.vercel.app The branch-scoped ASSET_URL_SIGNING_SECRET and ASSET_PROXY_ALLOWED_HOSTS settings are configured. Vercel build and deployment completed successfully; the original automatic Vercel check remains marked canceled from the dashboard, while Validate Console and CodeQL are green. The paid end-to-end cost smoke test remains gated on PymtHouse returning the caller-supplied job_* correlation ID. |
|
Pulled the existing trial-observability pieces together in 65d1bd9:
Local test, Console test, lint, typecheck, and production build are green. The additive migration rollback rehearsal correctly stopped because the available preview DATABASE_URL is the least-privilege runtime role. Before redeploying this head, the approved Neon owner connection must apply drizzle-baseline/0001_run_observability_and_lineage.sql and grant the runtime role SELECT/INSERT on run_usage_receipts and run_asset_links. No production database was queried or changed. |
|
Preview verification is ready at https://console-history-cost-assets.vercel.app After signing in, open Calls and choose Create preview verification records. It creates three records scoped to your account: a single receipt, a two-receipt aggregate with asset reuse, and an unmatched/failed run showing The preview database migration and least-privilege runtime access have been verified. Smoke checks pass, and all GitHub Actions checks are green. The repository Vercel check is still marked failed only because its automatic deployment was canceled from the Vercel Dashboard; the linked manual preview built successfully from |
|
Updated preview behavior at https://console-history-cost-assets.vercel.app No setup button is required now. On the first authenticated History read, the preview automatically provisions an idempotent, account-scoped dataset and returns a matching preview usage balance. The visible dataset contains four richer records covering image generation, asset reuse, text inference, and a provider failure. Run drawers include stored media, prompts, parameters, provider results/timing, costs, receipts, and input/output lineage. Superseded button-era fixture rows are hidden in preview responses without deleting Neon data. Verified in the deployed UI: $5 preview balance, four History rows, and a Flux Schnell drawer with image, prompt, seed, image size, safety setting, $0.0100 cost, and 1.28s render status. This supersedes my earlier instruction to click Create preview verification records. |
|
@eliteprox this is ready for your approval. It folds #75 and #77 together and now has a reviewer-ready Neon-backed preview at https://console-history-cost-assets.vercel.app The preview auto-provisions four account-scoped History runs, so there is no setup button or paid request required. The main review points are:
Validate Console and CodeQL are green. The stable preview is built from current head The remaining release prerequisite is still the downstream PymtHouse correction that returns the Console-supplied |
There was a problem hiding this comment.
🟡 Changes recommended
The billing sync endpoint has a confirmed loop-control/performance defect (matched IDs are never removed from the wanted set), and there’s an easy type-safety fix that should be applied before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR consolidates run History cost computation and first-party asset preview handling by persisting authoritative billing receipts in Neon, deriving a single billing summary for both the History table and detail drawer, and proxying asset playback through owner-bound signed URLs. It also replaces the legacy tooltip implementation/Radix usage with registry-aligned Base UI primitives and moves provider schema loading off the run-detail critical path.
Changes:
- Persist append-only billing receipts (
run_usage_receipts) and surface a Neon-derived billing summary (billing) in run list + detail flows. - Introduce first-party asset signing + proxying (
/api/assets/:id) and rewrite captured run payloads to remove provider media URLs. - Add preview-only fixture seeding and UI/contract coverage for history, billing sync, asset security, and tooltip/dialog primitives.
File summaries
| File | Description |
|---|---|
| tests/integration/run-records.test.ts | Extends integration coverage for durable asset lineage (run_asset_links) and receipt aggregation into billing. |
| tests/integration/mcp-assets.test.ts | Updates asset serialization expectations to use first-party signed URLs. |
| tests/contracts/ui-primitive-dependencies.test.ts | Adds contract test preventing Radix dependency/import reintroduction in UI primitives. |
| tests/contracts/session-allowance.test.tsx | Verifies preview usage can be served without contacting PymtHouse when preview fixtures are enabled. |
| tests/contracts/run-security.test.ts | Expands redaction/output extraction contract to cover *_urls array outputs. |
| tests/contracts/run-http.test.ts | Verifies preview fixture seeding occurs before listing runs and legacy preview rows are filtered. |
| tests/contracts/run-execution.test.ts | Ensures durable execution returns first-party asset URLs and does not leak provider media URLs/status URLs. |
| tests/contracts/preview-run-fixtures-route.test.ts | Adds contract coverage for preview-only fixture creation endpoint and seeded receipt lineage. |
| tests/contracts/home-history-surface.test.tsx | Updates History surface tests for tooltip provider usage and Neon-derived cost behavior. |
| tests/contracts/call-detail-media.test.tsx | Adds detailed UI contract coverage for media stage behavior, expiry, tooltips, and captured field presentation. |
| tests/contracts/billing-sync-route.test.ts | Adds contract tests for bounded, exact-match billing sync and run-id limits. |
| tests/contracts/asset-proxy.test.ts | Adds security contract tests for signed asset proxying (range requests, allowlist, private IP refusal, redirect validation). |
| tests/contracts/admin-runs-preview.test.tsx | Updates admin drawer expectations to present captured fields without raw JSON section labels. |
| tests/contracts/account-history.test.ts | Ensures cost lookups omit year window to preserve current-month ticket feed correctness. |
| tests/contracts/account-history-route.test.ts | Updates route contract for new receipt metadata fields and recordRunUsage return type. |
| lib/runs/types.ts | Introduces billing summary types, input schema types, and extends asset shape with role/displayName. |
| lib/runs/store.ts | Implements receipt persistence + aggregation, asset lineage linking, and list/detail billing attachment. |
| lib/runs/preview-fixtures.ts | Adds preview-only deterministic seeding of owner-scoped fixture runs + receipts and synthetic account usage payload. |
| lib/runs/outputs.ts | Extends output extraction to include image_urls, video_urls, audio_urls. |
| lib/runs/execute.ts | Rewrites execution payload to return first-party asset URLs and strips provider URL leakage from returned data. |
| lib/runs/billing.ts | Adds decimal-safe aggregation helpers for receipt totals and billing summaries. |
| lib/runs/billing.test.ts | Validates decimal-safe arithmetic and receipt aggregation behavior. |
| lib/mcp/store.ts | Switches asset serialization to first-party signed URLs and adds getAssetSource for proxying. |
| lib/mcp/mcp-server.ts | Updates MCP asset listing serialization to include principal-bound signed URLs. |
| lib/mcp/fal-input-schema.ts | Adds schema parsing + caching for Fal OpenAPI input metadata to support History UI help. |
| lib/mcp/fal-input-schema.test.ts | Tests Fal schema parsing and capability resolution behavior. |
| lib/db/schema/runs.ts | Adds run_usage_receipts table for normalized, queryable billing evidence. |
| lib/db/schema/mcp-assets.ts | Adds run_asset_links table for durable input/output lineage edges. |
| lib/db/full-baseline.test.ts | Updates baseline/journal expectations to include additive migration(s). |
| lib/console/useRunHistory.ts | Loads run input schema asynchronously after detail fetch to remove it from critical path. |
| lib/console/usage-capability-display.ts | Improves model display names by stripping provider/catalog namespaces. |
| lib/console/types.ts | Adds capabilityId to activity rows for pricing/joining context. |
| lib/console/signed-ticket-activity.ts | Maps signed tickets to activity rows with capabilityId set for consistent joins. |
| lib/console/run-activity.ts | Simplifies cost mapping to rely on persisted billing summary instead of inferred events/feeds. |
| lib/console/run-activity.test.ts | Updates tests to reflect billing summary-driven cost display, including multi-receipt aggregation. |
| lib/console/pymthouse-bff.ts | Adds recentWindow option to avoid from/to windows that drop current-month pricing tickets. |
| lib/console/dev-mock.ts | Adds dev-mock run history/detail fixtures, schema mocks, and local /api/assets/* redirects for previews. |
| lib/console/capability-modality.ts | Adds capabilityPresentation and modality labels to improve user-facing titles. |
| lib/console/capability-modality.test.ts | Adds tests for capability title cleanup and namespace-stripped model display names. |
| lib/console/billing-receipts.ts | Centralizes and tightens sanitization of upstream billing receipt fields before persistence. |
| lib/console/activity-output-match.ts | Removes fuzzy/orchestrator-id inference; only exact gateway correlation is treated as authoritative. |
| lib/console/activity-output-match.test.ts | Updates tests to ensure non-exact tickets do not attach assets. |
| lib/console/activity-assets.ts | Rewrites attached output URLs to use first-party signed asset URLs. |
| lib/assets/public.ts | Implements asset URL signing, captured payload rewriting, and provider URL stripping for public run detail responses. |
| lib/assets/public.test.ts | Tests first-party URL rewriting and provider URL redaction behavior. |
| drizzle-baseline/meta/_journal.json | Adds new migration entry to the baseline journal. |
| drizzle-baseline/0001_run_observability_and_lineage.sql | Adds schema migration creating run_asset_links and run_usage_receipts with indexes/constraints. |
| docs/early-access/run-records.md | Updates documentation to describe normalized receipts, sync bounds, lineage tables, and signed asset proxy behavior. |
| components/ui/tooltip.tsx | Adds Base UI tooltip wrapper components used across the app. |
| components/design-system/Tooltip.tsx | Removes legacy bespoke tooltip implementation. |
| components/console/ModalityChip.tsx | Introduces shared ModalityChip used by table and drawer. |
| components/console/CallsTable.tsx | Migrates tooltips to Base UI and adopts ModalityChip for consistent modality styling. |
| components/console/CallsSection.tsx | Adds bounded authenticated billing sync for visible runs while keeping History resilient to billing outages. |
| components/console/CallDetailDrawer.tsx | Migrates to Base UI dialog + tooltips, improves media stage behavior, and presents captured fields without raw JSON sections. |
| components/admin/RunsPreview.tsx | Configures drawer variant for admin presentation mode. |
| app/layout.tsx | Wraps the app in TooltipProvider for consistent tooltip behavior. |
| app/api/pymthouse/account-usage/route.ts | Serves preview usage payload when preview fixtures are enabled. |
| app/api/pymthouse/account-requests/route.ts | Uses receipt sanitization helper and enables recent-window behavior for correlation mode. |
| app/api/console/runs/route.ts | Seeds preview fixtures (preview-only) before listing and filters legacy preview records. |
| app/api/console/runs/preview-fixtures/route.ts | Adds preview-only authenticated endpoint to seed fixtures. |
| app/api/console/runs/billing-sync/route.ts | Adds bounded, exact-match billing sync endpoint for visible run IDs. |
| app/api/console/runs/[id]/schema/route.ts | Adds endpoint for loading run input schema metadata (Fal) off the detail critical path. |
| app/api/console/runs/[id]/route.ts | Returns publicRunDetail to ensure asset URLs are signed and provider URLs are removed. |
| app/api/assets/[id]/route.ts | Adds signed asset proxy route with origin/redirect validation and bounded caching. |
| app/api/admin/runs/[id]/route.ts | Applies publicRunDetail for admin run detail responses as well. |
| .env.example | Documents required asset signing/proxy env vars and preview fixture toggles. |
Review details
- Files reviewed: 66/67 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Production usage ids are CloudEvent 8-hex, not console job_* keys. Keep the current-month ticket feed and match by capability and time so the request drawer can show a real fee.
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
components/console/CallDetailDrawer.tsx:1142
- The admin asset list renders every asset ID as a button, including inputs, but this lookup searches only
outputAssets. Clicking an input therefore updatesselectedAssetand immediately falls back to the default output, making the button ineffective for keyboard and pointer users. Resolve the selected ID againstactiveDetail.assets(or render input IDs as non-buttons).
outputAssets?.find(
(item) =>
selectedAsset &&
selectedAsset.runId === row?.id &&
item.id === selectedAsset.id
components/console/CallDetailDrawer.tsx:1143
- This only prefers the first renderable image/video/audio, not the actual 3D preview.
detail()orders assets bycreatedAt, id, whiletransitionRun()inserts model, preview, and texture assets in one transaction, so the texture can sort beforepreview_imageand be rendered as the drawer's main image. Preserve the output ordinal/preview role in the detail payload and select the preview before texture assets.
const asset =
outputAssets?.find(
(item) =>
selectedAsset &&
selectedAsset.runId === row?.id &&
item.id === selectedAsset.id
) ?? outputAssets?.find((item) => /^(?:image|video|audio)(?:\/|$)/i.test(item.mediaType ?? "")) ?? outputAssets?.[0];
components/console/CallsSection.tsx:35
- The component accepts a
requestthat is treated as either a run ID or agatewayRequestIdinfoundbelow, but this hook always fetches/api/console/runs/${requestId}and that route looks up onlyruns.id. A/home?request=job_*deep link therefore finds the row but the detail request returns 404. Resolve the gateway ID to the run ID before calling the hook, or make the detail route support the same lookup.
const detail = useRunDetail(
"/api/console/runs",
requestId,
ownerKey,
isConnected
lib/runs/store.ts:198
runAssetLinksis the canonical lineage source, but this fallback overrides an explicit output link whenever the asset's producerrunIddiffers from the current run (or is null for legacy assets). Such an asset is returned asrole: "input", so it disappears fromoutputAssetsand cannot be previewed. Honor the link direction whenever a link exists, and userunIdonly for unlinked legacy records.
- Files reviewed: 86/91 changed files
- Comments generated: 1
- Review effort level: Lite
…ty sync path (#83) * refactor(auth): land signed-in users without the identity sync route Replace `identitySyncPath` and the deprecated `/api/identity/sync` route with direct `/home` redirects and a shared `signedInLandingPath` that chooses `/admin`, `/home`, or `/access-pending` from the signed-in identity and access decision. Also abort in-flight History pagination when reloading so “Load older history” cannot stay blocked, and include a deep-linked `?request=` run in billing-sync even when it is off the first page.
- Introduced a new `wrapperKey` function to identify and strip provider URLs under generic keys (output, data, result) during media sanitization. - Added tests to verify the correct stripping of provider URLs when wrapped in these keys, ensuring sensitive information is not exposed in public responses.
|
I think ready to merge, but I also am not sure this polling mechanism is the right way to get the usage into the db
|
There was a problem hiding this comment.
🟡 Changes recommended
Critical billing-validation and expiry-handling issues, plus related correctness and accessibility fixes, remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (10)
Previously missed (1) — in code that hasn't changed since the last review.
components/console/CallDetailDrawer.tsx:1529
- The admin detail renders the internal
errorCodedirectly as user-facing text. Codes such asprovider_capacityandexecution_outcome_unknownare implementation identifiers rather than helpful render status copy; map them to a stable human-readable label and keep the raw code only in an operator/debug affordance.
app/api/console/runs/billing-sync/route.ts:22
- A
nullJSON body dereferencesbody.runIds, and malformed JSON also escapes as a generic exception;runErrortherefore returns 503 instead of the documented 400invalid_run_queryresponse for malformed client input. Parse intounknown, validate that it is a non-null object with an array of IDs, and translate JSON parse failures toinvalid_run_query.
const body = (await request.json()) as { runIds?: unknown };
if (
!Array.isArray(body.runIds) ||
body.runIds.length > 50 ||
body.runIds.some((id) => typeof id !== "string" || !id || id.length > 160)
)
throw new Error("invalid_run_query");
components/console/CallDetailDrawer.tsx:192
- The video element exposes native controls but no accessible name (
aria-label,title, or an associated label), so assistive-technology users cannot identify which preview they are controlling. Pass the media title into this preview and use it as the accessible name.
<video
src={src}
controls={loaded}
playsInline
onCanPlay={() => setLoaded(true)}
components/console/CallDetailDrawer.tsx:227
- The audio element exposes native controls but no accessible name (
aria-label,title, or an associated label), so assistive-technology users cannot identify which preview they are controlling. Pass the media title into this preview and use it as the accessible name.
<audio
src={src}
controls={loaded}
onCanPlay={() => setLoaded(true)}
components/console/CallsSection.tsx:43
requestIdis passed unchanged as both a durable run ID foruseRunDetailand as a billing-syncrunIdsentry, but the matching logic below also acceptsgatewayRequestId. A legacy/home?request=<gatewayRequestId>therefore requests/api/console/runs/<gateway-id>and never refreshes billing, whilegetOwnRunonly looks upruns.id; normalize gateway IDs to the matched run ID or add an owner-scoped gateway lookup before loading the detail.
const ids = history.page?.items.map((run) => run.id) ?? [];
return requestId
? [requestId, ...ids.filter((id) => id !== requestId)]
: ids;
components/console/CallsTable.tsx:279
- The shared table always links a row to
/home?request=<row.id>, but the app Jobs tab suppliesMOCK_RECENT_REQUESTSrows whose IDs arereq-*, not durable Neon run IDs. Clicking any Jobs row therefore leaves the app page and opens a History detail request that returnsrun_not_found; provide an app-specific navigation/selection handler or disable the History link for those rows.
href={`/home?request=${row.id}`}
lib/console/session-user.ts:64
- This token acquisition runs inside the shared admission path used by unrelated pages and APIs such as the session profile, device approval, admin checks, and run history. A cold start or token refresh now makes each of those requests await a PymtHouse network call; failures are then swallowed, so the added latency does not even guarantee a usable token. Keep end-user token minting lazy in the BFF consumers that actually need it, or explicitly separate account provisioning from request admission.
lib/identity/signed-in-landing.ts:17 - When the identity is pending, this redirects to
/access-pendingwithout carryingreturnTo.app/access-pending/page.tsxuses that query to redirect an approved user, so a user who signed in for/keys,/usage, or another protected page is sent to the default/homeafter approval. Preserve the validated return path in the pending URL, asrequireConsolePagedoes.
lib/runs/preview-fixtures.ts:315 - This fixture introduces a fractional
endUserBillableUsdMicrosvalue, but the sharedmicrosToUsdhelper currently callsBigIntand returns zero for fractional micros, so Usage/Sidebar/consumption will display this valid fixture as no spend. Update the conversion helper to handle the decimal precision before relying on fractional fixture values.
lib/runs/store.ts:1189 - The fixture is considered complete once the dispatch event, receipts, and (where applicable) assets exist, but successful fixtures never require
runs.resultto be non-null. A partial successful row can therefore skip the repair path and leave the detail drawer with no captured result; include the result (and repair the missing value) in the successful-fixture completeness contract.
- Files reviewed: 102/107 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One critical, four moderate, and one nit finding remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (5)
components/console/CallDetailDrawer.tsx:952
- When a run has an internal
errorCodebut noerrorMessage(for examplequeue_unavailableorexecution_outcome_unknown), this renders the snake_case identifier directly in the user-facing drawer. Use a human-readable fallback (or a reviewed code-to-message map) instead of exposing the storage/API code.
{errorMessage ?? errorCode}
components/console/CallDetailDrawer.tsx:1532
- The admin drawer also exposes the internal
errorCodebefore the optional message, so values such asrecovery_horizon_exceededbecome user-visible implementation details. Render the reviewed error message when present and a generic human-readable fallback otherwise; the code can remain in server-side logs.
{activeDetail.errorCode}
{activeDetail.errorMessage
? `: ${activeDetail.errorMessage}`
: ""}
components/console/CallDetailDrawer.tsx:1532
FIELD_HELPis keyed by the rendered label, but this new field is labeledCostwhile the matcher still only recognizesPrice. As a result, the network-cost help tooltip is never attached, so keyboard users and anyone relying on the detail help lose the explanation for this value. IncludeCostin the matcher (or pass the same canonical field name to both places).
{activeDetail.errorCode}
{activeDetail.errorMessage
? `: ${activeDetail.errorMessage}`
: ""}
components/console/CallsSection.tsx:45
useRunHistoryappends older pages tohistory.page.items, but slicing here permanently excludes every row loaded after the first page. Because the billing-sync endpoint is called only for these IDs, accepted manifests for older visible rows never refresh and their table/detail costs can remain stale or—. Keep the endpoint's 50-ID limit, but batch all loaded IDs (or queue newly appended IDs) so pagination is eventually consistent.
const visibleRunKey = visibleRunIds.slice(0, 50).join(",");
components/console/CallsSection.tsx:48
- The ref is cleared while a selected detail is still loading, so billing sync can finish first and skip the
detailReloadRefcall at the sync handler. In that race the table updates but the open drawer keeps its stale cost until it is reopened; retain the selected request id as the fallback whiledetail.detailis null.
openDetailIdRef.current = detail.detail?.id ?? null;
- Files reviewed: 102/107 changed files
- Comments generated: 1
- Review effort level: Lite
…ing sync process - Introduced `refreshOwnedRunBilling` and `refreshOwnedRunBillingByJob` functions to handle billing updates for owned runs, ensuring accurate fee tracking and manifest usage. - Updated the billing sync route to utilize the new billing refresh logic, improving response handling and reducing unnecessary polling. - Removed the setTimeout polling mechanism in the CallsSection component to prevent redundant requests when usage is pending. - Enhanced tests to cover new billing refresh functionality and ensure correct behavior during billing sync operations.
- Introduced a new function, `publicErrorMessage`, to strip provider URLs from error messages and return null for non-string values. - Updated the `publicRunDetail` function to utilize `publicErrorMessage`, ensuring sensitive information is not exposed in public responses. - Added tests to verify the correct sanitization of error messages, confirming that provider URLs are removed and empty messages are handled appropriately.
There was a problem hiding this comment.
🔵 Needs a closer look
Seven unresolved moderate findings remain in media preview behavior, billing synchronization and acceptance, reload cancellation, and asset lineage.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (7)
components/console/CallDetailDrawer.tsx:1137
- The admin asset list renders input assets as selectable buttons, but this lookup searches only
outputAssets. Clicking an input therefore can never change the preview or set itsaria-pressedstate; search all assets for an explicit selection, then use output assets only for the default preview choice (or make input rows non-selectable).
const outputAssets = activeDetail?.assets.filter(
(item) => item.role !== "input"
);
const asset =
components/console/CallDetailDrawer.tsx:1222
- The wheel handler exempts only
[data-detail-scroll], so wheel events over the media stage fall through topreventDefault()and history navigation. BecauseMediaStageis itselfoverflow-auto, large images/JSON cannot be scrolled and can unexpectedly switch to the adjacent run; mark the media stage as an exempt detail surface (or otherwise limit navigation to the surrounding chrome).
window.matchMedia("(max-width: 1023px)").matches ||
isWithinDetailPanel(event.target)
) {
components/console/CallDetailDrawer.tsx:1143
- The default preview is chosen by media type alone, so when the first image/video/audio output is already expired or marked unavailable, the drawer selects it and
MediaStageshows “Media unavailable” even if a later output is still playable. Filter the default candidate byunavailableAtand a valid futureexpiresAt(while preserving an explicitly selected asset as a fallback) so the drawer actually prefers a renderable preview.
) ?? outputAssets?.find((item) => /^(?:image|video|audio)(?:\/|$)/i.test(item.mediaType ?? "")) ?? outputAssets?.[0];
components/console/CallsSection.tsx:71
- If the billing-sync request resolves before the initial detail request,
openDetailIdRefis stillnull, so this branch reloads the table but never reloads the open drawer. The drawer can therefore keep showing—(or an old cost) while the table has the newly persisted manifest cost. Track the changed run IDs until the detail has loaded, or trigger the detail reload from the active request ID/detail effect rather than relying only on this ref snapshot.
const openId = openDetailIdRef.current;
if (openId && result.changedRunIds.includes(openId))
detailReloadRef.current();
lib/console/useRunHistory.ts:141
reloadcreates a separate fetch controller but never stores it inappendController, while the effect cleanup only aborts the initial controller and that ref. If billing sync or Retry triggers a reload and the component unmounts or changes account/query, the reload continues consuming network/server work; repeated reloads can also overlap. Store this controller in the shared ref so the hook cleanup and subsequent reloads can cancel it.
lib/runs/billing.ts:55- This summary sums every priced manifest, including a manifest that is still only
prepared. SincerecordManifestUsagecurrently stamps matching rows without checkingaccepted, a stale or inconsistent aggregate for an unaccepted payment can be presented as a charge even though no accepted payment attempt exists. Restrictpricedto accepted manifests (while keeping the existing pending check for accepted-but-unpriced manifests) so only accepted attempts contribute to the run cost.
lib/runs/store.ts:198 - When a durable output reuses a legacy
mcp_assetsrow whoserunIdis null, the output edge is still recorded inrunAssetLinks, but this condition forces the asset toinputbecauseasset.runId !== row.id. The drawer then filters it out ofoutputAssets, so the persisted output is not previewable and its lineage is mislabeled; use the link direction when an edge exists and fall back torunIdonly for unlinked assets.
- Files reviewed: 104/109 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One critical and nine moderate unresolved findings remain, including media sanitization, preview selection, fixture, refresh, token-cache, and origin issues.
Get a fresh assessment by requesting another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (9)
app/api/console/runs/[id]/route.ts:15
- The list route seeds preview fixtures, but this detail route does not. On a direct preview deep link, the detail request can arrive before (or without) the list request and
getOwnRunthen returnsrun_not_foundfor a fixture that should exist, leaving the drawer unavailable. Seed fixtures in this route as well, guarded bypreviewFixturesEnabled(), before loading the requested run.
return Response.json(publicRunDetail(result), { headers: RUN_HEADERS });
components/console/CallDetailDrawer.tsx:1145
- The fallback always chooses the first renderable output by media type, even when that asset is already unavailable or its exact expiry has passed. If a run has an expired preview followed by a valid preview, the drawer selects the expired one and shows “Media unavailable” instead of the usable asset. Exclude unavailable/expired assets from this fallback while retaining the final fallback for runs with no playable output.
) ?? outputAssets?.find((item) => /^(?:image|video|audio)(?:\/|$)/i.test(item.mediaType ?? "")) ?? outputAssets?.[0];
let media = row
? mediaSpecForRow({
components/console/CallDetailDrawer.tsx:1143
- Choosing the first playable output does not reliably select a 3D preview.
detail()orders assets bycreatedAt, id, while output links carry the meaningful ordinal and inserts in one transaction can share a timestamp with random IDs; a texture can therefore precedepreview_imageand become the drawer's default. Preserve/use the output-link order or an explicit preview designation before applying the generic playable fallback.
) ?? outputAssets?.find((item) => /^(?:image|video|audio)(?:\/|$)/i.test(item.mediaType ?? "")) ?? outputAssets?.[0];
components/console/CallDetailDrawer.tsx:1404
- This renders every asset, including
role: "input", as a selectable button, butassetis resolved only fromoutputAssetsabove. Clicking an Input therefore changesselectedAssetwithout changing the preview oraria-pressedstate, making this control misleading; either make input rows non-interactive or include input assets in the supported preview selection.
<button
type="button"
aria-pressed={asset?.id === item.id}
onClick={() =>
setSelectedAsset({
components/console/CallsSection.tsx:44
- When a deep-linked
requestIdis not on the first 50-row page, this array contains 51 IDs. The billing-sync route rejects any body over 50 (app/api/console/runs/billing-sync/route.ts:15), so the refresh fails for the entire page rather than updating any visible run. Keep the request within the server limit or split the refresh into bounded batches.
? [requestId, ...ids.filter((id) => id !== requestId)]
: ids;
}, [history.page, requestId]);
components/console/CallsSection.tsx:92
- This component accepts
requestIdas either a durable run ID or agatewayRequestIdbelow, but it passes the raw value to bothuseRunDetailand billing sync. A deep link such as?request=job_*therefore calls an API that only looks upruns.id, and the billing refresh receives an ID thatrefreshOwnedRunBillingalso cannot match; the row may be recognized locally while its detail and cost never load. Resolve gateway IDs to the owner-scoped durable run ID before these requests, or make the server endpoints resolve both forms.
detail.detail &&
(detail.detail.id === requestId ||
detail.detail.gatewayRequestId === requestId)
lib/console/end-user-token-cache.ts:65
nowis captured before the asynchronous mint resolves, so a slow token response starts its cache lifetime too early. For a short-lived token this can make the freshly returned token fail the 30-second refresh-skew check immediately and can cause unnecessary remints; anchorexpiresAtto completion (while preserving the injectable clock used by tests).
lib/console/end-user-token-cache.ts:71- The failed-mint fallback compares the cached expiry with the
nowcaptured when the mint started. If the refresh request stalls until that token has actually expired, this branch still returns the expired token and the caller sends an invalid credential; compare against the current clock (or elapsed time from the injected clock) before falling back.
lib/runs/preview-fixtures.ts:62 - These fixture URLs are built from the request host, but
assetReferences/ownedAssetReferenceand every generated signed URL only acceptpublicOrigin(). On a normal Vercel preview alias that differs from the configured application origin, the variation's first-party input is not linked (and is later stripped as foreign), while returned links point at the other origin. Use one canonical origin for all fixture URLs and public asset URLs.
- Files reviewed: 104/109 changed files
- Comments generated: 1
- Review effort level: Lite
| } | ||
| const row = value as Record<string, unknown>; |
Summary
History now joins runs to PymtHouse billing through captured payment manifests, so a billing event ID such as
4096f6f3does not have to equal Console'sjob_*ID. Costs come from PymtHouse's existing per-manifest usage aggregates and are persisted in Neon for the same table/drawer presentation. This replaces the ten-page raw-receipt scan and model/time guessing.This PR also consolidates #75 and #77's History detail, first-party asset previews, durable asset lineage, and Base UI Dialog/Tooltip work.
Billing behavior
@pymthouse/gateway-web@0.3.5(published 2026-09-11, v0.3.5) is the validated payment-observer artifact. Upstream now ships the awaitedonPaymentcallback in ESM and CommonJS across single-shot, persistent, and failover paths, replacing the earlier local pnpm patch on 0.3.4. Console saves the payment manifest before charging and marks acceptance before awaiting provider completion. Persistence failures stop without triggering another paid attempt.run_payment_manifestsenforces one run per account-scoped manifest and stores replacement-based cumulative usage snapshots. Multiple paid attempts are summed once; repeat or stale refreshes cannot double-count or overwrite newer observations./api/v1/user/usage?groupBy=manifestendpoint over the relevant date range, including month boundaries. It matches exact captured manifests and refreshes while usage is pending. It does not scan unrelated receipt pages.Assets and UI
Live verification
An instrumented paid Flux Schnell request captured manifest
bdcd8fddat payment time. The authenticated PymtHouse aggregate and filtered receipt both returnedbdcd8fdd, with aggregate network cost2982USD micros ($0.002982). The receipt's gateway/event ID was4096f6f3, different from the caller-suppliedjob_manifest_probe_581020f9de434292.This verifies the manifest join for a successful single-shot run. SDK contracts cover persistent execution, paid failover, and persistence failures; transactional integration tests cover owner isolation, deduplication, replacement snapshots, stale responses, and table/detail parity.
Deployment prerequisites
0001_run_observability_and_lineage.sqland0002_run_payment_manifests.sqlapplied, with runtime grants verified on 2026-09-11. Production must apply both pending migrations before serving this revision: SELECT/INSERT onrun_usage_receiptsandrun_asset_links, and SELECT/INSERT/UPDATE onrun_payment_manifests. No additional migration is needed for the review fixes.ASSET_URL_SIGNING_SECRETandASSET_PROXY_ALLOWED_HOSTSconfigured.Migration 0002 was rehearsed and applied to the isolated runtime preview; production has not been migrated by this work. Database regression tests run only on the marked disposable Neon integration branch and clean up their test schemas/data. The existing manual preview at https://console-history-cost-assets.vercel.app predates this manifest amendment; its fixture rows demonstrate the earlier History/assets work, not the new callback path.
Copilot review fixes
availableUntilremains an availability guarantee, distinct from hardexpiresAt.Follow-up findings addressed
Validation
Local build warnings concern missing local Auth0 configuration and the existing Edge runtime deprecation. CI will rerun on the updated head; no new CI result is claimed here.
Supersedes #75 and #77. Neither original PR is closed by this update.