Skip to content

Commit ed8d649

Browse files
committed
fix(dashboard-agent): stop reporting a queue as missing when only its kind was guessed wrong
The metrics route answers an unknown queue with zeroes, so asking for the wrong kind read as an idle queue. get_queue now tries the other kind before believing them.
1 parent 46e72f3 commit ed8d649

4 files changed

Lines changed: 98 additions & 23 deletions

File tree

apps/webapp/app/components/dashboard-agent/view-actions.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { ActionsBlockAction } from "@internal/dashboard-agent-contracts";
22
import { readFileSync } from "node:fs";
33
import { describe, expect, it } from "vitest";
4-
import { cardAlreadyOffersWatch, renderableActions, withoutWatchActions } from "./view-actions";
4+
import {
5+
answerContinuesAfter,
6+
cardAlreadyOffersWatch,
7+
renderableActions,
8+
withoutWatchActions,
9+
} from "./view-actions";
510

611
const watchAction: ActionsBlockAction = {
712
label: "Set up a watch",
@@ -16,7 +21,6 @@ const watchAction: ActionsBlockAction = {
1621
},
1722
},
1823
};
19-
import { answerContinuesAfter, renderableActions } from "./view-actions";
2024

2125
const askAction: ActionsBlockAction = {
2226
label: "Investigate it",

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

Lines changed: 10 additions & 10 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": 24349,
8-
"estimatedTokens": 6087,
7+
"chars": 24504,
8+
"estimatedTokens": 6126,
99
},
1010
"tools": {
1111
"chars": 49210,
1212
"count": 24,
1313
"estimatedTokens": 12303,
1414
},
1515
"total": {
16-
"chars": 73560,
17-
"estimatedTokens": 18390,
18-
"fingerprint": "881ce5e7",
16+
"chars": 73715,
17+
"estimatedTokens": 18429,
18+
"fingerprint": "376f30ba",
1919
},
2020
},
2121
"code": {
2222
"prompt": {
23-
"chars": 27104,
24-
"estimatedTokens": 6776,
23+
"chars": 27259,
24+
"estimatedTokens": 6815,
2525
},
2626
"tools": {
2727
"chars": 52219,
2828
"count": 28,
2929
"estimatedTokens": 13055,
3030
},
3131
"total": {
32-
"chars": 79324,
33-
"estimatedTokens": 19831,
34-
"fingerprint": "d1322cde",
32+
"chars": 79479,
33+
"estimatedTokens": 19870,
34+
"fingerprint": "2cdf079b",
3535
},
3636
},
3737
}

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

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ import type { InvestigationRenderer } from "./tool-investigations";
4444
* The API read tools, in the frozen key order `dashboardAgentToolSchemas` declares:
4545
* a different order is a different cached prompt prefix.
4646
*/
47+
/**
48+
* Whether a metrics answer carries no evidence the queue exists. Zeroes across the board
49+
* are what the route returns for a name it has never seen, and also what a genuinely idle
50+
* queue looks like — so this only decides whether to try the other queue kind, never what
51+
* to tell the user.
52+
*/
53+
export function queueMetricsAreEmpty(data: unknown): boolean {
54+
const d = data as {
55+
peakQueued?: number;
56+
startedCount?: number;
57+
throttledCount?: number;
58+
depthTrend?: unknown[];
59+
waitMs?: { p50?: number | null; p95?: number | null };
60+
} | null;
61+
if (!d) return true;
62+
return (
63+
(d.peakQueued ?? 0) === 0 &&
64+
(d.startedCount ?? 0) === 0 &&
65+
(d.throttledCount ?? 0) === 0 &&
66+
(d.depthTrend ?? []).length === 0 &&
67+
d.waitMs?.p50 == null &&
68+
d.waitMs?.p95 == null
69+
);
70+
}
71+
4772
export function buildApiTools(args: {
4873
ctx: DashboardAgentToolContext;
4974
client: DashboardAgentApiClient;
@@ -316,20 +341,35 @@ export function buildApiTools(args: {
316341
get_queue: tool({
317342
...getQueueSchema,
318343
execute: async ({ queue, type, period }) => {
319-
const sp = new URLSearchParams({ type: type ?? "task" });
320-
if (period) sp.append("period", period);
321-
// Double-encoded: a task queue's ClickHouse name carries a `task/` prefix, and
322-
// the route un-escapes `%2F` back to `/` itself.
323-
const result = await envApiGet(
324-
`/api/v1/queues/${encodeURIComponent(queue)}/metrics?${sp.toString()}`
325-
);
326-
if (!result) return { error: "No current environment is available to read queues from." };
327-
if (!result.ok) {
344+
// The metrics route answers an unknown queue with zeroes rather than a 404, so a
345+
// wrong `type` reads exactly like an idle queue — and the wrong half of that pair
346+
// is easy to pick, since a named queue and a task's own queue look alike. Try the
347+
// other kind before believing the zeroes, and say which one answered.
348+
const read = async (kind: "task" | "custom") => {
349+
const sp = new URLSearchParams({ type: kind });
350+
if (period) sp.append("period", period);
351+
// Double-encoded: a task queue's ClickHouse name carries a `task/` prefix, and
352+
// the route un-escapes `%2F` back to `/` itself.
353+
const result = await envApiGet(
354+
`/api/v1/queues/${encodeURIComponent(queue)}/metrics?${sp.toString()}`
355+
);
356+
return result;
357+
};
358+
359+
const first = await read(type ?? "task");
360+
if (!first) return { error: "No current environment is available to read queues from." };
361+
if (!first.ok) {
328362
return {
329-
error: `Couldn't get metrics for the ${queue} queue (status ${result.status}).`,
363+
error: `Couldn't get metrics for the ${queue} queue (status ${first.status}).`,
330364
};
331365
}
332-
return result.data;
366+
if (queueMetricsAreEmpty(first.data)) {
367+
const other = await read(type === "custom" ? "task" : "custom");
368+
if (other?.ok && !queueMetricsAreEmpty(other.data)) {
369+
return { ...(other.data as object), queueType: type === "custom" ? "task" : "custom" };
370+
}
371+
}
372+
return { ...(first.data as object), queueType: type ?? "task" };
333373
},
334374
}),
335375

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from "vitest";
2+
import { queueMetricsAreEmpty } from "./tool-api";
3+
4+
/**
5+
* The metrics route answers an unknown queue with zeroes rather than a 404, so asking for
6+
* the wrong queue kind reads exactly like an idle queue. `get_queue` retries with the other
7+
* kind before believing that, which is what stops "no queue named email-sends exists" being
8+
* said about a queue holding thousands of runs.
9+
*/
10+
describe("queueMetricsAreEmpty", () => {
11+
const zeroes = {
12+
peakQueued: 0,
13+
startedCount: 0,
14+
throttledCount: 0,
15+
depthTrend: [],
16+
waitMs: { p50: null, p95: null },
17+
};
18+
19+
it("treats an all-zero answer as no evidence the queue exists", () => {
20+
expect(queueMetricsAreEmpty(zeroes)).toBe(true);
21+
expect(queueMetricsAreEmpty(null)).toBe(true);
22+
});
23+
24+
it("takes any single sign of life as evidence", () => {
25+
expect(queueMetricsAreEmpty({ ...zeroes, peakQueued: 4800 })).toBe(false);
26+
expect(queueMetricsAreEmpty({ ...zeroes, startedCount: 3 })).toBe(false);
27+
expect(queueMetricsAreEmpty({ ...zeroes, throttledCount: 1 })).toBe(false);
28+
expect(queueMetricsAreEmpty({ ...zeroes, depthTrend: [0, 0] })).toBe(false);
29+
expect(queueMetricsAreEmpty({ ...zeroes, waitMs: { p50: 0, p95: null } })).toBe(false);
30+
});
31+
});

0 commit comments

Comments
 (0)