From b62166301efd36d53ee93aa38cbeec200ad7a9eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 04:24:40 -0400 Subject: [PATCH] fix: annotate message-tool-only replies in Codex tool spec Thread sourceReplyDeliveryMode into Codex/OpenClaw tool construction and annotate the message tool description for message-tool-only turns so visible replies use message(action=send).\n\nAlso adds focused regression coverage and a changelog entry. --- CHANGELOG.md | 1 + .../codex/src/app-server/run-attempt.ts | 1 + src/agents/openclaw-tools.ts | 4 +++ src/agents/pi-embedded-runner/compact.ts | 1 + src/agents/pi-embedded-runner/run/attempt.ts | 1 + ...tools.create-openclaw-coding-tools.test.ts | 17 +++++++++ src/agents/pi-tools.ts | 4 +++ src/agents/tools/message-tool.test.ts | 35 +++++++++++++++++++ src/agents/tools/message-tool.ts | 32 +++++++++++++++-- 9 files changed, 94 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1c882a043..754a383b2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai - Discord/voice: add bounded realtime gateway logs for voice channel joins, realtime model/voice selection, transcripts, consult routing/answers, and playback start, allow OpenAI realtime Discord sessions to disable input-triggered response interruption for echo-heavy rooms while keeping explicit Discord barge-in available for new and already-active speakers, and allow voice turns to target an existing Discord channel agent session. - Discord/voice: include a bounded one-line STT transcript preview in verbose voice logs so live voice debugging shows what speakers said before the agent reply. - Codex app-server: pin the managed Codex harness and Codex CLI smoke package to `@openai/codex@0.129.0`, defer OpenClaw integration dynamic tools behind Codex tool search by default, and accept current Codex service-tier values so legacy `fast` settings survive the stable harness upgrade as `priority`. +- Codex app-server: annotate message-tool-only direct chat turns in the dynamic `message` tool spec so visible replies are sent through `message(action="send")` instead of staying private. (#79704) - Codex app-server: default implicit local stdio app-server permissions to guardian when Codex system requirements disallow the YOLO approval, reviewer, or sandbox value, including hostname-scoped remote sandbox entries, avoiding turn-start failures on managed hosts that permit only reviewed approval or narrower sandboxes. - Plugins/install: run managed npm-root install, uninstall, prune, and repair commands from the managed root without a redundant `--prefix .`, avoiding npm 10.9.3 Arborist crashes on native Windows WhatsApp plugin installs. Fixes #78514. (#78902) Thanks @melihselamett-stack. - Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner. diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 27d7e6b0bf3..2ed92363e34 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -1821,6 +1821,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) { modelHasVision, requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey), + sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, disableMessageTool: params.disableMessageTool, forceMessageTool: shouldForceMessageTool(params), enableHeartbeatTool: params.trigger === "heartbeat", diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 32324ef18e3..55e26d4e28c 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -1,3 +1,4 @@ +import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import { selectApplicableRuntimeConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { callGateway } from "../gateway/call.js"; @@ -117,6 +118,8 @@ export function createOpenClawTools( cronSelfRemoveOnlyJobId?: string; /** Require explicit message targets (no implicit last-route sends). */ requireExplicitMessageTarget?: boolean; + /** Visible source replies must be sent through the message tool when set to message_tool_only. */ + sourceReplyDeliveryMode?: SourceReplyDeliveryMode; /** If true, omit the message tool from the tool list. */ disableMessageTool?: boolean; /** If true, include the heartbeat response tool for structured heartbeat outcomes. */ @@ -294,6 +297,7 @@ export function createOpenClawTools( hasRepliedRef: options?.hasRepliedRef, sandboxRoot: options?.sandboxRoot, requireExplicitTarget: options?.requireExplicitMessageTarget, + sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode, requesterSenderId: options?.requesterSenderId ?? undefined, senderIsOwner: options?.senderIsOwner, }); diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 79810272482..6c3e06e1fec 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -721,6 +721,7 @@ async function compactEmbeddedPiSessionDirectOnce( workspaceDir: effectiveWorkspace, config: params.config, abortSignal: runAbortController.signal, + sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, modelProvider: model.provider, modelId, modelCompat: extractModelCompat(effectiveModel), diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 4fd7eb3cecc..d274ec3ec36 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -901,6 +901,7 @@ export async function runEmbeddedAttempt( modelHasVision: params.model.input?.includes("image") ?? false, requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey), + sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, disableMessageTool: params.disableMessageTool, forceMessageTool: params.forceMessageTool, enableHeartbeatTool: params.enableHeartbeatTool, diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts index f73e95130a3..2c331c5f708 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts @@ -200,6 +200,23 @@ describe("createOpenClawCodingTools", () => { ); }); + it("passes source reply delivery mode to OpenClaw tool construction", () => { + const createOpenClawToolsMock = vi.mocked(createOpenClawTools); + createOpenClawToolsMock.mockClear(); + + createOpenClawCodingTools({ + config: testConfig, + forceMessageTool: true, + sourceReplyDeliveryMode: "message_tool_only", + }); + + expect(createOpenClawToolsMock).toHaveBeenCalledWith( + expect.objectContaining({ + sourceReplyDeliveryMode: "message_tool_only", + }), + ); + }); + it("skips unrelated tool families when construction is planned from a narrow allowlist", () => { const createOpenClawToolsMock = vi.mocked(createOpenClawTools); createOpenClawToolsMock.mockClear(); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 38740c9cec3..8446cde8389 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -1,4 +1,5 @@ import { createCodingTools, createReadTool } from "@mariozechner/pi-coding-agent"; +import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -341,6 +342,8 @@ export function createOpenClawCodingTools(options?: { modelHasVision?: boolean; /** Require explicit message targets (no implicit last-route sends). */ requireExplicitMessageTarget?: boolean; + /** Visible source replies must be sent through the message tool when set to message_tool_only. */ + sourceReplyDeliveryMode?: SourceReplyDeliveryMode; /** If true, omit the message tool from the tool list. */ disableMessageTool?: boolean; /** Keep the message tool available even when the selected profile omits it. */ @@ -743,6 +746,7 @@ export function createOpenClawCodingTools(options?: { hasRepliedRef: options?.hasRepliedRef, modelHasVision: options?.modelHasVision, requireExplicitMessageTarget: options?.requireExplicitMessageTarget, + sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode, disableMessageTool: options?.disableMessageTool, enableHeartbeatTool, disablePluginTools: !includePluginTools, diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 31853c6d984..b3486fe0e10 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -325,6 +325,41 @@ async function executeSend(params: { } describe("message tool secret scoping", () => { + it("marks message-tool-only source replies in the tool description", () => { + const scopedTool = createMessageTool({ + sourceReplyDeliveryMode: "message_tool_only", + }); + const explicitTargetTool = createMessageTool({ + requireExplicitTarget: true, + sourceReplyDeliveryMode: "message_tool_only", + }); + const defaultTool = createMessageTool(); + + expect(scopedTool.description).toContain( + 'visible replies to the current source conversation must use action="send"', + ); + expect(scopedTool.description).toContain("target defaults to the current source conversation"); + expect(scopedTool.description).toContain("Normal final answers are private"); + expect(explicitTargetTool.description).toContain("Include target when sending"); + expect(explicitTargetTool.description).not.toContain( + "target defaults to the current source conversation", + ); + expect(defaultTool.description).not.toContain( + "visible replies to the current source conversation", + ); + }); + + it("forwards source reply delivery mode through createOpenClawTools", () => { + const tool = createOpenClawTools({ + config: {} as never, + sourceReplyDeliveryMode: "message_tool_only", + }).find((candidate) => candidate.name === "message"); + + expect(tool?.description).toContain( + 'visible replies to the current source conversation must use action="send"', + ); + }); + it("scopes command-time secret resolution to the selected channel/account", async () => { mockSendResult({ channel: "discord", to: "discord:123" }); mocks.getRuntimeConfig.mockReturnValue({ diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index db8da223899..0e25237353e 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -1,4 +1,5 @@ import { Type, type TSchema } from "typebox"; +import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import { listChannelPlugins } from "../../channels/plugins/index.js"; import { channelSupportsMessageCapability, @@ -520,6 +521,7 @@ type MessageToolOptions = { hasRepliedRef?: { value: boolean }; sandboxRoot?: string; requireExplicitTarget?: boolean; + sourceReplyDeliveryMode?: SourceReplyDeliveryMode; requesterSenderId?: string; senderIsOwner?: boolean; }; @@ -648,6 +650,8 @@ function buildMessageToolDescription(options?: { sessionKey?: string; sessionId?: string; agentId?: string; + requireExplicitTarget?: boolean; + sourceReplyDeliveryMode?: SourceReplyDeliveryMode; requesterSenderId?: string; senderIsOwner?: boolean; }): string { @@ -679,13 +683,35 @@ function buildMessageToolDescription(options?: { ChannelMessageActionName | "send" >; return appendMessageToolReadHint( - `${baseDescription} Supports actions: ${sortedActions.join(", ")}.`, + appendMessageToolVisibleReplyHint( + `${baseDescription} Supports actions: ${sortedActions.join(", ")}.`, + resolvedOptions.sourceReplyDeliveryMode, + resolvedOptions.requireExplicitTarget, + ), sortedActions, ); } } - return `${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`; + return appendMessageToolVisibleReplyHint( + `${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`, + resolvedOptions.sourceReplyDeliveryMode, + resolvedOptions.requireExplicitTarget, + ); +} + +function appendMessageToolVisibleReplyHint( + description: string, + sourceReplyDeliveryMode?: SourceReplyDeliveryMode, + requireExplicitTarget?: boolean, +): string { + if (sourceReplyDeliveryMode !== "message_tool_only") { + return description; + } + const targetGuidance = requireExplicitTarget + ? "Include target when sending." + : "The target defaults to the current source conversation, so omit target unless sending elsewhere."; + return `${description} For this turn, visible replies to the current source conversation must use action="send" with message. ${targetGuidance} Normal final answers are private and are not posted.`; } function appendMessageToolReadHint( @@ -743,6 +769,8 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { sessionKey: options?.agentSessionKey, sessionId: options?.sessionId, agentId: resolvedAgentId, + requireExplicitTarget: options?.requireExplicitTarget, + sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode, requesterSenderId: options?.requesterSenderId, senderIsOwner: options?.senderIsOwner, });