feat(webapp): dashboard agent — Watch - #4525
Conversation
🦋 Changeset detectedLatest commit: 2b75a1e 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 |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
<hidden_range_assignment> 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 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 425 measured of 441 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. |
|
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. |
…light status, normalize queue names (review)
…ions, single-sourced cadence options, tx-narrowed advisory lock
…ontainer-test hygiene ()
…es on chat deletion, ac)
… nitpicks — tolerant )
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.