fix: restore code queue task submission
- catch async route rejections before Bun fallback HTML\n- return structured JSON for missing reference task ids\n- harden frontend direct proxy JSON handling
This commit is contained in:
@@ -707,6 +707,7 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise<Response> {
|
||||
}
|
||||
const overviewCacheKey = `${suffix}${url.search}`;
|
||||
const canUseOverviewCache = req.method === "GET" && suffix === "/api/tasks/overview";
|
||||
const expectsJsonResponse = suffix === "/health" || suffix.startsWith("/api/");
|
||||
if (req.method !== "GET" && req.method !== "HEAD") invalidateCodeQueueOverviewCache();
|
||||
if (canUseOverviewCache) {
|
||||
const cached = cachedCodeQueueOverview(overviewCacheKey);
|
||||
@@ -736,8 +737,28 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise<Response> {
|
||||
const isJsonResponse = (upstreamContentType ?? "").toLowerCase().includes("json");
|
||||
let parsedJson: unknown = null;
|
||||
let jsonText = "";
|
||||
if (canUseOverviewCache && upstream.ok && isJsonResponse) {
|
||||
if (expectsJsonResponse) {
|
||||
jsonText = new TextDecoder().decode(upstreamBody);
|
||||
if (!isJsonResponse) {
|
||||
logger("warn", "code_queue_direct_proxy_non_json", {
|
||||
path: suffix,
|
||||
upstreamUrl: upstreamUrl.toString(),
|
||||
status: upstream.status,
|
||||
contentType: upstreamContentType,
|
||||
bodyBytes: upstreamBody.byteLength,
|
||||
preview: safePreview(jsonText),
|
||||
});
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
error: {
|
||||
message: "code-queue upstream returned non-JSON response",
|
||||
status: upstream.status,
|
||||
contentType: upstreamContentType,
|
||||
bodyBytes: upstreamBody.byteLength,
|
||||
preview: safePreview(jsonText),
|
||||
},
|
||||
}, 502);
|
||||
}
|
||||
try {
|
||||
parsedJson = jsonText ? JSON.parse(jsonText) as unknown : null;
|
||||
} catch (error) {
|
||||
@@ -757,14 +778,15 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise<Response> {
|
||||
detail,
|
||||
status: upstream.status,
|
||||
bodyBytes: upstreamBody.byteLength,
|
||||
contentType: upstreamContentType,
|
||||
preview: safePreview(jsonText),
|
||||
},
|
||||
}, 502);
|
||||
}
|
||||
}
|
||||
if (canUseOverviewCache && upstream.ok && isJsonResponse) {
|
||||
if (expectsJsonResponse) {
|
||||
const text = jsonText;
|
||||
if (typeof parsedJson === "object" && parsedJson !== null) {
|
||||
if (canUseOverviewCache && upstream.ok && typeof parsedJson === "object" && parsedJson !== null) {
|
||||
codeQueueOverviewCache.set(codeQueueOverviewCacheKey(overviewCacheKey), { at: Date.now(), payload: parsedJson as JsonValue, text });
|
||||
}
|
||||
return new Response(text, { status: upstream.status, headers: responseHeaders });
|
||||
|
||||
@@ -108,7 +108,7 @@ import {
|
||||
taskForListResponse,
|
||||
transcriptChunkResponse,
|
||||
} from "./queue-api";
|
||||
import { configureReferences, injectReferencedTaskContext, taskReferenceIds } from "./references";
|
||||
import { ReferenceTaskLookupError, configureReferences, injectReferencedTaskContext, taskReferenceIds } from "./references";
|
||||
import { configureSelfTests, runJudgeInfraSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest } from "./self-tests";
|
||||
import {
|
||||
configureTaskView,
|
||||
@@ -2686,6 +2686,34 @@ async function readJson(req: Request): Promise<unknown> {
|
||||
return JSON.parse(text) as unknown;
|
||||
}
|
||||
|
||||
function requestErrorResponse(error: unknown): Response | null {
|
||||
if (error instanceof ReferenceTaskLookupError) {
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
error: error.message,
|
||||
missingReferenceTaskIds: error.missingIds,
|
||||
}, 400);
|
||||
}
|
||||
if (error instanceof SyntaxError && /json/iu.test(error.message)) {
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
error: "invalid JSON request body",
|
||||
detail: error.message,
|
||||
}, 400);
|
||||
}
|
||||
if (error instanceof Error && (
|
||||
error.message === "request body must be an object"
|
||||
|| error.message === "prompt is required"
|
||||
|| error.message === "a task cannot reference itself while editing prompt"
|
||||
|| error.message.startsWith("referenceTaskIds supports at most ")
|
||||
|| error.message.startsWith("queueId must match ")
|
||||
|| error.message.startsWith("queue name must be ")
|
||||
)) {
|
||||
return jsonResponse({ ok: false, error: error.message }, 400);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTask(id: string): QueueTask | null {
|
||||
return state.tasks.find((task) => task.id === id) ?? null;
|
||||
}
|
||||
@@ -3033,10 +3061,10 @@ async function route(req: Request): Promise<Response> {
|
||||
});
|
||||
}
|
||||
const devContainerStartMatch = url.pathname.match(/^\/api\/dev-containers(?:\/([^/]+))?\/start$/u);
|
||||
if (devContainerStartMatch !== null && req.method === "POST") return startDevContainer(req, devContainerStartMatch[1] === undefined ? null : decodeURIComponent(devContainerStartMatch[1]));
|
||||
if (devContainerStartMatch !== null && req.method === "POST") return await startDevContainer(req, devContainerStartMatch[1] === undefined ? null : decodeURIComponent(devContainerStartMatch[1]));
|
||||
const devContainerStatusMatch = url.pathname.match(/^\/api\/dev-containers(?:\/([^/]+))?\/status$/u);
|
||||
if (devContainerStatusMatch !== null && req.method === "GET") return devContainerStatus(devContainerStatusMatch[1] === undefined ? null : decodeURIComponent(devContainerStatusMatch[1]));
|
||||
if (url.pathname === "/api/judge/probe" && (req.method === "GET" || req.method === "POST")) return runJudgeProbe();
|
||||
if (devContainerStatusMatch !== null && req.method === "GET") return await devContainerStatus(devContainerStatusMatch[1] === undefined ? null : decodeURIComponent(devContainerStatusMatch[1]));
|
||||
if (url.pathname === "/api/judge/probe" && (req.method === "GET" || req.method === "POST")) return await runJudgeProbe();
|
||||
if (url.pathname === "/api/judge/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runJudgeInfraSelfTest());
|
||||
if (url.pathname === "/api/queue-order/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runQueueOrderingSelfTest());
|
||||
if (url.pathname === "/api/reference-injection/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runReferenceInjectionSelfTest());
|
||||
@@ -3062,17 +3090,17 @@ async function route(req: Request): Promise<Response> {
|
||||
const tasks = await loadAllTasksForRead();
|
||||
return jsonResponse({ ok: true, queues: perQueueSummaries(tasks), queue: queueSummary(false, tasks) });
|
||||
}
|
||||
if (url.pathname === "/api/queues" && req.method === "POST") return createQueue(req);
|
||||
if (url.pathname === "/api/queues" && req.method === "POST") return await createQueue(req);
|
||||
const queueMatch = url.pathname.match(/^\/api\/queues\/([^/]+)$/u);
|
||||
if (queueMatch !== null && (req.method === "PATCH" || req.method === "PUT" || req.method === "POST")) return updateQueue(decodeURIComponent(queueMatch[1] ?? ""), req);
|
||||
if (url.pathname === "/api/tasks/read-all" && req.method === "POST") return markTerminalTasksRead(url);
|
||||
if (queueMatch !== null && (req.method === "PATCH" || req.method === "PUT" || req.method === "POST")) return await updateQueue(decodeURIComponent(queueMatch[1] ?? ""), req);
|
||||
if (url.pathname === "/api/tasks/read-all" && req.method === "POST") return await markTerminalTasksRead(url);
|
||||
if (url.pathname === "/api/tasks/stats" && req.method === "GET") {
|
||||
const queueId = url.searchParams.get("queueId");
|
||||
const allTasks = await loadAllTasksForRead();
|
||||
const statsTasks = queueId === null ? allTasks : allTasks.filter((task) => queueIdOf(task) === safeQueueId(queueId));
|
||||
return jsonResponse({ ok: true, statistics: taskStatisticsSummary(statsTasks, statsDaysFromUrl(url)), queue: queueSummary(false, allTasks) });
|
||||
}
|
||||
if (url.pathname === "/api/tasks/overview" && req.method === "GET") return tasksOverviewResponse(url);
|
||||
if (url.pathname === "/api/tasks/overview" && req.method === "GET") return await tasksOverviewResponse(url);
|
||||
if (url.pathname === "/api/tasks" && req.method === "GET") {
|
||||
const status = url.searchParams.get("status");
|
||||
const queueId = url.searchParams.get("queueId");
|
||||
@@ -3104,29 +3132,29 @@ async function route(req: Request): Promise<Response> {
|
||||
},
|
||||
});
|
||||
}
|
||||
if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/batch") && req.method === "POST") return createTasks(req);
|
||||
if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/batch") && req.method === "POST") return await createTasks(req);
|
||||
const outputMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/output$/u);
|
||||
if (outputMatch !== null && req.method === "GET") {
|
||||
const task = await findTaskForRead(decodeURIComponent(outputMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return outputChunkResponse(task, url);
|
||||
return await outputChunkResponse(task, url);
|
||||
}
|
||||
const transcriptMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/transcript$/u);
|
||||
if (transcriptMatch !== null && req.method === "GET") {
|
||||
const task = await findTaskForRead(decodeURIComponent(transcriptMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return transcriptChunkResponse(task, url);
|
||||
return await transcriptChunkResponse(task, url);
|
||||
}
|
||||
const promptMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/prompt$/u);
|
||||
if (promptMatch !== null && req.method === "GET") {
|
||||
const task = await findTaskForRead(decodeURIComponent(promptMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return taskPromptDetailResponse(task, url);
|
||||
return await taskPromptDetailResponse(task, url);
|
||||
}
|
||||
if (promptMatch !== null && req.method === "PATCH") {
|
||||
const task = await findTaskForMutation(decodeURIComponent(promptMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return editQueuedTaskPrompt(task, req);
|
||||
return await editQueuedTaskPrompt(task, req);
|
||||
}
|
||||
const traceSummaryMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace-summary$/u);
|
||||
if (traceSummaryMatch !== null && req.method === "GET") {
|
||||
@@ -3138,13 +3166,13 @@ async function route(req: Request): Promise<Response> {
|
||||
if (traceStepsMatch !== null && req.method === "GET") {
|
||||
const task = await findTaskForRead(decodeURIComponent(traceStepsMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return taskTraceStepsResponse(task, url);
|
||||
return await taskTraceStepsResponse(task, url);
|
||||
}
|
||||
const traceStepMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace-step$/u);
|
||||
if (traceStepMatch !== null && req.method === "GET") {
|
||||
const task = await findTaskForRead(decodeURIComponent(traceStepMatch[1] ?? ""));
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
return taskTraceStepDetailResponse(task, url);
|
||||
return await taskTraceStepDetailResponse(task, url);
|
||||
}
|
||||
const summaryMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/summary$/u);
|
||||
if (summaryMatch !== null && req.method === "GET") {
|
||||
@@ -3156,27 +3184,32 @@ async function route(req: Request): Promise<Response> {
|
||||
if (match !== null) {
|
||||
const action = match[2];
|
||||
const taskId = decodeURIComponent(match[1] ?? "");
|
||||
if (action === "read" && req.method === "POST") return markTaskReadById(taskId);
|
||||
if (action === "read" && req.method === "POST") return await markTaskReadById(taskId);
|
||||
const task = action === undefined && req.method === "GET"
|
||||
? await findTaskForRead(taskId)
|
||||
: await findTaskForMutation(taskId);
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
if (action === "retry" && req.method === "POST") return manualRetry(task, req);
|
||||
if (action === "steer" && req.method === "POST") return steerTask(task, req);
|
||||
if (action === "interrupt" && req.method === "POST") return interruptTask(task);
|
||||
if (action === "move" && req.method === "POST") return moveTaskToQueue(task, req);
|
||||
if (action === "edit" && (req.method === "POST" || req.method === "PATCH")) return editQueuedTaskPrompt(task, req);
|
||||
if (action === "retry" && req.method === "POST") return await manualRetry(task, req);
|
||||
if (action === "steer" && req.method === "POST") return await steerTask(task, req);
|
||||
if (action === "interrupt" && req.method === "POST") return await interruptTask(task);
|
||||
if (action === "move" && req.method === "POST") return await moveTaskToQueue(task, req);
|
||||
if (action === "edit" && (req.method === "POST" || req.method === "PATCH")) return await editQueuedTaskPrompt(task, req);
|
||||
if (action !== undefined) return jsonResponse({ ok: false, error: "not found" }, 404);
|
||||
if (req.method === "GET") {
|
||||
if (url.searchParams.get("meta") === "1") return jsonResponse({ ok: true, task: taskForMetaResponse(task) });
|
||||
const includeRaw = url.searchParams.get("raw") === "1" || url.searchParams.get("full") === "1";
|
||||
return jsonResponse({ ok: true, task: taskForResponse(task, true, includeRaw) });
|
||||
}
|
||||
if (req.method === "DELETE") return interruptTask(task);
|
||||
if (req.method === "DELETE") return await interruptTask(task);
|
||||
return jsonResponse({ ok: false, error: "method not allowed" }, 405);
|
||||
}
|
||||
return jsonResponse({ ok: false, error: "not found", path: url.pathname }, 404);
|
||||
} catch (error) {
|
||||
const requestError = requestErrorResponse(error);
|
||||
if (requestError !== null) {
|
||||
logger("warn", "request_rejected", { path: url.pathname, error: errorToJson(error) });
|
||||
return requestError;
|
||||
}
|
||||
logger("error", "request_failed", { path: url.pathname, error: error instanceof Error ? error.stack ?? error.message : String(error) });
|
||||
return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import { lastAssistantMessage } from "./task-view";
|
||||
import { resolvedReferenceContextTitle, userPromptForDisplay } from "./prompts";
|
||||
import type { QueueTask, QueueTaskRequest, ReferenceInjectionRecord, ReferenceInjectionSummaryItem } from "./types";
|
||||
|
||||
export class ReferenceTaskLookupError extends Error {
|
||||
missingIds: string[];
|
||||
|
||||
constructor(missingIds: string[]) {
|
||||
super(`referenced Code Queue task not found: ${missingIds.join(", ")}`);
|
||||
this.name = "ReferenceTaskLookupError";
|
||||
this.missingIds = [...missingIds];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReferencesContext {
|
||||
addUniqueTaskId: (ids: string[], value: string) => void;
|
||||
findTask: (id: string) => QueueTask | null;
|
||||
@@ -142,7 +152,7 @@ function injectReferencedTaskContext(request: QueueTaskRequest, finder: (id: str
|
||||
if (ids.length > 5) throw new Error(`referenceTaskIds supports at most 5 task ids, got ${ids.length}`);
|
||||
const referencedTasks = ids.map((id) => finder(id));
|
||||
const missing = ids.filter((_id, index) => referencedTasks[index] === null);
|
||||
if (missing.length > 0) throw new Error(`referenced Code Queue task not found: ${missing.join(", ")}`);
|
||||
if (missing.length > 0) throw new ReferenceTaskLookupError(missing);
|
||||
const userPrompt = request.basePrompt ?? userPromptForDisplay(request.prompt);
|
||||
const graph = collectReferenceGraph(ids, ctx().referenceInjectionMaxRounds, finder);
|
||||
const injection: ReferenceInjectionRecord = {
|
||||
|
||||
Reference in New Issue
Block a user