Skip to content

Commit f36a1b0

Browse files
clawsweeper[bot]rozmiarDTakhoffman
authored
fix(codex): preserve streamed command output (#83222)
Summary: - The PR buffers Codex command-output deltas per command item and uses them as a fallback for transcripts, trajectory output, final tool output, and after-tool-call errors when `aggregatedOutput` is empty. - Reproducibility: yes. A source-level reproduction is clear: send current-turn command-output delta notificat ... aggregatedOutput: null`; current main has no final transcript or trajectory fallback for the streamed text. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(codex): preserve streamed command output Validation: - ClawSweeper review passed for head 07393a3. - Required merge gates passed before the squash merge. Prepared head SHA: 07393a3 Review: #83222 (comment) Co-authored-by: 0x505badc0de <32790662+rozmiarD@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 3e76526 commit f36a1b0

3 files changed

Lines changed: 105 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
2727

2828
### Fixes
2929

30+
- Codex app-server: preserve streamed native command output in mirrored transcripts and trajectory exports when final snapshots omit aggregated output. (#83200) Thanks @rozmiarD.
3031
- Feishu: return bound subagent delivery origins from session thread setup so Feishu subagent completions route back to the same DM or topic. (#83190) Thanks @100menotu001.
3132
- CLI/update: tailor post-update Gateway recovery hints by platform, showing systemd, LaunchAgent, Scheduled Task, or generic service-manager guidance instead of macOS-only recovery text. (#83096) Thanks @rubencu.
3233
- Plugins: apply a default 15-second timeout to legacy `before_agent_start` hooks so hung plugin handlers no longer block agent startup. Fixes #48534. (#83136) Thanks @therahul-yo.

extensions/codex/src/app-server/event-projector.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,74 @@ describe("CodexAppServerEventProjector", () => {
13381338
});
13391339
});
13401340

1341+
it("uses streamed command output when final command snapshots omit aggregated output", async () => {
1342+
const onAgentEvent = vi.fn();
1343+
const trajectoryRecorder = {
1344+
filePath: "trajectory.jsonl",
1345+
recordEvent: vi.fn(),
1346+
flush: vi.fn(async () => undefined),
1347+
};
1348+
const projector = await createProjector(
1349+
{
1350+
...(await createParams()),
1351+
onAgentEvent,
1352+
},
1353+
{
1354+
trajectoryRecorder,
1355+
},
1356+
);
1357+
1358+
await projector.handleNotification(
1359+
forCurrentTurn("item/commandExecution/outputDelta", {
1360+
itemId: "cmd-1",
1361+
delta: "status passed\n",
1362+
}),
1363+
);
1364+
await projector.handleNotification(
1365+
forCurrentTurn("item/commandExecution/outputDelta", {
1366+
itemId: "cmd-1",
1367+
delta: "json /tmp/scenario.json\n",
1368+
}),
1369+
);
1370+
await projector.handleNotification(
1371+
turnCompleted([
1372+
{
1373+
type: "commandExecution",
1374+
id: "cmd-1",
1375+
command: "python scripts/run_demo_scenario.py",
1376+
cwd: "/workspace",
1377+
processId: null,
1378+
source: "agent",
1379+
status: "completed",
1380+
commandActions: [],
1381+
aggregatedOutput: null,
1382+
exitCode: 0,
1383+
durationMs: 42,
1384+
},
1385+
]),
1386+
);
1387+
1388+
const result = projector.buildResult(buildEmptyToolTelemetry());
1389+
const toolResultMessage = requireRecord(result.messagesSnapshot[2], "tool result message");
1390+
const toolResultContent = requireArray(toolResultMessage.content, "tool result content");
1391+
const toolResultContentItem = requireRecord(toolResultContent[0], "tool result content item");
1392+
expect(toolResultContentItem.content).toBe("status passed\njson /tmp/scenario.json");
1393+
expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith(
1394+
"tool.result",
1395+
expect.objectContaining({
1396+
itemId: "cmd-1",
1397+
output: "status passed\njson /tmp/scenario.json",
1398+
}),
1399+
);
1400+
const toolResult = findAgentEvent(onAgentEvent, {
1401+
stream: "tool",
1402+
phase: "result",
1403+
itemId: "cmd-1",
1404+
name: "bash",
1405+
}).data;
1406+
expect(toolResult.result).toEqual({ status: "completed", exitCode: 0, durationMs: 42 });
1407+
});
1408+
13411409
it("does not duplicate native tool starts when the snapshot completes a started item", async () => {
13421410
const onAgentEvent = vi.fn();
13431411
const trajectoryRecorder = {

extensions/codex/src/app-server/event-projector.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export class CodexAppServerEventProjector {
146146
string,
147147
{ chars: number; messages: number; truncated: boolean }
148148
>();
149+
private readonly toolResultOutputTextByItem = new Map<string, string>();
149150
private readonly toolMetas = new Map<string, { toolName: string; meta?: string }>();
150151
private readonly toolTranscriptMessages: AgentMessage[] = [];
151152
private readonly toolTranscriptCallIds = new Set<string>();
@@ -706,7 +707,11 @@ export class CodexAppServerEventProjector {
706707
private handleOutputDelta(params: JsonObject, toolName: string): void {
707708
const itemId = readString(params, "itemId");
708709
const delta = readString(params, "delta");
709-
if (!itemId || !delta || !this.shouldEmitToolOutput()) {
710+
if (!itemId || !delta) {
711+
return;
712+
}
713+
appendToolOutputDeltaText(this.toolResultOutputTextByItem, itemId, delta);
714+
if (!this.shouldEmitToolOutput()) {
710715
return;
711716
}
712717
if (
@@ -953,7 +958,7 @@ export class CodexAppServerEventProjector {
953958
return;
954959
}
955960
const toolResult = itemToolResult(params.item).result;
956-
const output = itemOutputText(params.item);
961+
const output = itemOutputText(params.item, this.toolResultOutputTextByItem);
957962
this.options.trajectoryRecorder?.recordEvent("tool.result", {
958963
threadId: this.threadId,
959964
turnId: this.turnId,
@@ -1029,7 +1034,7 @@ export class CodexAppServerEventProjector {
10291034
}
10301035
this.afterToolCallObservedItemIds.add(item.id);
10311036
const result = itemToolResult(item).result;
1032-
const error = itemToolError(item, status);
1037+
const error = itemToolError(item, status, this.toolResultOutputTextByItem);
10331038
const startedAt =
10341039
typeof item.durationMs === "number" ? Date.now() - Math.max(0, item.durationMs) : undefined;
10351040
const hookParams = {
@@ -1097,7 +1102,7 @@ export class CodexAppServerEventProjector {
10971102
return;
10981103
}
10991104
const toolName = itemName(item);
1100-
const output = itemOutputText(item);
1105+
const output = itemOutputText(item, this.toolResultOutputTextByItem);
11011106
if (!toolName || !output) {
11021107
return;
11031108
}
@@ -1190,7 +1195,7 @@ export class CodexAppServerEventProjector {
11901195
this.recordToolTranscriptResult({
11911196
id: item.id,
11921197
name,
1193-
text: itemTranscriptResultText(item),
1198+
text: itemTranscriptResultText(item, this.toolResultOutputTextByItem),
11941199
isError: isNonSuccessItemStatus(itemStatus(item)),
11951200
});
11961201
}
@@ -1789,14 +1794,15 @@ function itemFileChanges(item: CodexThreadItem): Array<{ path: string; kind: str
17891794
function itemToolError(
17901795
item: CodexThreadItem,
17911796
status: ReturnType<typeof itemStatus>,
1797+
outputTextByItem?: ReadonlyMap<string, string>,
17921798
): string | undefined {
17931799
if (status === "blocked") {
17941800
return "codex native tool blocked";
17951801
}
17961802
if (status !== "failed") {
17971803
return undefined;
17981804
}
1799-
return itemOutputText(item) ?? "codex native tool failed";
1805+
return itemOutputText(item, outputTextByItem) ?? "codex native tool failed";
18001806
}
18011807

18021808
function itemMeta(
@@ -1823,9 +1829,12 @@ function itemMeta(
18231829
return undefined;
18241830
}
18251831

1826-
function itemOutputText(item: CodexThreadItem): string | undefined {
1832+
function itemOutputText(
1833+
item: CodexThreadItem,
1834+
outputTextByItem?: ReadonlyMap<string, string>,
1835+
): string | undefined {
18271836
if (item.type === "commandExecution") {
1828-
return item.aggregatedOutput?.trim() || undefined;
1837+
return item.aggregatedOutput?.trim() || outputTextByItem?.get(item.id)?.trim() || undefined;
18291838
}
18301839
if (item.type === "dynamicToolCall") {
18311840
return collectDynamicToolContentText(item.contentItems).trim() || undefined;
@@ -1839,15 +1848,32 @@ function itemOutputText(item: CodexThreadItem): string | undefined {
18391848
return undefined;
18401849
}
18411850

1842-
function itemTranscriptResultText(item: CodexThreadItem): string | undefined {
1843-
const output = itemOutputText(item);
1851+
function itemTranscriptResultText(
1852+
item: CodexThreadItem,
1853+
outputTextByItem?: ReadonlyMap<string, string>,
1854+
): string | undefined {
1855+
const output = itemOutputText(item, outputTextByItem);
18441856
if (output) {
18451857
return output;
18461858
}
18471859
const result = itemToolResult(item).result;
18481860
return result ? stringifyJsonValue(result) : itemStatus(item);
18491861
}
18501862

1863+
function appendToolOutputDeltaText(
1864+
outputTextByItem: Map<string, string>,
1865+
itemId: string,
1866+
delta: string,
1867+
): void {
1868+
const current = outputTextByItem.get(itemId) ?? "";
1869+
if (current.length >= TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS) {
1870+
return;
1871+
}
1872+
const remaining = TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS - current.length;
1873+
const next = current + (delta.length > remaining ? delta.slice(0, remaining) : delta);
1874+
outputTextByItem.set(itemId, next);
1875+
}
1876+
18511877
function normalizeToolTranscriptArguments(value: unknown): Record<string, unknown> {
18521878
if (!value || typeof value !== "object" || Array.isArray(value)) {
18531879
return {};

0 commit comments

Comments
 (0)