mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix: recover multiline codex app-server messages
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw.
|
||||
- Media/completions: treat media-only message-tool sends as delivered async completion output, avoiding duplicate raw `MEDIA:` fallback posts after video or music generation finishes.
|
||||
- Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc.
|
||||
- Codex/app-server: recover JSON-RPC frames split by raw command-output newlines and include a redacted preview when malformed app-server messages still reach the console. Thanks @vincentkoc.
|
||||
- Replies/typing: keep typing alive for queued follow-up messages that are genuinely waiting behind an active run, instead of making chat surfaces look idle while work is queued. Fixes #65685. Thanks @papag00se.
|
||||
- ACP/Discord: suppress completion announce delivery for inline thread-bound ACP session runs, so Discord thread-bound ACP replies are not delivered twice. Fixes #60780. Thanks @solavrc.
|
||||
- Discord/threads: ignore webhook-authored copies in already-bound Discord session threads even when the webhook id differs, preventing PluralKit proxy copies from creating duplicate turn pressure. Fixes #52005. Thanks @acgh213.
|
||||
|
||||
@@ -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