Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 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