Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/server/src/provider/acp/XAiAcpExtension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,68 @@ describe("XAiAcpExtension", () => {
).toMatchObject([{ taskId }]);
});

it("hydrates each task of a wait_all MultiResult from its own entry (#16214)", () => {
// Live 2026-10-14 (lcr-bench fe-10784 grok-4.7-medium): one TaskOutput
// waited on two auto-backgrounded commands. Grok answered with
// MultiResult.results[] and no Result or text header, so both tasks were
// reported running and the turn never ended.
const first = "call-c0b011f1-de42-43de-ac33-a3417c0a748f-117";
const second = "call-c0b011f1-de42-43de-ac33-a3417c0a748f-118";
expect(
extractXAiBackgroundTaskCompletion({
toolCallId: "call-5ef78a10-7efb-43a2-9820-bf461bb83e6c-119",
title: "multi-wait (wait_all)",
status: "completed",
data: {
rawInput: { variant: "TaskOutput", task_ids: [first, second], timeout_ms: 300000 },
rawOutput: {
type: "TaskOutput",
MultiResult: {
mode: "wait_all",
results: [
{ task_id: first, status: "completed", exit_code: 0, output: "tests ok\n" },
{ task_id: second, status: "failed", exit_code: 1, output: "catalog drift\n" },
],
summary: "2/2 tasks completed (wait_all)",
},
},
},
}),
).toEqual([
{ taskId: first, status: "completed", appendOutput: "tests ok" },
{ taskId: second, status: "failed", appendOutput: "catalog drift" },
]);
});

it("keeps a MultiResult entry without a terminal status running (#16214)", () => {
const done = "019f44a6-4820-7402-925d-bc862ee711dd";
const open = "call-c0b011f1-de42-43de-ac33-a3417c0a748f-118";
expect(
extractXAiBackgroundTaskCompletion({
toolCallId: "call-get-multi-any",
title: "multi-wait (wait_any)",
status: "completed",
data: {
rawInput: { variant: "TaskOutput", task_ids: [done, open, "not-a-task"] },
rawOutput: {
type: "TaskOutput",
MultiResult: {
mode: "wait_any",
results: [
{ task_id: done, exit_code: 0, output: "" },
{ task_id: open, status: "running", output: "still going\n" },
{ task_id: "not-a-task", status: "completed" },
],
},
},
},
}),
).toEqual([
{ taskId: done, status: "completed", appendOutput: "" },
{ taskId: open, status: "running", appendOutput: "still going" },
]);
});

