Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, it } from "vitest";
import { z } from "zod";

import { createCodexAppServerConnection } from "./app-server-connection.js";

// Echoes every request as a result whose string contains a raw U+2028.
const FAKE_SERVER = `
process.stdin.setEncoding("utf8");
let buffered = "";
process.stdin.on("data", (chunk) => {
buffered += chunk;
let index;
while ((index = buffered.indexOf("\\n")) !== -1) {
const line = buffered.slice(0, index);
buffered = buffered.slice(index + 1);
const { id } = JSON.parse(line);
const text = "before\\u2028after";
process.stdout.write(JSON.stringify({ id, result: { text } }) + "\\n");
}
});
`;

it("keeps a JSON line intact when it contains U+2028", async () => {
const connection = createCodexAppServerConnection({
command: process.execPath,
args: ["-e", FAKE_SERVER],
cwd: process.cwd(),
env: process.env,
recordThreadId: null,
onNotification: () => {},
onRequest: () => {},
onExit: () => {},
});
try {
const result = await connection.request({
method: "thread/resume",
params: {},
resultSchema: z.object({ text: z.string() }),
timeoutMs: 5_000,
});
expect(result.text).toBe("before\u2028after");
} finally {
connection.kill();
}
});
34 changes: 29 additions & 5 deletions plugins/provider-codex/src/bridge/app-server-connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn, type ChildProcess } from "node:child_process";
import { createInterface, type Interface } from "node:readline";
import { createInterface } from "node:readline";
import { StringDecoder } from "node:string_decoder";
import { experimental_recordProviderChildIo } from "@get-bb/plugin-sdk/provider-bridge";
import type { z } from "zod";

Expand Down Expand Up @@ -111,7 +112,7 @@ export function createCodexAppServerConnection(
signal: NodeJS.Signals | null;
} | null = null;
let closeGraceTimer: NodeJS.Timeout | null = null;
let stdoutLines: Interface | null = null;
let stdoutLines: { close(): void } | null = null;

function writeLine(message: object): void {
const stdin = child.stdin;
Expand Down Expand Up @@ -159,8 +160,15 @@ export function createCodexAppServerConnection(
}

if (child.stdout) {
stdoutLines = createInterface({ input: child.stdout, terminal: false });
stdoutLines.on("line", (line) => {
// Split strictly on "\n". `node:readline` also treats U+2028 and U+2029 as
// line breaks, which codex may emit unescaped inside JSON strings. That
// fragmented large responses (for example `thread/resume` for a thread whose
// history contained U+2028) into unparseable pieces, so the request never
// resolved and timed out.
const stdout = child.stdout;
const decoder = new StringDecoder("utf8");
let buffered = "";
const handleStdoutLine = (line: string) => {
if (finalized) {
return;
}
Expand Down Expand Up @@ -222,7 +230,23 @@ export function createCodexAppServerConnection(
}

options.onNotification(message.method, message.params);
});
};
const onStdoutData = (chunk: Buffer) => {
buffered += decoder.write(chunk);
let newlineIndex = buffered.indexOf("\n");
while (newlineIndex !== -1) {
const line = buffered.slice(0, newlineIndex);
buffered = buffered.slice(newlineIndex + 1);
handleStdoutLine(line.endsWith("\r") ? line.slice(0, -1) : line);
newlineIndex = buffered.indexOf("\n");
}
};
stdout.on("data", onStdoutData);
stdoutLines = {
close() {
stdout.off("data", onStdoutData);
},
};
}

if (child.stderr) {
Expand Down
Loading