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.
This commit is contained in:
Peter Steinberger
2026-05-09 04:24:40 -04:00
committed by GitHub
parent 657d2331b3
commit b62166301e
9 changed files with 94 additions and 2 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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,
});

View File

@@ -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),

View File

@@ -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,

View File

@@ -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();

View File

@@ -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,

View File

@@ -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({

View File

@@ -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,
});