it("still ignores TaskOutput ids that are neither a UUID nor a Grok call id", () => {
for (const taskId of [
"call-get-mon",
Expand Down
83 changes: 64 additions & 19 deletions apps/server/src/provider/acp/XAiAcpExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,20 @@ export function extractXAiBackgroundTaskCompletion(toolCall: AcpToolCallState):
if (!isXAiGetSubagentOutputTool(toolCall, rawInput)) return [];
// Caller matches taskId against registered background tools (monitors).
// Subagent hydration stays on extractXAiAcpSubagentUpdate.
// One call can wait on several task_ids; Grok then reports each task in its
// own MultiResult entry and sends no Result or task header (board #16214).
const multiResults = multiResultsFromGetOutputTool(toolCall);
if (multiResults !== undefined) {
return multiResults.flatMap((entry) => {
const taskId = nonEmptyString(entry.task_id) ?? nonEmptyString(entry.taskId);
if (taskId === undefined || !new RegExp(`^${XAI_BACKGROUND_TASK_ID_RE}$`, "i").test(taskId)) {
return [];
}
const status =
toolCall.status === "failed" ? "failed" : (structuredTaskStatus(entry) ?? "running");
return [{ taskId, status, appendOutput: structuredTaskOutput(entry) ?? "" }];
});
}
const text = xAiToolOutputText(toolCall);
const status = statusFromGetOutputTool(toolCall, text);
const appendOutput = resultFromGetOutputTool(toolCall, text) ?? "";
Expand Down Expand Up @@ -838,25 +852,61 @@ function resultTaskIdFromGetOutputTool(
return taskHeader;
}

function resultFromGetOutputTool(
/** The per-task entries of a multi-task get_output envelope, when present. */
function multiResultsFromGetOutputTool(
toolCall: AcpToolCallState,
output: string | undefined,
): string | null {
): ReadonlyArray<Record<string, unknown>> | undefined {
const rawOutput = unknownRecord(toolCall.data.rawOutput);
const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
const multi = unknownRecord(rawOutput?.MultiResult) ?? unknownRecord(rawOutput?.multiResult);
if (!Array.isArray(multi?.results)) return undefined;
return multi.results.flatMap((entry: unknown) => {
const record = unknownRecord(entry);
return record === undefined ? [] : [record];
});
}

/**
* Output text of one structured task result. `undefined` when the result has no
* output field; `null` when it has one that is empty after cleaning.
*/
function structuredTaskOutput(
result: Record<string, unknown> | undefined,
): string | null | undefined {
const structured =
nonEmptyString(result?.output) ??
nonEmptyString(result?.text) ??
decodeByteText(result?.output);
if (structured !== undefined) {
// Drop trailing machine meta blocks when present.
const cleaned = structured
.replace(/<subagent_meta>[\s\S]*?<\/subagent_meta>/gi, "")
.replace(/<subagent_result>[\s\S]*?<\/subagent_result>/gi, "")
.trim();
if (cleaned.length > 0) return cleaned;
return null;
if (structured === undefined) return undefined;
// Drop trailing machine meta blocks when present.
const cleaned = structured
.replace(/<subagent_meta>[\s\S]*?<\/subagent_meta>/gi, "")
.replace(/<subagent_result>[\s\S]*?<\/subagent_result>/gi, "")
.trim();
return cleaned.length > 0 ? cleaned : null;
}

/** Task status from a structured result's status or exit code, if it has one. */
function structuredTaskStatus(
result: Record<string, unknown> | undefined,
): "running" | "completed" | "failed" | undefined {
const structuredStatus = nonEmptyString(result?.status)?.toLowerCase();
if (structuredStatus === "completed" || structuredStatus === "success") return "completed";
if (structuredStatus === "failed" || structuredStatus === "error") return "failed";
if (structuredStatus === "running" || structuredStatus === "pending") return "running";
if (typeof result?.exit_code === "number") {
return result.exit_code === 0 ? "completed" : "failed";
}
return undefined;
}

function resultFromGetOutputTool(
toolCall: AcpToolCallState,
output: string | undefined,
): string | null {
const rawOutput = unknownRecord(toolCall.data.rawOutput);
const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
const structured = structuredTaskOutput(result);
if (structured !== undefined) return structured;
if (output === undefined) return null;
const marker = output.match(/=== Output ===\s*([\s\S]*)$/i);
const body = (marker?.[1] ?? output).trim();
Expand All @@ -870,13 +920,8 @@ function statusFromGetOutputTool(
if (toolCall.status === "failed") return "failed";
const rawOutput = unknownRecord(toolCall.data.rawOutput);
const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
const structuredStatus = nonEmptyString(result?.status)?.toLowerCase();
if (structuredStatus === "completed" || structuredStatus === "success") return "completed";
if (structuredStatus === "failed" || structuredStatus === "error") return "failed";
if (structuredStatus === "running" || structuredStatus === "pending") return "running";
if (typeof result?.exit_code === "number") {
return result.exit_code === 0 ? "completed" : "failed";
}
const structuredStatus = structuredTaskStatus(result);
if (structuredStatus !== undefined) return structuredStatus;
if (output !== undefined) {
if (/Status:\s*failed/i.test(output) || /Exit Code:\s*-?(?!0\b)\d+/i.test(output)) {
return "failed";
Expand Down
Loading