Skip to content

Commit 30e079d

Browse files
committed
fix(channels): honor reasoning defaults in previews (#71817) (thanks @anagnorisis2peripeteia)
1 parent 5c58967 commit 30e079d

9 files changed

Lines changed: 153 additions & 10 deletions

File tree

CHANGELOG.md

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

77
### Changes
88

9+
- Telegram/Feishu: honor configured per-agent and global `reasoningDefault` values when deciding whether channel reasoning previews should stream or stay hidden, addressing the preview-default part of #73182. Thanks @anagnorisis2peripeteia.
910
- Docker: run the runtime image under `tini` so long-lived containers reap orphaned child processes and forward signals correctly. (#77885) Thanks @VintageAyu.
1011
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` and `google-gemini-cli/gemini-3-pro-preview` selections to `google/gemini-3.1-pro-preview` before they are written to model config.
1112
- Amazon Bedrock: support `serviceTier` parameter for Bedrock models, configurable via `agents.defaults.params.serviceTier` or per-model in `agents.defaults.models`. Valid values: `default`, `flex`, `priority`, `reserved`. (#64512) Thanks @mobilinkd.

docs/tools/thinking.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ title: "Thinking levels"
106106
- `stream` (Telegram only): streams reasoning into the Telegram draft bubble while the reply is generating, then sends the final answer without reasoning.
107107
- Alias: `/reason`.
108108
- Send `/reasoning` (or `/reasoning:`) with no argument to see the current reasoning level.
109-
- Resolution order: inline directive, then session override, then per-agent default (`agents.list[].reasoningDefault`), then fallback (`off`).
109+
- Resolution order: inline directive, then session override, then per-agent default (`agents.list[].reasoningDefault`), then global default (`agents.defaults.reasoningDefault`), then fallback (`off`).
110110

111111
Malformed local-model reasoning tags are handled conservatively. Closed `<think>...</think>` blocks stay hidden on normal replies, and unclosed reasoning after already visible text is also hidden. If a reply is fully wrapped in a single unclosed opening tag and would otherwise deliver as empty text, OpenClaw removes the malformed opening tag and delivers the remaining text.
112112

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ClawdbotConfig } from "./bot-runtime-api.js";
2+
3+
type ReasoningDefault = "on" | "stream" | "off";
4+
5+
const DEFAULT_AGENT_ID = "main";
6+
7+
function normalizeAgentId(value: string | undefined | null): string {
8+
const normalized = (value ?? "").trim().toLowerCase();
9+
return normalized || DEFAULT_AGENT_ID;
10+
}
11+
12+
export function resolveFeishuConfigReasoningDefault(
13+
cfg: ClawdbotConfig,
14+
agentId: string,
15+
): ReasoningDefault {
16+
const id = normalizeAgentId(agentId);
17+
const agentDefault = cfg.agents?.list?.find(
18+
(entry) => normalizeAgentId(entry?.id) === id,
19+
)?.reasoningDefault;
20+
return agentDefault ?? cfg.agents?.defaults?.reasoningDefault ?? "off";
21+
}

extensions/feishu/src/bot.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,8 @@ export async function handleFeishuMessage(params: {
13571357
},
13581358
};
13591359
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
1360+
cfg,
1361+
agentId,
13601362
storePath: agentStorePath,
13611363
sessionKey: agentSessionKey,
13621364
});
@@ -1532,6 +1534,8 @@ export async function handleFeishuMessage(params: {
15321534
agentId: route.agentId,
15331535
});
15341536
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
1537+
cfg,
1538+
agentId: route.agentId,
15351539
storePath,
15361540
sessionKey: route.sessionKey,
15371541
});

extensions/feishu/src/reasoning-preview.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { ClawdbotConfig } from "./bot-runtime-api.js";
23
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
34

45
const { loadSessionStoreMock } = vi.hoisted(() => ({
@@ -20,6 +21,8 @@ afterAll(() => {
2021
});
2122

2223
describe("resolveFeishuReasoningPreviewEnabled", () => {
24+
const emptyCfg: ClawdbotConfig = {};
25+
2326
beforeEach(() => {
2427
vi.clearAllMocks();
2528
});
@@ -32,12 +35,16 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
3235

3336
expect(
3437
resolveFeishuReasoningPreviewEnabled({
38+
cfg: emptyCfg,
39+
agentId: "main",
3540
storePath: "/tmp/feishu-sessions.json",
3641
sessionKey: "agent:main:feishu:dm:ou_sender_1",
3742
}),
3843
).toBe(true);
3944
expect(
4045
resolveFeishuReasoningPreviewEnabled({
46+
cfg: emptyCfg,
47+
agentId: "main",
4148
storePath: "/tmp/feishu-sessions.json",
4249
sessionKey: "agent:main:feishu:dm:ou_sender_2",
4350
}),
@@ -51,13 +58,55 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
5158

5259
expect(
5360
resolveFeishuReasoningPreviewEnabled({
61+
cfg: emptyCfg,
62+
agentId: "main",
63+
storePath: "/tmp/feishu-sessions.json",
64+
sessionKey: "agent:main:feishu:dm:ou_sender_1",
65+
}),
66+
).toBe(false);
67+
expect(
68+
resolveFeishuReasoningPreviewEnabled({
69+
cfg: emptyCfg,
70+
agentId: "main",
71+
storePath: "/tmp/feishu-sessions.json",
72+
}),
73+
).toBe(false);
74+
});
75+
76+
it("falls back to configured stream defaults", () => {
77+
loadSessionStoreMock.mockReturnValue({
78+
"agent:main:feishu:dm:ou_sender_1": {},
79+
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "off" },
80+
});
81+
82+
const cfg: ClawdbotConfig = {
83+
agents: {
84+
defaults: { reasoningDefault: "stream" },
85+
list: [{ id: "Ops", reasoningDefault: "off" }],
86+
},
87+
};
88+
89+
expect(
90+
resolveFeishuReasoningPreviewEnabled({
91+
cfg,
92+
agentId: "main",
5493
storePath: "/tmp/feishu-sessions.json",
5594
sessionKey: "agent:main:feishu:dm:ou_sender_1",
5695
}),
96+
).toBe(true);
97+
expect(
98+
resolveFeishuReasoningPreviewEnabled({
99+
cfg,
100+
agentId: "ops",
101+
storePath: "/tmp/feishu-sessions.json",
102+
}),
57103
).toBe(false);
58104
expect(
59105
resolveFeishuReasoningPreviewEnabled({
106+
cfg,
107+
agentId: "main",
60108
storePath: "/tmp/feishu-sessions.json",
109+
sessionKey: "agent:main:feishu:dm:ou_sender_2",
61110
}),
62111
).toBe(false);
63112
});
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1+
import { resolveFeishuConfigReasoningDefault } from "./agent-config.js";
12
import { loadSessionStore, resolveSessionStoreEntry } from "./bot-runtime-api.js";
3+
import type { ClawdbotConfig } from "./bot-runtime-api.js";
24

35
export function resolveFeishuReasoningPreviewEnabled(params: {
6+
cfg: ClawdbotConfig;
7+
agentId: string;
48
storePath: string;
59
sessionKey?: string;
610
}): boolean {
11+
const configDefault = resolveFeishuConfigReasoningDefault(params.cfg, params.agentId);
12+
713
if (!params.sessionKey) {
8-
return false;
14+
return configDefault === "stream";
915
}
1016

1117
try {
1218
const store = loadSessionStore(params.storePath, { skipCache: true });
13-
return (
14-
resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
15-
?.reasoningLevel === "stream"
16-
);
19+
const level = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
20+
?.reasoningLevel;
21+
if (level === "on" || level === "stream" || level === "off") {
22+
return level === "stream";
23+
}
1724
} catch {
1825
return false;
1926
}
27+
return configDefault === "stream";
2028
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2+
3+
type ReasoningDefault = "on" | "stream" | "off";
4+
5+
const DEFAULT_AGENT_ID = "main";
6+
7+
function normalizeAgentId(value: string | undefined | null): string {
8+
const normalized = (value ?? "").trim().toLowerCase();
9+
return normalized || DEFAULT_AGENT_ID;
10+
}
11+
12+
export function resolveTelegramConfigReasoningDefault(
13+
cfg: OpenClawConfig,
14+
agentId: string,
15+
): ReasoningDefault {
16+
const id = normalizeAgentId(agentId);
17+
const agentDefault = cfg.agents?.list?.find(
18+
(entry) => normalizeAgentId(entry?.id) === id,
19+
)?.reasoningDefault;
20+
return agentDefault ?? cfg.agents?.defaults?.reasoningDefault ?? "off";
21+
}

extensions/telegram/src/bot-message-dispatch.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,16 @@ describe("dispatchTelegramMessage draft streaming", () => {
409409
});
410410
}
411411

412+
function createReasoningDefaultContext(): TelegramMessageContext {
413+
loadSessionStore.mockReturnValue({
414+
s1: {},
415+
});
416+
return createContext({
417+
ctxPayload: { SessionKey: "s1" } as unknown as TelegramMessageContext["ctxPayload"],
418+
route: { agentId: "ops" } as unknown as TelegramMessageContext["route"],
419+
});
420+
}
421+
412422
it("streams drafts in private threads and forwards thread id", async () => {
413423
const draftStream = createDraftStream();
414424
createTelegramDraftStream.mockReturnValue(draftStream);
@@ -1149,6 +1159,33 @@ describe("dispatchTelegramMessage draft streaming", () => {
11491159
expect(deliverReplies).not.toHaveBeenCalled();
11501160
});
11511161

1162+
it("streams reasoning from configured defaults", async () => {
1163+
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
1164+
answerMessageId: 2001,
1165+
reasoningMessageId: 3001,
1166+
});
1167+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1168+
async ({ dispatcherOptions, replyOptions }) => {
1169+
await replyOptions?.onReasoningStream?.({ text: "<think>Thinking</think>" });
1170+
await dispatcherOptions.deliver({ text: "Answer" }, { kind: "final" });
1171+
return { queuedFinal: true };
1172+
},
1173+
);
1174+
1175+
await dispatchWithContext({
1176+
context: createReasoningDefaultContext(),
1177+
cfg: {
1178+
agents: {
1179+
defaults: { reasoningDefault: "off" },
1180+
list: [{ id: "Ops", reasoningDefault: "stream" }],
1181+
},
1182+
},
1183+
});
1184+
1185+
expect(reasoningDraftStream.update).toHaveBeenCalledWith("Reasoning:\n_Thinking_");
1186+
expect(answerDraftStream.update).toHaveBeenCalledWith("Answer");
1187+
});
1188+
11521189
it("suppresses reasoning-only finals without raw text fallback", async () => {
11531190
setupDraftStreams({ answerMessageId: 2001, reasoningMessageId: 3001 });
11541191
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
logVerbose,
4343
sleepWithAbort,
4444
} from "openclaw/plugin-sdk/runtime-env";
45+
import { resolveTelegramConfigReasoningDefault } from "./agent-config.js";
4546
import type { TelegramBotDeps } from "./bot-deps.js";
4647
import type { TelegramMessageContext } from "./bot-message-context.js";
4748
import {
@@ -214,8 +215,9 @@ function resolveTelegramReasoningLevel(params: {
214215
telegramDeps: TelegramBotDeps;
215216
}): TelegramReasoningLevel {
216217
const { cfg, sessionKey, agentId, telegramDeps } = params;
218+
const configDefault = resolveTelegramConfigReasoningDefault(cfg, agentId);
217219
if (!sessionKey) {
218-
return "off";
220+
return configDefault;
219221
}
220222
try {
221223
const storePath = telegramDeps.resolveStorePath(cfg.session?.store, { agentId });
@@ -224,13 +226,13 @@ function resolveTelegramReasoningLevel(params: {
224226
});
225227
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
226228
const level = entry?.reasoningLevel;
227-
if (level === "on" || level === "stream") {
229+
if (level === "on" || level === "stream" || level === "off") {
228230
return level;
229231
}
230232
} catch {
231-
// Fall through to default.
233+
return "off";
232234
}
233-
return "off";
235+
return configDefault;
234236
}
235237

236238
const MAX_PROGRESS_MARKDOWN_TEXT_CHARS = 300;

0 commit comments

Comments
 (0)