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.
Every Watch condition was proven by hand-run Redis and ClickHouse surgery. Now
each one is a verb:
pnpm --filter webapp run scenarios:watch -- queue:fill email-sends 400
pnpm --filter webapp run scenarios:watch -- queue:drain email-sends
pnpm --filter webapp run scenarios:watch -- error:recur
pnpm --filter webapp run scenarios:watch -- run:fail 90
pnpm --filter webapp run scenarios:watch -- health:degrade | health:recover
Each verb is idempotent, runs on top of the seeded agent-examples stand, refuses
a non-local Redis or ClickHouse, and prints the dashboard step that follows it.
Prerequisites fail with the command to run.
SCENARIOS.md walks every scenario end to end — the command, the clicks, the
wording that arrives and the tick cadence — including the two run tasks, whose
source lives in the references repo and is carried here as a snippet.
The heartbeat kept a review stand from ageing by appending a minute of fresh
telemetry every 30s. It was a crutch for demos, not something a developer needs,
and it carried a state file, a tag-and-prune cycle and a per-tick Redis top-up
with it. All of it goes; --degrade / --recover / --showcase and the base seed
stay.
The Redis key shapes and the depth staging move to seed-agent-examples-redis.mts
so the scenario kit can stage the same keys the same way.
The routes this branch adds logged their failures without saying whose request
they were, and several catch clauses answered every error the same way.
- every failure-path log now carries the tenant ids the route has in hand
(userId / organizationId / projectId / environmentId)
- the body-parse try blocks guard only the parse; the shape check moved out
- the boundary catches rethrow a thrown Response instead of turning it into a 500
- routes that relied on the central handler get a log-and-rethrow boundary, so
the response is unchanged and the failure is named
- the advisory watch email-alert state moved to the alerts service, where the
rest of that logic lives
The env JWT exchange now stamps a signed `act` claim (acting user + client
kind), auth surfaces it as `actor`, and the tenant context prefers it over
`orgMember`, which only exists on dev environments. Identity only — the JWT
still authorizes as the environment.
The card catalogs move out of Chat UI onto their own storybook pages (view blocks, report view, investigation card, watch card), all sharing the manifest and the page shell. Cuts near-duplicate states: 97 sections down to 52, section ids unchanged. The screenshot script now walks every page.
Chats belong to (organization, user); several queries enforced only the user,
so a user's own chat from another org could be opened, renamed, pinned or
appended to through a different org's route.
The wake seeded the card and said it had started looking, then nothing ran
it — the findings were left to a turn only the user could start. The watcher
now reports a delivered consented wake to the webapp, which mints the same
delegated user-actor token a turn gets and sends a `watch.investigate` action
into the chat; the agent conducts a real investigating turn on that card and
delivers the findings as its own message. Best-effort throughout: nothing here
can retry or invalidate the wake.
A wake read on screen before the next poll never toasted. The toast list is
now recent deliveries (15 min, id-deduped client-side); the dot still counts
unread only.
Sonner stamps data-theme="light" (its default) on the toast list; since the
theme system remaps tokens by that attribute, every custom toast rendered
light regardless of the page's theme.