Skip to content

Commit 979655c

Browse files
authored
feat: Sessions dashboard, task_kind, and chat-ready hardening (1/4) (#3542)
## Summary A `/sessions` dashboard for inspecting durable Sessions, an `AGENT` / `SCHEDULED` task-kind filter for the runs list, and the server-side hardening (rate-limit exemption for packets, retry-with-backoff on stream appends, typed too-large-chunk error) that the `chat.agent` runtime in #3543 needs. Builds on the Sessions primitive shipped in #3417. ## Design The Sessions list + detail routes mirror the run inspector pattern. `TaskTriggerSource` gains `AGENT` and `SCHEDULED` values, persisted on `BackgroundWorker.taskKind` and `TaskRun.taskKind` (plus a matching Clickhouse column), so the runs list can filter by kind. New `@trigger.dev/core` modules — `sessionStreams`, `inputStreams`, a `sessionStreamInstance` for realtime streams, and the `realtime-streams-api` / `session-streams-api` surfaces — expose the typed shapes that chat.agent will use to drive `session.out`. `ChatChunkTooLargeError` lets the runtime drop oversized chunks with a typed surface instead of failing the run. `s2Append` retries transient failures with exponential backoff. `/api/v[12]/packets/*` is exempt from customer rate limits so chat snapshot reads and writes don't get throttled under load. ## Stack Part of a 4-PR stack. Merge bottom-up. 1. **This PR** (#3542) → `main` 2. #3543#3542 — `chat.agent` runtime + browser transport 3. #3545#3543 — agent-view dashboard 4. #3546#3545 — ai-chat reference + MCP tooling Replaces #3173 (closed). <!-- GitButler Footer Boundary Top --> --- This is **part 5 of 5 in a stack** made with GitButler: - <kbd>&nbsp;5&nbsp;</kbd> #3612 - <kbd>&nbsp;4&nbsp;</kbd> #3546 - <kbd>&nbsp;3&nbsp;</kbd> #3545 - <kbd>&nbsp;2&nbsp;</kbd> #3543 - <kbd>&nbsp;1&nbsp;</kbd> #3542 👈 <!-- GitButler Footer Boundary Bottom -->
2 parents 09f5354 + be1a6cf commit 979655c

96 files changed

Lines changed: 6292 additions & 206 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Add `ChatChunkTooLargeError` and ApiClient methods for subscribing to session streams. Lays the groundwork for the upcoming `chat.agent`.

.changeset/sessions-primitive.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@trigger.dev/sdk": minor
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Adds the Sessions primitive — a durable, run-aware stream channel keyed
7+
on a stable `externalId`. Public SDK additions: `tasks.triggerAndSubscribe()`
8+
and the `chat.agent` runtime built on top of Sessions. See
9+
https://trigger.dev/docs/ai-chat/overview for the full feature surface.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ apps/**/public/build
6565
/packages/trigger-sdk/src/package.json
6666
/packages/python/src/package.json
6767
**/.claude/settings.local.json
68+
.claude/architecture/
69+
.claude/docs-plans/
70+
.claude/review-guides/
71+
.claude/scheduled_tasks.lock
6872
.mcp.log
6973
.mcp.json
7074
.cursor/debug.log
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
New Sessions page in the dashboard for inspecting `chat.agent` Session rows alongside their underlying runs, plus a "Task source" filter on the Runs list (Standard / Scheduled / Agent) so agent runs can be sliced out of mixed workloads at a glance.

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code when working with this repository. Su
66

77
This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
88

9+
**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.
10+
911
```bash
1012
pnpm run docker # Start Docker services (PostgreSQL, Redis, Electric)
1113
pnpm run db:migrate # Run database migrations

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ArrowsRightLeftIcon,
23
BeakerIcon,
34
BellAlertIcon,
45
BookOpenIcon,
@@ -189,6 +190,28 @@ export function BatchesNone() {
189190
);
190191
}
191192

193+
export function SessionsNone() {
194+
return (
195+
<InfoPanel
196+
title="Sessions"
197+
icon={ArrowsRightLeftIcon}
198+
iconClassName="text-teal-500"
199+
panelClassName="max-w-full"
200+
accessory={
201+
<LinkButton to={docsPath("/ai-chat/overview")} variant="docs/small" LeadingIcon={BookOpenIcon}>
202+
Sessions docs
203+
</LinkButton>
204+
}
205+
>
206+
<Paragraph spacing variant="small">
207+
You have no sessions in this environment. Sessions are durable, typed, bidirectional I/O
208+
primitives that outlive a single run — used by <InlineCode>chat.agent</InlineCode> and any
209+
long-running task that needs streaming input and output.
210+
</Paragraph>
211+
</InfoPanel>
212+
);
213+
}
214+
192215
export function TestHasNoTasks() {
193216
const organization = useOrganization();
194217
const project = useProject();

apps/webapp/app/components/BulkActionFilterSummary.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,19 @@ export function BulkActionFilterSummary({
240240
/>
241241
);
242242
}
243+
case "sources": {
244+
const values = Array.isArray(value) ? value : [`${value}`];
245+
return (
246+
<AppliedFilter
247+
variant="minimal/medium"
248+
key={key}
249+
label={filterTitle(key)}
250+
icon={filterIcon(key)}
251+
value={appliedSummary(values)}
252+
removable={false}
253+
/>
254+
);
255+
}
243256
default: {
244257
assertNever(typedKey);
245258
}

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Ariakit from "@ariakit/react";
22
import {
33
CalendarIcon,
44
ClockIcon,
5+
CpuChipIcon,
56
FingerPrintIcon,
67
PlusIcon,
78
RectangleStackIcon,
@@ -190,6 +191,9 @@ export const TaskRunListSearchFilters = z.object({
190191
`Machine presets to filter by (${machines.join(", ")})`
191192
),
192193
errorId: z.string().optional().describe("Error ID to filter runs by (e.g. error_abc123)"),
194+
sources: StringOrStringArray.describe(
195+
"Task trigger sources to filter by (STANDARD, SCHEDULED, AGENT)"
196+
),
193197
});
194198

195199
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
@@ -231,6 +235,8 @@ export function filterTitle(filterKey: string) {
231235
return "Version";
232236
case "errorId":
233237
return "Error ID";
238+
case "sources":
239+
return "Source";
234240
default:
235241
return filterKey;
236242
}
@@ -271,6 +277,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
271277
return <IconRotateClockwise2 className="size-4" />;
272278
case "errorId":
273279
return <IconBugFilled className="size-4" />;
280+
case "sources":
281+
return <CpuChipIcon className="size-4" />;
274282
default:
275283
return undefined;
276284
}
@@ -318,6 +326,10 @@ export function getRunFiltersFromSearchParams(
318326
? searchParams.getAll("versions")
319327
: undefined,
320328
errorId: searchParams.get("errorId") ?? undefined,
329+
sources:
330+
searchParams.getAll("sources").filter((v) => v.length > 0).length > 0
331+
? searchParams.getAll("sources")
332+
: undefined,
321333
};
322334

323335
const parsed = TaskRunListSearchFilters.safeParse(params);
@@ -359,7 +371,8 @@ export function RunsFilters(props: RunFiltersProps) {
359371
searchParams.has("queues") ||
360372
searchParams.has("machines") ||
361373
searchParams.has("versions") ||
362-
searchParams.has("errorId");
374+
searchParams.has("errorId") ||
375+
searchParams.has("sources");
363376

364377
return (
365378
<div className="flex flex-row flex-wrap items-center gap-1.5">
@@ -395,6 +408,7 @@ const filterTypes = [
395408
{ name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> },
396409
{ name: "bulk", title: "Bulk action", icon: <ListCheckedIcon className="size-4" /> },
397410
{ name: "error", title: "Error ID", icon: <IconBugFilled className="size-4" /> },
411+
{ name: "source", title: "Source", icon: <CpuChipIcon className="size-4" /> },
398412
] as const;
399413

400414
type FilterType = (typeof filterTypes)[number]["name"];
@@ -448,6 +462,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) {
448462
<AppliedScheduleIdFilter />
449463
<AppliedBulkActionsFilter bulkActions={bulkActions} />
450464
<AppliedErrorIdFilter />
465+
<AppliedSourceFilter />
451466
</>
452467
);
453468
}
@@ -482,6 +497,8 @@ function Menu(props: MenuProps) {
482497
return <VersionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
483498
case "error":
484499
return <ErrorIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
500+
case "source":
501+
return <SourceDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
485502
}
486503
}
487504

@@ -1739,3 +1756,101 @@ function AppliedErrorIdFilter() {
17391756
</FilterMenuProvider>
17401757
);
17411758
}
1759+
1760+
const sourceOptions: { value: TaskTriggerSource; title: string }[] = [
1761+
{ value: "STANDARD", title: "Standard" },
1762+
{ value: "SCHEDULED", title: "Scheduled" },
1763+
{ value: "AGENT", title: "Agent" },
1764+
];
1765+
1766+
function SourceDropdown({
1767+
trigger,
1768+
clearSearchValue,
1769+
searchValue,
1770+
onClose,
1771+
}: {
1772+
trigger: ReactNode;
1773+
clearSearchValue: () => void;
1774+
searchValue: string;
1775+
onClose?: () => void;
1776+
}) {
1777+
const { values, replace } = useSearchParams();
1778+
1779+
const handleChange = (values: string[]) => {
1780+
clearSearchValue();
1781+
replace({ sources: values, cursor: undefined, direction: undefined });
1782+
};
1783+
1784+
const filtered = useMemo(() => {
1785+
return sourceOptions.filter((item) =>
1786+
item.title.toLowerCase().includes(searchValue.toLowerCase())
1787+
);
1788+
}, [searchValue]);
1789+
1790+
return (
1791+
<SelectProvider value={values("sources")} setValue={handleChange} virtualFocus={true}>
1792+
{trigger}
1793+
<SelectPopover
1794+
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
1795+
hideOnEscape={() => {
1796+
if (onClose) {
1797+
onClose();
1798+
return false;
1799+
}
1800+
return true;
1801+
}}
1802+
>
1803+
<ComboBox placeholder={"Filter by source..."} value={searchValue} />
1804+
<SelectList>
1805+
{filtered.map((item, index) => (
1806+
<SelectItem
1807+
key={item.value}
1808+
value={item.value}
1809+
icon={
1810+
<TaskTriggerSourceIcon source={item.value} className="size-4 flex-none" />
1811+
}
1812+
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
1813+
>
1814+
{item.title}
1815+
</SelectItem>
1816+
))}
1817+
</SelectList>
1818+
</SelectPopover>
1819+
</SelectProvider>
1820+
);
1821+
}
1822+
1823+
function AppliedSourceFilter() {
1824+
const { values, del } = useSearchParams();
1825+
const sources = values("sources");
1826+
1827+
if (sources.length === 0 || sources.every((v) => v === "")) {
1828+
return null;
1829+
}
1830+
1831+
return (
1832+
<FilterMenuProvider>
1833+
{(search, setSearch) => (
1834+
<SourceDropdown
1835+
trigger={
1836+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
1837+
<AppliedFilter
1838+
label="Source"
1839+
icon={<CpuChipIcon className="size-4" />}
1840+
value={appliedSummary(
1841+
sources.map(
1842+
(v) => sourceOptions.find((o) => o.value === v)?.title ?? v
1843+
)
1844+
)}
1845+
onRemove={() => del(["sources", "cursor", "direction"])}
1846+
variant="secondary/small"
1847+
/>
1848+
</Ariakit.Select>
1849+
}
1850+
searchValue={search}
1851+
clearSearchValue={() => setSearch("")}
1852+
/>
1853+
)}
1854+
</FilterMenuProvider>
1855+
);
1856+
}

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ import {
5555
filterableTaskRunStatuses,
5656
TaskRunStatusCombo,
5757
} from "./TaskRunStatus";
58+
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
5859
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
5960
import { useSearchParams } from "~/hooks/useSearchParam";
61+
import type { TaskTriggerSource } from "@trigger.dev/database";
6062

6163
type RunsTableProps = {
6264
total: number;
@@ -352,6 +354,10 @@ export function TaskRunsTable({
352354
</TableCell>
353355
<TableCell to={path}>
354356
<span className="flex items-center gap-x-1">
357+
<TaskTriggerSourceIcon
358+
source={run.taskKind as TaskTriggerSource}
359+
className="size-3.5 flex-none"
360+
/>
355361
{run.taskIdentifier}
356362
{run.rootTaskRunId === null ? <Badge variant="extra-small">Root</Badge> : null}
357363
</span>

apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ClockIcon } from "@heroicons/react/20/solid";
1+
import { ClockIcon, CpuChipIcon } from "@heroicons/react/20/solid";
22
import type { TaskTriggerSource } from "@trigger.dev/database";
33
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
44
import { cn } from "~/utils/cn";
@@ -12,13 +12,20 @@ export function TaskTriggerSourceIcon({
1212
}) {
1313
switch (source) {
1414
case "STANDARD": {
15-
return <TaskIconSmall className="size-[1.125rem] min-w-[1.125rem] text-tasks" />;
15+
return (
16+
<TaskIconSmall className={cn("size-[1.125rem] min-w-[1.125rem] text-tasks", className)} />
17+
);
1618
}
1719
case "SCHEDULED": {
1820
return (
1921
<ClockIcon className={cn("size-[1.125rem] min-w-[1.125rem] text-schedules", className)} />
2022
);
2123
}
24+
case "AGENT": {
25+
return (
26+
<CpuChipIcon className={cn("size-[1.125rem] min-w-[1.125rem] text-indigo-500", className)} />
27+
);
28+
}
2229
}
2330
}
2431

@@ -30,5 +37,8 @@ export function taskTriggerSourceDescription(source: TaskTriggerSource) {
3037
case "SCHEDULED": {
3138
return "Scheduled task";
3239
}
40+
case "AGENT": {
41+
return "Agent";
42+
}
3343
}
3444
}

0 commit comments

Comments
 (0)