Skip to content

Commit 2f27582

Browse files
committed
fix(dashboard-agent): tell get_queue who feeds a custom queue
A custom queue's name has nothing to do with any task id, but the agent searched the deployed task list for a task named after the queue, found none, and reported the queue's tasks as deleted or renamed. get_queue now returns consumerTasks for a custom queue — the deployed task slugs whose queue config names it — and the prompt says an absent task of that name is not evidence about the queue.
1 parent 70d44bc commit 2f27582

7 files changed

Lines changed: 103 additions & 20 deletions

File tree

.changeset/quiet-hounds-shave.md

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+
The current-worker API now reports each task's queue, so you can see which tasks write to a given queue.

apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
7070
triggerSource: true,
7171
createdAt: true,
7272
payloadSchema: true,
73+
queueConfig: true,
7374
},
7475
orderBy: {
7576
slug: "asc",
@@ -100,6 +101,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
100101
triggerSource: task.triggerSource,
101102
createdAt: task.createdAt,
102103
payloadSchema: task.payloadSchema,
104+
queueConfig: task.queueConfig,
103105
})),
104106
},
105107
urls,

internal-packages/dashboard-agent/src/__snapshots__/prompt-prefix.test.ts.snap

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,34 @@ exports[`the prefix stays inside its budget > matches the committed measurement
44
{
55
"assistant": {
66
"prompt": {
7-
"chars": 25593,
8-
"estimatedTokens": 6398,
7+
"chars": 25947,
8+
"estimatedTokens": 6487,
99
},
1010
"tools": {
11-
"chars": 49377,
11+
"chars": 49485,
1212
"count": 24,
13-
"estimatedTokens": 12344,
13+
"estimatedTokens": 12371,
1414
},
1515
"total": {
16-
"chars": 74971,
17-
"estimatedTokens": 18743,
18-
"fingerprint": "6cfbe831",
16+
"chars": 75433,
17+
"estimatedTokens": 18858,
18+
"fingerprint": "5418eb6b",
1919
},
2020
},
2121
"code": {
2222
"prompt": {
23-
"chars": 28348,
24-
"estimatedTokens": 7087,
23+
"chars": 28702,
24+
"estimatedTokens": 7176,
2525
},
2626
"tools": {
27-
"chars": 52386,
27+
"chars": 52494,
2828
"count": 28,
29-
"estimatedTokens": 13097,
29+
"estimatedTokens": 13124,
3030
},
3131
"total": {
32-
"chars": 80735,
33-
"estimatedTokens": 20184,
34-
"fingerprint": "4c412f1e",
32+
"chars": 81197,
33+
"estimatedTokens": 20299,
34+
"fingerprint": "9202c360",
3535
},
3636
},
3737
}

internal-packages/dashboard-agent/src/tool-api.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ export function queueMetricsAreEmpty(data: unknown): boolean {
6969
);
7070
}
7171

72+
/**
73+
* The deployed tasks whose `queueConfig` points at this queue, from the current worker's
74+
* task list. A custom queue's name is unrelated to any task id, so who consumes it can
75+
* only be read off the tasks — never guessed from the name.
76+
*/
77+
export function consumerTasksForQueue(workers: unknown, queueName: string): string[] {
78+
const tasks = (workers as { worker?: { tasks?: unknown } } | null)?.worker?.tasks;
79+
if (!Array.isArray(tasks)) return [];
80+
const slugs = new Set<string>();
81+
for (const task of tasks as Array<{ slug?: unknown; queueConfig?: { name?: unknown } | null }>) {
82+
if (typeof task?.slug !== "string") continue;
83+
if (task.queueConfig?.name === queueName) slugs.add(task.slug);
84+
}
85+
return [...slugs].sort();
86+
}
87+
7288
/**
7389
* Metrics plus the queue's live row. `paused` is the part the model must lead with: a queue
7490
* someone stopped explains its own emptiness, and every metric below it is a consequence
@@ -389,6 +405,26 @@ export function buildApiTools(args: {
389405
return result?.ok ? (result.data as Record<string, unknown>) : undefined;
390406
};
391407

408+
// Only a custom queue needs this read: a task queue's consumer is the task it is
409+
// named after, while a custom queue's name says nothing about who writes to it.
410+
const answer = async (
411+
metrics: unknown,
412+
kind: "task" | "custom",
413+
state: Record<string, unknown> | undefined
414+
) => {
415+
const base = withLiveState(metrics, kind, state);
416+
if (base.queueType !== "custom" || !hasAuth || !projectRef || !environmentName) {
417+
return base;
418+
}
419+
const workers = await apiGet(
420+
origin,
421+
`/api/v1/projects/${projectRef}/${environmentName}/workers/current`,
422+
userActorToken!
423+
);
424+
if (!workers.ok) return base;
425+
return { ...base, consumerTasks: consumerTasksForQueue(workers.data, queue) };
426+
};
427+
392428
const first = await read(type ?? "task");
393429
if (!first) return { error: "No current environment is available to read queues from." };
394430
if (!first.ok) {
@@ -400,16 +436,16 @@ export function buildApiTools(args: {
400436
const otherKind = type === "custom" ? "task" : "custom";
401437
const other = await read(otherKind);
402438
if (other?.ok && !queueMetricsAreEmpty(other.data)) {
403-
return withLiveState(other.data, otherKind, await live(otherKind));
439+
return await answer(other.data, otherKind, await live(otherKind));
404440
}
405441
// Neither kind has metrics, so the live row is the only thing that can tell them
406442
// apart: a paused or empty queue that exists, against a name that doesn't.
407443
const kind = type ?? "task";
408444
const state = (await live(kind)) ?? (await live(otherKind));
409-
return withLiveState(first.data, kind, state);
445+
return await answer(first.data, kind, state);
410446
}
411447
const kind = type ?? "task";
412-
return withLiveState(first.data, kind, await live(kind));
448+
return await answer(first.data, kind, await live(kind));
413449
},
414450
}),
415451

internal-packages/dashboard-agent/src/tool-queue.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { queueMetricsAreEmpty } from "./tool-api";
2+
import { consumerTasksForQueue, queueMetricsAreEmpty } from "./tool-api";
33

44
/**
55
* The metrics route answers an unknown queue with zeroes rather than a 404, so asking for
@@ -29,3 +29,42 @@ describe("queueMetricsAreEmpty", () => {
2929
expect(queueMetricsAreEmpty({ ...zeroes, waitMs: { p50: 0, p95: null } })).toBe(false);
3030
});
3131
});
32+
33+
/**
34+
* The environment that produced the bug: `email-sends` is a custom queue two deployed tasks
35+
* write to, and no task is named after it. Reading the deployed task list for a task called
36+
* `email-sends` finds nothing, which is what let the agent invent a deleted task.
37+
*/
38+
describe("consumerTasksForQueue", () => {
39+
const workers = {
40+
worker: {
41+
tasks: [
42+
{ slug: "send-order-receipt", queueConfig: { name: "email-sends" } },
43+
{ slug: "send-welcome-email", queueConfig: { name: "email-sends" } },
44+
{ slug: "generate-monthly-report", queueConfig: { name: "reports-heavy" } },
45+
{ slug: "sync-inventory", queueConfig: { name: "webhooks" } },
46+
{ slug: "email-sends-audit", queueConfig: null },
47+
],
48+
},
49+
};
50+
51+
it("names the tasks that write to a custom queue nothing is named after", () => {
52+
expect(consumerTasksForQueue(workers, "email-sends")).toEqual([
53+
"send-order-receipt",
54+
"send-welcome-email",
55+
]);
56+
expect(consumerTasksForQueue(workers, "reports-heavy")).toEqual(["generate-monthly-report"]);
57+
});
58+
59+
it("matches the queue config's name, not the task slug", () => {
60+
// `email-sends-audit` has no queue config, so it is on its own task queue.
61+
expect(consumerTasksForQueue(workers, "email-sends-audit")).toEqual([]);
62+
expect(consumerTasksForQueue(workers, "send-order-receipt")).toEqual([]);
63+
});
64+
65+
it("says nothing rather than something wrong when the task list is missing", () => {
66+
expect(consumerTasksForQueue(null, "email-sends")).toEqual([]);
67+
expect(consumerTasksForQueue({ worker: {} }, "email-sends")).toEqual([]);
68+
expect(consumerTasksForQueue({ worker: { tasks: [{}] } }, "email-sends")).toEqual([]);
69+
});
70+
});

internal-packages/dashboard-agent/src/tool-schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const getReportSchema = tool({
190190

191191
export const getQueueSchema = tool({
192192
description:
193-
"Get one queue's metrics over a window: wait latency (p50/p95), peak depth, how many runs started (throughput), and how often the queue was throttled by its concurrency limit. Use this for 'how deep is the X queue', 'is X backed up', or 'why are runs waiting'. The answer also carries the queue's live row: `paused`, `queuedNow`, `runningNow`, `concurrencyLimit`, and `exists: false` when no queue of that name is there at all.",
193+
"Get one queue's metrics over a window: wait latency (p50/p95), peak depth, how many runs started (throughput), and how often the queue was throttled by its concurrency limit. Use this for 'how deep is the X queue', 'is X backed up', or 'why are runs waiting'. The answer also carries the queue's live row: `paused`, `queuedNow`, `runningNow`, `concurrencyLimit`, and `exists: false` when no queue of that name is there at all. For a custom queue it also carries `consumerTasks`: the deployed tasks whose queue config names this queue.",
194194
inputSchema: z.object({
195195
queue: z
196196
.string()
@@ -483,7 +483,7 @@ You have read-only tools that act as the user against their own account:
483483
- ask_support: ask the Trigger.dev support assistant about how Trigger.dev works (docs, concepts, features, configuration, how-tos).
484484
- render_view: render a structured view in the panel from the block catalog. The catalog has the "diagnosis" block (a failure card for a single run), the "chart" block (a line/bar chart of run_query results), the "actions" block (a row of 1-3 buttons offering next steps — a watch intent opens the watch card pre-filled, an ask intent sends the labelled question as the user's next message), and the "investigation" block (a live card for a hypothesis-driven investigation).
485485
- get_report: the composed health report for the current environment (flow, execution, liveness), with a severity and the metrics behind each.
486-
- get_queue: one queue's wait latency, peak depth, throughput, and throttling over a window, plus its live row. Lead with paused when it is true: a paused queue explains its own emptiness, and every metric under it is a consequence, not a finding — say it is paused, and only then the numbers. queuedNow is what is waiting right now, which a window of metrics cannot show; exists:false is the only thing that means the queue isn't there, never zeroed metrics.
486+
- get_queue: one queue's wait latency, peak depth, throughput, and throttling over a window, plus its live row. Lead with paused when it is true: a paused queue explains its own emptiness, and every metric under it is a consequence, not a finding — say it is paused, and only then the numbers. queuedNow is what is waiting right now, which a window of metrics cannot show; exists:false is the only thing that means the queue isn't there, never zeroed metrics. A custom queue's name is not a task id, so no task being named after it is not evidence about it — never conclude from list_tasks or a deployment that a queue is unconsumed, undeployed, deleted, or renamed. consumerTasks is the answer to "who feeds this queue": empty means nothing deployed writes to it, and absent means you did not ask a custom queue.
487487
- list_deploys: recent deployments (versions) in the current environment, with status and commit message.
488488
- get_deploy: one deployment's detail, or the current promoted one when you omit the version.
489489
- correlate_version: the version, commit, and pull request a specific run actually ran.

packages/core/src/v3/schemas/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export const GetWorkerTaskResponse = z.object({
126126
triggerSource: z.string(),
127127
createdAt: z.coerce.date(),
128128
payloadSchema: z.any().nullish(),
129+
queueConfig: z.any().nullish(),
129130
});
130131

131132
export const GetWorkerByTagResponse = z.object({

0 commit comments

Comments
 (0)