Skip to content

Commit 1699ebb

Browse files
committed
fix(messages): keep Codex direct replies automatic
1 parent 1e5450f commit 1699ebb

11 files changed

Lines changed: 61 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
3838

3939
### Fixes
4040

41+
- Messages/Codex: keep direct Codex-backed chats on the same automatic final-reply default as other source chats unless `messages.visibleReplies: "message_tool"` is explicitly configured.
4142
- Codex app-server: preserve network access for sandboxed Codex code-mode turns when the OpenClaw sandbox allows outbound egress. Fixes #83347. Thanks @YusukeIt0.
4243
- QA-Lab: keep the OTLP smoke decoder independent of removed OpenTelemetry generated-root internals.
4344
- Messages: default group/channel visible replies to automatic final delivery again, keeping `message_tool` opt-in for ambient/shared rooms and tool-reliable models.

docs/channels/groups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ If the message tool is unavailable under the active tool policy, OpenClaw falls
5151
back to automatic visible replies instead of silently suppressing the response.
5252
`openclaw doctor` warns about this mismatch.
5353

54-
For direct chats and any other source event, use `messages.visibleReplies: "message_tool"` to apply the same tool-only visible-reply behavior globally. Harnesses can also choose this as their unset default; the Codex harness does this for Codex-mode direct chats. `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms.
54+
For direct chats and any other source event, use `messages.visibleReplies: "message_tool"` to apply the same tool-only visible-reply behavior globally. When unset, direct/source chats use automatic final delivery across runtimes, including Codex. `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms.
5555

5656
This replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode, doing nothing visible simply means not calling the message tool.
5757

docs/gateway/config-channels.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ See the full channel index: [Channels](/channels).
787787

