mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 04:26:16 +00:00
fix(codex): preserve native web search action metadata (#85378)
This commit is contained in:
@@ -102,7 +102,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945.
|
||||
- Gateway chat display: preserve OpenAI-compatible `prompt_tokens`, `completion_tokens`, and `total_tokens` usage fields in sanitized chat history so llama.cpp sessions keep context counts. Fixes #77992. Thanks @MarTT79.
|
||||
- Dashboard/CLI: allow macOS browser launching through `open` even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44.
|
||||
- Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.
|
||||
- Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving available action query metadata in tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.
|
||||
- OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated `kimi-k2.6` turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.
|
||||
- Browser/CDP: add a WSL2 portproxy self-loop hint when Chrome DevTools endpoints accept connections but return an empty HTTP reply. Fixes #59209. Thanks @Owlock.
|
||||
- Agents/tools: add bounded tool-policy audit log entries that identify which allow/deny rule removed tools or blocked a sandboxed tool call. Fixes #55801. Thanks @justinjkline.
|
||||
|
||||
@@ -1947,7 +1947,88 @@ describe("CodexAppServerEventProjector", () => {
|
||||
expect(event.params).toEqual({ query: "native tool observability" });
|
||||
expect(event.runId).toBe("run-1");
|
||||
expect(event.toolCallId).toBe("search-observed");
|
||||
expect(event.result).toEqual({ status: "completed" });
|
||||
expect(event.result).toEqual({
|
||||
status: "completed",
|
||||
durationMs: 5,
|
||||
query: "native tool observability",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses Codex web search action metadata when the top-level query is empty", async () => {
|
||||
const afterToolCall = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
|
||||
);
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("item/completed", {
|
||||
item: {
|
||||
type: "webSearch",
|
||||
id: "search-observed",
|
||||
query: "",
|
||||
action: {
|
||||
type: "search",
|
||||
query: "native action query",
|
||||
queries: ["native action query", "secondary query"],
|
||||
},
|
||||
status: "completed",
|
||||
durationMs: 5,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
|
||||
const event = requireRecord(
|
||||
mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
|
||||
"after_tool_call event",
|
||||
);
|
||||
expect(event.toolName).toBe("web_search");
|
||||
expect(event.params).toEqual({
|
||||
query: "native action query",
|
||||
queries: ["native action query", "secondary query"],
|
||||
});
|
||||
expect(event.result).toEqual({
|
||||
status: "completed",
|
||||
durationMs: 5,
|
||||
query: "native action query",
|
||||
queries: ["native action query", "secondary query"],
|
||||
});
|
||||
});
|
||||
|
||||
it("marks unavailable Codex web search queries explicitly", async () => {
|
||||
const afterToolCall = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
|
||||
);
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("item/completed", {
|
||||
item: {
|
||||
type: "webSearch",
|
||||
id: "search-observed",
|
||||
query: "",
|
||||
action: { type: "other" },
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
|
||||
const event = requireRecord(
|
||||
mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
|
||||
"after_tool_call event",
|
||||
);
|
||||
expect(event.params).toEqual({
|
||||
action: "other",
|
||||
queryUnavailable: true,
|
||||
});
|
||||
expect(event.result).toEqual({
|
||||
status: "completed",
|
||||
action: "other",
|
||||
queryUnavailable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("records dynamic OpenClaw tool calls in mirrored transcript snapshots", async () => {
|
||||
|
||||
@@ -1528,6 +1528,32 @@ function readString(record: JsonObject, key: string): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return value.trim() || undefined;
|
||||
}
|
||||
|
||||
function readNonEmptyString(record: JsonObject, key: string): string | undefined {
|
||||
return normalizeNonEmptyString(record[key]);
|
||||
}
|
||||
|
||||
function readNonEmptyStringArray(record: JsonObject, key: string): string[] {
|
||||
const value = record[key];
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const entries: string[] = [];
|
||||
for (const entry of value) {
|
||||
const normalized = normalizeNonEmptyString(entry);
|
||||
if (normalized) {
|
||||
entries.push(normalized);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function readNullableString(record: JsonObject, key: string): string | null | undefined {
|
||||
const value = record[key];
|
||||
if (value === null) {
|
||||
@@ -1818,8 +1844,8 @@ function itemToolArgs(item: CodexThreadItem): Record<string, unknown> | undefine
|
||||
changes: itemFileChanges(item),
|
||||
});
|
||||
}
|
||||
if (item.type === "webSearch" && typeof item.query === "string") {
|
||||
return sanitizeCodexAgentEventRecord({ query: item.query });
|
||||
if (item.type === "webSearch") {
|
||||
return webSearchToolArgs(item);
|
||||
}
|
||||
if (item.type === "dynamicToolCall" || item.type === "mcpToolCall") {
|
||||
return sanitizeCodexToolArguments(item.arguments);
|
||||
@@ -1827,6 +1853,39 @@ function itemToolArgs(item: CodexThreadItem): Record<string, unknown> | undefine
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function webSearchToolArgs(item: CodexThreadItem): Record<string, unknown> {
|
||||
const action = isJsonObject(item.action) ? item.action : undefined;
|
||||
const actionType = action ? readNonEmptyString(action, "type") : undefined;
|
||||
const queries =
|
||||
action && actionType === "search" ? readNonEmptyStringArray(action, "queries") : [];
|
||||
const query =
|
||||
normalizeNonEmptyString(item.query) ??
|
||||
(action && actionType === "search" ? readNonEmptyString(action, "query") : undefined) ??
|
||||
queries[0];
|
||||
const url = action ? readNonEmptyString(action, "url") : undefined;
|
||||
const pattern = action ? readNonEmptyString(action, "pattern") : undefined;
|
||||
const args: Record<string, unknown> = {};
|
||||
if (query) {
|
||||
args.query = query;
|
||||
}
|
||||
if (queries.length > 0) {
|
||||
args.queries = queries;
|
||||
}
|
||||
if (actionType && actionType !== "search") {
|
||||
args.action = actionType;
|
||||
}
|
||||
if (url) {
|
||||
args.url = url;
|
||||
}
|
||||
if (pattern) {
|
||||
args.pattern = pattern;
|
||||
}
|
||||
if (!query && !url && !pattern) {
|
||||
args.queryUnavailable = true;
|
||||
}
|
||||
return sanitizeCodexAgentEventRecord(args);
|
||||
}
|
||||
|
||||
function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknown> } {
|
||||
if (item.type === "commandExecution") {
|
||||
return {
|
||||
@@ -1856,11 +1915,19 @@ function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknow
|
||||
};
|
||||
}
|
||||
if (item.type === "webSearch") {
|
||||
return { result: sanitizeCodexAgentEventRecord({ status: "completed" }) };
|
||||
return { result: webSearchToolResult(item) };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function webSearchToolResult(item: CodexThreadItem): Record<string, unknown> {
|
||||
return sanitizeCodexAgentEventRecord({
|
||||
status: itemStatus(item),
|
||||
...(typeof item.durationMs === "number" ? { durationMs: item.durationMs } : {}),
|
||||
...webSearchToolArgs(item),
|
||||
});
|
||||
}
|
||||
|
||||
function itemFileChanges(item: CodexThreadItem): Array<{ path: string; kind: string }> {
|
||||
return Array.isArray(item.changes)
|
||||
? item.changes.map((change) => ({ path: change.path, kind: change.kind }))
|
||||
@@ -1895,8 +1962,8 @@ function itemMeta(
|
||||
{ detailMode },
|
||||
);
|
||||
}
|
||||
if (item.type === "webSearch" && typeof item.query === "string") {
|
||||
return item.query;
|
||||
if (item.type === "webSearch") {
|
||||
return inferToolMetaFromArgs("web_search", webSearchToolArgs(item), { detailMode });
|
||||
}
|
||||
const toolName = itemName(item);
|
||||
if ((item.type === "dynamicToolCall" || item.type === "mcpToolCall") && toolName) {
|
||||
|
||||
@@ -529,7 +529,7 @@ type CodexAppServerRequestResultMap = {
|
||||
"turn/steer": JsonValue;
|
||||
};
|
||||
|
||||
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
||||
export function isJsonObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user