Skip to content
Merged
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
2 changes: 2 additions & 0 deletions gui/tests/apikeys-refresh-preserve.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ test("successful key delete keeps last-good keys visible when follow-up refresh
expect(deleteBtn).toBeTruthy();
await act(async () => {
deleteBtn!.click();
});
await act(async () => {
await new Promise<void>((resolve) => testWindow.setTimeout(resolve, 310));
});

Expand Down
2 changes: 1 addition & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ export function warnAgentTaskRecoveryStartup(config: {
if (config.agentTaskRecovery?.enabled !== true) return;
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
console.warn(" Recovered model output is retained only in a bounded in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
}

export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
Expand Down
1 change: 0 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,7 +1779,6 @@ async function handleResponsesInner(
);
}
}
toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
} catch {
unreadableEncryptedAgentTask = true;
}
Expand Down
57 changes: 57 additions & 0 deletions tests/agent-task-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { createTranslatorBudget } from "../src/lib/translator-budget";
import { warnAgentTaskRecoveryStartup } from "../src/server";
import { resetAgentTaskRecoveryState } from "../src/server/responses/agent-task-recovery";
import { agentTaskRecoveryWaiterCountForTests } from "../src/server/responses/agent-task-recovery-cache";
Expand Down Expand Up @@ -116,6 +117,8 @@ describe("agent task recovery (opt-in, default off)", () => {
const warnings = capture({ enabled: true });
expect(warnings).toHaveLength(3);
expect(warnings.join("\n")).toContain("Experimental encrypted V2 task recovery is enabled");
expect(warnings.join("\n")).toContain("Recovered plaintext assignment data");
expect(warnings.join("\n")).toContain("process-local in-memory cache");
expect(warnings.join("\n")).not.toContain(secret);
} finally {
console.warn = originalWarn;
Expand Down Expand Up @@ -176,6 +179,60 @@ describe("agent task recovery (opt-in, default off)", () => {
expect(forwardedBodies[1].match(/Message Type: NEW_TASK/g)).toHaveLength(1);
});

test("charges namespaced tool bridge maps only once across recovery reparse", async () => {
const recoveryRequests: Request[] = [];
const providerRequests: Request[] = [];
const requestHeaders = codexHeaders();
globalThis.fetch = (async (input, init) => {
const request = new Request(input, init);
if (request.url.includes("chatgpt.com")) {
recoveryRequests.push(request);
return new Response(recoverySse("Use the advertised tool."), { status: 200 });
}
providerRequests.push(request);
return providerResponse();
}) as typeof fetch;
const namespace = "mcp__review";
const name = "read_file";
const wireName = `${namespace}__${name}`;
const mappingBytes = new TextEncoder().encode(JSON.stringify([wireName, namespace, name])).byteLength;
const budget = createTranslatorBudget();
const originalCharge = budget.chargeRetained.bind(budget);
const mappingCharges: number[] = [];
budget.chargeRetained = (bytes, scope) => {
if (scope.kind === "retained_collectors" && bytes === mappingBytes) mappingCharges.push(bytes);
originalCharge(bytes, scope);
};

try {
const response = await post(
routedConfig(),
"xai/grok-4.5",
encryptedInput(),
requestHeaders,
undefined,
{
translatorBudget: budget,
tools: [{
type: "namespace",
name: namespace,
tools: [{ type: "function", name, parameters: { type: "object" } }],
}],
},
);

expect(response.status).toBe(200);
expect(recoveryRequests).toHaveLength(1);
expect(recoveryRequests[0]?.headers.get("authorization"))
.toBe(requestHeaders.get("authorization"));
expect(recoveryRequests[0]?.headers.get("chatgpt-account-id")).toBe("acct-caller");
expect(providerRequests).toHaveLength(1);
expect(mappingCharges).toHaveLength(1);
} finally {
budget.dispose();
}
});

test("accepts function-call-arguments SSE events", async () => {
const assignment = "Handle the recovered task.";
let providerBody = "";
Expand Down
6 changes: 4 additions & 2 deletions tests/helpers/agent-task-recovery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { handleResponses } from "../../src/server/responses";
import type { TranslatorBudget } from "../../src/lib/translator-budget";
import type { OcxConfig } from "../../src/types";

export const originalFetch = globalThis.fetch;
Expand Down Expand Up @@ -146,15 +147,16 @@ export async function post(
input: unknown[],
headers: HeadersInit = {},
abortSignal?: AbortSignal,
options: { tools?: unknown[]; translatorBudget?: TranslatorBudget } = {},
): Promise<Response> {
return handleResponses(new Request("http://localhost/v1/responses", {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(headers)),
},
body: JSON.stringify({ model, input, stream: false }),
}), config, { model: "", provider: "" }, { abortSignal });
body: JSON.stringify({ model, input, stream: false, ...(options.tools ? { tools: options.tools } : {}) }),
}), config, { model: "", provider: "" }, { abortSignal, translatorBudget: options.translatorBudget });
}

export function encryptedInput(options: {
Expand Down
Loading