Skip to content

Commit be495dc

Browse files
committed
fix(dashboard-agent): guard repo tools' target, surface a run's queue
Repo tools (get_repo_info/list_files/read_file/search_code) and the source-read ledger skipped the branch-needs-environment check; add it at their shared snapshotFor chokepoint. curateRun/curateRuns dropped `queue` even though schedule_watch now tells the model to use it — the field wasn't in the API response at all, so add it to the run presenters (ApiRetrieveRunPresenter, ApiRunListPresenter) and surface it in curation.
1 parent 3ffdef4 commit be495dc

10 files changed

Lines changed: 57 additions & 16 deletions

File tree

‎apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const commonRunSelect = {
3939
createdAt: true,
4040
startedAt: true,
4141
queuedAt: true,
42+
queue: true,
4243
updatedAt: true,
4344
completedAt: true,
4445
expiredAt: true,
@@ -567,6 +568,7 @@ async function createCommonRunStructure(
567568
createdAt: run.createdAt,
568569
startedAt: run.startedAt ?? undefined,
569570
queuedAt: run.queuedAt ?? undefined,
571+
queue: run.queue,
570572
// Mirrors dashboardAgentWatchRunChecks.describeRunWait: a resumed/retried/paused run's
571573
// queuedAt is a leftover from an earlier enqueue, not this attempt's wait.
572574
queueWaitReliable: run.queuedAt !== null && !STALE_QUEUED_AT_STATUSES.has(run.status),
@@ -696,6 +698,7 @@ export function synthesiseFoundRunFromBuffer(buffered: SyntheticRun): FoundRun {
696698
startedAt: null,
697699
// Buffered runs live in Redis until the drainer replays them into Postgres — never queued there yet.
698700
queuedAt: null,
701+
queue: buffered.queue ?? "",
699702
updatedAt: buffered.cancelledAt ?? buffered.createdAt,
700703
// PG-resident SYSTEM_FAILURE rows always have `completedAt` set by
701704
// the engine; the buffer-synth path must match so SDK consumers

‎apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ export class ApiRunListPresenter extends BasePresenter {
331331
delayedUntil: run.delayUntil ? new Date(run.delayUntil) : undefined,
332332
queuedAt: run.queuedAt ? new Date(run.queuedAt) : undefined,
333333
queueWaitReliable: run.queueWaitReliable,
334+
queue: run.queue,
334335
isTest: run.isTest,
335336
ttl: run.ttl ?? undefined,
336337
expiredAt: run.expiredAt ? new Date(run.expiredAt) : undefined,

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

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ describe("the head-start and agent prefixes are the same prefix", () => {
8888
* drift. The snapshot below is the itemised diff a reviewer reads.
8989
*/
9090
const PREFIX_BUDGET = {
91-
assistant: { chars: 80_200, estimatedTokens: 20_100, tools: 25, promptChars: 27_500 },
92-
code: { chars: 88_000, estimatedTokens: 22_000, tools: 29, promptChars: 30_100 },
91+
assistant: { chars: 80_300, estimatedTokens: 20_100, tools: 25, promptChars: 27_500 },
92+
code: { chars: 88_100, estimatedTokens: 22_100, tools: 29, promptChars: 30_100 },
9393
} as const;
9494

9595
describe("the prefix stays inside its budget", () => {

‎internal-packages/dashboard-agent/src/repo-tools.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,17 @@ describe("repo-tools", () => {
263263
expect(res.files).toContain("src/trigger/order.ts");
264264
});
265265
});
266+
267+
describe("repo tools: branch without environment", () => {
268+
const CASES: Array<[string, any]> = [
269+
["get_repo_info", tools.get_repo_info],
270+
["list_files", tools.list_files],
271+
["read_file", tools.read_file],
272+
["search_code", tools.search_code],
273+
];
274+
275+
it.each(CASES)("%s is refused before any snapshot fetch", async (_name, tool) => {
276+
const res: any = await call(tool, { path: "README.md", branch: "feat/x" });
277+
expect(res.error).toBe("branch needs environment: preview or dev.");
278+
});
279+
});

‎internal-packages/dashboard-agent/src/repo-tools.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
66
import { promisify } from "node:util";
77
import { sliceWellFormed } from "@internal/dashboard-agent-contracts";
88
import { tool, type ToolSet } from "ai";
9-
import { crossProjectTarget, type ApiTarget, type TargetInput } from "./tool-api-client";
9+
import {
10+
crossProjectTarget,
11+
targetInputError,
12+
type ApiTarget,
13+
type TargetInput,
14+
} from "./tool-api-client";
1015
import {
1116
getRepoInfoSchema,
1217
listFilesSchema,
@@ -225,6 +230,10 @@ export function buildRepoTools(
225230
runId?: string,
226231
target?: TargetInput
227232
): Promise<RepoSnapshot | { error: string }> {
233+
if (target) {
234+
const targetError = targetInputError(target);
235+
if (targetError) return { error: targetError };
236+
}
228237
if (!runId) return defaultSnapshot;
229238
if (!resolveRunSnapshot)
230239
return { error: "Reading a specific run's source isn't available here." };

‎internal-packages/dashboard-agent/src/tool-curation.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ describe("computed run wait", () => {
196196
});
197197
});
198198

199+
describe("run queue name", () => {
200+
it("carries the run's actual queue name through curateRun", () => {
201+
const run = curateRun({ id: "run_1", queue: "task/send-receipt" });
202+
expect(run.queue).toBe("task/send-receipt");
203+
});
204+
205+
it("carries the run's actual queue name through curateRuns", () => {
206+
const runs = curateRuns({ data: [{ id: "run_1", queue: "my-custom-queue" }] });
207+
expect(runs.runs[0]!.queue).toBe("my-custom-queue");
208+
});
209+
});
210+
199211
describe("curateTrace emits spanId", () => {
200212
it("carries each span's id, required to cite it as evidence", () => {
201213
const out = curateTrace({

‎internal-packages/dashboard-agent/src/tool-curation.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function curateRun(run: any) {
108108
status: run.status,
109109
taskIdentifier: run.taskIdentifier,
110110
version: run.version,
111+
queue: run.queue,
111112
isQueued: run.isQueued,
112113
isExecuting: run.isExecuting,
113114
isCompleted: run.isCompleted,
@@ -149,6 +150,7 @@ export function curateRuns(data: unknown) {
149150
status: r.status,
150151
taskIdentifier: r.taskIdentifier,
151152
version: r.version,
153+
queue: r.queue,
152154
isTest: r.isTest,
153155
createdAt: r.createdAt,
154156
startedAt: r.startedAt,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const listRunsSchema = tool({
106106
});
107107

108108
export const getRunSchema = tool({
109-
description: `Get the status, timing, cost, and error details for a single run in the current environment, by its run id (run_...). The \`wait\` field is the already-computed queue wait (or, when unreliable, time since creation) — never recompute it from createdAt/startedAt. A 404 (in the error message) means this run isn't in this scope, never that it doesn't exist: call locate, then retry here with the scope it names.`,
109+
description: `Get the status, timing, cost, and error details for a single run in the current environment, by its run id (run_...). The \`wait\` field is the already-computed queue wait (or, when unreliable, time since creation) — never recompute it from createdAt/startedAt. \`queue\` is the run's actual queue name — use it for schedule_watch or get_queue, never guess. A 404 (in the error message) means this run isn't in this scope, never that it doesn't exist: call locate, then retry here with the scope it names.`,
110110
inputSchema: z.object({
111111
runId: z.string().describe("The run id, e.g. run_abc123."),
112112
...targetFields,

‎internal-packages/dashboard-agent/src/tool-source-ledger.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ToolSet } from "ai";
2-
import { apiGet, crossProjectTarget, type ApiTarget } from "./tool-api-client";
2+
import { apiGet, crossProjectTarget, targetInputError, type ApiTarget } from "./tool-api-client";
33
import type { RepoSnapshot } from "./repo-tools";
44

55
/**
@@ -137,7 +137,7 @@ export function createSourceReadLedger(ctx: SourceLedgerContext): SourceReadLedg
137137
execute: async (input: any, options: any) => {
138138
const result = await execute(input, options);
139139
const path = (result as { path?: string } | undefined)?.path;
140-
if (path && !(result as { error?: unknown }).error) {
140+
if (path && !(result as { error?: unknown }).error && !targetInputError(input ?? {})) {
141141
const snap = input?.runId
142142
? await resolveRunSnapshot(input.runId, crossProjectTarget(input))
143143
: ctx.repoSnapshot;

0 commit comments

Comments
 (0)