feat(webapp): dashboard agent — Watch - #4525
Conversation
🦋 Changeset detectedLatest commit: 59378cf 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 | ✅ 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 |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx (1)
141-168: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winA reload requested after a write can reuse an older in-flight request.
loadHistoryreturns the in-flight promise when one exists. The callers at Line 417 (submitWatch) and Line 501 (cancelWatch) run right after a server write. If a history fetch started before that write, the caller receives the older response, and the new watch chip or the removal does not appear until the next reload.Consider queuing a follow-up request when one is already in flight.
♻️ Sketch: chain a fresh request instead of reusing the in-flight one
- const loadHistory = useCallback(async () => { - if (historyInFlight.current) return historyInFlight.current; - const request = (async () => { + const loadHistory = useCallback(async () => { + const previous = historyInFlight.current; + const request = (async () => { + // Wait out an older request, so a reload after a write never reuses its response. + if (previous) await previous; try {
🧹 Nitpick comments (22)
apps/webapp/app/v3/commonWorker.server.ts (1)
165-174: 🩺 Stability & Availability | 🔵 TrivialNote the overlap between the visibility timeout and the cron period.
visibilityTimeoutMsis 5 minutes and the cron period is also 5 minutes. If a sweep exceeds the visibility timeout, the message becomes visible again and a second run can start while the first is still working.maxAttempts: 1limits retries but does not prevent that re-delivery.The existing
dashboardAgent.maintenanceentry uses the same values, so this matches current practice. Confirm thatsweepDashboardAgentWatchesandrearmDashboardAgentWatchBatchesare safe to run concurrently, or raise the visibility timeout above the period.internal-packages/emails/src/index.tsx (1)
136-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using
headlinein the subject.The subject interpolates
data.identity, which is an internal condition key such asrun_finished:run_abc123. The payload also carriesheadline, the human sentence the panel shows. Readingheadlinefirst gives a clearer subject and keeps the email consistent with the in-app wording.
headlineis optional, so keepidentityas the fallback.♻️ Proposed subject change
case "alert-dashboard-agent-watch": { return { - subject: `[${data.organization}] Watch update: ${data.identity}`, + subject: `[${data.organization}] Watch update: ${data.headline ?? data.identity}`, component: <AlertDashboardAgentWatchEmail {...data} />, }; }apps/webapp/app/services/dashboardAgentWatchRunChecks.ts (1)
19-36: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winKeep the terminal-status list in sync with
~/v3/taskStatus.
FINAL_STATUSEScurrently matchesFINAL_RUN_STATUSES, but imports should derive from the canonical source when possible. If the module must keep no server-side imports, add a type-level assertion that fails when this list drifts fromFINAL_RUN_STATUSES.apps/webapp/app/services/dashboardAgentWatches.server.ts (1)
930-954: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueHoist the shared
TriggerClientifTriggerOptionsincludestrigger.
TriggerClientis exported from@trigger.dev/sdk,tasks.triggeracceptsdelay,idempotencyKey, andversion, butidempotencyKeyTTLis only accepted by batch trigger options. If the single-task schedule path needs TTL, move it to batch scheduling; otherwise, create one module-level client for eachapiOriginto avoid repeated setup on each watch tick.Source: Coding guidelines
apps/webapp/app/services/dashboardAgentWatchSweep.server.ts (1)
126-139: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueA rejected authorization promise is cached for the whole sweep.
authorizeOncePerSweepstores the pending promise before it settles. Ifauthorizerejects, for example on a transient database error, every remaining watch in that group reuses the rejected promise. All those rows are then counted as failed in one sweep run instead of being retried independently. The next sweep run recovers them, so the impact is bounded. Consider evicting the entry on rejection.♻️ Proposed eviction on rejection
const cached = seen.get(key); if (cached) return cached; - const pending = authorize(watch); + const pending = authorize(watch).catch((error) => { + seen.delete(key); + throw error; + }); seen.set(key, pending); return pending;apps/webapp/app/services/dashboardAgentWatchToken.server.ts (1)
176-181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
bearerTokenonly matches the exact schemeBearer.The scheme name in an
Authorizationheader is case-insensitive. A header ofbearer <token>leaves the prefix in place, so the extracted value fails the token prefix check and the caller receives 401. Match the scheme case-insensitively and allow repeated whitespace.♻️ Proposed scheme matching
- const value = raw.replace(/^Bearer /, "").trim(); + const value = raw.replace(/^Bearer\s+/i, "").trim();apps/webapp/app/services/dashboardAgentWatchBatch.server.ts (1)
167-175: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe authorization cache key omits
environmentId.
authorizeOncekeys on user, organization, and project. The equivalent helper inapps/webapp/app/services/dashboardAgentWatchSweep.server.tsat Line 132 also includesenvironmentId. The batch is scoped to a single environment byparams, so the two keys agree today. Align the keys so a future change to the row loader cannot silently reuse another environment's authorization.♻️ Proposed key alignment
- const key = `${watch.userId}:${watch.organizationId}:${watch.projectId}`; + const key = `${watch.userId}:${watch.organizationId}:${watch.projectId}:${watch.environmentId}`;apps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.fired.ts (1)
94-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winA failing release masks the enqueue error and strands the claim.
If
releaseWatchAlertDispatchthrows, the rethrow at Line 98 is never reached and the caller sees the release error instead of the enqueue error. The claim also stays held, so no later attempt can send the alert. Swallow and log the release failure, then rethrow the original error.♻️ Proposed error handling
try { await enqueueWatchFiredAlert(watch, "fired"); } catch (error) { - await releaseWatchAlertDispatch(dashboardAgentDb, { id: watch.id, terminalStatus: "fired" }); + await releaseWatchAlertDispatch(dashboardAgentDb, { + id: watch.id, + terminalStatus: "fired", + }).catch((releaseError) => + logger.error("Dashboard agent watch alert claim couldn't be released", { + watchId, + releaseError, + }) + ); throw error; }internal-packages/dashboard-agent/src/watch-delivery.ts (1)
176-177: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winDetect a lost delivery fence when
markWatchDeliveredreturns null.
markWatchDeliveredreturnsWatch | null. A null result means theclaimIdfence no longer matches, so another deliverer took the claim after the stale window elapsed. The current code ignores that result and continues tonotifyFiredandnotifyInvestigate. Downstream dedup limits the damage, but the lost fence is invisible in logs.Log the null result so a duplicate-wake incident is diagnosable.
♻️ Proposed change
- await deps.store.markWatchDelivered({ id: claimed.id, claimId }); + const marked = await deps.store.markWatchDelivered({ id: claimed.id, claimId }); + if (!marked) { + // The claim was reclaimed after the stale window, so another deliverer may also + // append. The action id dedups the wake; record the race for diagnosis. + logger.warn("dashboard-agent watch delivery lost its claim after appending", { + watchId: claimed.id, + claimId, + }); + }internal-packages/dashboard-agent/src/watch-narration.ts (1)
50-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd an exhaustiveness guard to the
categoryswitch.The switch has no
default. IfWatchPresentation["category"]gains a member, this function returnsundefinedat runtime while its declared return type staysstring. An exhaustiveness check turns that into a compile error instead. The repository already usesassert-neverfor this pattern inapps/webapp/app/v3/services/alerts/deliverDashboardAgentWatchAlert.server.ts.apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsx (1)
98-120: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider running the promoted-prompt lookup and the activity read concurrently.
This loader runs on every environment-scoped page load.
getPromotedDashboardAgentPromptandreadDashboardAgentWakeActivityare both gated onhasDashboardAgentAccessand are independent, but they are awaited one after the other. That adds two serial round trips to a hot path.Run them with
Promise.allto remove one round trip. Keep the per-read failure isolation so a store outage still lets the dashboard load.♻️ Proposed concurrent read
- const promotedDashboardAgentPrompt = hasDashboardAgentAccess - ? await getPromotedDashboardAgentPrompt({ - orgFeatureFlags: (project.organization.featureFlags as Record<string, unknown>) ?? {}, - }) - : null; - - // One narrow read per page load, so the wake signal reaches a browser that has never opened - // the panel — including one whose watch hasn't fired yet. The poll never asks for this. - let dashboardAgentActivity: DashboardAgentWakeActivity = { - unreadWakes: 0, - hasActiveWatches: false, - }; - if (hasDashboardAgentAccess) { - try { - dashboardAgentActivity = await readDashboardAgentWakeActivity(dashboardAgentDb, { - organizationId: project.organization.id, - userId: user.id, - }); - } catch (error) { - // The dashboard must load even when the agent's store doesn't answer. - logger.error("Failed to read dashboard agent wake activity", { error }); - } - } + const NO_ACTIVITY: DashboardAgentWakeActivity = { unreadWakes: 0, hasActiveWatches: false }; + + // One narrow read per page load, so the wake signal reaches a browser that has never opened + // the panel — including one whose watch hasn't fired yet. The poll never asks for this. + const [promotedDashboardAgentPrompt, dashboardAgentActivity] = hasDashboardAgentAccess + ? await Promise.all([ + getPromotedDashboardAgentPrompt({ + orgFeatureFlags: (project.organization.featureFlags as Record<string, unknown>) ?? {}, + }), + readDashboardAgentWakeActivity(dashboardAgentDb, { + organizationId: project.organization.id, + userId: user.id, + }).catch((error) => { + // The dashboard must load even when the agent's store doesn't answer. + logger.error("Failed to read dashboard agent wake activity", { error }); + return NO_ACTIVITY; + }), + ]) + : [null, NO_ACTIVITY];apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx (1)
55-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the alert-type list into one constant.
The literal list is now repeated on line 57 and line 61. This change had to update both branches by hand. The next alert type carries the same risk: if only the array branch is updated, a single-checkbox submission fails validation while a multi-checkbox submission succeeds.
Declare the values once and reuse them in both branches.
♻️ Proposed single source for the alert types
+const AlertTypeEnum = z.enum([ + "TASK_RUN", + "DEPLOYMENT_FAILURE", + "DEPLOYMENT_SUCCESS", + "DASHBOARD_AGENT_WATCH", +]); + const FormSchema = z .object({ - alertTypes: z - .array( - z.enum(["TASK_RUN", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS", "DASHBOARD_AGENT_WATCH"]) - ) - .min(1) - .or( - z.enum(["TASK_RUN", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS", "DASHBOARD_AGENT_WATCH"]) - ), + alertTypes: z.array(AlertTypeEnum).min(1).or(AlertTypeEnum),apps/webapp/test/dashboardAgentWatchCardRequestId.test.ts (1)
142-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAssert the status of the keyed submit as well.
Line 160 only checks that the response body is not
invalid_request. A 500 response with a different error code also passes. Assert the expected status to keep the control case meaningful.♻️ Proposed change
- expect(await withKey.json()).not.toMatchObject({ code: "invalid_request" }); + expect(withKey.status).not.toBe(400); + expect(await withKey.json()).not.toMatchObject({ code: "invalid_request" });internal-packages/dashboard-agent-contracts/src/blocks.test.ts (1)
255-259: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAssert the parsed watch spec, not only the intent kind.
The strict schema could strip or default fields inside
intent.specand this assertion would still pass. Add an assertion on the parsed spec so the round-trip covers the payload.♻️ Proposed addition
const strict = viewBlockSchema.parse({ ...body, ...envelope }); expect(strict.type === "actions" && strict.actions[0].intent.kind).toBe("watch"); + expect(strict.type === "actions" && strict.actions[0].intent).toMatchObject(watchAction.intent);apps/webapp/test/dashboardAgentWatchToken.test.ts (1)
12-12: 📐 Maintainability & Code Quality | 🔵 TrivialImport
USER_ACTOR_TOKEN_PREFIXinstead of hardcoding"tr_uat_".
@trigger.dev/rbacre-exportsUSER_ACTOR_TOKEN_PREFIXfrom@trigger.dev/plugins, so use that constant instead of duplicating the prefix in the watch-token tests and keep the cross-token prefix changes in sync.[low_effort和low_reward]
apps/webapp/test/dashboardAgentWatches.test.ts (1)
1473-1481: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winNarrow
activeWatchonwatching.
result.okstill includes thewatching: falsebranch, which does not providewatchIdorexpiresAt. The callers read those for check/token operations, so add thewatchingguard before returning.♻️ Proposed narrowing
async function activeWatch(seeded: Seeded) { const result = await create({ seeded }); if (!result.ok) throw new Error(`watch not created: ${result.code}`); + if (!result.watching) throw new Error("expected an active watch"); return result; }Source: Coding guidelines
internal-packages/dashboard-agent/src/watch-actions.test.ts (1)
505-523: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the missing-
userIdfallback.
narrateWatchWakehas a branch atwatch-actions.tslines 540-547 that runs whenclientData.userIdis absent. It logs an error and callspersistMessageswith the whole transcript. The comment on lines 532-535 states that a wholesale write can drop host-appended blocks, so this branch trades correctness for delivery on purpose.No test in this file exercises it. A test that sends
WAKEwithclientDatalackinguserId, then assertscalls.appendMessageis empty andcalls.persistMessageshas one entry, pins that deliberate trade-off in place.Do you want me to generate this test?
internal-packages/dashboard-agent/src/watch-actions.ts (1)
423-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe Haiku wake lane emits no telemetry and no cache metrics.
The Sonnet branch on lines 439-455 ends with
...resolved.toAISDKTelemetry(). The Haiku branch on lines 425-438 does not.conductWatchInvestigation(lines 796-818) and the agent's ownrun(lines 571-582 indashboard-agent.ts) both record telemetry and callrecordPromptCacheUsage.The comment on lines 371-375 says Haiku handles the common wake and Sonnet only the consented-investigation wake. So the lane with the most traffic is the one with no observability.
Add telemetry to the Haiku branch so wake narrations appear alongside every other model call.
♻️ Proposed change
maxOutputTokens: HAIKU_WAKE_MAX_OUTPUT_TOKENS, + ...resolved.toAISDKTelemetry(), })apps/webapp/app/components/dashboard-agent/WakeBanner.tsx (1)
113-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare one semantic-icon map. Both files declare an identical
SEMANTIC_ICONrecord that mapsWatchSemanticIconto a Heroicons glyph. A new icon added to the contract must then be added in two places, and the two maps can drift.
apps/webapp/app/components/dashboard-agent/WakeBanner.tsx#L113-L119: export this map (or move it to a small shared module next toagent-badges) so it is the single definition.apps/webapp/app/components/dashboard-agent/WatchChips.tsx#L38-L44: delete the local copy and import the shared map. This file already importswakePresentationfromWakeBanner.apps/webapp/app/components/dashboard-agent/WatchCard.tsx (1)
131-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAssociate the
Fieldlabel with its controls.
Fieldrenders the label as a plainspan. The pickers built fromChoiceare bare buttons. A screen reader announces "when it finishes" with no indication that it belongs to "Tell me". The numeric inputs carryaria-label, so only the picker rows are affected.Add a group role and connect it to the label.
♻️ Proposed change
function Field({ label, children }: { label: string; children: React.ReactNode }) { + const labelId = useId(); return ( <div className="flex flex-col gap-1"> - <span className="text-xxs uppercase tracking-wide text-text-faint">{label}</span> - <div className="flex flex-wrap items-center gap-1">{children}</div> + <span id={labelId} className="text-xxs uppercase tracking-wide text-text-faint"> + {label} + </span> + <div role="group" aria-labelledby={labelId} className="flex flex-wrap items-center gap-1"> + {children} + </div> </div> ); }apps/webapp/app/components/dashboard-agent/watch-card.ts (2)
118-121: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
withWindowclamps but does not snap to an offered option.The module comment at Line 5 states that "the window is always one of the offered options".
withWindowonly clamps betweenWATCH_WINDOW_HOURS_OPTIONS[0]andWATCH_MAX_HOURS. A value such as7passes through unchanged and is not an offered option. Today the card only passes values fromWATCH_WINDOW_HOURS_OPTIONS, so the mismatch is not visible.WatchCarddocuments a free-text pre-fill path at Line 145, which could pass an arbitrary number.Snap to the nearest offered option, in the same way
clampCadencedoes.♻️ Proposed change
export function withWindow(draft: WatchDraft, maxHours: number): WatchDraft { - const clamped = Math.min(Math.max(maxHours, WATCH_WINDOW_HOURS_OPTIONS[0]), WATCH_MAX_HOURS); + const clamped = Math.min(Math.max(maxHours, WATCH_WINDOW_HOURS_OPTIONS[0]!), WATCH_MAX_HOURS); + const snapped = + WATCH_WINDOW_HOURS_OPTIONS.find((option) => option >= clamped) ?? + WATCH_WINDOW_HOURS_OPTIONS[WATCH_WINDOW_HOURS_OPTIONS.length - 1]!; - return { ...draft, spec: { ...draft.spec, maxHours: clamped } as WatchSpec }; + return { ...draft, spec: { ...draft.spec, maxHours: snapped } as WatchSpec }; }
157-181: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
watchDraftErrorruns a Zod parse on every card render.
WatchCardcallswatchDraftError(draft)at Line 164 ofapps/webapp/app/components/dashboard-agent/WatchCard.tsx, directly in the render body. Each keystroke in the threshold input reparses the spec throughwatchSpecSchema.Memoize the result in the card, keyed on
draft.Based on learnings, this repository treats Zod as a boundary validation tool for API handlers and storage reads/writes, not as inline render-time validation inside React components, to avoid per-render schema-parse overhead.
♻️ Proposed change in `WatchCard.tsx`
- const localError = watchDraftError(draft); + const localError = useMemo(() => watchDraftError(draft), [draft]);Source: Learnings
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f5885caf-6411-4004-a096-446ba10f9f27
⛔ Files ignored due to path filters (2)
apps/webapp/test/__snapshots__/dashboardAgentWatchWording.test.ts.snapis excluded by!**/*.snapinternal-packages/dashboard-agent/src/__snapshots__/prompt-prefix.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (140)
.server-changes/dashboard-agent.mdapps/webapp/app/components/dashboard-agent/DashboardAgent.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentHistory.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsxapps/webapp/app/components/dashboard-agent/ReportView.tsxapps/webapp/app/components/dashboard-agent/WakeBanner.tsxapps/webapp/app/components/dashboard-agent/WatchButton.tsxapps/webapp/app/components/dashboard-agent/WatchCard.tsxapps/webapp/app/components/dashboard-agent/WatchChips.tsxapps/webapp/app/components/dashboard-agent/WatchResultBlock.tsxapps/webapp/app/components/dashboard-agent/WatchWakeToast.tsxapps/webapp/app/components/dashboard-agent/chat-layout.test.tsapps/webapp/app/components/dashboard-agent/chat-layout.tsxapps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsxapps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.tsapps/webapp/app/components/dashboard-agent/list-row.tsxapps/webapp/app/components/dashboard-agent/message-quota.tsapps/webapp/app/components/dashboard-agent/pending-intents.test.tsapps/webapp/app/components/dashboard-agent/pending-intents.tsapps/webapp/app/components/dashboard-agent/report-sparkline.tsxapps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/prompt-chips.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.test.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.tsapps/webapp/app/components/dashboard-agent/tool-labels.tsapps/webapp/app/components/dashboard-agent/turn-error.test.tsapps/webapp/app/components/dashboard-agent/turn-error.tsapps/webapp/app/components/dashboard-agent/view-actions.test.tsapps/webapp/app/components/dashboard-agent/view-catalog.tsxapps/webapp/app/components/dashboard-agent/wake-banner.test.tsapps/webapp/app/components/dashboard-agent/wake-poll.test.tsapps/webapp/app/components/dashboard-agent/wake-poll.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.tsapps/webapp/app/components/dashboard-agent/watch-card.test.tsapps/webapp/app/components/dashboard-agent/watch-card.tsapps/webapp/app/components/dashboard-agent/watch-chips.test.tsapps/webapp/app/components/dashboard-agent/watch-chips.tsapps/webapp/app/components/dashboard-agent/watch-recommendations.tsapps/webapp/app/components/queues/queue-thresholds.tsapps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.tsapps/webapp/app/presenters/v3/dashboardAgent/block-text.tsapps/webapp/app/presenters/v3/dashboardAgent/index.tsapps/webapp/app/presenters/v3/dashboardAgent/watch-wording.tsapps/webapp/app/presenters/v3/reports/ReportPresenter.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsxapps/webapp/app/routes/api.v1.dashboard-agent.alerts.$channelId.tsapps/webapp/app/routes/api.v1.dashboard-agent.alerts.tsapps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.check.tsapps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.fired.tsapps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.investigate.tsapps/webapp/app/routes/api.v1.dashboard-agent.watches.batch-check.tsapps/webapp/app/routes/api.v1.dashboard-agent.watches.tsapps/webapp/app/routes/resources.dashboard-agent.alerts.$channelId.unsubscribe.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.tsapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsxapps/webapp/app/services/dashboardAgentAlertContext.server.tsapps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.tsapps/webapp/app/services/dashboardAgentWatchBatch.server.tsapps/webapp/app/services/dashboardAgentWatchCheckBase.tsapps/webapp/app/services/dashboardAgentWatchChecks.server.tsapps/webapp/app/services/dashboardAgentWatchChecks.tsapps/webapp/app/services/dashboardAgentWatchErrorChecks.tsapps/webapp/app/services/dashboardAgentWatchHealthChecks.tsapps/webapp/app/services/dashboardAgentWatchInvestigate.server.tsapps/webapp/app/services/dashboardAgentWatchQueueChecks.tsapps/webapp/app/services/dashboardAgentWatchRunChecks.tsapps/webapp/app/services/dashboardAgentWatchSweep.server.tsapps/webapp/app/services/dashboardAgentWatchToken.server.tsapps/webapp/app/services/dashboardAgentWatches.server.tsapps/webapp/app/v3/alertsWorker.server.tsapps/webapp/app/v3/commonWorker.server.tsapps/webapp/app/v3/services/alerts/deliverAlert.server.tsapps/webapp/app/v3/services/alerts/deliverDashboardAgentWatchAlert.server.tsapps/webapp/package.jsonapps/webapp/seed-watch-scenarios.mtsapps/webapp/test/dashboardAgentBodyCap.test.tsapps/webapp/test/dashboardAgentTranscriptStore.test.tsapps/webapp/test/dashboardAgentWakeActivity.test.tsapps/webapp/test/dashboardAgentWatchAlertFanout.test.tsapps/webapp/test/dashboardAgentWatchAlertOwnerScope.test.tsapps/webapp/test/dashboardAgentWatchBatchFairness.test.tsapps/webapp/test/dashboardAgentWatchBatchRecording.test.tsapps/webapp/test/dashboardAgentWatchCardAtomicity.test.tsapps/webapp/test/dashboardAgentWatchCardRequestId.test.tsapps/webapp/test/dashboardAgentWatchChecks.test.tsapps/webapp/test/dashboardAgentWatchCreationReads.test.tsapps/webapp/test/dashboardAgentWatchInvestigate.test.tsapps/webapp/test/dashboardAgentWatchSweepBoundary.test.tsapps/webapp/test/dashboardAgentWatchTenancy.test.tsapps/webapp/test/dashboardAgentWatchToken.test.tsapps/webapp/test/dashboardAgentWatchWording.test.tsapps/webapp/test/dashboardAgentWatches.test.tsapps/webapp/test/reportHealth.test.tsinternal-packages/dashboard-agent-contracts/src/blocks.test.tsinternal-packages/dashboard-agent-contracts/src/blocks.tsinternal-packages/dashboard-agent-contracts/src/contracts.test.tsinternal-packages/dashboard-agent-contracts/src/index.tsinternal-packages/dashboard-agent-contracts/src/intent.tsinternal-packages/dashboard-agent-contracts/src/watch-wording.tsinternal-packages/dashboard-agent/GUIDEBOOK.mdinternal-packages/dashboard-agent/README.mdinternal-packages/dashboard-agent/src/agent-runtime.tsinternal-packages/dashboard-agent/src/compaction.test.tsinternal-packages/dashboard-agent/src/compaction.tsinternal-packages/dashboard-agent/src/dashboard-agent.eval.tsinternal-packages/dashboard-agent/src/dashboard-agent.test.tsinternal-packages/dashboard-agent/src/dashboard-agent.tsinternal-packages/dashboard-agent/src/eval-policy.tsinternal-packages/dashboard-agent/src/index.tsinternal-packages/dashboard-agent/src/step-cache.tsinternal-packages/dashboard-agent/src/tool-alerts.tsinternal-packages/dashboard-agent/src/tool-investigations.tsinternal-packages/dashboard-agent/src/tool-schemas.tsinternal-packages/dashboard-agent/src/tools.tsinternal-packages/dashboard-agent/src/watch-actions.test.tsinternal-packages/dashboard-agent/src/watch-actions.tsinternal-packages/dashboard-agent/src/watch-batch.tsinternal-packages/dashboard-agent/src/watch-delivery.tsinternal-packages/dashboard-agent/src/watch-lifecycle.tsinternal-packages/dashboard-agent/src/watch-narration.test.tsinternal-packages/dashboard-agent/src/watch-narration.tsinternal-packages/dashboard-agent/src/watch-task-adapters.tsinternal-packages/dashboard-agent/src/watch-tick.test.tsinternal-packages/dashboard-agent/src/watch-tick.tsinternal-packages/dashboard-agent/src/watch-tools.tsinternal-packages/database/prisma/migrations/20260729120000_add_dashboard_agent_watch_alert_type/migration.sqlinternal-packages/database/prisma/schema.prismainternal-packages/emails/emails/alert-dashboard-agent-watch.tsxinternal-packages/emails/src/index.tsx
@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. |
create_alert reached for an `alert` envelope the route never sent, so it always reported undefined, and the 403 branch keyed on `reason` where the route sends `code` — the "email isn't set up on this instance" wording was unreachable. The unit fixtures were written against both invented shapes, so they hid it; they now mirror the route's bodies exactly. The subscribe path also inlined its deduplication key instead of calling `watchAlertDeduplicationKey`, the one record of whose channel it is.
Each failed check stored the whole previous `lastResult` under `previous`, so consecutive failures wrapped one another without bound. The row escapes: an unverified expiry copies `lastResult` into the wake facts, which the alert and the webhook body serialise. A failure record is now unwrapped before it is stored, so `previous` is always the last observation the check really made.
…r_view The flag was computed inside `ViewBlocks`, which sees the blocks of a single `render_view` part. A turn that renders the investigation card in one call and the actions block in another gave each call its own answer, and the duplicate Watch button came back. It is now computed where every part of the message is in scope; a card can still add its own offer, never drop the turn's.
The poll deduped fresh wakes against a 50-id localStorage set alone, so inside the feed's 15-minute window a second browser — or one whose site data was cleared — toasted wakes the user had already read. The payload's own `unread` flag now gates the toast as well. A wake landing in an open chat stays unread until that chat's next read, so it still toasts.
`SimpleTooltip` drops its trigger from the tab order unless `tabbable` is set, and the chip's label tooltip is the only place its status, cadence and expiry are written. The cancel tooltip beside it already passed the prop.
The seed script refused a non-local Redis or ClickHouse host outright, but sent the target's API key to whatever `APP_ORIGIN` named. All three now go through one guard, which also fixes it for a bracketed IPv6 host — `URL.hostname` hands back `[::1]`, which the old set membership never matched.
…t read The column was added nullable and every reader treats NULL as unread, so the first load after rollout reported every pre-existing chat unread.
… ClickHouse The chat route now pulls in the watch services, and through them a ClickHouse client built at import time from an unset env var.
The file the review comment named was missed: it opened a pool per case and never closed one. Adds the sibling files' afterEach and their 30s case timeout.
…that isn't there The queue's live row read collapsed every non-ok response into "no live row", so a 401, a 429 or a 5xx reached the model as exists:false — the queue does not exist. Only a 404 is evidence of absence now; anything else reports exists:"unknown" with the status, and the prompt says unknown is never missing.
The wake poll's callback closed over `open` from the render that started it, so once the panel opened the subtraction never applied. The panel's open state now goes through a ref, like the visible chat already does, rather than adding `open` to the effect's deps and restarting the poll on every open and close.
…watch A watch can expire or be cancelled with nothing written back into the transcript, so asking the summariser for "any watch that is running" preserved an old confirmation as current state.
The guard compared a slice against its own start, so it could not fail. Move the decision into takeNavigateIntent and drive it across two commits instead.
The unmount case repeated its neighbour and no query string can reach unmountTeardown. Guard the pathname tracking that does decide it.
…token pk_ is browser-shipped and environment-bound. Nothing routes it to the query API today, so the cap costs no caller anything and the helper stops promising the wrong thing.
The seeder and its `scenarios:watch` script were only ever a way to reproduce a watch condition by hand. The guidebook now states the conditions themselves, so there is nothing left for the kit to be the answer to.
…oduce it The guidebook was a walkthrough of the scenario kit: a command, some clicks, and the sentence that came back. It is now a reference of conditions, derived from the checks rather than from the old prose, so a reader can predict an outcome without running anything. Corrects, among others: the queue Investigate rule (a zero or unset concurrency limit is never saturation), the run-panel Investigate rule (it needs an error block as well as a failed status), the claim that a backed-up queue never offers an investigate chip (the page registry offers both), and the claim that every wake is an LLM call (only attention outcomes and consented investigations are).
…lable checks - 0003 catches the last_read_at backfill up on databases where 0002 already ran. - The per-watch check endpoint records a look, not a check, when it read nothing. - A suggested prompt is consumed once it is sent, not when it is clicked. - Closing the panel settles the launcher dot instead of waiting for the poll. - Say why the oldest-age reader's 50-key page cannot under-report. - Suppress error-classification on the check route, with the reason on the record.
6630047 to
59378cf
Compare
92efdcd to
2481567
Compare
Stacked on #4529, which is stacked on #4418. Merge those first.
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.
What's inside
(chatId, clientRequestId), so a retried submission replays instead of duplicating.How to review
GUIDEBOOK.md — local setup and a walkthrough of all 15 scenarios.