Skip to content

Commit 421b9e2

Browse files
authored
fix: restore Codex snapshot tool progress (#82917)
# Conflicts: # CHANGELOG.md
1 parent 3fad770 commit 421b9e2

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

CHANGELOG.md

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

99
- Agents/skills: apply the full effective tool policy pipeline to inline `command-dispatch: tool` skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525)
10+
- Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram `/verbose` stays visible when command events arrive only at completion.
1011
- Providers/GitHub Copilot: request identity-encoded Copilot API responses across token exchange, catalog, model calls, usage, and embeddings so compressed Business-account error payloads no longer reach JSON parsers as gzip bytes. Fixes #82871. Thanks @tonyfe01.
1112
- Telegram: preserve replied-to bot messages, captions, and media metadata in group reply chains so follow-up replies understand what the user is reacting to. (#82863)
1213
- Providers/Together: update PI runtime packages to 0.74.1 and emit Together-style `reasoning.enabled`/`max_tokens` controls for reasoning-capable OpenAI-completions models.

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

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,174 @@ describe("CodexAppServerEventProjector", () => {
12531253
expect(toolResultContentItem.content).toBe("ok");
12541254
});
12551255

1256+
it("synthesizes native tool progress from turn completion snapshots", async () => {
1257+
const onAgentEvent = vi.fn();
1258+
const onToolResult = vi.fn();
1259+
const projector = await createProjector({
1260+
...(await createParams()),
1261+
verboseLevel: "on",
1262+
onAgentEvent,
1263+
onToolResult,
1264+
});
1265+
1266+
await projector.handleNotification(
1267+
turnCompleted([
1268+
{
1269+
type: "commandExecution",
1270+
id: "cmd-snapshot",
1271+
command: "pnpm test extensions/codex",
1272+
cwd: "/workspace",
1273+
processId: null,
1274+
source: "agent",
1275+
status: "completed",
1276+
commandActions: [],
1277+
aggregatedOutput: "ok",
1278+
exitCode: 0,
1279+
durationMs: 42,
1280+
},
1281+
]),
1282+
);
1283+
1284+
const itemStart = findAgentEvent(onAgentEvent, {
1285+
stream: "item",
1286+
phase: "start",
1287+
itemId: "cmd-snapshot",
1288+
}).data;
1289+
expect(itemStart.kind).toBe("command");
1290+
expect(itemStart.name).toBe("bash");
1291+
expect(itemStart.suppressChannelProgress).toBe(true);
1292+
const toolStart = findAgentEvent(onAgentEvent, {
1293+
stream: "tool",
1294+
phase: "start",
1295+
itemId: "cmd-snapshot",
1296+
name: "bash",
1297+
}).data;
1298+
expect(toolStart.args).toEqual({ command: "pnpm test extensions/codex", cwd: "/workspace" });
1299+
const toolResult = findAgentEvent(onAgentEvent, {
1300+
stream: "tool",
1301+
phase: "result",
1302+
itemId: "cmd-snapshot",
1303+
name: "bash",
1304+
}).data;
1305+
expect(toolResult.status).toBe("completed");
1306+
expect(toolResult.isError).toBe(false);
1307+
expect(onToolResult).toHaveBeenCalledWith({
1308+
text: "🛠️ `run tests (workspace)`",
1309+
});
1310+
});
1311+
1312+
it("does not duplicate native tool starts when the snapshot completes a started item", async () => {
1313+
const onAgentEvent = vi.fn();
1314+
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
1315+
const commandItem = {
1316+
type: "commandExecution",
1317+
id: "cmd-started",
1318+
command: "pnpm test extensions/codex",
1319+
cwd: "/workspace",
1320+
processId: null,
1321+
source: "agent",
1322+
status: "completed",
1323+
commandActions: [],
1324+
aggregatedOutput: "ok",
1325+
exitCode: 0,
1326+
durationMs: 42,
1327+
};
1328+
1329+
await projector.handleNotification(
1330+
forCurrentTurn("item/started", {
1331+
item: { ...commandItem, status: "inProgress", aggregatedOutput: null, exitCode: null },
1332+
}),
1333+
);
1334+
await projector.handleNotification(turnCompleted([commandItem]));
1335+
1336+
const toolEvents = onAgentEvent.mock.calls
1337+
.map((call) => requireRecord(call[0], "agent event"))
1338+
.filter((event) => event.stream === "tool")
1339+
.map((event) => requireRecord(event.data, "agent event data"));
1340+
expect(
1341+
toolEvents.filter((event) => event.phase === "start" && event.itemId === "cmd-started"),
1342+
).toHaveLength(1);
1343+
expect(
1344+
toolEvents.filter((event) => event.phase === "result" && event.itemId === "cmd-started"),
1345+
).toHaveLength(1);
1346+
});
1347+
1348+
it("does not synthesize completed progress for running turn completion snapshots", async () => {
1349+
const onAgentEvent = vi.fn();
1350+
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
1351+
1352+
await projector.handleNotification(
1353+
turnCompleted([
1354+
{
1355+
type: "commandExecution",
1356+
id: "cmd-running-snapshot",
1357+
command: "pnpm test extensions/codex",
1358+
cwd: "/workspace",
1359+
processId: null,
1360+
source: "agent",
1361+
status: "inProgress",
1362+
commandActions: [],
1363+
aggregatedOutput: null,
1364+
exitCode: null,
1365+
durationMs: null,
1366+
},
1367+
]),
1368+
);
1369+
1370+
const toolEvents = onAgentEvent.mock.calls
1371+
.map((call) => requireRecord(call[0], "agent event"))
1372+
.filter((event) => event.stream === "tool")
1373+
.map((event) => requireRecord(event.data, "agent event data"));
1374+
expect(toolEvents).toEqual([]);
1375+
});
1376+
1377+
it("does not synthesize progress for stale prior-turn snapshot items", async () => {
1378+
const onAgentEvent = vi.fn();
1379+
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
1380+
1381+
await projector.handleNotification(
1382+
turnCompleted([
1383+
{
1384+
type: "commandExecution",
1385+
id: "cmd-prior-turn",
1386+
turnId: "turn-old",
1387+
command: "pnpm test extensions/codex",
1388+
cwd: "/workspace",
1389+
processId: null,
1390+
source: "agent",
1391+
status: "completed",
1392+
commandActions: [],
1393+
aggregatedOutput: "ok",
1394+
exitCode: 0,
1395+
durationMs: 42,
1396+
},
1397+
{
1398+
type: "commandExecution",
1399+
id: "cmd-current-turn",
1400+
turnId: TURN_ID,
1401+
command: "pnpm test extensions/codex",
1402+
cwd: "/workspace",
1403+
processId: null,
1404+
source: "agent",
1405+
status: "completed",
1406+
commandActions: [],
1407+
aggregatedOutput: "ok",
1408+
exitCode: 0,
1409+
durationMs: 42,
1410+
},
1411+
]),
1412+
);
1413+
1414+
const toolEvents = onAgentEvent.mock.calls
1415+
.map((call) => requireRecord(call[0], "agent event"))
1416+
.filter((event) => event.stream === "tool")
1417+
.map((event) => requireRecord(event.data, "agent event data"));
1418+
expect(toolEvents.map((event) => event.itemId)).toEqual([
1419+
"cmd-current-turn",
1420+
"cmd-current-turn",
1421+
]);
1422+
});
1423+
12561424
it("orders declined native tool diagnostics after their start event", async () => {
12571425
const projector = await createProjector();
12581426
const diagnosticEvents: DiagnosticEventPayload[] = [];

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ export class CodexAppServerEventProjector {
644644
this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
645645
}
646646
this.recordToolMeta(item);
647+
this.emitSnapshotOnlyNativeToolProgress(item);
647648
this.recordNativeToolTranscriptCall(item);
648649
this.recordNativeToolTranscriptResult(item);
649650
this.emitAfterToolCallObservation(item);
@@ -654,6 +655,31 @@ export class CodexAppServerEventProjector {
654655
await this.maybeEndReasoning();
655656
}
656657

658+
private emitSnapshotOnlyNativeToolProgress(item: CodexThreadItem): void {
659+
if (
660+
!shouldSynthesizeToolProgressForItem(item) ||
661+
!this.isCurrentTurnSnapshotItem(item) ||
662+
this.completedItemIds.has(item.id) ||
663+
itemStatus(item) === "running"
664+
) {
665+
return;
666+
}
667+
const wasStarted = this.activeItemIds.has(item.id);
668+
if (!wasStarted) {
669+
this.emitStandardItemEvent({ phase: "start", item });
670+
this.emitNormalizedToolItemEvent({ phase: "start", item });
671+
}
672+
this.activeItemIds.delete(item.id);
673+
this.emitStandardItemEvent({ phase: "end", item });
674+
this.emitNormalizedToolItemEvent({ phase: "result", item });
675+
this.completedItemIds.add(item.id);
676+
}
677+
678+
private isCurrentTurnSnapshotItem(item: CodexThreadItem): boolean {
679+
const itemTurnId = readItemString(item, "turnId") ?? readItemString(item, "turn_id");
680+
return itemTurnId === undefined || itemTurnId === this.turnId;
681+
}
682+
657683
private handleOutputDelta(params: JsonObject, toolName: string): void {
658684
const itemId = readString(params, "itemId");
659685
const delta = readString(params, "delta");

0 commit comments

Comments
 (0)