diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fbbe2ef83..54c8fbcc69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash. - Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme. - Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer. diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 6642cec6973..e03a79624f9 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -803,6 +803,8 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("use `message(action=send)` for visible channel output"); expect(prompt).toContain("The target defaults to the current source channel"); expect(prompt).toContain("final answers are private in this mode"); + expect(prompt).not.toContain("## Silent Replies"); + expect(prompt).not.toContain(SILENT_REPLY_TOKEN); expect(prompt).not.toContain( `respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies)`, ); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 201fd41fb59..1306649fa80 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -351,6 +351,9 @@ function buildMessagingSection(params: { const showGenericInlineButtonHint = params.runtimeChannel !== "slack"; const hasSessionsSpawn = params.availableTools.has("sessions_spawn"); const hasSubagents = params.availableTools.has("subagents"); + const completionEventGuidance = messageToolOnly + ? "- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to a silent placeholder)." + : `- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to ${SILENT_REPLY_TOKEN}).`; const subagentOrchestrationGuidance = hasSessionsSpawn ? hasSubagents ? '- Sub-agent orchestration → use `sessions_spawn(...)` to start delegated work; omit `context` for isolated children, set `context:"fork"` only when the child needs the current transcript; use `subagents(action=list|steer|kill)` to manage already-spawned children.' @@ -365,7 +368,7 @@ function buildMessagingSection(params: { : "- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)", "- Cross-session messaging → use sessions_send(sessionKey, message)", subagentOrchestrationGuidance, - `- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to ${SILENT_REPLY_TOKEN}).`, + completionEventGuidance, "- Never use exec/curl for provider messaging; OpenClaw handles all routing internally.", params.availableTools.has("message") ? [ @@ -665,7 +668,10 @@ export function buildAgentSystemPrompt(params: { const messageChannelOptions = listDeliverableMessageChannels().join("|"); const promptMode = params.promptMode ?? "full"; const isMinimal = promptMode === "minimal" || promptMode === "none"; - const silentReplyPromptMode = params.silentReplyPromptMode ?? "generic"; + const sourceMessageToolOnly = params.sourceReplyDeliveryMode === "message_tool_only"; + const silentReplyPromptMode = sourceMessageToolOnly + ? "none" + : (params.silentReplyPromptMode ?? "generic"); const sandboxContainerWorkspace = params.sandboxInfo?.containerWorkspaceDir?.trim(); const sanitizedWorkspaceDir = sanitizeForPromptLiteral(params.workspaceDir); const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 9515b7431c0..224a2dcbbaf 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -450,6 +450,7 @@ export async function runPreparedReply( const directChatContext = isDirectChat ? buildDirectChatContext({ sessionCtx: promptSessionCtx, + sourceReplyDeliveryMode: opts?.sourceReplyDeliveryMode, silentReplyPolicy: silentReplySettings.policy, silentReplyRewrite: silentReplySettings.rewrite, silentToken: SILENT_REPLY_TOKEN, diff --git a/src/auto-reply/reply/groups.test.ts b/src/auto-reply/reply/groups.test.ts index 74c442e3ad2..5d4d70f5401 100644 --- a/src/auto-reply/reply/groups.test.ts +++ b/src/auto-reply/reply/groups.test.ts @@ -87,6 +87,19 @@ describe("group runtime loading", () => { silentToken: "NO_REPLY", }), ).toContain('reply with exactly "NO_REPLY"'); + + const toolOnlyContext = groups.buildDirectChatContext({ + sessionCtx: { ChatType: "direct", Provider: "telegram" }, + sourceReplyDeliveryMode: "message_tool_only", + silentReplyPolicy: "allow", + silentReplyRewrite: true, + silentToken: "NO_REPLY", + }); + expect(toolOnlyContext).toContain("Normal final replies are private"); + expect(toolOnlyContext).toContain("message tool with action=send"); + expect(toolOnlyContext).toContain("do not call message(action=send)"); + expect(toolOnlyContext).not.toContain("NO_REPLY"); + expect(toolOnlyContext).not.toContain("Your replies are automatically sent"); }); it("gates group silent-token instructions on the resolved silent reply policy", async () => { diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index 81037f68a61..a2b903bfca6 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -283,13 +283,24 @@ export function buildGroupChatContext(params: { export function buildDirectChatContext(params: { sessionCtx: TemplateContext; + sourceReplyDeliveryMode?: SourceReplyDeliveryMode; silentReplyPolicy?: SilentReplyPolicy; silentReplyRewrite?: boolean; silentToken: string; }): string { const providerLabel = resolveProviderLabel(params.sessionCtx.Provider); + const messageToolOnly = params.sourceReplyDeliveryMode === "message_tool_only"; const lines: string[] = []; lines.push(`You are in a ${providerLabel} direct conversation.`); + if (messageToolOnly) { + lines.push( + "Normal final replies are private and are not automatically sent to this conversation. To post visible output here, use the message tool with action=send; the target defaults to this conversation.", + ); + lines.push( + "If no visible direct response is needed, do not call message(action=send). Your normal final answer stays private and will not be posted to the conversation.", + ); + return lines.join(" "); + } lines.push("Your replies are automatically sent to this conversation."); if (params.silentReplyPolicy === "allow") { lines.push(