788788
Group messages default to **require mention** (metadata mention or safe regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
789789

790-
Visible replies are controlled separately. Normal group/channel requests default to `messages.groupChat.visibleReplies: "automatic"`: final assistant text posts through the legacy visible reply path. Set `"message_tool"` when a shared room should only post visible output after the agent calls `message(action=send)`. If the model returns final text without calling the message tool, that final text stays private and the gateway verbose log records suppressed payload metadata. To apply the same tool-only visible-reply behavior to direct chats too, set `messages.visibleReplies: "message_tool"`; the Codex harness also uses that tool-only behavior as its unset direct-chat default.
790+
Visible replies are controlled separately. Normal direct, group, and channel requests default to automatic final delivery: final assistant text posts through the legacy visible reply path. Set `"message_tool"` when a chat should only post visible output after the agent calls `message(action=send)`. If the model returns final text without calling the message tool, that final text stays private and the gateway verbose log records suppressed payload metadata.
791791

792792
Tool-only visible replies require a model/runtime that reliably calls tools, and are recommended for shared ambient rooms on latest-generation models such as GPT 5.5. If
793793
the session log shows assistant text with `didSendViaMessagingTool: false`, the
@@ -810,7 +810,7 @@ The gateway hot-reloads `messages` config after the file is saved. Restart only
810810
```json5
811811
{
812812
messages: {
813-
visibleReplies: "automatic", // global default for direct/source chats; Codex harness defaults unset direct chats to message_tool
813+
visibleReplies: "automatic", // global default for direct/source chats
814814
groupChat: {
815815
historyLimit: 50,
816816
unmentionedInbound: "room_event", // always-on unmentioned room chatter becomes quiet context
@@ -827,7 +827,7 @@ The gateway hot-reloads `messages` config after the file is saved. Restart only
827827

828828
`messages.groupChat.unmentionedInbound: "room_event"` submits unmentioned always-on group/channel messages as quiet room context on supported channels. Mentioned messages, commands, and direct messages remain user requests. See [Ambient room events](/channels/ambient-room-events) for complete Discord, Slack, and Telegram examples.
829829

830-
`messages.visibleReplies` is the global source-event default; `messages.groupChat.visibleReplies` overrides it for group/channel source events. When `messages.visibleReplies` is unset, a harness can provide its own direct/source default; the Codex harness defaults to `message_tool`. Channel allowlists and mention gating still decide whether an event is processed.
830+
`messages.visibleReplies` is the global source-event default; `messages.groupChat.visibleReplies` overrides it for group/channel source events. When `messages.visibleReplies` is unset, direct/source chats use automatic final delivery across runtimes. Channel allowlists and mention gating still decide whether an event is processed.
831831

832832
#### DM history limits
833833

docs/plugins/codex-harness-runtime.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ newly selected model.
3333

3434
## Visible replies and heartbeats
3535

36-
When a source chat turn runs through the Codex harness, visible replies default
37-
to the OpenClaw `message` tool if the deployment has not explicitly configured
38-
`messages.visibleReplies`. The agent can still finish its Codex turn privately;
39-
it only posts to the channel when it calls `message(action="send")`. Set
40-
`messages.visibleReplies: "automatic"` to keep direct-chat final replies on the
41-
legacy automatic delivery path.
36+
When a source chat turn runs through the Codex harness, visible replies follow
37+
the same source-delivery defaults as other runtimes. Direct chats, normal group
38+
requests, and normal channel requests automatically post final assistant text
39+
unless config opts into tool-only delivery. Set `messages.visibleReplies:
40+
"message_tool"` when direct/source chats should post visible output only after
41+
the agent calls `message(action="send")`.
4242

4343
Codex heartbeat turns also get `heartbeat_respond` in the searchable OpenClaw
4444
tool catalog by default, so the agent can record whether the wake should stay

extensions/codex/harness.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ export function createCodexAppServerAgentHarness(options?: {
2323
return {
2424
id: options?.id ?? "codex",
2525
label: options?.label ?? "Codex agent harness",
26-
deliveryDefaults: {
27-
sourceVisibleReplies: "message_tool",
28-
},
2926
supports: (ctx) => {
3027
const provider = ctx.provider.trim().toLowerCase();
3128
if (providerIds.has(provider)) {

extensions/codex/index.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ describe("codex plugin", () => {
7272
expect(providerRegistration.label).toBe("Codex");
7373
expect(agentHarnessRegistration.id).toBe("codex");
7474
expect(agentHarnessRegistration.label).toBe("Codex agent harness");
75-
expect(agentHarnessRegistration.deliveryDefaults).toEqual({
76-
sourceVisibleReplies: "message_tool",
77-
});
75+
expect(agentHarnessRegistration.deliveryDefaults).toBeUndefined();
7876
expect(typeof agentHarnessRegistration.dispose).toBe("function");
7977
expect(mediaProviderRegistration?.id).toBe("codex");
8078
expect(mediaProviderRegistration?.capabilities).toEqual(["image"]);
@@ -121,7 +119,7 @@ describe("codex plugin", () => {
121119
it("only claims the codex provider by default", () => {
122120
const harness = createCodexAppServerAgentHarness();
123121

124-
expect(harness.deliveryDefaults?.sourceVisibleReplies).toBe("message_tool");
122+
expect("deliveryDefaults" in harness).toBe(false);
125123
expect(
126124
harness.supports({ provider: "codex", modelId: "gpt-5.4", requestedRuntime: "auto" })
127125
.supported,

src/agents/harness/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ export type AgentHarnessResultClassification =
5959

6060
export type AgentHarnessDeliveryDefaults = {
6161
/**
62-
* Preferred default for visible source replies when user config has not
63-
* explicitly selected automatic or message-tool delivery.
62+
* @deprecated Prefer `messages.visibleReplies` / `messages.groupChat.visibleReplies`
63+
* config. Kept for existing harness plugins.
6464
*/
6565
sourceVisibleReplies?: "automatic" | "message_tool";
6666
};

src/auto-reply/reply/dispatch-from-config.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5249,12 +5249,11 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
52495249
expect(firstFinalReplyPayload(dispatcher)?.text).toBe("visible direct reply");
52505250
});
52515251

5252-
it("uses harness defaults for direct source delivery when config is unset", async () => {
5252+
it("keeps Codex direct source delivery automatic when config is unset", async () => {
52535253
setNoAbort();
52545254
registerAgentHarness({
52555255
id: "codex",
52565256
label: "Codex",
5257-
deliveryDefaults: { sourceVisibleReplies: "message_tool" },
52585257
supports: () => ({ supported: true, priority: 100 }),
52595258
runAttempt: vi.fn(async () => ({}) as never),
52605259
});
@@ -5266,7 +5265,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
52665265
};
52675266
const dispatcher = createDispatcher();
52685267
const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
5269-
expect(opts?.sourceReplyDeliveryMode).toBe("message_tool_only");
5268+
expect(opts?.sourceReplyDeliveryMode).toBe("automatic");
52705269
return { text: "final reply" } satisfies ReplyPayload;
52715270
});
52725271

@@ -5281,6 +5280,44 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
52815280
replyResolver,
52825281
});
52835282

5283+
expect(replyResolver).toHaveBeenCalledTimes(1);
5284+
expect(result.queuedFinal).toBe(true);
5285+
expect(firstFinalReplyPayload(dispatcher)?.text).toBe("final reply");
5286+
});
5287+
5288+
it("preserves non-Codex harness direct source delivery defaults", async () => {
5289+
setNoAbort();
5290+
registerAgentHarness({
5291+
id: "custom",
5292+
label: "Custom",
5293+
deliveryDefaults: { sourceVisibleReplies: "message_tool" },
5294+
supports: () => ({ supported: true, priority: 200 }),
5295+
runAttempt: vi.fn(async () => ({}) as never),
5296+
});
5297+
sessionStoreMocks.currentEntry = {
5298+
sessionId: "s1",
5299+
updatedAt: 0,
5300+
agentHarnessId: "custom",
5301+
sendPolicy: "allow",
5302+
};
5303+
const dispatcher = createDispatcher();
5304+
const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
5305+
expect(opts?.sourceReplyDeliveryMode).toBe("message_tool_only");
5306+
return { text: "private final reply" } satisfies ReplyPayload;
5307+
});
5308+
5309+
const result = await dispatchReplyFromConfig({
5310+
ctx: buildTestCtx({
5311+
ChatType: "direct",
5312+
CommandSource: undefined,
5313+
Provider: "custom",
5314+
SessionKey: "agent:main:main",
5315+
}),
5316+
cfg: emptyConfig,
5317+
dispatcher,
5318+
replyResolver,
5319+
});
5320+
52845321
expect(replyResolver).toHaveBeenCalledTimes(1);
52855322
expect(result.queuedFinal).toBe(false);
52865323
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();

test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
These fixtures capture the default OpenAI/Codex happy path for prompt review:
66

77
- OpenAI model through the Codex harness and Codex app-server runtime.
8-
- `messages.visibleReplies: "message_tool"`, which is the Codex-harness default for visible source replies.
8+
- `messages.visibleReplies: "message_tool"` opt-in coverage for tool-only visible source replies.
99
- Telegram direct chat, Discord group chat, and a heartbeat turn with `heartbeat_respond` available through searchable dynamic tools.
1010

1111
The Markdown files show selected app-server thread/turn params plus a reconstructed model-bound prompt layer stack: Codex `gpt-5.5` model instructions from a pinned Codex model catalog fixture, Codex permission developer instructions for the happy-path yolo profile, simulated OpenClaw workspace bootstrap config instructions, OpenClaw developer instructions, user turn input, and references to the complete dynamic tool catalog.

test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
## Scope
66

7-
- Default happy path: OpenAI model through the Codex harness/runtime, Telegram direct conversation, and message-tool-only visible replies.
8-
- A quiet turn is represented by not calling `message(action=send)`; the normal final assistant text is private to OpenClaw/Codex.
7+
- Opt-in message-tool path: OpenAI model through the Codex harness/runtime, Telegram direct conversation, and message-tool-only visible replies.
8+
- This scenario forces tool-only delivery; the default Codex direct path uses automatic final replies.
99
- This captures the OpenClaw-owned Codex app-server inputs and reconstructs the stable Codex model/permission layers from committed Codex prompt fixtures.
1010
- This also simulates workspace bootstrap files forwarded through Codex `config.instructions`: `SOUL.md`, `TOOLS.md`, and `HEARTBEAT.md`.
1111

0 commit comments

Comments
 (0)