Compaction start/completion notices carry isCompactionNotice: true on
the ReplyPayload. Guard maybeApplyTtsToPayload() with an early return
so these informational UI signals are never synthesised as speech,
regardless of TTS mode or auto-mode configuration.
Addresses review feedback from jalehman on PR #38805.
Compaction notices carried replyToCurrent=true, which caused them to
pass through the allowExplicitReplyTagsWhenOff path in
createReplyToModeFilter("off") and retain replyToId. In
replyToMode=off sessions this made the transient status messages
appear in-thread while normal assistant replies stayed off-thread,
contradicting the off-mode expectation.
Add an !isCompactionNotice guard to the explicit-tag fast-path so
compaction payloads always fall through to the strip branch and have
their replyToId removed — consistent with how every other payload is
treated in off mode.
In replyToMode=first, the hasThreaded flag was set by the first assistant
chunk, causing the completion notice (emitted after flush) to hit the
`if (hasThreaded)` branch and have its replyToId stripped — making it
an unthreaded top-level message.
Fix: add an isCompactionNotice exemption inside the `hasThreaded` branch
so that compaction notices (both start and completion) always retain their
replyToId regardless of hasThreaded state, while non-notice payloads
continue to behave as before.
Addresses review comment https://github.com/openclaw/openclaw/pull/38805#discussion_r2901465625
Compaction start/end notices are transient status messages that should
be threaded (appear in-context) but must not advance the hasThreaded
flag inside createReplyToModeFilter when mode=first.
Before this fix, the compaction start notice was the "first" threaded
message, so all real assistant reply chunks that followed had replyToId
stripped and were sent as unthreaded top-level messages.
Fix: skip advancing hasThreaded when payload.isCompactionNotice is true.
The notice still receives replyToId (so it appears in the thread), but
the filter's stateful "first" slot is preserved for the actual assistant
reply that follows.
onAgentEvent is fired fire-and-forget (void ctx.params.onAgentEvent?.(...)
in pi-embedded-subscribe.handlers.compaction.ts), so any rejection from the
awaited onBlockReply call would escape unobserved.
Wrap the delivery in a try/catch that swallows the error and logs a warning
via params.logger, consistent with other non-critical notice delivery paths.
Add isCompactionNotice flag to ReplyPayload and set it on both the
compaction start notice (agent-runner-execution.ts) and the completion
notice (agent-runner.ts). dispatch-from-config.ts skips accumulation
into accumulatedBlockText when the flag is set, so compaction status
lines (🧹 / ✅) are never synthesised into the fallback TTS audio for
block-streaming runs with tts.mode=final.
Previously the start notice was routed through blockReplyHandler which
enqueues into blockReplyPipeline, setting didStream() = true. This
caused buildReplyPayloads to drop all final payloads (shouldDropFinalPayloads
path), discarding the real assistant reply on non-streaming model paths
where assistantTexts is populated from the final message (not block chunks).
Fix: send the start notice directly via opts.onBlockReply, bypassing the
pipeline entirely. applyReplyToMode is still applied so replyToId threading
(replyToMode=all|first) is honoured. This mirrors how the completion notice
in agent-runner.ts avoids the pipeline after flush()/stop().
P2-1 (agent-runner.ts): Restrict direct completion notice to
block-streaming runs. The condition now checks blockStreamingEnabled
in addition to opts?.onBlockReply, preventing duplicate completion
notices in non-streaming sessions where verboseNotices already handles
the compaction-complete text.
P2-2 (agent-runner-execution.ts): Emit compaction start notice when
streaming is off. blockReplyHandler is a no-op for non-streaming runs,
so add a direct fallback path: when blockStreamingEnabled is false and
opts.onBlockReply is present, send the start notice directly with
applyReplyToMode threading applied.
Enqueueing the completion notice into blockReplyPipeline before flush
caused didStream() to return true even when no assistant content was
streamed. buildReplyPayloads drops all finalPayloads when didStream()
is true, so the real assistant reply could be silently discarded on
non-streaming model paths (e.g. pi-embedded-subscribe) that fill
assistantTexts without emitting block replies.
Fix: move the completion notice send to *after* pipeline flush+stop,
using a fire-and-forget Promise.race with blockReplyTimeoutMs. This
keeps the timeout guarantee (satisfying the previous P1) while not
touching didStream() at all.
Non-streaming fallback (verboseNotices) is unchanged.
Addresses P1 review comment on PR #38805.
Previously the completion notice bypassed the block-reply pipeline by
calling opts.onBlockReply directly after the pipeline had already been
flushed and stopped. This meant timeout/abort handling and serial
delivery guarantees did not apply to the notice, risking stalls or
out-of-order delivery in streaming/routed runs.
Fix: enqueue the completion notice into blockReplyPipeline *before*
flush so it is delivered through the same path as every other block
reply. The non-streaming fallback (verboseNotices) is preserved for
runs where no pipeline exists.
Also removes the now-unnecessary direct opts.onBlockReply call and
cleans up the redundant suffix in the pre-flush path (count suffix is
still included in the verboseNotices fallback path where count is
available).
Addresses P1 review comment on PR #38805.
Compaction start and completion notices were sent via raw
opts.onBlockReply, bypassing createBlockReplyDeliveryHandler and the
applyReplyToMode pipeline. In channels configured with
replyToMode=all|first, this caused compaction notices to be delivered
as unthreaded top-level messages while all other replies stayed
threaded — inconsistent and noisy.
Fix agent-runner-execution.ts: extract createBlockReplyDeliveryHandler
result into blockReplyHandler and share it between onBlockReply and the
compaction start notice in onAgentEvent. Both now use the same handler.
Fix agent-runner.ts: inject currentMessageId + replyToCurrent into the
completion notice payload before passing through applyReplyToMode, so
threading directives are honoured consistently with normal replies.
Closes the P2 review comment on PR #38805 (agent-runner.ts:701).
In block-streaming mode, the reply pipeline bypasses buildReplyPayloads,
so notices only pushed to verboseNotices were never delivered to the user.
The start notice ("🧹 Compacting context...") was already sent via
opts.onBlockReply directly in agent-runner-execution.ts; mirror the same
path for the completion notice.
- If opts.onBlockReply is present (streaming mode): await onBlockReply
with the completion text directly, so it reaches the user immediately.
- Otherwise (non-streaming): push to verboseNotices as before so it gets
prepended to the final payload batch.
Also consolidate the verbose vs. non-verbose text selection into a single
completionText variable, removing the redundant pop/push pattern.
During auto-compaction the agent goes silent for several seconds while
the context is summarised. Users on every channel (Discord, Feishu,
Telegram, webchat …) had no indication that something was happening —
leading to confusion and duplicate messages.
Changes:
- agent-runner-execution.ts: listen for compaction phase='start' event
and immediately deliver a "🧹 Compacting context..." notice via the
existing onBlockReply callback. This fires for every channel because
onBlockReply is the universal in-run delivery path.
- agent-runner.ts: make the completion notice unconditional (was
previously guarded behind verboseEnabled). Non-verbose users now see
"✅ Context compacted (count N)."; verbose users continue to see the
legacy "🧹 Auto-compaction complete (count N)." wording.
Why onBlockReply for start?
onBlockReply is already wired to every channel adapter and fires during
the live run, so the notice arrives in-band with zero new plumbing.
Using verboseNotices (appended after the run) would be too late and
would miss the start signal entirely.
Fixes: users seeing silent pauses of 5-15 s with no feedback during
compaction on any channel.
- Added a test to ensure no warnings for legacy Brave config when bundled web search allowlist compatibility is applied.
- Updated validation logic to incorporate compatibility configuration for bundled web search plugins.
- Refactored the ensureRegistry function to utilize the new compatibility handling.
* test: align extension runtime mocks with plugin-sdk
Update stale extension tests to mock the plugin-sdk runtime barrels that production code now imports, and harden the Signal tool-result harness around system-event assertions so the channels lane matches current extension boundaries.
Regeneration-Prompt: |
Verify the failing channels-lane tests against current origin/main in an isolated worktree before changing anything. If the failures reproduce on main, keep the fix test-only unless production behavior is clearly wrong. Recent extension refactors moved Telegram, WhatsApp, and Signal code onto plugin-sdk runtime barrels, so update stale tests that still mock old core module paths to intercept the seams production code now uses. For Signal reaction notifications, avoid brittle assertions that depend on shared queued system-event state when a direct harness spy on enqueue behavior is sufficient. Preserve scope: only touch the failing tests and their local harness, then rerun the reproduced targeted tests plus the full channels lane and repo check gate.
* test: fix extension test drift on main
* fix: lazy-load bundled web search plugin registry
* test: make matrix sweeper failure injection portable
* fix: split heavy matrix runtime-api seams
* fix: simplify bundled web search id lookup
* test: tolerate windows env key casing
Reuse pi-ai's Anthropic client injection seam for streaming, and add
the OpenClaw-side provider discovery, auth, model catalog, and tests
needed to expose anthropic-vertex cleanly.
Signed-off-by: sallyom <somalley@redhat.com>