mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: preserve codex raw assistant replies
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__testing,
|
||||
@@ -49,6 +50,24 @@ describe("CodexAppServerClient", () => {
|
||||
expect(outbound.method).toBe("model/list");
|
||||
});
|
||||
|
||||
it("logs a redacted preview for malformed app-server messages", async () => {
|
||||
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
harness.process.stdout.write('{"token":"secret-value"} trailing\n');
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"failed to parse codex app-server message",
|
||||
expect.objectContaining({
|
||||
linePreview: '{"token":"<redacted>"} trailing',
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(JSON.stringify(warn.mock.calls)).not.toContain("secret-value");
|
||||
});
|
||||
|
||||
it("preserves JSON-RPC error codes", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { createWebSocketTransport } from "./transport-websocket.js";
|
||||
import { closeCodexAppServerTransport, type CodexAppServerTransport } from "./transport.js";
|
||||
|
||||
export const MIN_CODEX_APP_SERVER_VERSION = "0.118.0";
|
||||
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
||||
|
||||
type PendingRequest = {
|
||||
method: string;
|
||||
@@ -234,7 +235,10 @@ export class CodexAppServerClient {
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
embeddedAgentLog.warn("failed to parse codex app-server message", { error });
|
||||
embeddedAgentLog.warn("failed to parse codex app-server message", {
|
||||
error,
|
||||
linePreview: redactCodexAppServerLinePreview(trimmed),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
@@ -416,6 +420,19 @@ function numericVersionParts(version: string): number[] {
|
||||
.map((part) => (Number.isFinite(part) ? part : 0));
|
||||
}
|
||||
|
||||
function redactCodexAppServerLinePreview(value: string): string {
|
||||
const compact = value.replace(/\s+/g, " ").trim();
|
||||
const redacted = compact
|
||||
.replace(/(Bearer\s+)[A-Za-z0-9._~+/-]+/gi, "$1<redacted>")
|
||||
.replace(
|
||||
/("(?:api_?key|authorization|token|access_token|refresh_token)"\s*:\s*")([^"]+)(")/gi,
|
||||
"$1<redacted>$3",
|
||||
);
|
||||
return redacted.length > CODEX_APP_SERVER_PARSE_LOG_MAX
|
||||
? `${redacted.slice(0, CODEX_APP_SERVER_PARSE_LOG_MAX)}...`
|
||||
: redacted;
|
||||
}
|
||||
|
||||
const CODEX_APP_SERVER_APPROVAL_REQUEST_METHODS = new Set([
|
||||
"item/commandExecution/requestApproval",
|
||||
"item/fileChange/requestApproval",
|
||||
@@ -438,4 +455,5 @@ function formatExitValue(value: unknown): string {
|
||||
|
||||
export const __testing = {
|
||||
closeCodexAppServerTransport,
|
||||
redactCodexAppServerLinePreview,
|
||||
} as const;
|
||||
|
||||
@@ -208,6 +208,27 @@ describe("CodexAppServerEventProjector", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses raw assistant response items when turn completion omits items", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("rawResponseItem/completed", {
|
||||
item: {
|
||||
type: "message",
|
||||
id: "raw-1",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "OK from raw" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await projector.handleNotification(turnCompleted());
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(result.assistantTexts).toEqual(["OK from raw"]);
|
||||
expect(result.lastAssistant?.content).toEqual([{ type: "text", text: "OK from raw" }]);
|
||||
});
|
||||
|
||||
it("normalizes snake_case current token usage fields", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
|
||||
@@ -115,6 +115,9 @@ export class CodexAppServerEventProjector {
|
||||
case "turn/completed":
|
||||
await this.handleTurnCompleted(params);
|
||||
break;
|
||||
case "rawResponseItem/completed":
|
||||
this.handleRawResponseItemCompleted(params);
|
||||
break;
|
||||
case "error":
|
||||
this.promptError = readString(params, "message") ?? "codex app-server error";
|
||||
this.promptErrorSource = "prompt";
|
||||
@@ -424,6 +427,20 @@ export class CodexAppServerEventProjector {
|
||||
await this.maybeEndReasoning();
|
||||
}
|
||||
|
||||
private handleRawResponseItemCompleted(params: JsonObject): void {
|
||||
const item = isJsonObject(params.item) ? params.item : undefined;
|
||||
if (!item || readString(item, "role") !== "assistant") {
|
||||
return;
|
||||
}
|
||||
const text = extractRawAssistantText(item);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const itemId = readString(item, "id") ?? `raw-assistant-${this.assistantItemOrder.length + 1}`;
|
||||
this.rememberAssistantItem(itemId);
|
||||
this.assistantTextByItem.set(itemId, text);
|
||||
}
|
||||
|
||||
private async maybeEndReasoning(): Promise<void> {
|
||||
if (!this.reasoningStarted || this.reasoningEnded) {
|
||||
return;
|
||||
@@ -661,6 +678,24 @@ function collectTextValues(map: Map<string, string>): string[] {
|
||||
return [...map.values()].filter((text) => text.trim().length > 0);
|
||||
}
|
||||
|
||||
function extractRawAssistantText(item: JsonObject): string | undefined {
|
||||
const content = Array.isArray(item.content) ? item.content : [];
|
||||
const text = content
|
||||
.flatMap((entry) => {
|
||||
if (!isJsonObject(entry)) {
|
||||
return [];
|
||||
}
|
||||
const type = readString(entry, "type");
|
||||
if (type !== "output_text" && type !== "text") {
|
||||
return [];
|
||||
}
|
||||
const value = readString(entry, "text");
|
||||
return value ? [value] : [];
|
||||
})
|
||||
.join("");
|
||||
return text.trim() || undefined;
|
||||
}
|
||||
|
||||
function itemKind(
|
||||
item: CodexThreadItem,
|
||||
): "tool" | "command" | "patch" | "search" | "analysis" | undefined {
|
||||
|
||||
Reference in New Issue
Block a user