Skip to content

Commit 81c9bd3

Browse files
committed
fix: add CLI empty response diagnostics
1 parent 9824061 commit 81c9bd3

4 files changed

Lines changed: 99 additions & 0 deletions

File tree

src/agents/cli-output.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,28 @@ type CliUsage = {
2121
total?: number;
2222
};
2323

24+
export type CliProcessDiagnostics = {
25+
backendId: string;
26+
processReason: string;
27+
exitCode: number | null;
28+
exitSignal: NodeJS.Signals | number | null;
29+
durationMs: number;
30+
stdoutBytes: number;
31+
stdoutHash: string;
32+
stderrBytes: number;
33+
stderrHash: string;
34+
useResume: boolean;
35+
};
36+
2437
/** Normalized result from a CLI-backed model provider turn. */
2538
export type CliOutput = {
2639
text: string;
2740
rawText?: string;
2841
sessionId?: string;
2942
usage?: CliUsage;
43+
diagnostics?: {
44+
process?: CliProcessDiagnostics;
45+
};
3046
finalPromptText?: string;
3147
didSendViaMessagingTool?: boolean;
3248
didDeliverSourceReplyViaMessageTool?: boolean;

src/agents/cli-runner.spawn.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,42 @@ describe("runCliAgent spawn path", () => {
870870
}
871871
});
872872

873+
it("returns process diagnostics with byte counts and bounded output hashes", async () => {
874+
supervisorSpawnMock.mockResolvedValueOnce(
875+
createManagedRun({
876+
reason: "exit",
877+
exitCode: 0,
878+
exitSignal: null,
879+
durationMs: 75,
880+
stdout: "ok",
881+
stderr: "warn\n",
882+
timedOut: false,
883+
noOutputTimedOut: false,
884+
}),
885+
);
886+
887+
const result = await executePreparedCliRun(
888+
buildPreparedCliRunContext({
889+
provider: "codex-cli",
890+
model: "gpt-5.4",
891+
runId: "run-process-diagnostics",
892+
}),
893+
);
894+
895+
expect(result.diagnostics?.process).toEqual({
896+
backendId: "codex-cli",
897+
processReason: "exit",
898+
exitCode: 0,
899+
exitSignal: null,
900+
durationMs: 75,
901+
stdoutBytes: 2,
902+
stdoutHash: "2689367b205c",
903+
stderrBytes: 5,
904+
stderrHash: "7597e6b3a377",
905+
useResume: false,
906+
});
907+
});
908+
873909
it("passes Codex system prompts through model_instructions_file", async () => {
874910
let promptFileText = "";
875911
supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {

src/agents/cli-runner.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ function shouldRetryFreshCliSessionAfterFailover(params: {
101101
}
102102
}
103103

104+
function formatCliEmptyOutputDiagnostics(output: CliOutput): string | undefined {
105+
const process = output.diagnostics?.process;
106+
if (!process) {
107+
return undefined;
108+
}
109+
return [
110+
`backend=${process.backendId}`,
111+
`reason=${process.processReason}`,
112+
`exitCode=${process.exitCode ?? "null"}`,
113+
`exitSignal=${process.exitSignal ?? "null"}`,
114+
`durationMs=${process.durationMs}`,
115+
`stdoutBytes=${process.stdoutBytes}`,
116+
`stdoutHash=${process.stdoutHash}`,
117+
`stderrBytes=${process.stderrBytes}`,
118+
`stderrHash=${process.stderrHash}`,
119+
`useResume=${process.useResume ? "true" : "false"}`,
120+
].join(" ");
121+
}
122+
104123
/** Checks whether a Claude CLI session binding has reached its transcript file. */
105124
export async function isCliBindingFlushed(
106125
sessionId: string | undefined,
@@ -780,6 +799,10 @@ export async function runPreparedCliAgent(
780799
!output.didSendViaMessagingTool &&
781800
params.allowEmptyAssistantReplyAsSilent !== true
782801
) {
802+
const emptyOutputDiagnostics = formatCliEmptyOutputDiagnostics(output);
803+
if (emptyOutputDiagnostics) {
804+
cliBackendLog.warn(`cli empty response diagnostics: ${emptyOutputDiagnostics}`);
805+
}
783806
throw attachCliMessagingDeliveryEvidence(
784807
new FailoverError("CLI backend returned an empty response.", {
785808
reason: "empty_response",

src/agents/cli-runner/execute.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,9 +1110,13 @@ export async function executePreparedCliRun(
11101110
});
11111111
let stdoutTail: Buffer = Buffer.alloc(0);
11121112
let stdoutParseBuffer: Buffer = Buffer.alloc(0);
1113+
let stdoutBytes = 0;
1114+
const stdoutHash = crypto.createHash("sha256");
11131115
let stdoutParseExceeded = false;
11141116
let stderrTail: Buffer = Buffer.alloc(0);
11151117
let stderrParseBuffer: Buffer = Buffer.alloc(0);
1118+
let stderrBytes = 0;
1119+
const stderrHash = crypto.createHash("sha256");
11161120
let stderrParseExceeded = false;
11171121

11181122
params.onExecutionPhase?.({
@@ -1135,6 +1139,8 @@ export async function executePreparedCliRun(
11351139
input: stdinPayload,
11361140
captureOutput: false,
11371141
onStdout: (chunk: string) => {
1142+
stdoutBytes += Buffer.byteLength(chunk);
1143+
stdoutHash.update(chunk);
11381144
stdoutTail = appendCliOutputTail(stdoutTail, chunk);
11391145
if (!stdoutParseExceeded) {
11401146
const nextStdoutParse = appendCliOutputParseBuffer(stdoutParseBuffer, chunk);
@@ -1144,6 +1150,8 @@ export async function executePreparedCliRun(
11441150
streamingParser?.push(chunk);
11451151
},
11461152
onStderr: (chunk: string) => {
1153+
stderrBytes += Buffer.byteLength(chunk);
1154+
stderrHash.update(chunk);
11471155
stderrTail = appendCliOutputTail(stderrTail, chunk);
11481156
if (!stderrParseExceeded) {
11491157
const nextStderrParse = appendCliOutputParseBuffer(stderrParseBuffer, chunk);
@@ -1191,6 +1199,18 @@ export async function executePreparedCliRun(
11911199
const stdoutDiagnostic = stdoutTail.toString("utf8").trim();
11921200
const stderr = stderrParseBuffer.toString("utf8").trim();
11931201
const stderrDiagnostic = stderrTail.toString("utf8").trim();
1202+
const processDiagnostics = {
1203+
backendId: context.backendResolved.id,
1204+
processReason: result.reason,
1205+
exitCode: result.exitCode,
1206+
exitSignal: result.exitSignal,
1207+
durationMs: result.durationMs,
1208+
stdoutBytes,
1209+
stdoutHash: stdoutHash.digest("hex").slice(0, 12),
1210+
stderrBytes,
1211+
stderrHash: stderrHash.digest("hex").slice(0, 12),
1212+
useResume,
1213+
};
11941214
if (logOutputText) {
11951215
if (stdoutDiagnostic) {
11961216
cliBackendLog.info(`cli stdout:\n${stdoutDiagnostic}`);
@@ -1347,6 +1367,10 @@ export async function executePreparedCliRun(
13471367
);
13481368
runOutput = {
13491369
...parsed,
1370+
diagnostics: {
1371+
...(parsed.diagnostics ?? {}),
1372+
process: processDiagnostics,
1373+
},
13501374
rawText,
13511375
finalPromptText: prompt,
13521376
text: applyPluginTextReplacements(

0 commit comments

Comments
 (0)