mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 21:10:43 +00:00
fix: recover multiline codex app-server messages
This commit is contained in:
@@ -61,6 +61,7 @@ describe("CodexAppServerClient", () => {
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"failed to parse codex app-server message",
|
||||
expect.objectContaining({
|
||||
consoleMessage: expect.stringContaining("<redacted>"),
|
||||
linePreview: '{"token":"<redacted>"} trailing',
|
||||
}),
|
||||
),
|
||||
@@ -68,6 +69,32 @@ describe("CodexAppServerClient", () => {
|
||||
expect(JSON.stringify(warn.mock.calls)).not.toContain("secret-value");
|
||||
});
|
||||
|
||||
it("recovers app-server messages split by raw newlines inside JSON strings", async () => {
|
||||
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
const notifications: unknown[] = [];
|
||||
harness.client.addNotificationHandler((notification) => {
|
||||
notifications.push(notification);
|
||||
});
|
||||
|
||||
harness.process.stdout.write(
|
||||
'{"method":"item/commandExecution/outputDelta","params":{"delta":"first' +
|
||||
"\n" +
|
||||
'second"}}\n',
|
||||
);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(notifications).toEqual([
|
||||
{
|
||||
method: "item/commandExecution/outputDelta",
|
||||
params: { delta: "first\nsecond" },
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves JSON-RPC error codes", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
@@ -25,6 +25,8 @@ import { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||
|
||||
export { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
||||
const CODEX_APP_SERVER_PARSE_BUFFER_MAX = 1_000_000;
|
||||
const CODEX_APP_SERVER_PARSE_BUFFER_MAX_LINES = 1_000;
|
||||
const CODEX_DYNAMIC_TOOL_SERVER_REQUEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
type PendingRequest = {
|
||||
@@ -64,6 +66,13 @@ export class CodexAppServerClient {
|
||||
private nextId = 1;
|
||||
private initialized = false;
|
||||
private closed = false;
|
||||
private pendingParse:
|
||||
| {
|
||||
text: string;
|
||||
lineCount: number;
|
||||
firstError: unknown;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
private constructor(child: CodexAppServerTransport) {
|
||||
this.child = child;
|
||||
@@ -262,7 +271,12 @@ export class CodexAppServerClient {
|
||||
}
|
||||
|
||||
private handleLine(line: string): void {
|
||||
const trimmed = line.trim();
|
||||
const rawLine = line.endsWith("\r") ? line.slice(0, -1) : line;
|
||||
if (this.pendingParse) {
|
||||
this.handlePendingParseLine(rawLine);
|
||||
return;
|
||||
}
|
||||
const trimmed = rawLine.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
@@ -270,12 +284,43 @@ export class CodexAppServerClient {
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
embeddedAgentLog.warn("failed to parse codex app-server message", {
|
||||
error,
|
||||
linePreview: redactCodexAppServerLinePreview(trimmed),
|
||||
});
|
||||
if (shouldBufferCodexAppServerParseFailure(trimmed, error)) {
|
||||
this.pendingParse = { text: trimmed, lineCount: 1, firstError: error };
|
||||
return;
|
||||
}
|
||||
logCodexAppServerParseFailure(trimmed, error, 1);
|
||||
return;
|
||||
}
|
||||
this.handleParsedMessage(parsed);
|
||||
}
|
||||
|
||||
private handlePendingParseLine(line: string): void {
|
||||
const pending = this.pendingParse;
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
const candidate = `${pending.text}\\n${line}`;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(candidate);
|
||||
} catch (error) {
|
||||
const lineCount = pending.lineCount + 1;
|
||||
if (
|
||||
candidate.length <= CODEX_APP_SERVER_PARSE_BUFFER_MAX &&
|
||||
lineCount <= CODEX_APP_SERVER_PARSE_BUFFER_MAX_LINES
|
||||
) {
|
||||
this.pendingParse = { text: candidate, lineCount, firstError: pending.firstError };
|
||||
return;
|
||||
}
|
||||
this.pendingParse = undefined;
|
||||
logCodexAppServerParseFailure(candidate, error, lineCount);
|
||||
return;
|
||||
}
|
||||
this.pendingParse = undefined;
|
||||
this.handleParsedMessage(parsed);
|
||||
}
|
||||
|
||||
private handleParsedMessage(parsed: unknown): void {
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return;
|
||||
}
|
||||
@@ -547,6 +592,30 @@ function redactCodexAppServerLinePreview(value: string): string {
|
||||
: redacted;
|
||||
}
|
||||
|
||||
function shouldBufferCodexAppServerParseFailure(value: string, error: unknown): boolean {
|
||||
if (!value.startsWith("{") && !value.startsWith("[")) {
|
||||
return false;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
message.includes("Unterminated string") || message.includes("Unexpected end of JSON input")
|
||||
);
|
||||
}
|
||||
|
||||
function logCodexAppServerParseFailure(value: string, error: unknown, fragmentCount: number): void {
|
||||
const linePreview = redactCodexAppServerLinePreview(value);
|
||||
const suffix = fragmentCount > 1 ? ` fragments=${fragmentCount}` : "";
|
||||
embeddedAgentLog.warn("failed to parse codex app-server message", {
|
||||
error,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
fragmentCount,
|
||||
linePreview,
|
||||
consoleMessage: `failed to parse codex app-server message${suffix}: preview=${JSON.stringify(
|
||||
linePreview,
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
const CODEX_APP_SERVER_APPROVAL_REQUEST_METHODS = new Set([
|
||||
"item/commandExecution/requestApproval",
|
||||
"item/fileChange/requestApproval",
|
||||
|
||||
Reference in New Issue
Block a user