feat(webapp): dashboard agent — Watch - #4525
Conversation
🦋 Changeset detectedLatest commit: f6c90ec The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdded watch creation and configuration for runs, queues, errors, and health reports. Added scheduled checks, lifecycle handling, wake notifications, automatic investigations, unread tracking, and cross-browser activity polling. Added email, Slack, and webhook alert delivery with subscription management. Added dashboard-agent tools, APIs, persistence, worker tasks, scenario tooling, documentation, and extensive unit and integration coverage. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
Observability mapAs of 20/100 over 424 measured of 440 entry points (base 19, up 1) What this PR changed
FIX FIRST
AUDIT 3 of 50 sensitive mutations record an actor. 47 without one. What the score is made ofThe score and findings here are report-only and never gate the merge. Separately, a required test suite keeps this tool's symbol and route lists in sync with the code they name, and can fail a pull request that renames or removes a symbol they reference, or that adds the first route with a segment they anticipate. Each failure names the list to edit. The rules and their reasons: internal-packages/observability-map/README.md. |
712396b to
e110e90
Compare
bd4d4a0 to
887f5b6
Compare
c0f0058 to
e7432a8
Compare
887f5b6 to
17a0f07
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
…at/dashboard-agent-flows-watch
… watch schema descriptions
…at/dashboard-agent-flows-watch
…s-watch # Conflicts: # apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx
The offer line is the last sentence, the actions block comes after it. Also widens the no-duplicate-offer exemption to a health report card that already offers a recovery watch.
Keep the precomputed blocksByPart (winners filter already applied) and pass watchOfferedInTurn through the new slot factory; actions render last inside the wake slot too.
…' into feat/dashboard-agent-flows-watch
…s-watch # Conflicts: # internal-packages/dashboard-agent/src/__snapshots__/prompt-prefix.test.ts.snap
…at/dashboard-agent-flows-watch
| } finally { | ||
| // No `onTurnComplete` on an action, so the guard runs here: a card left at | ||
| // in_progress is a spinner nothing else will ever stop. `closeCard` is the settle, | ||
| // so nothing settles the row separately first. | ||
| await closeCard(investigationId, answered ? [...uiMessages, answered] : uiMessages); | ||
| // Only once the close committed: a retry needs the tracked entry to still be there. | ||
| clearOpenInvestigations(chatId); | ||
| } |
There was a problem hiding this comment.
🔍 A transient model failure still settles the consented investigation's card before the action retries
In conductWatchInvestigation the closing write runs in a finally, so any throw out of streamText / chat.pipe (rate limit, provider blip, network) settles the row and appends the closing card before the error propagates and the task retries.
On the retry, the dedupe guard reads uiMessages (the session snapshot), which will not contain the closing card that was written straight to the read-model DB — settleInvestigationCard appends there, not to the session's out stream. So alreadyAnswered is false and latestCards(uiMessages) has no entry for the id, meaning the retry re-runs the whole investigation, revises the already-terminal row via render_view, and calls closeCardInTranscript again (which bumps the revision even though appendChatMessageOnce dedupes the message). The user can see the card go inconclusive and then move again, and the row's revision can end up ahead of the card the transcript holds.
The tests cover the case where the closing card itself can't be written (internal-packages/dashboard-agent/src/watch-actions.test.ts — "fails the action when the closing card can't be written"), but not the case where the model call throws first. Worth deciding whether the settle should be skipped when the turn is going to be retried, e.g. only closing on a clean exit and leaving the stale-investigation sweep to handle a crashed run.
Was this helpful? React with 👍 or 👎 to provide feedback.
Watch is the agent noticing something later: you ask it to tell you when a condition holds, and it answers when it does — or when it can't any more.
A watch is a durable one-shot promise. The condition is checked on a schedule by deterministic code (no LLM in the checks), the answer lands in the chat once, and then the watch is over. Ten kinds: three on a run, five on a queue, error recurrence, health recovery.
Stack
Stacked on #4529 (UI), which is stacked on #4418 (chat, reports, investigate). Merge those first. #4516 (storybook gallery) sits on top of this branch.
How to review
GUIDEBOOK.md on this branch is the behaviour reference — it states the conditions rather than the code, so you can predict what happens without running anything. "The ten watch kinds, and what makes each fire" and "Creating a watch" describe exactly this PR, and the tables there are the spec the code is written against.
What's inside
dashboardAgentWatch*Checks.ts), with the spec union indashboard-agent-contracts/src/watch.ts.(environment, cadence)group can be checked together in one batch pass, with a sweep as the backstop for expiry, redelivery and retention.DASHBOARD_AGENT_WATCHalert channel, so it shows on the project's Alerts page with one-click unsubscribe), and an optional investigation when the outcome needs attention.watch_submissions, keyed(chat_id, client_request_id), so a retried card submission replays the recorded outcome instead of creating a second watch.Key decisions
A check result is a 4-way, and only two of them are verdicts.
satisfied/terminal_unsatisfiedare answers;pendingandunavailableare not. Any exception inside any check is caught in one place and becomesunavailablewith an unverified observation — a check that failed is never evidence.A completed window is an answer, and whether it is good or bad news is declared per kind, never inferred. There is a table for that in the guidebook:
run_failedcompleting its window is good news ("hasn't failed"),backlog_draincompleting it is not. One rule overrides the table: a window that completed on an unverified observation is neutral and says only that the watch ended without a confirmed answer. An unreadable source is never a negative answer — and, because investigations only open onattention, it never starts one either.Identity is
(chat, project, environment)plus the condition, enforced by a partial unique index over active rows (watches_chat_active_identity_key), not by the read-then-insert check. Cadence, window, note andticksare deliberately not part of it. Two different chats may watch the same thing — a watch is a promise to a chat.The server resolves the target's name, whatever the model calls it. The model can't tell a task queue (
task/<id>) from a custom queue, so both spellings are tried and the stored one wins — and the rewrite happens before identity and before the row is written, so the identity, the checks, the link and the wording all see one spelling.Freshness fences. Depth falls back from the live counter to the newest 60 s ClickHouse bucket, which only counts as current within 60 s of now. A non-current reading at or below the quiet line is refused as
unavailablerather than believed, so a stale empty bucket is never read as "drained". The stall streak is the one piece of carried state: it lives in the previous check's facts and freezes on an unreadable reading rather than breaking.Chain reliability. There is no shared cron — each watch (or batch group) schedules its own next tick, so the failure mode to review is the chain dying. A failed batch check is caught, the next tick is scheduled anyway and the run resolves rather than failing, so the chain survives a check that couldn't run; the sweep re-arms groups and finalizes anything still active past its deadline, even when delivery isn't configured. Wake redelivery is id-deduped rather than conditional, because the sweep can't know whether the user was already told. Access is re-authorized on every check against the primary — replica lag would extend access the user has already lost.
Wording lives in one place.
watch-wording.tsis read by the card, banner, toast, email and the agent's own narration, and the numbers come from the frozen observation rather than a fresh read, so a retry produces the same sentence. Replay reproduces the recorded decision instead of deciding again — the transcript is append-once, so a second decision would contradict it forever.Cancellation is the ending without an answer — no resolution, no wake. One exception, decided during testing: a watch the user cancelled leaves a single neutral transcript line ("Stopped watching …"), keyed off the watch id so a retry can't repeat it. The other four reasons stay silent.
Email is opt-in and only a fired watch emails. An expiry is narrated in the chat and nowhere else. Both gates (agent access, a configured email transport) are checked at subscribe time and again at delivery, and the subscription outcome is frozen on the ledger row so a retry replays it. Neither gate is a plan check.
One watch offer per turn. The prompt and the renderer guard this independently — if the turn already proposed a watch card, the action button is dropped, because the card is the better affordance. Two eval cases pin the prompt side: exactly one offer with the line last and the button after it, and zero offers when the rendered card already carries one — deterministic assertions, over a real-model run.
Testing
Unit tests (vitest, testcontainers, no mocks) under
apps/webapp/test/dashboardAgentWatch*.test.tsandinternal-packages/dashboard-agent/src/watch-*.test.tscover the invariants above: the 4-way check results and the freshness fences, identity/dedup and the submission ledger, queue-name resolution, the batch chain surviving a failed check, sweep boundaries and alert-once, tenancy and the watch token's scope, and the wording snapshot. The load-bearing ones were verified by control-breaking the guard first and checking the test goes red.Live-tested end to end against a local stack, following the guidebook: all ten watch kinds firing and expiring, cancellation, the email pair (a fired watch mails, an expired one does not), and watch recovery from a health report.