mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(codex): preserve app-server exit diagnostics
This commit is contained in:
@@ -69,6 +69,14 @@ describe("CodexAppServerClient", () => {
|
||||
expect(JSON.stringify(warn.mock.calls)).not.toContain("secret-value");
|
||||
});
|
||||
|
||||
it("redacts prefixed env credential names from app-server previews", () => {
|
||||
expect(
|
||||
__testing.redactCodexAppServerLinePreview(
|
||||
"fatal OPENAI_API_KEY=sk-live ANTHROPIC_API_KEY='anthropic-secret' OTHER=value",
|
||||
),
|
||||
).toBe("fatal OPENAI_API_KEY=<redacted> ANTHROPIC_API_KEY='<redacted>' OTHER=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();
|
||||
@@ -305,9 +313,23 @@ describe("CodexAppServerClient", () => {
|
||||
// an unhandled exception tearing down the gateway.
|
||||
await expect(pending).rejects.toThrow("write EPIPE");
|
||||
|
||||
// Subsequent requests are rejected immediately (client is closed).
|
||||
// Subsequent requests keep the original close reason so startup logs stay actionable.
|
||||
await expect(harness.client.request("another/method")).rejects.toThrow("write EPIPE");
|
||||
});
|
||||
|
||||
it("preserves redacted app-server stderr on exit errors", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
const pending = harness.client.request("test/method");
|
||||
harness.process.stderr.write('fatal token="secret-value" while booting\n');
|
||||
harness.process.emit("exit", 1, null);
|
||||
|
||||
await expect(pending).rejects.toThrow(
|
||||
'codex app-server exited: code=1 signal=null stderr="fatal token=\\"<redacted>\\" while booting"',
|
||||
);
|
||||
await expect(harness.client.request("another/method")).rejects.toThrow(
|
||||
"codex app-server client is closed",
|
||||
"codex app-server exited: code=1 signal=null",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ 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;
|
||||
const CODEX_APP_SERVER_STDERR_TAIL_MAX = 2_000;
|
||||
|
||||
type PendingRequest = {
|
||||
method: string;
|
||||
@@ -76,6 +77,8 @@ export class CodexAppServerClient {
|
||||
private nextId = 1;
|
||||
private initialized = false;
|
||||
private closed = false;
|
||||
private closeError: Error | undefined;
|
||||
private stderrTail = "";
|
||||
private pendingParse:
|
||||
| {
|
||||
text: string;
|
||||
@@ -89,20 +92,18 @@ export class CodexAppServerClient {
|
||||
this.lines = createInterface({ input: child.stdout });
|
||||
this.lines.on("line", (line) => this.handleLine(line));
|
||||
child.stderr.on("data", (chunk: Buffer | string) => {
|
||||
const text = chunk.toString("utf8").trim();
|
||||
if (text) {
|
||||
embeddedAgentLog.debug(`codex app-server stderr: ${text}`);
|
||||
const text = chunk.toString("utf8");
|
||||
this.stderrTail = appendBoundedTail(this.stderrTail, text, CODEX_APP_SERVER_STDERR_TAIL_MAX);
|
||||
const trimmed = text.trim();
|
||||
if (trimmed) {
|
||||
embeddedAgentLog.debug(`codex app-server stderr: ${trimmed}`);
|
||||
}
|
||||
});
|
||||
child.once("error", (error) =>
|
||||
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
|
||||
);
|
||||
child.once("exit", (code, signal) => {
|
||||
this.closeWithError(
|
||||
new Error(
|
||||
`codex app-server exited: code=${formatExitValue(code)} signal=${formatExitValue(signal)}`,
|
||||
),
|
||||
);
|
||||
this.closeWithError(buildCodexAppServerExitError(code, signal, this.stderrTail));
|
||||
});
|
||||
// Guard against unhandled EPIPE / write-after-close errors on the stdin
|
||||
// stream. When the child process terminates abruptly the pipe can break
|
||||
@@ -171,7 +172,7 @@ export class CodexAppServerClient {
|
||||
): Promise<T> {
|
||||
options ??= {};
|
||||
if (this.closed) {
|
||||
return Promise.reject(new Error("codex app-server client is closed"));
|
||||
return Promise.reject(this.closeError ?? new Error("codex app-server client is closed"));
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
return Promise.reject(new Error(`${method} aborted`));
|
||||
@@ -451,6 +452,7 @@ export class CodexAppServerClient {
|
||||
return false;
|
||||
}
|
||||
this.closed = true;
|
||||
this.closeError = error;
|
||||
this.lines.close();
|
||||
this.rejectPendingRequests(error);
|
||||
return true;
|
||||
@@ -596,12 +598,31 @@ function redactCodexAppServerLinePreview(value: string): string {
|
||||
.replace(
|
||||
/("(?:api_?key|authorization|token|access_token|refresh_token)"\s*:\s*")([^"]+)(")/gi,
|
||||
"$1<redacted>$3",
|
||||
)
|
||||
.replace(
|
||||
/\b([a-z0-9_]*(?:api_?key|authorization|access_token|refresh_token|token))(\s*=\s*)(["']?)[^\s"']+(\3)/gi,
|
||||
"$1$2$3<redacted>$4",
|
||||
);
|
||||
return redacted.length > CODEX_APP_SERVER_PARSE_LOG_MAX
|
||||
? `${redacted.slice(0, CODEX_APP_SERVER_PARSE_LOG_MAX)}...`
|
||||
: redacted;
|
||||
}
|
||||
|
||||
function appendBoundedTail(current: string, next: string, maxLength: number): string {
|
||||
const combined = `${current}${next}`;
|
||||
return combined.length > maxLength ? combined.slice(combined.length - maxLength) : combined;
|
||||
}
|
||||
|
||||
function buildCodexAppServerExitError(code: unknown, signal: unknown, stderrTail: string): Error {
|
||||
const stderrPreview = redactCodexAppServerLinePreview(stderrTail);
|
||||
const suffix = stderrPreview ? ` stderr=${JSON.stringify(stderrPreview)}` : "";
|
||||
return new Error(
|
||||
`codex app-server exited: code=${formatExitValue(code)} signal=${formatExitValue(
|
||||
signal,
|
||||
)}${suffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
function shouldBufferCodexAppServerParseFailure(value: string, error: unknown): boolean {
|
||||
if (!value.startsWith("{") && !value.startsWith("[")) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user