Skip to content

Dashboard Agent V1 — chat, reports, Investigate, Watch - #4418

Draft
kathiekiwi wants to merge 244 commits into
mainfrom
feat/dashboard-agent-flows
Draft

Dashboard Agent V1 — chat, reports, Investigate, Watch#4418
kathiekiwi wants to merge 244 commits into
mainfrom
feat/dashboard-agent-flows

Conversation

@kathiekiwi

@kathiekiwi kathiekiwi commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

An AI assistant in a side panel on every dashboard page, behind the dashboard-agent feature flag. It reads runs, errors, queues, deploys and health through the public API (read-only, delegated user token), answers with rich cards, and can keep watching things after the conversation ends.

What's inside

  • Foundation@internal/dashboard-agent-contracts (trigger:// URI grammar, intents, watch specs, block envelope), investigations + watches tables, head-start reliability fix, eval sample-rate gate.
  • Reportsget_report renders the deterministic health report as a card (metric grid, sparklines, Next steps button row); stale telemetry is flagged and never trusted for advice.
  • Investigate — hypothesis-driven investigation on a live card with system-owned identity and revisions; entry buttons on failed runs, errors, backed-up queues and waiting runs; code-grounded when a repo is connected; server-generated follow-ups (Show code, View similar, Watch for a repeat).
  • Watch (Dashboard Agent: Watch (background condition watches + wake notifications + alerts) #4456) — one-shot background watches with a compact creation card on run/queue/error/health pages: resolution + observed outcome model, exactly-once wake delivery, expiry sweep, optional investigate-on-attention, standing email/Slack/webhook alerts with one-click unsubscribe. Queue conditions: drain, above/below N, stalled, oldest-age SLA.
  • UI — blank-state hero (Ask AI) with a Tab-to-accept placeholder and colored smart prompts, fullscreen mode, one persistent agent spinner across all phases.
  • Smart prompts — page-aware chips on every env page (37 routes, 24 page kinds); investigate/status chips appear only on loader-backed abnormal state.
  • Tooling — navigation, TRQL queries with live charts, deploy correlation, docs answers; golden eval suite; seeder for a live playground project (db:seed:agent-examples, with --heartbeat / --degrade / --recover for demos).

How to review

GUIDEBOOK.md — 10-minute local setup and a hands-on walkthrough of every case. Component gallery at /storybook/agent-ui.

Notes

  • Everything is gated by canAccessDashboardAgent; no behavior change with the flag off.
  • The seeder's --heartbeat mode is a review-stand crutch and will be removed before merge.

ericallam added 30 commits July 13, 2026 20:38
…signals

Gauges are read inside the enqueue/dequeue Lua and returned on the script reply
as a 2-tuple; counters are cumulative odometers. The run-queue Redis carries no
metrics stream of its own.
…counters

entryOrderKey returns a string built with BigInt math so ordering stays correct at real epoch magnitudes. Odometer keys are namespaced by definition name. The consumer reports null lag for a missing consumer group instead of 0, and empty gauge values parse as NaN rather than 0.
…ng order keys

The wait-time quantile materialized view now excludes wait_ms = 0 rows so it matches the count aggregation. order_key accepts a string or a number. Migration comments no longer contain semicolons that split the migration into invalid statements.
…rride

The queues list tolerates a metrics query failure by rendering without metrics and logging a warning. UsageSparkline renders its total override even when every bucket is zero. The queue detail page returns 404 and its loader skips the metrics query when the feature flag is off. The seed script validates bucket size and only writes ClickHouse against a local host.
A bucket-led ORDER BY DESC combined with fillGaps emitted an ascending WITH FILL (positive step, ascending bounds), which produces invalid or empty fills. Skip the gap-fill rewrite for descending orders and let the plain descending query stand. Adds a DESC fillGaps test.
Packs the stream sequence with a 1e6 factor (was 1e5) so up to 1M entries per millisecond per shard fit before a seq could spill into the next millisecond's range, far above what a single Redis stream can produce. ms*1e6 stays within UInt64. Also fixes the webapp mapping test that still expected a numeric order_key after the switch to a BigInt-derived string.
The queues list and queue detail pages now use the shared TimeFilter (any preset period or a custom date range) and everything on the page follows it: header tiles, per queue metric columns, charts, and stats. The custom period buttons, hand rolled chart cards, and duplicated metric fetch loops are replaced by the ChartCard and Chart primitives, UsageSparkline, and a shared useMetricResourceQuery hook. The ClickHouse list queries take an explicit end bound so fixed ranges query only their window.
Queries using deltaSumTimestampMerge failed with an unknown function error, which broke the queue detail stats and the started counts on the built in Queues dashboard.
The queues list header tiles now render the same line chart, grid, and tooltip as the rest of the metrics charts instead of a row sparkline, with the headline value in the tile header. The env saturation tile draws the environment concurrency limit and burst limit as labeled reference lines. Chart tooltips keep a gap between the series label and the value, and the shared line chart gains showDots and referenceLines options.
Adds an Allocation tab to the Queues page (behind the queue metrics UI flag): overview cards, a burst-aware capacity bar showing each queue allocation and its live usage in a distinct color, an inline-editable limits table with per-queue locks, load-weighted auto-balance, and a review dialog that bulk-applies limits as overrides through the existing concurrency system.

The queue list now defaults to Busiest ordering (with Backlog and Name options). ClickHouse ranks queues by activity over the last 15 minutes and returns just the requested page of names, so the cost per page is one small aggregate regardless of environment size; idle queues follow in name order and any failure falls back to name ordering. The classic page keeps plain name order.
The fallback WHERE injection only targeted the top-level SELECT, so a
query shaped as an outer aggregation over a FROM subquery failed to
compile: the time column only exists inside the subquery. Descend into
the subquery so the fallback lands next to the table reference.
Adds two rollups fed from the raw landing table: a per-queue 5-minute
tier and an environment-level 1-minute tier (gauges plus TDigest wait
quantiles). Ranking now reads the 5m tier and returns the page and the
ranked total in one windowed query instead of two scans.

The 5m materialized view reads raw rather than cascading off the 10s
table: deltaSumTimestamp states hold a single first/last segment, so
merging states in an MV's hash-ordered GROUP BY double-counts bridging
spans. For the same reason the env tier carries no counter columns, and
env-wide counter totals must group by queue before summing.
The built-in queues dashboard's enqueued vs started chart merged counter
states across queues, which mixes unrelated cumulative counters and
returns wrong totals; it now merges per queue and sums outside. Env
header tiles and saturation charts read the environment rollup, so their
cost no longer scales with queue count, and coarse-bucket ranges are
served from the 5m rollup automatically. Queue list ranking runs as one
query, time bounds are aligned to the bucket grid, and repeated
auto-refresh reads share ClickHouse query-cache entries.
… rollup

The env rollup's win comes from dropping the queue dimension, not from
coarser buckets: row count is queue-independent (~8640/day/env), so full
10-second granularity stays cheap at any range. Env header tiles and
saturation charts now resolve short-range detail exactly like the
per-queue charts, and the current-value tiles read the latest 10-second
bucket instead of a minute-wide one.
The simulator's --reset only cleared the raw and 10s tables, leaving
stale rows in the 5m and env rollups. It also force-merges the rollups
after seeding so current-value widgets read cleanly.
Raw charcoal read as black bars in light-mode prose; the header uses the
theme-mapped --muted and cells the grid token.
…ding the dashboard

- the seeder now stages the email-sends QUEUE counter (what the watch checks
  and queue pages read) alongside the env-level one — a drain watch on the
  stand no longer one-shots against an empty live counter
- vite ignores seed-*.mts: editing or running a seeder was full-reloading
  every open dashboard tab every few seconds
'Morning after the deploy': report, chart, a two-revision investigation
(latest-wins), two watch confirmations, a one-shot result, a real wake with
backing watch rows (one fired, one still active for the live chip), and
docs citations. Seeded idempotently; --showcase re-seeds just this chat
over a running stand.
…xit agent fullscreen on navigation

An already-late queue was recommended the age SLA — already true, so every
watch one-shot with "that already happened". Late queue now recommends the
drain (the recovery); a healthy queue recommends the age SLA.

Navigating to another page drops the fullscreen takeover back to the side
panel.
A chat born from the watch card (0 LLM) never had a session, so the wake's
in.send 404'd and the delivery stayed pending. On 404 the wake now creates
the session (idempotent on externalId) and retries once.
…d of overwriting it

A wake has no client to carry the stored transcript, so the session view can
miss host-appended blocks — a card-born chat starts with only those, and the
wholesale persistMessages write dropped the watch confirmation. The wake's
display copy is now an id-deduped single-message append.
The px-3 inset belongs to the chat's docked placement, not the card — in the
hero it doubled the inset and the card came out narrower than the field.
The free-text path used to POST the watch straight from the tool, so a
"yeah, set one up" created it with no card and no consent. The tool now
validates the spec and returns a `watch` intent; the panel scans
`tool-schedule_watch` results the same way it scans navigate_to and opens
the pre-filled configuration card, replay-safe. The card's submit is the
only creator, so it owns the opt-ins, the cap, dedup, and the one-shot
result — prompt wording updated to match.
Ask AI -> Ask Trigger; send/stop centered in the docked composer; Watch
button wears the chat's eye glyph; a delayed history reload picks up the
generated chat name after the first answer; dashboard agent temporarily
open to everyone for V1 rollout testing.
The appended-message hand-off wasn't scoped to a chat: a chat mounted after a
watch was created started with a fresh dedupe ref, saw the pending seq, and
adopted another chat's confirmation into its transcript. The hand-off now
carries its chatId and only the matching chat receives it.
… subject; 12px chat code blocks

Customize kept the recommendation's original note across condition/threshold
changes, so the wake quoted a condition the user never watched — the note is
now restated whenever the condition or its number changes. The wake prompt
also hands the model a ready trigger:// markdown link to the watched object.
Chat code examples drop to 12px.
The poll only ran while the panel was closed, so a wake in any chat other
than the visible one announced nothing until the panel closed.
The toast still shows for every wake; the dot only counts wakes the user
isn't already looking at.
…t queries

A chart block's query runs after the turn, so the model never sees its
error — a camelCase column produced a permanently broken chart.
A card opened by a turn that died, or opened for a later turn that never
came (a wake's narration does this), sat in_progress forever — a spinner on
the card and an Investigating marker in History. A new sweep on the existing
dashboard-agent cron settles anything untouched for 30 minutes to
inconclusive, with the same wording the turn-level settle uses, guarded on
the row still being in_progress so a live turn always wins.
A chart block's TRQL query used to run only in the panel, after the turn, so a
bad query left a broken chart the model never learned about. render_view now
runs each chart query through the query API first and fails by name with the
query error, so the model fixes it in the same turn. The rows are discarded —
the panel stays the runner. Skipped when the turn has no delegated token or the
validation request itself fails.
Markdown renderers won't link an unknown scheme, so a cited trigger:// target
rendered dead. Prose links now rewrite through the panel's resolver; while
unresolved they degrade to their plain label.
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.

3 participants