Skip to content

Commit 47eb4ca

Browse files
authored
fix: prompt Codex to send visible channel replies (#84397)
* fix: prompt codex to send visible channel replies * chore: add codex reply changelog entry * test: refresh codex prompt snapshots
1 parent 9eee202 commit 47eb4ca

16 files changed

Lines changed: 64 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
2929
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.
3030
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
3131
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)
32+
- Codex/message: tell message-tool-only Codex turns to send visible channel output with `message(action="send")` before ending, so direct replies do not stay private. Fixes #84129. (#84397) Thanks @joshavant.
3233
- CLI: preserve embedded equals signs in inline root option values instead of truncating after the second separator. (#83995) Thanks @ThiagoCAltoe.
3334
- Matrix/config: accept `messages.queue.byChannel.matrix` queue overrides and keep queue provider schema/type keys aligned for Matrix, Google Chat, and Mattermost. Thanks @bdjben.
3435
- CLI: format `openclaw acp client` failures through the shared error formatter so object-shaped errors stay readable instead of printing `[object Object]`. Fixes #83904. (#84080)

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,12 @@ describe("runCodexAppServerAttempt", () => {
14021402
testing.buildDeveloperInstructions(params, {
14031403
dynamicTools: [createMessageDynamicTool("Message test tool")],
14041404
}),
1405-
).toContain("To send a visible message, use the `message` tool.");
1405+
).toContain('call `message` with `action="send"` before ending the turn');
1406+
expect(
1407+
testing.buildDeveloperInstructions(params, {
1408+
dynamicTools: [createMessageDynamicTool("Message test tool")],
1409+
}),
1410+
).toContain("Do not rely on normal final assistant text for visible delivery");
14061411

14071412
const withoutMessageToolInstructions = testing.buildDeveloperInstructions(params, {
14081413
dynamicTools: [],
@@ -1413,7 +1418,7 @@ describe("runCodexAppServerAttempt", () => {
14131418
params.sourceReplyDeliveryMode = "automatic";
14141419
const automaticInstructions = testing.buildDeveloperInstructions(params);
14151420
expect(automaticInstructions).toContain("active Codex delivery path");
1416-
expect(automaticInstructions).not.toContain("use the `message` tool");
1421+
expect(automaticInstructions).not.toContain('call `message` with `action="send"`');
14171422
});
14181423

14191424
it("includes Codex app-server scoped plugin command guidance in developer instructions", () => {

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,12 @@ function buildVisibleReplyInstruction(
884884
? dynamicTools.some((tool) => tool.name.trim() === "message")
885885
: params.disableMessageTool !== true;
886886
if (params.sourceReplyDeliveryMode === "message_tool_only" && messageToolAvailable) {
887-
return "To send a visible message, use the `message` tool.";
887+
return [
888+
"Preserve channel/session context.",
889+
'If this turn needs visible output in the current channel, call `message` with `action="send"` before ending the turn.',
890+
"Do not rely on normal final assistant text for visible delivery; final text is private to OpenClaw/Codex in this mode.",
891+
'If no visible channel response is needed, do not call `message(action="send")`.',
892+
].join(" ");
888893
}
889894
return "To send a visible reply, use the active Codex delivery path.";
890895
}

src/agents/tools/message-tool.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,16 +385,18 @@ describe("message tool secret scoping", () => {
385385
const defaultTool = createMessageTool();
386386

387387
expect(scopedTool.description).toContain(
388-
'use action="send" with message for visible replies to the current source conversation',
388+
'if visible output is needed in the current source conversation, call action="send" with message before ending',
389389
);
390390
expect(scopedTool.description).toContain("target defaults to the current source conversation");
391-
expect(scopedTool.description).toContain("Normal final answers stay private");
391+
expect(scopedTool.description).toContain(
392+
"Normal final answers stay private and are not visible in this mode",
393+
);
392394
expect(explicitTargetTool.description).toContain("Include target when sending");
393395
expect(explicitTargetTool.description).not.toContain(
394396
"target defaults to the current source conversation",
395397
);
396398
expect(defaultTool.description).not.toContain(
397-
"visible replies to the current source conversation",
399+
"if visible output is needed in the current source conversation",
398400
);
399401
});
400402

@@ -405,7 +407,7 @@ describe("message tool secret scoping", () => {
405407
}).find((candidate) => candidate.name === "message");
406408

407409
expect(tool?.description).toContain(
408-
'use action="send" with message for visible replies to the current source conversation',
410+
'if visible output is needed in the current source conversation, call action="send" with message before ending',
409411
);
410412
});
411413

src/agents/tools/message-tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ function appendMessageToolVisibleReplyHint(
859859
const targetGuidance = requireExplicitTarget
860860
? "Include target when sending."
861861
: "target defaults to the current source conversation; omit unless sending elsewhere.";
862-
return `${description} This turn: use action="send" with message for visible replies to the current source conversation. ${targetGuidance} Normal final answers stay private.`;
862+
return `${description} This turn: if visible output is needed in the current source conversation, call action="send" with message before ending. ${targetGuidance} Normal final answers stay private and are not visible in this mode.`;
863863
}
864864

865865
function appendMessageToolReadHint(

src/auto-reply/reply/dispatch-acp.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ describe("tryDispatchAcpReply", () => {
513513
expect(managerMocks.runTurn).toHaveBeenCalledTimes(1);
514514
const text = runTurnCall().text;
515515
expect(text).toContain("Source channel delivery is private by default");
516-
expect(text).toContain("message(action=send)");
516+
expect(text).toContain('call `message` with `action="send"` before ending');
517517
expect(text).toContain("The target defaults to the current source channel");
518518
expect(text).toContain("reply privately unless you send explicitly");
519519
});

src/auto-reply/reply/dispatch-acp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function resolveAcpTurnText(params: {
127127
[
128128
"Source channel delivery is private by default for this turn.",
129129
"Normal ACP final output will not be automatically posted to the source channel.",
130-
"To send visible output, use message(action=send). The target defaults to the current source channel.",
130+
'If visible output is needed in the current source channel, call `message` with `action="send"` before ending. The target defaults to the current source channel.',
131131
].join(" "),
132132
);
133133
return params.promptText ? `${guidance}\n\n${params.promptText}` : guidance;

src/auto-reply/reply/groups.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ describe("group runtime loading", () => {
4545
silentToken: "NO_REPLY",
4646
});
4747
expect(toolOnlyContext).toContain("Normal final replies are private");
48-
expect(toolOnlyContext).toContain("message tool with action=send");
48+
expect(toolOnlyContext).toContain('message tool with action="send" before ending');
4949
expect(toolOnlyContext).toContain("Be a good group participant");
5050
expect(toolOnlyContext).toContain("wrap bare URLs");
5151
expect(toolOnlyContext).toContain("<https://example.com>");
52-
expect(toolOnlyContext).toContain("do not call message(action=send)");
52+
expect(toolOnlyContext).toContain('do not call message(action="send")');
5353
expect(toolOnlyContext).not.toContain('reply with exactly "NO_REPLY"');
5454
expect(
5555
isolatedGroups.buildGroupIntro({
@@ -82,8 +82,8 @@ describe("group runtime loading", () => {
8282
sourceReplyDeliveryMode: "message_tool_only",
8383
});
8484
expect(toolOnlyContext).toContain("Normal final replies are private");
85-
expect(toolOnlyContext).toContain("message tool with action=send");
86-
expect(toolOnlyContext).toContain("do not call message(action=send)");
85+
expect(toolOnlyContext).toContain('message tool with action="send" before ending');
86+
expect(toolOnlyContext).toContain('do not call message(action="send")');
8787
expect(toolOnlyContext).not.toContain("NO_REPLY");
8888
expect(toolOnlyContext).not.toContain("Your replies are automatically sent");
8989
});

src/auto-reply/reply/groups.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export function buildGroupChatContext(params: {
231231
lines.push(`You are in a ${providerLabel} group chat.`);
232232
if (messageToolOnly) {
233233
lines.push(
234-
"Normal final replies are private and are not automatically sent to this group chat. To post visible output here, use the message tool with action=send; the target defaults to this group chat.",
234+
'Normal final replies are private and are not automatically sent to this group chat. If this turn needs visible output here, call the message tool with action="send" before ending; the target defaults to this group chat.',
235235
);
236236
} else {
237237
lines.push(
@@ -255,7 +255,7 @@ export function buildGroupChatContext(params: {
255255
!messageToolOnly && params.silentToken && params.silentReplyPolicy !== "disallow";
256256
if (messageToolOnly) {
257257
lines.push(
258-
"If no visible group response is needed, do not call message(action=send). Your normal final answer stays private and will not be posted to the group.",
258+
'If no visible group response is needed, do not call message(action="send"). Your normal final answer stays private and will not be posted to the group.',
259259
);
260260
}
261261
if (canUseSilentReply) {
@@ -286,10 +286,10 @@ export function buildDirectChatContext(params: {
286286
lines.push(`You are in a ${providerLabel} direct conversation.`);
287287
if (messageToolOnly) {
288288
lines.push(
289-
"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.",
289+
'Normal final replies are private and are not automatically sent to this conversation. If this turn needs visible output here, call the message tool with action="send" before ending; the target defaults to this conversation.',
290290
);
291291
lines.push(
292-
"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.",
292+
'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.',
293293
);
294294
return lines.join(" ");
295295
}

src/auto-reply/reply/inbound-meta.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,9 @@ describe("buildInboundUserContextPrefix", () => {
300300
{ sourceReplyDeliveryMode: "message_tool_only" },
301301
);
302302

303-
expect(text).toContain("Delivery: to send a message, use the `message` tool.");
303+
expect(text).toContain(
304+
'Delivery: if this turn needs visible output in the current source conversation, call `message` with `action="send"` before ending.',
305+
);
304306
expect(text.indexOf("Delivery:")).toBeLessThan(text.indexOf("Conversation info"));
305307
expect(text).toContain("Conversation info (untrusted metadata):");
306308
});
@@ -317,7 +319,7 @@ describe("buildInboundUserContextPrefix", () => {
317319
{ sourceReplyDeliveryMode: "automatic" },
318320
);
319321

320-
expect(text).not.toContain("Delivery: to send a message");
322+
expect(text).not.toContain("Delivery: if this turn needs visible output");
321323
expect(text).toContain("Conversation info (untrusted metadata):");
322324
});
323325

0 commit comments

Comments
 (0)