fix(openai): preserve hosted search in Code Mode (#121812)

Keep provider-hosted web_search alongside Code Mode's constrained function surface when OpenAI policy authorizes it. Enforce the same authorization at Responses transport egress.

Refs #121803.
This commit is contained in:
Peter Steinberger
2026-08-10 19:59:51 -07:00
committed by GitHub
parent 9ae3583375
commit 00fb85e48f
9 changed files with 338 additions and 56 deletions
+4 -1
View File
@@ -92,7 +92,10 @@ export function createOpenAINativeWebSearchWrapper(
): StreamFn {
return createPayloadPatchStreamWrapper(
baseStreamFn,
({ payload }) => {
({ payload, options }) => {
(
options as { openclawCodeModeAllowedHostedToolTypes?: Set<string> } | undefined
)?.openclawCodeModeAllowedHostedToolTypes?.add(OPENAI_WEB_SEARCH_TOOL.type);
patchOpenAINativeWebSearchPayload(payload);
},
{
+74 -2
View File
@@ -192,6 +192,11 @@ function runWrappedPayloadCase(params: {
agentId?: string;
nativeWebSearchAllowedByToolPolicy?: boolean;
payload?: Record<string, unknown>;
context?: Context;
streamOptions?: SimpleStreamOptions & {
openclawCodeModeToolSurface?: boolean;
openclawCodeModeAllowedHostedToolTypes?: Set<string>;
};
}) {
const payload = params.payload ?? { store: false };
let capturedOptions: SimpleStreamOptions | undefined;
@@ -212,8 +217,7 @@ function runWrappedPayloadCase(params: {
streamFn: baseStreamFn,
} as never);
const context: Context = { messages: [] };
void streamFn?.(params.model, context, {});
void streamFn?.(params.model, params.context ?? { messages: [] }, params.streamOptions ?? {});
return {
payload,
@@ -2338,6 +2342,53 @@ describe("buildOpenAIProvider", () => {
]);
});
it("authorizes native OpenAI web search through the code mode wrapper chain", () => {
const provider = buildOpenAIProvider();
const wrap = provider.wrapStreamFn;
if (!wrap) {
throw new Error("expected OpenAI wrapper");
}
const allowedHostedToolTypes = new Set<string>();
const result = runWrappedPayloadCase({
wrap,
provider: "openai",
modelId: "gpt-5.4",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://api.openai.com/v1",
} as Model<"openai-responses">,
context: {
messages: [],
tools: [
{ name: "exec", description: "", parameters: {} },
{ name: "wait", description: "", parameters: {} },
],
},
streamOptions: {
openclawCodeModeToolSurface: true,
openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes,
},
payload: {
tools: [
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "function", name: "rogue" },
{ type: "function", name: "web_search" },
],
},
});
expect(result.payload.tools).toEqual([
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "web_search" },
]);
expect(allowedHostedToolTypes).toEqual(new Set(["web_search"]));
});
it("keeps one native OpenAI web search tool when the payload is already patched", () => {
const provider = buildOpenAIProvider();
const wrap = provider.wrapStreamFn;
@@ -2373,12 +2424,17 @@ describe("buildOpenAIProvider", () => {
throw new Error("expected OpenAI wrapper");
}
const allowedHostedToolTypes = new Set<string>();
const result = runWrappedPayloadCase({
wrap,
provider: "openai",
modelId: "gpt-5.4",
agentId: "main",
nativeWebSearchAllowedByToolPolicy: false,
streamOptions: {
openclawCodeModeToolSurface: true,
openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes,
},
cfg: {
agents: {
list: [
@@ -2407,6 +2463,7 @@ describe("buildOpenAIProvider", () => {
{ type: "function", name: "read" },
{ type: "function", name: "web_search" },
]);
expect(allowedHostedToolTypes).toEqual(new Set());
});
it("raises minimal reasoning when native OpenAI web search is injected", () => {
@@ -2444,11 +2501,15 @@ describe("buildOpenAIProvider", () => {
throw new Error("expected OpenAI wrapper");
}
const disabledAllowedHostedToolTypes = new Set<string>();
const disabled = runWrappedPayloadCase({
wrap,
provider: "openai",
modelId: "gpt-5.4",
cfg: { tools: { web: { search: { enabled: false } } } },
streamOptions: {
openclawCodeModeAllowedHostedToolTypes: disabledAllowedHostedToolTypes,
},
model: {
api: "openai-responses",
provider: "openai",
@@ -2457,6 +2518,7 @@ describe("buildOpenAIProvider", () => {
} as Model<"openai-responses">,
payload: { tools: [{ type: "function", name: "web_search" }] },
});
const proxiedAllowedHostedToolTypes = new Set<string>();
const proxied = runWrappedPayloadCase({
wrap,
provider: "openai",
@@ -2467,11 +2529,16 @@ describe("buildOpenAIProvider", () => {
id: "gpt-5.4",
baseUrl: "https://example-proxy.invalid/v1",
} as Model<"openai-responses">,
streamOptions: {
openclawCodeModeAllowedHostedToolTypes: proxiedAllowedHostedToolTypes,
},
payload: { tools: [{ type: "function", name: "web_search" }] },
});
expect(disabled.payload.tools).toEqual([{ type: "function", name: "web_search" }]);
expect(proxied.payload.tools).toEqual([{ type: "function", name: "web_search" }]);
expect(disabledAllowedHostedToolTypes).toEqual(new Set());
expect(proxiedAllowedHostedToolTypes).toEqual(new Set());
});
it("keeps managed web_search when another search provider is configured", () => {
@@ -2482,11 +2549,15 @@ describe("buildOpenAIProvider", () => {
throw new Error("expected OpenAI wrapper");
}
const allowedHostedToolTypes = new Set<string>();
const result = runWrappedPayloadCase({
wrap,
provider: "openai",
modelId: "gpt-5.4",
cfg: { tools: { web: { search: { enabled: true, provider: "brave" } } } },
streamOptions: {
openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes,
},
model: {
api: "openai-responses",
provider: "openai",
@@ -2497,6 +2568,7 @@ describe("buildOpenAIProvider", () => {
});
expect(result.payload.tools).toEqual([{ type: "function", name: "web_search" }]);
expect(allowedHostedToolTypes).toEqual(new Set());
});
it("preserves explicit OpenAI responses transport overrides", () => {
+1
View File
@@ -60,6 +60,7 @@ export type BaseOpenAIStreamOptions = StreamOptions & {
firstEventTimeoutMs?: number;
onFirstEventTimeout?: (reason: Error) => void;
openclawCodeModeToolSurface?: boolean;
openclawCodeModeAllowedHostedToolTypes?: Set<string>;
frequencyPenalty?: number;
presencePenalty?: number;
seed?: number;
@@ -255,8 +255,9 @@ function createResponsesTransportExecutor(config: ResponsesTransportExecutorOpti
?.openclawCodeModeToolSurface === true
) {
const visibleToolNames = resolveCodeModeResponsesVisibleToolNames(context);
enforceCodeModeResponsesToolSurface(params, visibleToolNames);
assertCodeModeResponsesToolSurface(params, visibleToolNames);
const allowedHostedToolTypes = responsesOptions?.openclawCodeModeAllowedHostedToolTypes;
enforceCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes);
assertCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes);
}
return params;
};
@@ -416,6 +416,7 @@ describe("OpenAI Responses provider prompt observer", () => {
context: createContext(prompt, { tools: [tool("exec"), tool("wait")] as never }),
options: {
openclawCodeModeToolSurface: true,
openclawCodeModeAllowedHostedToolTypes: new Set(["web_search"]),
onPayload: async () => {
await Promise.resolve();
return {
@@ -430,7 +431,13 @@ describe("OpenAI Responses provider prompt observer", () => {
content: [{ type: "input_image", image_url: "data:image/png;base64,invalid!" }],
},
],
tools: [tool("exec"), tool("wait"), tool("rogue")],
tools: [
tool("exec"),
tool("wait"),
tool("rogue"),
{ type: "web_search" },
{ type: "file_search" },
],
};
},
},
@@ -439,7 +446,7 @@ describe("OpenAI Responses provider prompt observer", () => {
expect(run.order).toEqual(["observe", "openai.create"]);
expect(run.observations[0]?.matchesAssembledPrompt).toBe(true);
expect(run.requests[0]?.metadata).toEqual({ caller: "kept", host: "added" });
expect(run.requests[0]?.tools).toEqual([tool("exec"), tool("wait")]);
expect(run.requests[0]?.tools).toEqual([tool("exec"), tool("wait"), { type: "web_search" }]);
expect(JSON.stringify(run.requests[0]?.input)).toContain("omitted image payload");
});
@@ -98,7 +98,7 @@ describe("OpenAI Code Mode payload tool filtering", () => {
it("keeps the existing Responses and Completions enforcement contract", () => {
const payload = {
tools: [{ name: "exec" }, { name: "web_search" }, { name: "wait" }],
tools: [{ name: "exec" }, { name: "web_search" }, { type: "web_search" }, { name: "wait" }],
};
enforceCodeModeResponsesToolSurface(payload, visibleToolNames);
@@ -107,6 +107,34 @@ describe("OpenAI Code Mode payload tool filtering", () => {
expect(() => assertCodeModeResponsesToolSurface(payload, visibleToolNames)).not.toThrow();
});
it("keeps only explicitly allowed hosted tools alongside visible client functions", () => {
const allowedHostedToolTypes = new Set(["web_search"]);
const payload = {
tools: [
{ type: "function", name: "exec" },
{ type: "function", name: "rogue" },
{ type: "web_search" },
{ type: "file_search" },
{ type: "web_search", name: "exec" },
{ type: "web_search", function: { name: "wait" } },
{ type: "web_search", functionDeclarations: [{ name: "exec" }] },
{ type: "function", name: "wait" },
],
};
filterCodeModePayloadTools(payload, visibleToolNames, allowedHostedToolTypes);
enforceCodeModeResponsesToolSurface(payload, visibleToolNames, allowedHostedToolTypes);
expect(payload.tools).toEqual([
{ type: "function", name: "exec" },
{ type: "web_search" },
{ type: "function", name: "wait" },
]);
expect(() =>
assertCodeModeResponsesToolSurface(payload, visibleToolNames, allowedHostedToolTypes),
).not.toThrow();
});
it.each(["functionDeclarations", "function_declarations"] as const)(
"rejects and removes grouped %s from final OpenAI payloads",
(field) => {
@@ -139,6 +167,24 @@ describe("OpenAI Code Mode payload tool filtering", () => {
);
});
it.each([
[
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "web_search" },
{ type: "web_search" },
],
[
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "file_search" },
],
])("rejects duplicate or undeclared hosted tools", (...tools) => {
expect(() =>
assertCodeModeResponsesToolSurface({ tools }, visibleToolNames, new Set(["web_search"])),
).toThrow(/tool surface violation/);
});
it("fails closed when the asserted payload tools getter throws", () => {
expect(() =>
assertCodeModeResponsesToolSurface(withThrowingGetter("tools"), visibleToolNames),
@@ -42,9 +42,40 @@ export function readCodeModePayloadToolName(tool: unknown): string | undefined {
return typeof fnName === "string" ? fnName : undefined;
}
function readCodeModePayloadToolIdentity(
tool: unknown,
visibleToolNames: ReadonlySet<string>,
allowedHostedToolTypes?: ReadonlySet<string>,
): string | false | undefined {
if (!isRecord(tool)) {
return undefined;
}
const type = readToolPayloadField(tool, "type");
if (typeof type === "string" && allowedHostedToolTypes?.has(type)) {
try {
if (
Object.hasOwn(tool, "name") ||
Object.hasOwn(tool, "function") ||
Object.hasOwn(tool, "functionDeclarations") ||
Object.hasOwn(tool, "function_declarations")
) {
return false;
}
} catch {
return false;
}
return `hosted:${type}`;
}
const name = readCodeModePayloadToolName(tool);
return typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames)
? `client:${name}`
: undefined;
}
export function filterCodeModePayloadTools(
payload: unknown,
visibleToolNames: ReadonlySet<string>,
allowedHostedToolTypes?: ReadonlySet<string>,
): void {
if (!isRecord(payload)) {
return;
@@ -54,10 +85,17 @@ export function filterCodeModePayloadTools(
return;
}
payload.tools = tools.flatMap((tool) => {
const name = readCodeModePayloadToolName(tool);
if (typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames)) {
const identity = readCodeModePayloadToolIdentity(
tool,
visibleToolNames,
allowedHostedToolTypes,
);
if (identity) {
return [tool];
}
if (identity === false) {
return [];
}
if (!isRecord(tool)) {
return [];
}
@@ -95,6 +133,7 @@ export function resolveCodeModeResponsesVisibleToolNames(
export function enforceCodeModeResponsesToolSurface(
payload: unknown,
visibleToolNames: ReadonlySet<string>,
allowedHostedToolTypes?: ReadonlySet<string>,
): void {
if (!isRecord(payload)) {
return;
@@ -103,31 +142,36 @@ export function enforceCodeModeResponsesToolSurface(
if (!Array.isArray(tools)) {
return;
}
payload.tools = tools.filter((tool) => {
const name = readCodeModePayloadToolName(tool);
return typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames);
});
payload.tools = tools.filter((tool) =>
Boolean(readCodeModePayloadToolIdentity(tool, visibleToolNames, allowedHostedToolTypes)),
);
}
export function assertCodeModeResponsesToolSurface(
payload: unknown,
visibleToolNames: ReadonlySet<string>,
allowedHostedToolTypes?: ReadonlySet<string>,
): void {
const tools = isRecord(payload) ? readToolPayloadField(payload, "tools") : undefined;
if (!Array.isArray(tools)) {
throw new Error("Code mode payload tool surface violation: expected exec,wait; got no tools");
}
const names = tools
.map(readCodeModePayloadToolName)
.filter((name): name is string => typeof name === "string" && name.length > 0)
const identities = tools.map((tool) =>
readCodeModePayloadToolIdentity(tool, visibleToolNames, allowedHostedToolTypes),
);
const names = identities
.flatMap((identity) =>
typeof identity === "string" && identity.startsWith("client:")
? [identity.slice("client:".length)]
: [],
)
.toSorted((left, right) => left.localeCompare(right));
if (
names.length >= 2 &&
names.length === tools.length &&
new Set(names).size === names.length &&
identities.every((identity): identity is string => typeof identity === "string") &&
new Set(identities).size === identities.length &&
names.includes("exec") &&
names.includes("wait") &&
names.every((name) => isCodeModeModelVisibleToolName(name, visibleToolNames))
names.includes("wait")
) {
return;
}
@@ -116,9 +116,11 @@ describe("createOpenAICompletionsToolsCompatWrapper", () => {
});
describe("createCodexNativeWebSearchWrapper", () => {
it("does not inject native web_search when code mode owns the tool surface", () => {
it("keeps native_active web_search alongside the code mode tool surface", () => {
let observedOptions: Parameters<StreamFn>[2];
const payloads: Array<Record<string, unknown>> = [];
const baseStreamFn: StreamFn = (model, context, options) => {
observedOptions = options;
const payload: Record<string, unknown> = {
model: model.id,
tools: [
@@ -178,7 +180,12 @@ describe("createCodexNativeWebSearchWrapper", () => {
expect(payloads[0]?.tools).toEqual([
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "web_search" },
]);
expect(
(observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set<string> } | undefined)
?.openclawCodeModeAllowedHostedToolTypes,
).toEqual(new Set(["web_search"]));
});
it("filters async replacement payloads when code mode owns the tool surface", async () => {
@@ -189,12 +196,22 @@ describe("createCodexNativeWebSearchWrapper", () => {
};
const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, {
codeModeToolSurfaceEnabled: true,
config: {
tools: {
web: {
search: {
enabled: true,
openaiCodex: { enabled: true, mode: "cached" },
},
},
},
},
});
const model = {
api: "openai-responses",
provider: "openai",
api: "openai-chatgpt-responses",
provider: "gateway",
id: "gpt-5.5",
} as Model<"openai-responses">;
} as Model<"openai-chatgpt-responses">;
void wrapped(
model,
@@ -224,6 +241,7 @@ describe("createCodexNativeWebSearchWrapper", () => {
},
{ type: "function", name: "wait" },
{ type: "web_search" },
{ type: "file_search" },
],
}),
},
@@ -236,8 +254,66 @@ describe("createCodexNativeWebSearchWrapper", () => {
{ type: "function", name: "sessions_yield" },
{ type: "function", name: "structured_output" },
{ type: "function", name: "wait" },
{ type: "web_search" },
],
});
expect(
(observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set<string> } | undefined)
?.openclawCodeModeAllowedHostedToolTypes,
).toEqual(new Set(["web_search"]));
});
it("does not authorize hosted search when runtime tool policy denies it in code mode", () => {
let observedOptions: Parameters<StreamFn>[2];
const payloads: Array<Record<string, unknown>> = [];
const baseStreamFn: StreamFn = (model, _context, options) => {
observedOptions = options;
const payload = {
tools: [
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
{ type: "web_search" },
],
};
options?.onPayload?.(payload, model);
payloads.push(structuredClone(payload));
return createAssistantMessageEventStream();
};
const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, {
codeModeToolSurfaceEnabled: true,
nativeWebSearchAllowedByToolPolicy: false,
config: {
tools: {
web: {
search: {
enabled: true,
openaiCodex: { enabled: true, mode: "cached" },
},
},
},
},
});
void wrapped(
codexModel,
{
messages: [],
tools: [
{ name: "exec", description: "", parameters: {} },
{ name: "wait", description: "", parameters: {} },
],
},
{},
);
expect(payloads[0]?.tools).toEqual([
{ type: "function", name: "exec" },
{ type: "function", name: "wait" },
]);
expect(
(observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set<string> } | undefined)
?.openclawCodeModeAllowedHostedToolTypes,
).toEqual(new Set());
});
it("does not enable code-mode transport enforcement when config is on but controls are inactive", () => {
+63 -31
View File
@@ -3,7 +3,6 @@ import {
supportsOpenAIReasoningEffort,
} from "@openclaw/ai/internal/openai";
import {
emitModelTransportDebug,
filterCodeModePayloadTools,
isCodeModeModelVisibleToolName,
readCodeModePayloadToolName,
@@ -49,6 +48,7 @@ type OpenAIServiceTier = "auto" | "default" | "flex" | "priority";
type DynamicFastMode = boolean | (() => boolean | undefined);
type OpenClawSimpleStreamOptions = SimpleStreamOptions & {
openclawCodeModeToolSurface?: boolean;
openclawCodeModeAllowedHostedToolTypes?: Set<string>;
};
type OpenAIResponsesReplayOptions = Parameters<StreamFn>[2] & {
replayResponsesItemIds?: boolean;
@@ -146,9 +146,10 @@ function filterCodeModePayloadHookResult(
payload: unknown,
nextPayload: unknown,
visibleToolNames: ReadonlySet<string>,
allowedHostedToolTypes: ReadonlySet<string>,
): unknown {
const finalPayload = nextPayload === undefined ? payload : nextPayload;
filterCodeModePayloadTools(finalPayload, visibleToolNames);
filterCodeModePayloadTools(finalPayload, visibleToolNames, allowedHostedToolTypes);
return nextPayload === undefined ? undefined : finalPayload;
}
@@ -648,31 +649,81 @@ export function createCodexNativeWebSearchWrapper(
const codeModeSurfaceFromOptions =
(options as OpenClawSimpleStreamOptions | undefined)?.openclawCodeModeToolSurface === true;
const codeModeVisibleToolNames = resolveCodeModeVisibleToolNames(context);
const resolveNativeSearchActivation = () =>
resolveCodexNativeSearchActivation({
config: params.config,
modelProvider: readStringValue(model.provider),
modelApi: readStringValue(model.api),
modelId: readStringValue(model.id),
agentId: params.agentId,
sessionKey: params.sessionKey,
sandboxToolPolicy: params.sandboxToolPolicy,
messageProvider: params.messageProvider,
agentAccountId: params.agentAccountId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
agentDir: params.agentDir,
});
if (
(params.codeModeToolSurfaceEnabled === true ||
codeModeSurfaceFromOptions ||
isCodeModeEnabled(params.config)) &&
codeModeVisibleToolNames
) {
emitModelTransportDebug(
log,
`skipping Codex native web search because code mode owns the model tool surface for ${
model.provider ?? "unknown"
}/${model.id ?? "unknown"}`,
);
// Every spread below must retain this request-scoped Set so the provider policy owner
// and final Responses egress agree on the same hosted-tool authorization fact.
const allowedHostedToolTypes =
(options as OpenClawSimpleStreamOptions | undefined)
?.openclawCodeModeAllowedHostedToolTypes ?? new Set<string>();
const activation =
params.nativeWebSearchAllowedByToolPolicy === false
? undefined
: resolveNativeSearchActivation();
if (activation?.state === "native_active") {
allowedHostedToolTypes.add("web_search");
}
if (activation?.state === "native_active" || activation?.codexNativeEnabled) {
const outcome =
activation.state === "native_active"
? `activating (${activation.codexMode})`
: `skipping (${activation.inactiveReason ?? "inactive"})`;
log.debug(
`${outcome} Codex native web search alongside code mode for ${model.provider ?? "unknown"}/${model.id ?? "unknown"}`,
);
}
const originalOnPayload = options?.onPayload;
const codeModeOptions: OpenClawSimpleStreamOptions = {
...options,
openclawCodeModeToolSurface: true,
openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes,
onPayload: (payload) => {
filterCodeModePayloadTools(payload, codeModeVisibleToolNames);
if (activation?.state === "native_active") {
patchCodexNativeWebSearchPayload({ payload, config: params.config });
}
filterCodeModePayloadTools(payload, codeModeVisibleToolNames, allowedHostedToolTypes);
const nextPayload = originalOnPayload?.(payload, model);
if (isPromiseLike(nextPayload)) {
return Promise.resolve(nextPayload).then((resolvedPayload) =>
filterCodeModePayloadHookResult(payload, resolvedPayload, codeModeVisibleToolNames),
filterCodeModePayloadHookResult(
payload,
resolvedPayload,
codeModeVisibleToolNames,
allowedHostedToolTypes,
),
);
}
return filterCodeModePayloadHookResult(payload, nextPayload, codeModeVisibleToolNames);
return filterCodeModePayloadHookResult(
payload,
nextPayload,
codeModeVisibleToolNames,
allowedHostedToolTypes,
);
},
};
return underlying(model, context, codeModeOptions);
@@ -687,26 +738,7 @@ export function createCodexNativeWebSearchWrapper(
return underlying(model, context, options);
}
const activation = resolveCodexNativeSearchActivation({
config: params.config,
modelProvider: readStringValue(model.provider),
modelApi: readStringValue(model.api),
modelId: readStringValue(model.id),
agentId: params.agentId,
sessionKey: params.sessionKey,
sandboxToolPolicy: params.sandboxToolPolicy,
messageProvider: params.messageProvider,
agentAccountId: params.agentAccountId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
agentDir: params.agentDir,
});
const activation = resolveNativeSearchActivation();
if (activation.state !== "native_active") {
if (activation.codexNativeEnabled) {