diff --git a/CHANGELOG.md b/CHANGELOG.md index f020b229684..a3e54afe2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/codex/src/app-server/event-projector.test.ts b/extensions/codex/src/app-server/event-projector.test.ts index 0c56e4210bc..1ab2eb2b5a1 100644 --- a/extensions/codex/src/app-server/event-projector.test.ts +++ b/extensions/codex/src/app-server/event-projector.test.ts @@ -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[] = []; diff --git a/extensions/codex/src/app-server/event-projector.ts b/extensions/codex/src/app-server/event-projector.ts index cab90155c85..f9b50389dae 100644 --- a/extensions/codex/src/app-server/event-projector.ts +++ b/extensions/codex/src/app-server/event-projector.ts @@ -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");