From 0e4d28aa9e657d4b753a92890efe0bbdd05a40c5 Mon Sep 17 00:00:00 2001 From: Vishal Jain <51826812+VishalJ99@users.noreply.github.com> Date: Sun, 3 May 2026 15:29:07 +0100 Subject: [PATCH] fix(codex): force message tool for source replies (#76663) * fix(codex): force message tool for source replies * docs: credit codex source reply fix (#76663) (thanks @VishalJ99) --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + .../codex/src/app-server/run-attempt.test.ts | 31 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 1 + 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8716c75b0c..34bc081683d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Chat delivery: make `/verbose on|full|off` changes affect subsequent tool-use chat bubbles again, including channels with draft preview tool progress enabled, while preserving one-shot verbose directives. - CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects (WebSocket close, timeout, connection drop) with bounded exponential backoff (up to 8 retries, capped at 30 s) and stderr retry warnings, while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059) Thanks @shashank-poola. - CLI/logs: announce `--follow` recovery with a `[logs] gateway reconnected` notice once a poll succeeds after a transient outage, and emit JSON `notice` records in `--json` mode for both the retry warning and the reconnect transition, so live monitoring scripts can react to the recovery. Carries forward #75059. (#75372) Thanks @romneyda. +- Codex/WhatsApp: keep the `message` dynamic tool available when Codex source replies are configured for message-tool delivery, so coding-profile chat agents do not complete turns privately without a visible channel reply. Fixes #76660. (#76663) Thanks @VishalJ99. - Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc. - Agents/web_search: keep installed runtime provider discovery enabled when web-search metadata is missing, so externally installed official providers such as Brave remain visible to agent and cron turns instead of falling back to bundled-only lookup. Fixes #76626. Thanks @amknight. - Tests/plugins: expose the Discord npm onboarding Docker lane as a package script and assert planned Docker lanes point at real scripts, so external-channel onboarding coverage can actually run. Thanks @vincentkoc. diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 3c31b1249ef..a64bac60617 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -440,6 +440,37 @@ describe("runCodexAppServerAttempt", () => { ); }); + it("forces the message dynamic tool for message-tool-only source replies", async () => { + const harness = createStartedThreadHarness(); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.disableTools = false; + params.config = { tools: { profile: "coding" } }; + params.sourceReplyDeliveryMode = "message_tool_only"; + params.messageProvider = "whatsapp"; + params.timeoutMs = 60_000; + + const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 5 }); + await harness.waitForMethod("thread/start"); + await harness.waitForMethod("turn/start"); + + const startRequest = harness.requests.find((request) => request.method === "thread/start"); + const dynamicToolNames = ( + (startRequest?.params as { dynamicTools?: Array<{ name: string }> } | undefined) + ?.dynamicTools ?? [] + ).map((tool) => tool.name); + expect(dynamicToolNames).toContain("message"); + + await new Promise((resolve) => setImmediate(resolve)); + await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); + await expect(run).resolves.toMatchObject({ + timedOut: false, + aborted: false, + }); + }); + it("returns a failed dynamic tool response when an app-server tool call exceeds the deadline", async () => { vi.useFakeTimers(); let capturedSignal: AbortSignal | undefined; diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 2f3a0dfbbe1..fa9a01e95de 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -1482,6 +1482,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) { requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey), disableMessageTool: params.disableMessageTool, + forceMessageTool: params.sourceReplyDeliveryMode === "message_tool_only", enableHeartbeatTool: params.trigger === "heartbeat", forceHeartbeatTool: params.trigger === "heartbeat", onYield: (message) => {