Skip to content

Commit 45fbf2d

Browse files
kurplunkinsteipete
andauthored
fix(channels): honor /verbose in group sessions (#85488)
* codex: honor verbose in group dispatch * codex: address group verbose review findings Record the final local review pass for the group /verbose PR. Codex review against origin/main completed clean after tightening the shared group progress gate, keeping public plugin hook types stable, preserving ACP hidden tool boundaries, and adding regressions for live verbose gating and progress-callback suppression. * codex: require explicit group verbose progress Normal group tool/progress summaries now require an explicit session verbose override instead of inherited agent verbose defaults. This addresses the PR review concern that existing verboseDefault configurations could expose group progress after upgrade. DMs and forum-topic behavior continue to use the effective verbose state, while normal groups use the live explicit session verbose state set by /verbose on|full|off. * codex: document Slack group verbose caveat * fix(channels): simplify verbose progress gating * docs(changelog): note verbose channel fix * fix(channels): preserve quiet default for group progress * fix(channels): keep verbose error policy dynamic * fix(channels): default verbose progress off everywhere * fix(channels): keep followup verbose default quiet * fix(channels): latch visible tool-error progress * fix(channels): track failed verbose progress events * fix(channels): latch delivered tool errors * fix(channels): prevent progress opt-out bypass * fix(channels): isolate followup error warning state * fix(channels): keep full verbose followup warnings * fix(channels): latch tool errors after visible progress * fix(channels): require visible followup failure progress * fix(channels): refresh followup verbose state * fix(channels): honor live verbose for error details * test(channels): expect live verbose off warning mode * fix(channels): preserve static tool error suppression semantics * fix(channels): bypass acp for colon verbose commands * fix(channels): narrow dynamic tool warning override * fix(channels): gate compaction notices on live verbose * fix(channels): suppress quiet followup compaction callbacks * fix(channels): suppress tts for hidden tool summaries --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 2cd73d4 commit 45fbf2d

18 files changed

Lines changed: 1459 additions & 79 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
6565
- Checks/Windows: run managed child commands through explicit `cmd.exe` wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks.
6666
- Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs.
6767
- Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.
68+
- Channels: honor `/verbose on` for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.
6869
- CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.
6970
- Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud.
7071
- Status/usage: show configured cost estimates for AWS SDK models in full usage output while keeping token-only usage replies cost-free. (#85619) Thanks @ItsOtherMauridian.

docs/channels/groups.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ This replaces the old pattern of forcing the model to answer `NO_REPLY` for most
5757

5858
Typing indicators are still sent for direct group requests. Ambient always-on room events, when enabled, stay strict and quiet unless the agent calls the message tool.
5959

60+
Sessions suppress verbose tool/progress summaries by default. Use `/verbose on`
61+
to show those summaries for the current session while debugging, and
62+
`/verbose off` to return to final-reply-only behavior. The same verbose state
63+
applies across direct chats, groups, channels, and forum topics.
64+
6065
To submit unmentioned always-on group chatter as quiet room context instead of user requests, use [Ambient room events](/channels/ambient-room-events):
6166

6267
```json5

src/agents/pi-embedded-runner/run/params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export type RunEmbeddedPiAgentParams = {
131131
toolResultFormat?: ToolResultFormat;
132132
toolProgressDetail?: ToolProgressDetailMode;
133133
/** If true, suppress tool error warning payloads for this run (including mutating tools). */
134-
suppressToolErrorWarnings?: boolean;
134+
suppressToolErrorWarnings?: boolean | (() => boolean | undefined);
135135
/** Bootstrap context mode for workspace file injection. */
136136
bootstrapContextMode?: "full" | "lightweight";
137137
/** Run kind hint for context mode behavior. */

src/agents/pi-embedded-runner/run/payloads.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,32 @@ describe("buildEmbeddedRunPayloads tool-error warnings", () => {
476476
});
477477
});
478478

479+
it("keeps stale full-verbose tool errors compact when live verbose is off", () => {
480+
const payloads = buildPayloads({
481+
lastToolError: { toolName: "write", error: "permission denied" },
482+
suppressToolErrorWarnings: () => false,
483+
verboseLevel: "full",
484+
});
485+
486+
expectSingleToolErrorPayload(payloads, {
487+
title: "Write",
488+
absentDetail: "permission denied",
489+
});
490+
});
491+
492+
it("preserves full-verbose tool error details with static suppression disabled", () => {
493+
const payloads = buildPayloads({
494+
lastToolError: { toolName: "write", error: "permission denied" },
495+
suppressToolErrorWarnings: false,
496+
verboseLevel: "full",
497+
});
498+
499+
expectSingleToolErrorPayload(payloads, {
500+
title: "Write",
501+
detail: "permission denied",
502+
});
503+
});
504+
479505
it("keeps non-exec mutating tool failures visible", () => {
480506
const payloads = buildPayloads({
481507
lastToolError: { toolName: "write", error: "permission denied" },

src/agents/pi-embedded-runner/run/payloads.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,26 @@ function resolveToolErrorWarningPolicy(params: {
146146
hasUserFacingErrorReply: boolean;
147147
hasUserFacingFailureAcknowledgement: boolean;
148148
suppressToolErrors: boolean;
149-
suppressToolErrorWarnings?: boolean;
149+
suppressToolErrorWarnings?: boolean | (() => boolean | undefined);
150150
isCronTrigger?: boolean;
151151
sessionKey: string;
152152
verboseLevel?: VerboseLevel;
153153
}): ToolErrorWarningPolicy {
154154
const normalizedToolName = normalizeOptionalLowercaseString(params.lastToolError.toolName) ?? "";
155-
const includeDetails = shouldIncludeToolErrorDetails(params);
156-
if (params.suppressToolErrorWarnings) {
155+
let toolErrorWarningOverride: boolean | undefined;
156+
let dynamicToolErrorWarningsDisabled = false;
157+
if (typeof params.suppressToolErrorWarnings === "function") {
158+
toolErrorWarningOverride = params.suppressToolErrorWarnings();
159+
dynamicToolErrorWarningsDisabled = toolErrorWarningOverride === false;
160+
} else {
161+
toolErrorWarningOverride = params.suppressToolErrorWarnings;
162+
}
163+
const includeDetails = shouldIncludeToolErrorDetails({
164+
...params,
165+
verboseLevel: dynamicToolErrorWarningsDisabled ? "off" : params.verboseLevel,
166+
});
167+
const suppressToolErrorWarnings = toolErrorWarningOverride === true;
168+
if (suppressToolErrorWarnings) {
157169
return { showWarning: false, includeDetails };
158170
}
159171
// sessions_send timeouts and errors are transient inter-session communication
@@ -197,7 +209,7 @@ export function buildEmbeddedRunPayloads(params: {
197209
reasoningLevel?: ReasoningLevel;
198210
thinkingLevel?: ThinkLevel;
199211
toolResultFormat?: ToolResultFormat;
200-
suppressToolErrorWarnings?: boolean;
212+
suppressToolErrorWarnings?: boolean | (() => boolean | undefined);
201213
inlineToolResultsAllowed: boolean;
202214
didSendViaMessagingTool?: boolean;
203215
messagingToolSourceReplyPayloads?: MessagingToolSourceReplyPayload[];

src/auto-reply/get-reply-options.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export type GetReplyOptions = {
7575
bootstrapContextMode?: "full" | "lightweight";
7676
/** If true, suppress tool error warning payloads for this run. */
7777
suppressToolErrorWarnings?: boolean;
78+
/** Dynamic form used when verbose progress visibility can change mid-run. */
79+
shouldSuppressToolErrorWarnings?: () => boolean | undefined;
7880
/** If true, run the model without OpenClaw tools for this turn. */
7981
disableTools?: boolean;
8082
/** If true, include the heartbeat response tool for structured heartbeat outcomes. */

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

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,142 @@ describe("createAcpReplyProjector", () => {
224224
expect(deliveries).toEqual([{ kind: "final", text: "a".repeat(70) }]);
225225
});
226226

227+
it("rechecks the dynamic tool-summary gate for each ACP event", async () => {
228+
let allowToolSummaries = false;
229+
const deliveries: Delivery[] = [];
230+
const projector = createAcpReplyProjector({
231+
cfg: createCfg(
232+
createLiveCfgOverrides({
233+
tagVisibility: {
234+
tool_call: true,
235+
},
236+
}),
237+
),
238+
shouldSendToolSummaries: false,
239+
shouldSendToolSummariesNow: () => allowToolSummaries,
240+
deliver: async (kind, payload) => {
241+
deliveries.push({ kind, text: payload.text });
242+
return true;
243+
},
244+
});
245+
246+
await projector.onEvent({
247+
type: "tool_call",
248+
tag: "tool_call",
249+
toolCallId: "tool-1",
250+
status: "in_progress",
251+
title: "Run hidden command",
252+
text: "Run hidden command",
253+
});
254+
expect(deliveries).toEqual([]);
255+
256+
allowToolSummaries = true;
257+
await projector.onEvent({
258+
type: "tool_call",
259+
tag: "tool_call",
260+
toolCallId: "tool-2",
261+
status: "in_progress",
262+
title: "Run visible command",
263+
text: "Run visible command",
264+
});
265+
266+
expectToolCallSummary(deliveries[0]);
267+
});
268+
269+
it("drops buffered final-only tool summaries when the dynamic gate turns off before flush", async () => {
270+
let allowToolSummaries = true;
271+
const deliveries: Delivery[] = [];
272+
const projector = createAcpReplyProjector({
273+
cfg: createCfg({
274+
acp: {
275+
enabled: true,
276+
stream: {
277+
deliveryMode: "final_only",
278+
tagVisibility: {
279+
available_commands_update: true,
280+
tool_call: true,
281+
},
282+
},
283+
},
284+
}),
285+
shouldSendToolSummaries: true,
286+
shouldSendToolSummariesNow: () => allowToolSummaries,
287+
deliver: async (kind, payload) => {
288+
deliveries.push({ kind, text: payload.text });
289+
return true;
290+
},
291+
});
292+
293+
await projector.onEvent({
294+
type: "status",
295+
text: "available commands updated (7)",
296+
tag: "available_commands_update",
297+
});
298+
await projector.onEvent({
299+
type: "tool_call",
300+
tag: "tool_call",
301+
toolCallId: "tool-1",
302+
status: "in_progress",
303+
title: "Run hidden command",
304+
text: "Run hidden command",
305+
});
306+
await projector.onEvent({ type: "text_delta", text: "done", tag: "agent_message_chunk" });
307+
allowToolSummaries = false;
308+
309+
await projector.onEvent({ type: "done" });
310+
311+
expect(deliveries).toEqual([{ kind: "final", text: "done" }]);
312+
});
313+
314+
it("preserves final-only text boundary when a buffered tool summary is dropped", async () => {
315+
let allowToolSummaries = true;
316+
const deliveries: Delivery[] = [];
317+
const projector = createAcpReplyProjector({
318+
cfg: createCfg({
319+
acp: {
320+
enabled: true,
321+
stream: {
322+
deliveryMode: "final_only",
323+
hiddenBoundarySeparator: "space",
324+
tagVisibility: {
325+
tool_call: true,
326+
},
327+
},
328+
},
329+
}),
330+
shouldSendToolSummaries: true,
331+
shouldSendToolSummariesNow: () => allowToolSummaries,
332+
deliver: async (kind, payload) => {
333+
deliveries.push({ kind, text: payload.text });
334+
return true;
335+
},
336+
});
337+
338+
await projector.onEvent({
339+
type: "text_delta",
340+
text: "fallback.",
341+
tag: "agent_message_chunk",
342+
});
343+
await projector.onEvent({
344+
type: "tool_call",
345+
tag: "tool_call",
346+
toolCallId: "tool-dropped-before-flush",
347+
status: "in_progress",
348+
title: "Run test",
349+
text: "Run test (in_progress)",
350+
});
351+
await projector.onEvent({
352+
type: "text_delta",
353+
text: "I don't",
354+
tag: "agent_message_chunk",
355+
});
356+
allowToolSummaries = false;
357+
358+
await projector.onEvent({ type: "done" });
359+
360+
expect(deliveries).toEqual([{ kind: "final", text: "fallback. I don't" }]);
361+
});
362+
227363
it("does not suppress identical short text across terminal turn boundaries", async () => {
228364
const { deliveries, projector } = createProjectorHarness(
229365
createLiveCfgOverrides({
@@ -703,6 +839,48 @@ describe("createAcpReplyProjector", () => {
703839
});
704840
});
705841

842+
it("preserves hidden boundary when the dynamic tool-summary gate hides a visible tool event", async () => {
843+
const deliveries: Delivery[] = [];
844+
const projector = createAcpReplyProjector({
845+
cfg: createCfg(
846+
createHiddenBoundaryCfg({
847+
tagVisibility: {
848+
tool_call: true,
849+
},
850+
}),
851+
),
852+
shouldSendToolSummaries: false,
853+
shouldSendToolSummariesNow: () => false,
854+
deliver: async (kind, payload) => {
855+
deliveries.push({ kind, text: payload.text });
856+
return true;
857+
},
858+
});
859+
860+
await projector.onEvent({
861+
type: "text_delta",
862+
text: "fallback.",
863+
tag: "agent_message_chunk",
864+
});
865+
await projector.onEvent({
866+
type: "tool_call",
867+
tag: "tool_call",
868+
toolCallId: "call_dynamic_hidden",
869+
status: "in_progress",
870+
title: "Run test",
871+
text: "Run test (in_progress)",
872+
});
873+
await projector.onEvent({
874+
type: "text_delta",
875+
text: "I don't",
876+
tag: "agent_message_chunk",
877+
});
878+
await projector.flush(true);
879+
880+
expect(combinedBlockText(deliveries)).toBe("fallback. I don't");
881+
expect(deliveries.some((delivery) => delivery.kind === "tool")).toBe(false);
882+
});
883+
706884
it("preserves hidden boundary across nonterminal hidden tool updates", async () => {
707885
await runHiddenBoundaryCase({
708886
cfgOverrides: createHiddenBoundaryCfg({

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export type AcpReplyProjector = {
168168
export function createAcpReplyProjector(params: {
169169
cfg: OpenClawConfig;
170170
shouldSendToolSummaries: boolean;
171+
shouldSendToolSummariesNow?: () => boolean;
171172
deliver: (
172173
kind: ReplyDispatchKind,
173174
payload: ReplyPayload,
@@ -209,6 +210,9 @@ export function createAcpReplyProjector(params: {
209210
const pendingToolDeliveries: BufferedToolDelivery[] = [];
210211
const toolLifecycleById = new Map<string, ToolLifecycleState>();
211212

213+
const shouldSendToolSummaries = () =>
214+
params.shouldSendToolSummariesNow?.() ?? params.shouldSendToolSummaries;
215+
212216
const clearLiveIdleTimer = () => {
213217
if (!liveIdleTimer) {
214218
return;
@@ -282,6 +286,10 @@ export function createAcpReplyProjector(params: {
282286
if (!(settings.deliveryMode === "final_only" && force)) {
283287
return;
284288
}
289+
if (!shouldSendToolSummaries()) {
290+
pendingToolDeliveries.length = 0;
291+
return;
292+
}
285293
for (const entry of pendingToolDeliveries.splice(0)) {
286294
await params.deliver("tool", entry.payload, entry.meta);
287295
}
@@ -310,7 +318,7 @@ export function createAcpReplyProjector(params: {
310318
meta?: AcpProjectedDeliveryMeta,
311319
opts?: { dedupe?: boolean },
312320
) => {
313-
if (!params.shouldSendToolSummaries) {
321+
if (!shouldSendToolSummaries()) {
314322
return;
315323
}
316324
const bounded = truncateText(text.trim(), settings.maxSessionUpdateChars);
@@ -335,8 +343,18 @@ export function createAcpReplyProjector(params: {
335343
lastStatusHash = hash;
336344
};
337345

346+
const markHiddenToolBoundary = (event: Extract<AcpRuntimeEvent, { type: "tool_call" }>) => {
347+
if (!event.tag || !HIDDEN_BOUNDARY_TAGS.has(event.tag)) {
348+
return;
349+
}
350+
const status = normalizeToolStatus(event.status);
351+
const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false;
352+
pendingHiddenBoundary = pendingHiddenBoundary || event.tag === "tool_call" || isTerminal;
353+
};
354+
338355
const emitToolSummary = async (event: Extract<AcpRuntimeEvent, { type: "tool_call" }>) => {
339-
if (!params.shouldSendToolSummaries) {
356+
if (!shouldSendToolSummaries()) {
357+
markHiddenToolBoundary(event);
340358
return;
341359
}
342360
if (!isAcpTagVisible(settings, event.tag)) {
@@ -390,6 +408,7 @@ export function createAcpReplyProjector(params: {
390408
payload: { text: toolSummary },
391409
meta: deliveryMeta,
392410
});
411+
markHiddenToolBoundary(event);
393412
} else {
394413
await flush(true);
395414
await params.deliver("tool", { text: toolSummary }, deliveryMeta);
@@ -486,11 +505,7 @@ export function createAcpReplyProjector(params: {
486505

487506
if (event.type === "tool_call") {
488507
if (!isAcpTagVisible(settings, event.tag)) {
489-
if (event.tag && HIDDEN_BOUNDARY_TAGS.has(event.tag)) {
490-
const status = normalizeToolStatus(event.status);
491-
const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false;
492-
pendingHiddenBoundary = pendingHiddenBoundary || event.tag === "tool_call" || isTerminal;
493-
}
508+
markHiddenToolBoundary(event);
494509
return;
495510
}
496511
await emitToolSummary(event);

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1903,7 +1903,9 @@ export async function runAgentTurnWithFallback(params: {
19031903
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
19041904
})(),
19051905
toolProgressDetail: params.toolProgressDetail,
1906-
suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings,
1906+
suppressToolErrorWarnings:
1907+
params.opts?.shouldSuppressToolErrorWarnings ??
1908+
params.opts?.suppressToolErrorWarnings,
19071909
disableTools: params.opts?.disableTools,
19081910
enableHeartbeatTool: params.opts?.enableHeartbeatTool,
19091911
forceHeartbeatTool: params.opts?.forceHeartbeatTool,

0 commit comments

Comments
 (0)