fix: restore Codex snapshot tool progress (#82917)

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Peter Steinberger
2026-05-17 06:20:59 +01:00
committed by GitHub
parent 3fad770510
commit 421b9e2819
3 changed files with 195 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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)
- Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram `/verbose` stays visible when command events arrive only at completion.
- 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.
- 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)
- 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.

View File

@@ -1253,6 +1253,174 @@ describe("CodexAppServerEventProjector", () => {
expect(toolResultContentItem.content).toBe("ok");
});
it("synthesizes native tool progress from turn completion snapshots", async () => {
const onAgentEvent = vi.fn();
const onToolResult = vi.fn();
const projector = await createProjector({
...(await createParams()),
verboseLevel: "on",
onAgentEvent,
onToolResult,
});
await projector.handleNotification(
turnCompleted([
{
type: "commandExecution",
id: "cmd-snapshot",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ok",
exitCode: 0,
durationMs: 42,
},
]),
);
const itemStart = findAgentEvent(onAgentEvent, {
stream: "item",
phase: "start",
itemId: "cmd-snapshot",
}).data;
expect(itemStart.kind).toBe("command");
expect(itemStart.name).toBe("bash");
expect(itemStart.suppressChannelProgress).toBe(true);
const toolStart = findAgentEvent(onAgentEvent, {
stream: "tool",
phase: "start",
itemId: "cmd-snapshot",
name: "bash",
}).data;
expect(toolStart.args).toEqual({ command: "pnpm test extensions/codex", cwd: "/workspace" });
const toolResult = findAgentEvent(onAgentEvent, {
stream: "tool",
phase: "result",
itemId: "cmd-snapshot",
name: "bash",
}).data;
expect(toolResult.status).toBe("completed");
expect(toolResult.isError).toBe(false);
expect(onToolResult).toHaveBeenCalledWith({
text: "🛠️ `run tests (workspace)`",
});
});
it("does not duplicate native tool starts when the snapshot completes a started item", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
const commandItem = {
type: "commandExecution",
id: "cmd-started",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ok",
exitCode: 0,
durationMs: 42,
};
await projector.handleNotification(
forCurrentTurn("item/started", {
item: { ...commandItem, status: "inProgress", aggregatedOutput: null, exitCode: null },
}),
);
await projector.handleNotification(turnCompleted([commandItem]));
const toolEvents = onAgentEvent.mock.calls
.map((call) => requireRecord(call[0], "agent event"))
.filter((event) => event.stream === "tool")
.map((event) => requireRecord(event.data, "agent event data"));
expect(
toolEvents.filter((event) => event.phase === "start" && event.itemId === "cmd-started"),
).toHaveLength(1);
expect(
toolEvents.filter((event) => event.phase === "result" && event.itemId === "cmd-started"),
).toHaveLength(1);
});
it("does not synthesize completed progress for running turn completion snapshots", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
await projector.handleNotification(
turnCompleted([
{
type: "commandExecution",
id: "cmd-running-snapshot",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "inProgress",
commandActions: [],
aggregatedOutput: null,
exitCode: null,
durationMs: null,
},
]),
);
const toolEvents = onAgentEvent.mock.calls
.map((call) => requireRecord(call[0], "agent event"))
.filter((event) => event.stream === "tool")
.map((event) => requireRecord(event.data, "agent event data"));
expect(toolEvents).toEqual([]);
});
it("does not synthesize progress for stale prior-turn snapshot items", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
await projector.handleNotification(
turnCompleted([
{
type: "commandExecution",
id: "cmd-prior-turn",
turnId: "turn-old",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ok",
exitCode: 0,
durationMs: 42,
},
{
type: "commandExecution",
id: "cmd-current-turn",
turnId: TURN_ID,
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ok",
exitCode: 0,
durationMs: 42,
},
]),
);
const toolEvents = onAgentEvent.mock.calls
.map((call) => requireRecord(call[0], "agent event"))
.filter((event) => event.stream === "tool")
.map((event) => requireRecord(event.data, "agent event data"));
expect(toolEvents.map((event) => event.itemId)).toEqual([
"cmd-current-turn",
"cmd-current-turn",
]);
});
it("orders declined native tool diagnostics after their start event", async () => {
const projector = await createProjector();
const diagnosticEvents: DiagnosticEventPayload[] = [];

View File

@@ -644,6 +644,7 @@ export class CodexAppServerEventProjector {
this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
}
this.recordToolMeta(item);
this.emitSnapshotOnlyNativeToolProgress(item);
this.recordNativeToolTranscriptCall(item);
this.recordNativeToolTranscriptResult(item);
this.emitAfterToolCallObservation(item);
@@ -654,6 +655,31 @@ export class CodexAppServerEventProjector {
await this.maybeEndReasoning();
}
private emitSnapshotOnlyNativeToolProgress(item: CodexThreadItem): void {
if (
!shouldSynthesizeToolProgressForItem(item) ||
!this.isCurrentTurnSnapshotItem(item) ||
this.completedItemIds.has(item.id) ||
itemStatus(item) === "running"
) {
return;
}
const wasStarted = this.activeItemIds.has(item.id);
if (!wasStarted) {
this.emitStandardItemEvent({ phase: "start", item });
this.emitNormalizedToolItemEvent({ phase: "start", item });
}
this.activeItemIds.delete(item.id);
this.emitStandardItemEvent({ phase: "end", item });
this.emitNormalizedToolItemEvent({ phase: "result", item });
this.completedItemIds.add(item.id);
}
private isCurrentTurnSnapshotItem(item: CodexThreadItem): boolean {
const itemTurnId = readItemString(item, "turnId") ?? readItemString(item, "turn_id");
return itemTurnId === undefined || itemTurnId === this.turnId;
}
private handleOutputDelta(params: JsonObject, toolName: string): void {
const itemId = readString(params, "itemId");
const delta = readString(params, "delta");