Landmarks derived from the layout spec must appear in the same order in the server-rendered card
and in the markdown, over healthy, degraded, untrustworthy and empty reports.
The card no longer formats values, resolves codes or classifies footer codes of its own, and a
genuinely-unknown state now gets a neutral icon instead of a green tick.
The text surfaces gain the card's structure (headline, hero evidence, why: block, aggregated
read:, trust flag) and keep their alignment: fixed sparkline column, aligned notes.
Section order, section labels, the tone -> glyph vocabulary, the footer styles and every
string a renderer places now live in report-layout.ts. Renderers decide typography only.
/api/v1/query took its tenant scope from the request body, so a public access
token minted for one environment could read the whole organization by asking for
organization scope. The credential is now the ceiling and a wider ask is refused;
secret keys and the membership-authorized Query page are unchanged.
A turn that ends in an error was only a stream event, so reloading history showed
a turn that just stops. It is now appended to the transcript through the same
id-deduped path a wake uses, in the same wording the live stream shows, and the
panel drops its live callout once that record is the last message.
Every watch sentence (card, banner, toast, email, Slack, webhook) now comes from
app/presenters/v3/dashboardAgent. The condition is written once per kind in four
registers instead of in four switches across three files, and a plain-text block
renderer lets the non-React surfaces say what the card says.
Follows the title write moving to onBeforeTurnComplete: the gate reads as a
magic number without the note that a later turn cannot re-title anyway,
because setChatTitleIfDefault only writes over the default name.
The panel reloaded its chat list twice per turn — once on settle, then
again on a 5s timer — purely because the generated name landed after the
first reload and the list would otherwise sit on "New chat".
The name is now written before the client settles instead. Generation
starts in onTurnStart, so it runs alongside the model answering, and is
awaited in onBeforeTurnComplete — the last hook before the turn-complete
chunk closes the stream. onTurnComplete cannot do this: it fires after
that chunk, which is exactly why the write used to miss.
Costs nothing in practice (the cheap title model finishes long before the
answer does) and a failure only loses the generated name. The delayed
reload and its timer are gone, and a test pins the ordering — it fails if
the await is dropped.
A terminal watch is read by nothing once its wake has landed — the chip is
gone, dedup only looks at active rows, and the outcome's facts live in the
chat transcript from then on. So the row was pure accumulation.
The sweep now drops terminal rows whose last event was over 7 days ago, in
one guarded statement bounded to 500 per run. Guards: terminal status only,
delivery already settled (a row that still owes a wake is never taken), and
the age measured from the newest timestamp on the row so a late delivery
restarts the clock. Watches only — chats, messages and investigations are
untouched.
A report is ~9 ClickHouse queries, and the callers that dominate its
volume are periodic rather than interactive: every watch tick in an
environment asks for the same health verdict, so one sweep recomputed it
once per watch.
Caches the interpreted view model per (report, environment, period) for
90s — under the tick cadence, well over a burst. In-process and keyed by
environment id, so nothing crosses a tenant. The single-flight map stays:
it is what holds the first callers of a cold key to one load, and a
rejected load is still never cached.
A background tab has no dot the user can see and nowhere to put a toast,
so it stops asking; becoming visible triggers one immediate catch-up and
restarts the cadence from there. Each delay carries fresh jitter, so many
open tabs stop hitting the same second.
The dot's count and the toast's list both run on every poll of a closed
panel, and both drove off a full scan of the watches table: the tenant
predicates sat on the joined chat, so nothing narrowed the watch side
first.
Adds a partial index on (organization_id, user_id, resolution time desc)
over delivered wakes only, and asserts the same org + user on the watch
row so the planner can use it. That pair is redundant, not a new tenancy
rule — a watch snapshots its chat's owner at creation and the create path
rejects a chat from another org.
On 200k watches: the count goes 20.5ms -> 0.5ms and the recent list
21ms -> 0.02ms, with the parallel seq scan replaced by an index scan that
also absorbs the recency bound.