feat(webapp): dashboard agent — Watch - #4525
Conversation
🦋 Changeset detectedLatest commit: 3a64fb0 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 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
"Tell me when this run finishes", "ping me if that error comes back". A watch checks on its own cadence, reports once, and stops within 24 hours. The user confirms a pre-filled card, so nothing starts behind their back; the answer lands in the chat, and by email if they asked for it. Restores the feature this branch's base PR set aside, unchanged: the card and chips, the wake banner and toast, the unread badge, the checks for runs, queues, errors and health, the batch scheduler and its backstops, the alert channel and its email, and the agent's schedule_watch tool with the prompt that governs it.
An investigation card carries its own watch button, and the prompt asked for a watch offer on top of it. The card wins — it is the one with the pre-filled spec — and the prompt now says so too.
… cases the sibling timeout
…ent's queue metrics
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.
LEGACY_ASK_AI_SHORTCUT no longer exists, so the import was undefined and the assertion passed vacuously. Uses ASK_AI_SHORTCUT from ask-ai-channels and pins its key and modifiers.
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. |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal-packages/dashboard-agent/src/repo-tools.ts (1)
266-278: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle ranges that start beyond the end of the file.
If
startLineexceeds the file length,from > toandrange.contentis empty. Line 273 then returnsendLine: to, which is beforestartLineand does not identify a line that was served. Return an explicit out-of-range result, or define empty-range metadata before calculatingserved.
🧹 Nitpick comments (8)
apps/webapp/test/dashboardAgentWatchQueueName.test.ts (2)
120-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused chat created in
seed.
createForcreates its own chat for every watch. The chat created here is never referenced, so it only adds a write to each test.♻️ Proposed cleanup
- await createChat(ctx.agentDb, { - id: `chat_${suffix()}`, - organizationId: organization.id, - userId: user.id, - }); - return { user, organization, project, environment };
178-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSet an explicit timeout on the new container tests. Both new suites call
postgresTestwithout a timeout, while sibling suites such asapps/webapp/test/dashboardAgentTranscriptStore.test.tspass30_000. Container start plus migration replay can exceed the default timeout and cause flaky failures.
apps/webapp/test/dashboardAgentWatchQueueName.test.ts#L178-L203: pass30_000as the final argument to bothpostgresTestcases.apps/webapp/test/dashboardAgentWatchSweepAlertOnce.test.ts#L167-L209: pass30_000as the final argument to thepostgresTestcase.apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx (1)
161-190: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winCoalescing can return a list that predates the caller's write.
loadHistoryreturns the in-flight promise to any later caller.submitWatch(Line 433) andcancelWatch(Line 524) call it right after a write. If a history request was already in flight when the write completed, the returned list can be the response to that earlier request, so the new watch chip is missing, or the cancelled one is still present, until the next reload.Consider marking the in-flight request as stale and starting one more fetch when a write lands during it.
apps/webapp/app/components/dashboard-agent/watch-card-state.test.ts (1)
94-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe exact-source assertion is brittle.
Line 97 asserts a source string that includes exact indentation (
\n return ...). A formatting change insideclaimChatSlot, or a nesting change, fails this test without any behavior change. Thebumpscount check on Line 96 already carries most of the value.Consider matching with a whitespace-tolerant regular expression instead of a literal string.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsx (1)
105-128: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueTwo blocking reads are added to every environment-scoped page load.
readDashboardAgentWakeActivityandcountChatsWithUnreadWorkrun on every navigation into an env-scoped route, after the project query and the access check. They are correctly parallel with each other and correctly scoped byorganizationIdanduserId. The added latency lands on a hot path.Consider adding a timeout or a short per-user cache around these reads, so a slow agent store degrades the dot rather than the page. Note also that the comment on Line 105 says "One narrow read" while the code issues two.
internal-packages/dashboard-agent/src/tool-api.ts (1)
100-108: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
row.typeis trusted without a type check.Line 102 casts
row.typetostringand uses??, which only replacesnullandundefined. If the route ever returns a non-string or an empty string, that value reachesbase.queueTypeand the"custom"comparison on line 417 silently changes the consumer-task lookup. Validate the value instead.♻️ Proposed guard
- queueType: (row.type as string) ?? queueType, + queueType: row.type === "task" || row.type === "custom" ? row.type : queueType,packages/core/src/v3/schemas/api.ts (1)
129-129: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider a typed shape for
queueConfig.
z.any()gives SDK consumers no type information for a field they must read by name. The only known consumer readsqueueConfig.name. A minimal object shape documents the contract and keeps the field permissive.♻️ Proposed shape
- queueConfig: z.any().nullish(), + queueConfig: z.object({ name: z.string().nullish() }).passthrough().nullish(),This matches
payloadSchemain intent while still accepting absent and null values. Keepz.any()if the queue config shape is expected to change often.apps/webapp/test/dashboardAgentWatchErrorFingerprint.test.ts (1)
27-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftRemove module mocks from this integration test.
This test uses
postgresTest, but it replaces~/db.server,~/services/dashboardAgentDb.server, and~/v3/canAccessDashboardAgent.serverwithvi.mock(). Pass the real Testcontainers-backed clients through explicit service dependencies instead. This keeps the integration path representative and follows the test rule that dependencies must not be mocked.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 29dccd6c-57fd-4df3-a507-b8f5cc6e18bc
⛔ 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 (198)
.changeset/quiet-hounds-shave.md.server-changes/dashboard-agent.mdapps/webapp/app/components/dashboard-agent/ActionsBlock.tsxapps/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/DashboardAgentHeader.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.test.tsapps/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/agent-shortcuts.test.tsapps/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/explicit-prompt.test.tsapps/webapp/app/components/dashboard-agent/explicit-prompt.tsapps/webapp/app/components/dashboard-agent/list-row.tsxapps/webapp/app/components/dashboard-agent/message-quota.tsapps/webapp/app/components/dashboard-agent/panel-escape.test.tsapps/webapp/app/components/dashboard-agent/panel-escape.tsapps/webapp/app/components/dashboard-agent/pending-intents.test.tsapps/webapp/app/components/dashboard-agent/pending-intents.tsapps/webapp/app/components/dashboard-agent/pending-turn.test.tsapps/webapp/app/components/dashboard-agent/pending-turn.tsapps/webapp/app/components/dashboard-agent/report-sparkline.tsxapps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.test.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.tsapps/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/turn-navigation.test.tsapps/webapp/app/components/dashboard-agent/turn-navigation.tsapps/webapp/app/components/dashboard-agent/turn-teardown.test.tsapps/webapp/app/components/dashboard-agent/turn-teardown.tsapps/webapp/app/components/dashboard-agent/unread-counts.test.tsapps/webapp/app/components/dashboard-agent/unread-counts.tsapps/webapp/app/components/dashboard-agent/unread-work.test.tsapps/webapp/app/components/dashboard-agent/view-actions.test.tsapps/webapp/app/components/dashboard-agent/view-actions.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-state.test.tsapps/webapp/app/components/dashboard-agent/watch-card-state.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-name.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/api.v1.projects.$projectRef.$env.workers.$tagName.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/dashboardAgent.server.tsapps/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/utils/localHostGuard.test.tsapps/webapp/app/utils/localHostGuard.tsapps/webapp/app/v3/alertsWorker.server.tsapps/webapp/app/v3/commonWorker.server.tsapps/webapp/app/v3/queryScope.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/dashboardAgentCreateChatOrdering.test.tsapps/webapp/test/dashboardAgentInvestigationWinner.test.tsapps/webapp/test/dashboardAgentLastReadBackfill.test.tsapps/webapp/test/dashboardAgentToolScopes.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/dashboardAgentWatchErrorFingerprint.test.tsapps/webapp/test/dashboardAgentWatchInvestigate.test.tsapps/webapp/test/dashboardAgentWatchQueueAge.test.tsapps/webapp/test/dashboardAgentWatchQueueName.test.tsapps/webapp/test/dashboardAgentWatchSweepAlertOnce.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/queryScope.test.tsapps/webapp/test/reportCurationTrust.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/evidence.tsinternal-packages/dashboard-agent-contracts/src/index.tsinternal-packages/dashboard-agent-contracts/src/intent.tsinternal-packages/dashboard-agent-contracts/src/page-context.tsinternal-packages/dashboard-agent-contracts/src/watch-wording.tsinternal-packages/dashboard-agent-contracts/src/watch.test.tsinternal-packages/dashboard-agent-contracts/src/watch.tsinternal-packages/dashboard-agent-db/drizzle/0002_watches_and_chat_messages.sqlinternal-packages/dashboard-agent-db/src/ids.tsinternal-packages/dashboard-agent-db/src/queries.tsinternal-packages/dashboard-agent/GUIDEBOOK.mdinternal-packages/dashboard-agent/README.mdinternal-packages/dashboard-agent/package.jsoninternal-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-error-category.test.tsinternal-packages/dashboard-agent/src/eval-policy.tsinternal-packages/dashboard-agent/src/eval-redaction.test.tsinternal-packages/dashboard-agent/src/eval-turn.tsinternal-packages/dashboard-agent/src/index.tsinternal-packages/dashboard-agent/src/repo-tools.test.tsinternal-packages/dashboard-agent/src/repo-tools.tsinternal-packages/dashboard-agent/src/step-cache.tsinternal-packages/dashboard-agent/src/test-support.tsinternal-packages/dashboard-agent/src/tool-alerts.tsinternal-packages/dashboard-agent/src/tool-api-client.tsinternal-packages/dashboard-agent/src/tool-api.tsinternal-packages/dashboard-agent/src/tool-curation.tsinternal-packages/dashboard-agent/src/tool-investigations.tsinternal-packages/dashboard-agent/src/tool-queue.test.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.tsxpackages/core/src/v3/schemas/api.ts
🚧 Files skipped from review as they are similar to previous changes (115)
- apps/webapp/package.json
- apps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.ts
- apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
- apps/webapp/test/reportHealth.test.ts
- internal-packages/database/prisma/schema.prisma
- apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
- internal-packages/dashboard-agent/README.md
- apps/webapp/app/components/dashboard-agent/pending-intents.ts
- apps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.ts
- internal-packages/dashboard-agent/src/dashboard-agent.eval.ts
- apps/webapp/app/components/dashboard-agent/tool-labels.ts
- internal-packages/emails/src/index.tsx
- apps/webapp/app/presenters/v3/dashboardAgent/watch-wording.ts
- apps/webapp/app/components/queues/queue-thresholds.ts
- internal-packages/dashboard-agent/src/step-cache.ts
- apps/webapp/app/components/dashboard-agent/turn-error.test.ts
- apps/webapp/app/components/dashboard-agent/chat-layout.test.ts
- internal-packages/dashboard-agent/src/watch-narration.test.ts
- apps/webapp/app/components/dashboard-agent/wake-banner.test.ts
- apps/webapp/app/components/dashboard-agent/list-row.tsx
- apps/webapp/app/services/dashboardAgentWatchHealthChecks.ts
- apps/webapp/app/components/dashboard-agent/WatchButton.tsx
- apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts
- apps/webapp/app/components/dashboard-agent/watch-chips.test.ts
- internal-packages/dashboard-agent/src/tools.ts
- apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx
- apps/webapp/app/v3/services/alerts/deliverAlert.server.ts
- apps/webapp/app/presenters/v3/reports/ReportPresenter.server.ts
- internal-packages/dashboard-agent/src/tool-investigations.ts
- apps/webapp/app/routes/app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.$queueParam/route.tsx
- apps/webapp/test/dashboardAgentWatchSweepBoundary.test.ts
- internal-packages/dashboard-agent-contracts/src/watch-wording.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.alerts.$channelId.ts
- apps/webapp/test/dashboardAgentWatchToken.test.ts
- apps/webapp/app/services/dashboardAgentWatchQueueChecks.ts
- internal-packages/dashboard-agent-contracts/src/contracts.test.ts
- apps/webapp/app/components/dashboard-agent/WatchChips.tsx
- apps/webapp/app/services/dashboardAgentWatchToken.server.ts
- apps/webapp/test/dashboardAgentWatchWording.test.ts
- apps/webapp/test/dashboardAgentWakeActivity.test.ts
- apps/webapp/app/services/dashboardAgentWatchChecks.ts
- apps/webapp/test/dashboardAgentWatchBatchFairness.test.ts
- internal-packages/dashboard-agent-contracts/src/intent.ts
- apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
- apps/webapp/test/dashboardAgentWatchAlertFanout.test.ts
- apps/webapp/app/components/dashboard-agent/pending-intents.test.ts
- internal-packages/dashboard-agent-contracts/src/index.ts
- apps/webapp/app/components/dashboard-agent/WatchWakeToast.tsx
- apps/webapp/app/presenters/v3/dashboardAgent/block-text.ts
- apps/webapp/app/components/dashboard-agent/watch-recommendations.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.investigate.ts
- apps/webapp/app/components/dashboard-agent/WatchResultBlock.tsx
- apps/webapp/test/dashboardAgentWatchCreationReads.test.ts
- apps/webapp/test/dashboardAgentBodyCap.test.ts
- apps/webapp/app/components/dashboard-agent/report-sparkline.tsx
- apps/webapp/app/components/dashboard-agent/turn-error.ts
- apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx
- apps/webapp/app/components/dashboard-agent/watch-activity.test.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.watches.ts
- apps/webapp/app/components/dashboard-agent/ReportView.tsx
- apps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.check.ts
- internal-packages/dashboard-agent/src/watch-delivery.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.watches.batch-check.ts
- apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsx
- apps/webapp/app/components/dashboard-agent/message-quota.ts
- apps/webapp/app/v3/services/alerts/deliverDashboardAgentWatchAlert.server.ts
- internal-packages/dashboard-agent/src/dashboard-agent.ts
- apps/webapp/app/v3/commonWorker.server.ts
- apps/webapp/test/dashboardAgentWatchInvestigate.test.ts
- internal-packages/dashboard-agent/src/watch-narration.ts
- apps/webapp/app/presenters/v3/dashboardAgent/index.ts
- apps/webapp/test/dashboardAgentWatchBatchRecording.test.ts
- apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
- internal-packages/dashboard-agent/src/watch-lifecycle.ts
- apps/webapp/app/components/dashboard-agent/watch-card.ts
- apps/webapp/app/components/dashboard-agent/suggested-prompts/prompt-chips.ts
- apps/webapp/app/components/dashboard-agent/WatchCard.tsx
- apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts
- internal-packages/dashboard-agent/src/index.ts
- apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx
- apps/webapp/app/components/dashboard-agent/WakeBanner.tsx
- apps/webapp/test/dashboardAgentWatchCardAtomicity.test.ts
- apps/webapp/app/routes/resources.dashboard-agent.alerts.$channelId.unsubscribe.tsx
- apps/webapp/app/services/dashboardAgentAlertContext.server.ts
- apps/webapp/app/services/dashboardAgentWatchErrorChecks.ts
- internal-packages/dashboard-agent/src/watch-tools.ts
- apps/webapp/test/dashboardAgentWatchChecks.test.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.alerts.ts
- internal-packages/dashboard-agent-contracts/src/blocks.ts
- internal-packages/dashboard-agent/src/dashboard-agent.test.ts
- internal-packages/dashboard-agent/src/watch-task-adapters.ts
- internal-packages/dashboard-agent/src/watch-tick.test.ts
- apps/webapp/app/services/dashboardAgentWatchSweep.server.ts
- apps/webapp/seed-watch-scenarios.mts
- apps/webapp/test/dashboardAgentWatchTenancy.test.ts
- internal-packages/dashboard-agent/src/watch-batch.ts
- apps/webapp/test/dashboardAgentWatchCardRequestId.test.ts
- internal-packages/emails/emails/alert-dashboard-agent-watch.tsx
- apps/webapp/app/v3/alertsWorker.server.ts
- internal-packages/dashboard-agent/src/tool-schemas.ts
- apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts
- apps/webapp/app/components/dashboard-agent/watch-chips.ts
- apps/webapp/app/services/dashboardAgentWatches.server.ts
- internal-packages/dashboard-agent/src/watch-tick.ts
- apps/webapp/app/services/dashboardAgentWatchChecks.server.ts
- apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts
- apps/webapp/app/services/dashboardAgentWatchBatch.server.ts
- apps/webapp/app/routes/api.v1.dashboard-agent.watches.$watchId.fired.ts
- apps/webapp/app/services/dashboardAgentWatchCheckBase.ts
- apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts
- internal-packages/dashboard-agent/src/tool-alerts.ts
- apps/webapp/app/services/dashboardAgentWatchRunChecks.ts
- apps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.ts
- apps/webapp/app/components/dashboard-agent/chat-layout.tsx
- internal-packages/dashboard-agent/src/watch-actions.ts
| useEffect(() => { | ||
| if (!hasAccess || !watching) return; | ||
|
|
||
| let cancelled = false; | ||
| const load = async () => { | ||
| try { | ||
| // Bounded, so one stuck request can't hold the poll's in-flight guard. | ||
| const res = await fetch(`${actionPath}?unread=1`, { | ||
| signal: AbortSignal.timeout(UNREAD_REQUEST_TIMEOUT_MS), | ||
| }); | ||
| if (!res.ok) return; | ||
| const data = (await res.json()) as { | ||
| unreadWakes?: number; | ||
| unreadWork?: number; | ||
| wakes?: WatchWake[]; | ||
| }; | ||
| if (cancelled) return; | ||
| // The wakes list carries read ones too, so only unread ones are subtracted. | ||
| const unreadInView = (data.wakes ?? []).filter( | ||
| (wake) => wake.unread && wake.chatId === visibleChat.current | ||
| ).length; | ||
| setUnreadWakes(Math.max(0, (data.unreadWakes ?? 0) - unreadInView)); | ||
| // A chat open in the panel is being read right now, so it isn't unread work. | ||
| setUnreadWork(Math.max(0, (data.unreadWork ?? 0) - (open && visibleChat.current ? 1 : 0))); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
open is captured stale inside the poll effect.
The effect dependency array on Line 248 omits open. The poll callback closes over the open value from the render that started the poll. When the user opens the panel, the effect does not re-run, so open stays false inside load. The unread-work subtraction on Line 218 then never applies while a chat is on screen, and the launcher dot keeps counting the chat the user is reading.
Track open in a ref, as the code already does for visibleChat.
🐛 Proposed fix
// A wake in the on-screen chat toasts but must not light the dot.
const visibleChat = useRef<string | null>(null);
+ // Read by the poll, which must not re-subscribe when the panel opens.
+ const openRef = useRef(open);
+ openRef.current = open;- setUnreadWork(Math.max(0, (data.unreadWork ?? 0) - (open && visibleChat.current ? 1 : 0)));
+ setUnreadWork(
+ Math.max(0, (data.unreadWork ?? 0) - (openRef.current && visibleChat.current ? 1 : 0))
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!hasAccess || !watching) return; | |
| let cancelled = false; | |
| const load = async () => { | |
| try { | |
| // Bounded, so one stuck request can't hold the poll's in-flight guard. | |
| const res = await fetch(`${actionPath}?unread=1`, { | |
| signal: AbortSignal.timeout(UNREAD_REQUEST_TIMEOUT_MS), | |
| }); | |
| if (!res.ok) return; | |
| const data = (await res.json()) as { | |
| unreadWakes?: number; | |
| unreadWork?: number; | |
| wakes?: WatchWake[]; | |
| }; | |
| if (cancelled) return; | |
| // The wakes list carries read ones too, so only unread ones are subtracted. | |
| const unreadInView = (data.wakes ?? []).filter( | |
| (wake) => wake.unread && wake.chatId === visibleChat.current | |
| ).length; | |
| setUnreadWakes(Math.max(0, (data.unreadWakes ?? 0) - unreadInView)); | |
| // A chat open in the panel is being read right now, so it isn't unread work. | |
| setUnreadWork(Math.max(0, (data.unreadWork ?? 0) - (open && visibleChat.current ? 1 : 0))); | |
| // A wake in the on-screen chat toasts but must not light the dot. | |
| const visibleChat = useRef<string | null>(null); | |
| // Read by the poll, which must not re-subscribe when the panel opens. | |
| const openRef = useRef(open); | |
| openRef.current = open; | |
| useEffect(() => { | |
| if (!hasAccess || !watching) return; | |
| let cancelled = false; | |
| const load = async () => { | |
| try { | |
| // Bounded, so one stuck request can't hold the poll's in-flight guard. | |
| const res = await fetch(`${actionPath}?unread=1`, { | |
| signal: AbortSignal.timeout(UNREAD_REQUEST_TIMEOUT_MS), | |
| }); | |
| if (!res.ok) return; | |
| const data = (await res.json()) as { | |
| unreadWakes?: number; | |
| unreadWork?: number; | |
| wakes?: WatchWake[]; | |
| }; | |
| if (cancelled) return; | |
| // The wakes list carries read ones too, so only unread ones are subtracted. | |
| const unreadInView = (data.wakes ?? []).filter( | |
| (wake) => wake.unread && wake.chatId === visibleChat.current | |
| ).length; | |
| setUnreadWakes(Math.max(0, (data.unreadWakes ?? 0) - unreadInView)); | |
| // A chat open in the panel is being read right now, so it isn't unread work. | |
| setUnreadWork( | |
| Math.max(0, (data.unreadWork ?? 0) - (openRef.current && visibleChat.current ? 1 : 0)) | |
| ); |
| it("marks a dropped navigation handled, so it cannot fire on a later commit", () => { | ||
| const effect = chat.slice(chat.indexOf("const pending = pendingNavigateIntents(messages")); | ||
| expect(effect.indexOf("pendingNavigateIntents(messages")).toBeLessThan( | ||
| effect.indexOf("navigateIntentApplies({") | ||
| ); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
This test does not check what its name states.
effect starts at the pendingNavigateIntents(messages match, so effect.indexOf("pendingNavigateIntents(messages") is always 0. The assertion then only fails when navigateIntentApplies({ is absent from the remainder. Nothing here asserts that a dropped navigation is marked handled.
Assert the handled marking directly, for example that the slice contains the call that records the intent as handled before the applies branch returns.
| it("ignores the query string, so filtering a page is not leaving it", () => { | ||
| // Both sides are pathnames; a filter change never reaches this comparison. | ||
| expect(unmountTeardown({ renderedPath: path, livePath: path })).toBe("panel-closed"); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
This case repeats the previous assertion and proves nothing about query strings.
Lines 38 and 27 pass identical arguments and expect the same result. No query string reaches unmountTeardown, so the stated behavior stays untested. Either assert that a caller-supplied query string is stripped before the comparison, or delete this case as redundant.
💚 Option: exercise a value that carries a query string
- it("ignores the query string, so filtering a page is not leaving it", () => {
- // Both sides are pathnames; a filter change never reaches this comparison.
- expect(unmountTeardown({ renderedPath: path, livePath: path })).toBe("panel-closed");
- });
+ it("compares pathnames only, so a filter change is not leaving the page", () => {
+ // A caller that passes a search string must still read as the same page.
+ expect(
+ unmountTeardown({ renderedPath: path, livePath: `${path}?statuses=FAILED` })
+ ).toBe("panel-closed");
+ });This diff only holds if unmountTeardown normalizes its inputs. The current implementation compares the two strings directly, so confirm the intended contract before applying it.
| export function storedQueueName(queue: { type: string; name: string }): string { | ||
| return queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove every existing task/ prefix.
task/task/queue still returns task/task/queue. This contradicts the helper contract and produces a different stored queue name for a malformed but reachable input.
Proposed fix
- return queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
+ return queue.type === "task" ? `task/${queue.name.replace(/^(?:task\/)+/, "")}` : queue.name;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function storedQueueName(queue: { type: string; name: string }): string { | |
| return queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name; | |
| export function storedQueueName(queue: { type: string; name: string }): string { | |
| return queue.type === "task" ? `task/${queue.name.replace(/^(?:task\/)+/, "")}` : queue.name; |
| canAccessDashboardAgent: async () => true, | ||
| })); | ||
|
|
||
| process.env.SESSION_SECRET = "test-session-secret-for-watch-fingerprints"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Do not mutate process.env in the test module.
Remove this assignment and use the environment values supplied by apps/webapp/test/setup.ts. Module-level mutation can leak into other tests. The webapp rules also require environment access through env.server.ts rather than direct process.env.
Sources: Coding guidelines, Learnings
| it("leaves every other bearer credential uncapped", () => { | ||
| expect(queryScopeCeilingFor("PRIVATE")).toBe("unbounded"); | ||
| expect(queryScopeCeilingFor("PUBLIC")).toBe("unbounded"); | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find which authentication types can reach the query API and how the ceiling is applied.
rg -n 'queryScopeCeilingFor|resolveQueryScope' apps/webapp/app -A5
rg -n '"PUBLIC"' apps/webapp/app/services/apiAuth.server.ts -B3 -A3
fd -t f 'api.v1.*query*' apps/webapp/app/routes | xargs -r rg -n 'authenticate|allowJWT|allowPublicKey|scope'Repository: triggerdotdev/trigger.dev
Length of output: 4425
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== query route outline =="
ast-grep outline apps/webapp/app/routes/api.v1.query.ts --view expanded || true
echo "== relevant query route =="
cat -n apps/webapp/app/routes/api.v1.query.ts | sed -n '1,90p'
echo "== query schema options =="
cat -n apps/webapp/app/routes/api.v1.query.schema.ts | sed -n '1,90p'
echo "== auth helpers around PUBLIC handling =="
cat -n apps/webapp/app/services/apiAuth.server.ts | sed -n '130,250p'
echo "== all allowPublicKey usages =="
rg -n 'allowPublicKey' apps/webapp/app -A3 -B3Repository: triggerdotdev/trigger.dev
Length of output: 16000
Block PUBLIC access at this route
PUBLIC keys can reach the query API because api.v1.query.ts allows JWT auth, so PUBLIC_JVT is capped while regular PUBLIC publishable keys remain unbounded and can select organization scope. Disable PUBLIC keys for this route, or cap them with the same scope ceiling used by PUBLIC_JWT.
| **Watch** — "tell me when this run starts", "ping me if this error comes | ||
| back", the *Watch recovery* button on a degraded health report. A durable | ||
| condition the platform checks on a schedule (no LLM in the checks), which | ||
| wakes the chat with the outcome. Fires once, expires within 24h, max 3 per | ||
| chat. Five kinds: run start / run finished / backlog drain / error recurrence | ||
| / health recovery. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the watch capability list.
The list omits queue threshold, stalled-queue, queue wait-time, and run-failure conditions that Sections 2-5 and 9-12 document. Replace the “Five kinds” sentence with a complete description of the supported watch conditions.
| @@ -62,7 +67,8 @@ Write a summary in under 400 words, as notes rather than prose. Keep, in this or | |||
| 1. What the user is trying to do, in their own terms, and anything they asked to be remembered. | |||
| 2. Facts already established, with the run ids, queue names, task identifiers, error fingerprints and numbers they rest on. Never restate a number you cannot see. | |||
| 3. Any investigation that is open: its investigationId, its title and its current outcome. | |||
| 4. What was asked most recently and what is still unanswered. | |||
| 4. Any watch that is running or has reported, and what it said. | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not infer that a watch is currently running from the transcript.
A watch can expire or be cancelled without a transcript update. This instruction can preserve an old confirmation as a current running state and cause a later answer to report stale status. Summarize the watch as created or last reported, unless current state is verified from the watch store.
Proposed change
-4. Any watch that is running or has reported, and what it said.
+4. Any watch created or reported in the transcript, and its last recorded result. Do not state that a watch is currently active unless the transcript confirms it.| // Live state first: metrics are a window, and a window can't say "paused" or show a | ||
| // backlog that arrived after it. A queue nobody is running is not the same as a queue | ||
| // someone stopped, and the answer has to lead with which one it is. | ||
| const live = async (kind: "task" | "custom") => { | ||
| const result = await envApiGet( | ||
| `/api/v1/queues/${encodeURIComponent(queue)}?type=${kind}` | ||
| ); | ||
| return result?.ok ? (result.data as Record<string, unknown>) : undefined; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
live discards the failure reason, so an unreachable queue route reads as a missing queue.
live returns undefined for every non-ok response: 401, 403, 429, and 5xx included. withLiveState then reports exists: false on line 99. The model is told the queue does not exist, which is the exact wrong answer the surrounding comments aim to prevent.
Distinguish "the route said 404" from "the read failed" and let withLiveState report the unknown case instead of a negative existence claim.
🐛 Proposed fix: keep the status and only claim non-existence on 404
const live = async (kind: "task" | "custom") => {
const result = await envApiGet(
`/api/v1/queues/${encodeURIComponent(queue)}?type=${kind}`
);
- return result?.ok ? (result.data as Record<string, unknown>) : undefined;
+ if (!result) return { state: undefined, missing: false };
+ if (result.ok) return { state: result.data as Record<string, unknown>, missing: false };
+ return { state: undefined, missing: result.status === 404 };
};Then thread missing into withLiveState and return exists: false only when missing is true; otherwise omit exists so the answer does not assert either way.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/webapp/app/components/dashboard-agent/watch-activity.test.ts (1)
99-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the storage key into a named constant.
Lines 101, 108, and 114 repeat the raw literal
"tdev:dashboard-agent:watching". A rename ofSTORAGE_KEYinwatch-activity.tswould leave these tests passing against a key nothing reads. Declare one local constant at the top of the file and use it in every assertion.♻️ Proposed refactor
- store.set("tdev:dashboard-agent:watching", JSON.stringify({ org_1: true })); + store.set(STORAGE_KEY, JSON.stringify({ org_1: true }));Add near the top of the file:
const STORAGE_KEY = "tdev:dashboard-agent:watching";Based on the coding guideline "Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons".
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a9b85035-0dce-46a9-a815-d62281afcfa8
📒 Files selected for processing (5)
apps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.tsapps/webapp/app/routes/api.v1.dashboard-agent.alerts.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.tsapps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/webapp/app/routes/api.v1.dashboard-agent.alerts.ts
- apps/webapp/app/components/dashboard-agent/watch-activity.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead
**/*.{ts,tsx}: Prefer static imports over dynamicimport(); use dynamic imports only for unresolvable circular dependencies, genuine performance code splitting, or conditional runtime loading.
Import Trigger.dev tasks from@trigger.dev/sdk; never use@trigger.dev/sdk/v3or deprecatedclient.defineJob.
Add agentcrumbs while writing code using approved namespaces; mark lines with//@Crumbsor blocks with `// `#region` `@crumbs, and strip them before merging.
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use zod for validation in packages/core and apps/webapp
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use function declarations instead of default exports
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use vitest for all tests in the Trigger.dev repository
**/*.{test,spec}.{ts,tsx}: Use Vitest exclusively and never mock dependencies; use Testcontainers for integration dependencies.
Place test files next to the source files they test.
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
apps/webapp/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
apps/webapp/**/*.{ts,tsx}: Access environment variables through theenvexport ofenv.server.tsinstead of directly accessingprocess.env
Use subpath exports from@trigger.dev/corepackage instead of importing from the root@trigger.dev/corepathDo not reintroduce the removed v1 execution path;
RunEngineVersion.V1branches may only reject or finalize gracefully so v3 clients receive a clean 4xx, never a 5xx.
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
apps/webapp/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Do not import
env.server.tsdirectly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.ts
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
For apps, use
typecheckfor verification and never usebuildas the correctness check.
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
apps/webapp/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Test files must not import
app/env.server.ts; pass configuration as options instead.
Files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.ts
apps/webapp/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
apps/webapp/app/**/*.{ts,tsx}: For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.
UseuseCallbackanduseMemoonly for context provider values, expensive derived data used as a dependency, or stable references required by dependency arrays; do not wrap ordinary event handlers or trivial computations.
Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons.
Files:
apps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
apps/webapp/app/**/*.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
apps/webapp/app/**/*.ts: Never userequest.signalto detect client disconnects. UsegetRequestAbortSignal()fromapp/services/httpAsyncStorage.server.ts, which is wired to Express response close events.
Access environment variables through theenvexport fromapp/env.server.ts; never useprocess.envdirectly.
Always use PrismafindFirstinstead offindUnique.
Always use the$transactionhelper from~/db.server, never callprisma.$transactionor$replica.$transactiondirectly. Pass isolation levels as strings, useSerializablefor correctness-critical read-then-write invariants, and guard possibly undefined helper results when a definite value is required.
Files:
apps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
🧠 Learnings (20)
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-05-18T08:21:27.694Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3632
File: apps/webapp/sentry.server.ts:4-21
Timestamp: 2026-05-18T08:21:27.694Z
Learning: When handling Prisma error P1001 ("Can't reach database server") in TypeScript, don’t assume a single error shape. Prisma can surface P1001 via two different error classes/fields: `PrismaClientKnownRequestError` exposes it as `err.code === "P1001"` (common during mid-query connection drops), while `PrismaClientInitializationError` exposes it as `err.errorCode === "P1001"` (common on client startup failure). Therefore, predicates should use `err.code === "P1001" || err.errorCode === "P1001"`. Do not flag `err.code === "P1001"` as “unreachable/never matches,” as it is expected in production.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-05-18T08:21:27.694Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3632
File: apps/webapp/sentry.server.ts:4-21
Timestamp: 2026-05-18T08:21:27.694Z
Learning: When handling Prisma errors for P1001 ("Can't reach database server"), do not assume it only appears under a single property name. Prisma may surface P1001 via either `PrismaClientKnownRequestError` (`err.code === "P1001"`, e.g., mid-query connection drops) or `PrismaClientInitializationError` (`err.errorCode === "P1001"`, e.g., client startup connection failure). To reliably detect the condition, check `err.code === "P1001" || err.errorCode === "P1001"`, and avoid review rules that would incorrectly flag `err.code === "P1001"` as unreachable/never-matching.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-13T19:53:13.759Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3937
File: packages/trigger-sdk/skills/realtime-and-frontend/SKILL.md:258-260
Timestamp: 2026-06-13T19:53:13.759Z
Learning: When reviewing code that uses `trigger.dev/react-hooks`’s `useRealtimeRun`, preserve the call signature where the first argument is the full realtime handle object (not `handle.id`). This is intentional to maintain type-safety and is consistent with the official docs; do not suggest changing the first argument from the handle object to `handle.id`.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-17T17:13:49.929Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3948
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx:48-62
Timestamp: 2026-06-17T17:13:49.929Z
Learning: In triggerdotdev/trigger.dev, within `dashboardLoader`/`dashboardAction` (or similar context resolver code) whenever you resolve an organization ID from an organization slug for RBAC/enterprise authorization scope, always read from the primary Prisma client (`prisma`), not `$replica`. Using `$replica` can hit replica-lag and cause the RBAC lookup/authorization to run without the correct org scope (bypassing intended role enforcement). Implement the slug→org lookup with `prisma.organization.findFirst(...)` (or equivalent primary-client query) and add an inline comment documenting why the primary client is required (replica lag could lead to unscoped RBAC checks).
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-23T13:04:21.413Z
Learnt from: carderne
Repo: triggerdotdev/trigger.dev PR: 4023
File: apps/webapp/app/services/upsertBranch.server.ts:14-18
Timestamp: 2026-06-23T13:04:21.413Z
Learning: In TypeScript, it’s valid to `import { type X }` and then use `typeof X` in a type-only position, e.g. `type Alias = z.infer<typeof X>`. The `type` modifier suppresses the runtime import, but the type checker still has the full exported type so `z.infer<typeof X>` can resolve correctly. In code reviews, don’t flag this as a TypeScript compile error as long as `typeof X` is used in a type context (e.g., with `z.infer`, `type` aliases, generics), not as a runtime value.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-05-07T12:25:18.271Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3531
File: apps/webapp/test/sentryTraceContext.server.test.ts:9-47
Timestamp: 2026-05-07T12:25:18.271Z
Learning: In the triggerdotdev/trigger.dev webapp test suite, it is acceptable to leave `createInMemoryTracing()` calls that register a global `NodeTracerProvider` without `afterEach`/`afterAll` teardown. Do not flag this as a test-ordering risk when the code follows the established pattern used across webapp tests (e.g., replication service/benchmark/backfiller tests). This is considered safe because `trace.getActiveSpan()` when called outside a `context.with(...)` block reads `AsyncLocalStorage.getStore()` (undefined when no `run()` scope exists), so it falls back to `ROOT_CONTEXT` with no attached span—regardless of which provider is registered.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts
📚 Learning: 2026-05-28T20:02:10.647Z
Learnt from: myftija
Repo: triggerdotdev/trigger.dev PR: 3772
File: apps/webapp/test/findOrCreateBackgroundWorker.test.ts:1-1
Timestamp: 2026-05-28T20:02:10.647Z
Learning: In the triggerdotdev/trigger.dev monorepo, for the `apps/webapp` package use the established convention of storing Vitest tests (unit, integration, and e2e) under `apps/webapp/test/` rather than colocating them next to source files. Do not flag files located in `apps/webapp/test/` as violating any rule that says to colocate tests with source.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts
📚 Learning: 2026-07-30T18:43:56.874Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 4426
File: apps/webapp/test/memberDevEnvironments.server.test.ts:124-125
Timestamp: 2026-07-30T18:43:56.874Z
Learning: In the `apps/webapp` test suite (`apps/webapp/test/**`), respect the established test harness in `apps/webapp/test/setup.ts`: it loads `.env` and provides default values for required environment variables so that transitive imports (e.g., `~/env.server`) work without production-style wiring.
During code review, do not require dependency injection/refactoring solely to avoid this existing import path. Only introduce configuration injection if it delivers production-level value (for example, a more general `createEnvironment` abstraction that improves runtime behavior beyond test setup).
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts
📚 Learning: 2026-08-08T12:49:17.489Z
Learnt from: kathiekiwi
Repo: triggerdotdev/trigger.dev PR: 4525
File: apps/webapp/test/dashboardAgentWatchAlertOwnerScope.test.ts:101-109
Timestamp: 2026-08-08T12:49:17.489Z
Learning: In apps/webapp/test, every test suite that creates a client with createDashboardAgentDb(connectionUri, ...) must close the DashboardAgentDbClient in afterEach. Although postgresTest drops cloned databases with WITH (FORCE), it does not clean up client-side postgres-js pool sockets or idle timers, so explicitly closing the client prevents resource leaks and test interference.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts
📚 Learning: 2026-05-12T21:04:05.815Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3542
File: apps/webapp/app/components/sessions/v1/SessionStatus.tsx:1-3
Timestamp: 2026-05-12T21:04:05.815Z
Learning: In this Remix + TypeScript codebase, do not flag a server/client boundary violation when a file imports only types from a module matching `*.server`.
Specifically, it’s safe to import types using `import type { Foo } from "*.server"` or `import { type Foo } from "*.server"` because TypeScript erases type-only imports at compile time and they emit no JavaScript, so they won’t cross the Remix server/client bundle boundary.
Only raise the boundary concern for value imports (e.g., `import { Foo }` without `type`, or `import Foo`), since those produce JavaScript output.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-25T18:21:51.905Z
Learnt from: carderne
Repo: triggerdotdev/trigger.dev PR: 4039
File: apps/webapp/app/routes/invite-revoke.tsx:0-0
Timestamp: 2026-06-25T18:21:51.905Z
Learning: During the Zod v4 migration in the triggerdotdev/trigger.dev webapp, ensure any imports from `conform-to/zod` use the Zod-4 subpath: `conform-to/zod/v4` (e.g., `import { parseWithZod } from "conform-to/zod/v4"`). Do not import from the package root `conform-to/zod`, because it is the Zod 3 implementation and may load Zod-3-only symbols (e.g., `ZodBranded`, `ZodEffects`), which can throw at module load (notably with `zod4.4.3`). This should be enforced across `apps/webapp/**/*` where helpers like `parseWithZod` and `conformZodMessage` are used.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-07-03T17:10:21.498Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 4148
File: apps/webapp/app/models/orgMember.server.ts:149-168
Timestamp: 2026-07-03T17:10:21.498Z
Learning: In triggerdotdev/trigger.dev, `User.email` (Prisma schema: `internal-packages/database/prisma/schema.prisma`) currently does NOT use `citext` and does NOT have a `lower(email)` functional unique index. Therefore, do not introduce Prisma queries like `where: { email: { equals: <value>, mode: "insensitive" } }` (or any case-insensitive lookup) against `User.email`, because it can force sequential scans of the `users` table under load. During review, ensure email is normalized (e.g., lowercased/trimmed) before both writes and subsequent lookups, and if true case-insensitive behavior/uniqueness is required, implement it via a separate app-wide migration (e.g., switch to `citext` and/or add a functional unique index with backfill) rather than bolting it onto individual feature PRs.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-05-18T14:40:02.173Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3658
File: packages/core/src/v3/realtimeStreams/manager.test.ts:1-147
Timestamp: 2026-05-18T14:40:02.173Z
Learning: In the triggerdotdev/trigger.dev repo, the policy “Never mock anything — use testcontainers instead” should only be enforced for integration tests that interact with real external services (e.g., Redis, Postgres) via actual infrastructure. For unit tests that exercise pure in-memory logic (e.g., cache semantics) it is OK to stub collaborators such as `ApiClient` using Vitest (`vi.fn()`) to assert call counts or control behavior. Do not flag `vi.fn()`-based `ApiClient` stubs in unit tests as violations of the testcontainers policy.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.ts
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-09T17:58:04.699Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 3879
File: apps/webapp/app/models/vercelIntegration.server.ts:619-630
Timestamp: 2026-06-09T17:58:04.699Z
Learning: In this codebase, outbound raw `fetch` calls should typically rely on Node/undici’s default request timeout (about ~300s) rather than adding a per-call `AbortController` + `setTimeout` wrapper inside individual functions (e.g. in files like `apps/webapp/app/models/vercelIntegration.server.ts`). During code review, do not flag the absence of a per-call timeout on a single `fetch` as an issue; if per-call timeouts are needed, they should be implemented via a codebase-wide convention (e.g., a shared fetch wrapper or documented pattern) rather than ad-hoc per-function changes.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.tsapps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-06-16T09:19:47.637Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3960
File: apps/webapp/test/prismaInfrastructureErrorCapture.test.ts:0-0
Timestamp: 2026-06-16T09:19:47.637Z
Learning: In this repo’s Vitest setup, `vitest.config.ts` uses `globals: true`, so identifiers like `vi`, `describe`, `it`, and `expect` are available as globals in Vitest test files. During code review, do not flag missing `vi`/`describe`/`it`/`expect` imports as a runtime error or correctness issue when they’re used in `*.test.ts/tsx` or `*.spec.ts/tsx` files. Explicit imports are still preferred for consistency, but they’re not required for runtime behavior.
Applied to files:
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.tsapps/webapp/app/components/dashboard-agent/watch-activity.test.ts
📚 Learning: 2026-03-26T09:02:07.973Z
Learnt from: myftija
Repo: triggerdotdev/trigger.dev PR: 3274
File: apps/webapp/app/services/runsReplicationService.server.ts:922-924
Timestamp: 2026-03-26T09:02:07.973Z
Learning: When parsing Trigger.dev task run annotations in server-side services, keep `TaskRun.annotations` strictly conforming to the `RunAnnotations` schema from `trigger.dev/core/v3`. If the code already uses `RunAnnotations.safeParse` (e.g., in a `#parseAnnotations` helper), treat that as intentional/necessary for atomic, schema-accurate annotation handling. Do not recommend relaxing the annotation payload schema or using a permissive “passthrough” parse path, since the annotations are expected to be written atomically in one operation and should not contain partial/legacy payloads that would require a looser parser.
Applied to files:
apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
📚 Learning: 2026-05-05T09:38:02.512Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3523
File: apps/webapp/app/routes/api.v3.batches.ts:178-181
Timestamp: 2026-05-05T09:38:02.512Z
Learning: When reviewing code that catches `ServiceValidationError` in `*.server.ts` files, do not blindly forward `error.status` to HTTP responses, because SVEs may be thrown with non-default statuses (e.g., 400/500) and forwarding them can cause client-visible behavioral regressions (e.g., surfacing 500s to clients). Prefer a safe default response status of `error.status ?? 422`, but only after confirming via the reachable call graph that the caught `ServiceValidationError` instances are expected to carry those non-default statuses; otherwise, normalize to `422` to avoid unexpected client-visible 5xx behavior.
Applied to files:
apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts
🔇 Additional comments (3)
apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts (1)
7-11: LGTM!Also applies to: 184-266, 298-303
apps/webapp/test/dashboardAgentWatchAlertEnvironments.test.ts (2)
72-100: LGTM!Also applies to: 107-192, 194-293
62-69: 🩺 Stability & AvailabilityNo change needed:
alertsWorker.enqueueis already mocked.
apps/webapp/test/setup.tsmocks~/v3/alertsWorker.serverwithcreateWorkerStub(), andenqueueis avi.fn()used by this suite via the global setup.
…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.
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.