resolveChannelStreamingPreviewToolProgress guessed a mode when streaming.mode
was unset, so it could never be right for every channel: guessing "partial"
dropped Discord and Telegram's explicit progress.toolProgress opt-out, and my
earlier switch to "progress" broke it the other way for Slack, Mattermost, and
Teams, whose real default is partial. With both keys configured and disagreeing,
the wrong one won.
The helper has no channel identity, so it stops guessing and takes the mode the
caller already resolved. All nine call sites pass theirs. Omitting it keeps the
previous configured-mode reading, so untouched callers behave exactly as before.
Reported by ClawSweeper on #116143.
The label was smuggled into the rolling line array as a `{draftLabel}`
sentinel so it would participate in the maxLines window, then special-cased
in three places plus an index lookup to pull it back out for its own block.
A label is not a line; it is a block that yields its slot once real work
fills the window. Saying that directly removes the sentinel, every
`isLabelLine` check, and the double map over the line list, while keeping the
documented scroll-away behavior and the plan/tool budget split.
Adds the missing boundary test for a checklist that consumes the whole line
budget: `slice(-0)` returns every element, so that case needs the explicit
empty-window branch rather than falling out of the arithmetic.
A status headline replaced the rolling tool lines instead of sitting above
them, so a default Discord draft showed one preamble sentence for an entire
tool-heavy turn. Operators reached for `/verbose` to see any activity, which
delivers durable per-tool-call messages and floods the channel.
- Render the headline above the lines; both stay visible in one message.
- Shorten the start gate from 5s to 1.5s. The gate only creates the draft
when the timer fires and finalize cancels it, so quick answers still post
no draft while a 3s tool turn stops being silent.
- Drop Discord's label-gated tool-progress default so
resolveChannelStreamingPreviewToolProgress is the single owner. An explicit
`toolProgress: false` still silences the lines.
- Resolve that toggle against a "progress" mode guess when `streaming.mode`
is unset, so the progress-draft channels stop ignoring an explicit
`progress.toolProgress` opt-out.
Telegram now defaults to `streaming.mode: "progress"` like Discord; set
`"partial"` to keep streamed answer text. Its renderer draws work lines from
the compositor's structured lines, so `rendersRollingLinesNatively` keeps
them out of the composed text rather than printing every line twice.
* fix(auto-reply): deliver ingress-retried messages after their queued run is dropped
* fix(auto-reply): key queued dedupe release by adoption lifecycle
Overflow-summary compaction clones a queued run onto a new object, so a
release keyed by the run object missed runs completed via their clone and
the ingress retry stayed suppressed on the summarize drop policy. The
adoption lifecycle reference survives cloning and already keys admission
state, so it is the identity the release must use.
* fix(auto-reply): guard queued dedupe ownership
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): throw CompactionError when summarization fails
Transform compaction summarization from silent infinite failure loops
into clear, actionable errors.
Previously, when all summarization attempts failed, summarizeWithFallbackResult
returned a generic fallback string blaming "size limits". This caused:
- Misleading error messages (auth/network errors reported as "size limits")
- Callers unable to distinguish real summaries from failures
- Silent infinite retry loops where token counts climbed indefinitely
Now, CompactionError is thrown with the original error details preserved.
Callers immediately know compaction failed and can take appropriate action
(switch model, notify user, etc.).
Fixes#115413
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fixup: export CompactionError from agent-core index, remove dead code
* fixup: address ClawSweeper review - keep CompactionError internal, track last error, add failure-proof test
* fixup: type corrections in failure-proof test
* fixup: type annotate failure-proof messages as UserMessage[]
* fixup: avoid possibly-undefined array access in failure-proof test
* test(agents): prove compaction provider failure cancellation
---------
Co-authored-by: nebulacoder-v8.0 <noreply@zte.com.cn>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): deny all tools when scheduled authority names a removed account
resolveGroupToolPolicy returned { allow: [] } as its fail-closed sentinel
when a scheduled run named a channel account that is no longer configured.
The runtime matcher treats an empty allowlist as "allow everything not
denied", so revoking the creator account widened the scheduled tool surface
to the full set including exec and apply_patch instead of denying it.
The sentinel now carries an explicit wildcard deny, which the matcher and
the plugin-harness native-tool clamp both honor.
* test(agents): cover scheduled-run tool denial after owner account removal
Adds capability-profile level coverage that a scheduled run whose named
owner account was removed resolves to a deny-all group policy, exercising
the scheduled-authority handoff from resolveConversationCapabilityProfile
through resolveRequesterToolPolicies into resolveGroupToolPolicy.
* fix(gateway): enforce scheduled account authority for loopback tools
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cron): bound restart catch-up to the active schedule
Editing a recurring job's schedule made the gateway fire it immediately on
the next restart. Startup catch-up compares the new schedule's previous slot
against lastRunAtMs, which still belongs to the retired schedule, so a slot
that never existed under the old schedule counted as missed.
Record when scheduling inputs take effect and replay a missed slot only when
it is newer than that. Jobs whose schedule never changed carry no stamp and
keep replaying every computed slot, so catch-up is unchanged for them.
The missed-slot predicate was duplicated in the runnable check and the
backoff-deferral pass; both now share one helper so the bound cannot drift.
Fixes#91944
* fix(cron): protect schedule activation ownership
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): apply_patch rewrites bytes on hunk context lines
A fuzzy apply_patch update replaced the entire matched span with the
model-authored patch text, so trailing whitespace, typographic punctuation,
and tab indentation on lines the hunk marked as context were overwritten
while the tool reported plain success.
The parser now records which emitted lines came in as context and which old
line each one came from, and the update applier keeps the file's own bytes
for those lines. Added and removed lines are still written from the patch.
* test(agents): cover apply_patch context preservation
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
The standalone debug proxy wrote raw request and response headers into
capture rows while the patched-fetch runtime redacted at the parallel
call sites, so a capture taken through the proxy could persist
Authorization, Cookie, and API-key values to disk.
Move the runtime's redaction policy into a leaf module both writers
import instead of adding a second copy, so the two capture paths cannot
drift. The shared helper also flattens node's array-valued headers,
which the standalone proxy passes in directly, and keeps value-level
registered-secret redaction for header names that are not themselves
sensitive.
Reported by SebTardif in #90009; supersedes #82951, which redacted only
by header name and predates the proxy-server rewrite.
doctor --fix copies allowFrom into groupAllowFrom for any channel
whose capabilities allow the fallback, guarded by a check against
the compile-time generated channel schema metadata. That metadata
only covers core-compiled channels; extension-installed channels
like agentmail have no entry there. The guard treated a missing
schema as "no restriction" and wrote groupAllowFrom anyway, which
agentmail's own runtime schema then rejected, aborting the whole
doctor --fix batch (including unrelated pending migrations).
Fail closed instead: without schema info, don't write the field.
Fixes#116024
Doctor's dueling-scope repair removed the user-scope gateway unit on file
presence alone. A system unit file that is merely staged, disabled, failed, or
uncheckable would then cost the operator their only working gateway, and the
default confirm value is true so non-interactive repair amplifies it.
Gate the removal on isSystemUnitActiveOrEnabled: the system unit must be the
live or boot-configured supervisor. Otherwise doctor explains the ambiguity and
prints both fix directions instead of deleting anything. An erroring probe
fails closed for the same reason.
Applies the ClawSweeper rank-up move on #91221.
Address review: a marker-owned system unit with a *different* name (e.g. an
intentional rescue-bot gateway on the same host) must not be classified as a
duplicate of the canonical user unit, or doctor could remove a legitimate user
gateway. Dueling now requires user.unitName === system.unitName; the direct
canonical system path still matches the real #79375 case. Adds a regression
test for "user canonical + custom marker-owned system" -> kind:"user".
After upgrading on Linux, a user-scope unit
(~/.config/systemd/user/openclaw-gateway.service) and a system-scope unit
(/etc/systemd/system/openclaw-gateway.service) can both exist and both try to
manage the gateway. They bind the same port and each instance's stale-process
detection SIGTERMs the other, producing an endless restart cascade (#79375).
The detector `findInstalledSystemdGatewayScope` checked the user path first and
returned early, so it could never surface that both scopes coexist.
This adds, in three layers:
1. Detection: `findSystemdGatewayInstallation` — a discriminated union
(none/user/system/dueling) that reports every installed scope without
early-returning. `findInstalledSystemdGatewayScope` is refactored to delegate
to it while preserving the exact user-first preference its four lifecycle
callers rely on (no behavior change for stop/restart/is-enabled/runtime).
Adds `uninstallUserSystemdGatewayUnit` (removes only the $HOME user unit, no
root needed) and the pure `formatDuelingScopesWarning` helper.
2. Doctor: `maybeResolveDuelingSystemdGatewayScopes` detects the dueling state
and, after the existing confirm/policy gate, removes the redundant user-scope
unit while keeping the root-installed system unit authoritative. Declining or
an externally-managed policy falls back to the existing cleanup hints.
3. Startup guard: in service mode, when a stale-kill actually happened, log a
targeted remediation pointing at `openclaw doctor --fix` instead of letting
the loop look like routine stale cleanup. Diagnostic only — the kill decision
is unchanged.
The health monitor lets a pending-restart continuation skip the cooldown so a
timed-out recovery stop can finish on the next pass. That continuation was
never recorded, so an account stuck in restartPending with reconnectAttempts 0
restarted on every check forever, bypassing maxRestartsPerHour entirely: stop
and start thrash the channel manager with log spam and no give-up state.
Only the first continuation per pending episode is now free. Later passes
rejoin cooldown and the hourly budget, and the free pass re-arms only after
the account genuinely leaves restartPending (running again or the pending flag
dropped) so a transient reconnectAttempts bump cannot mint new free restarts.
* fix(gateway): evict idle agent terminal sessions under pool pressure
* fix(gateway): claim eviction victims and kill only after replacement spawn
* fix(gateway): revalidate eviction victims after the replacement spawn
* fix(gateway): count in-flight reservations when committing eviction
* fix(gateway): reselect eviction victim at commit time
* fix(gateway): release eviction claims on cancelled opens
* fix(agents): edit tool rewrites line endings on lines it did not touch
The edit tool detected one line ending from the file's first newline,
normalized the whole file to LF for matching, then re-applied that single
ending to every line on write. A one-line edit therefore rewrote the
terminator of every untouched line, and deleted lone carriage returns
that were data rather than line breaks. The diff and unified patch
returned to the model are computed on the normalized text, so the damage
never surfaces in the tool result or the TUI preview.
Rebuild the written text from the original terminators instead. A bare
carriage return is tracked as its own terminator, so lines the edits did
not touch keep their exact bytes and a rewritten line is written back
with the terminator bytes it had. Newly written lines take the
terminator of the original line they replaced. Matching still runs in LF
space, so LF oldText against a CRLF file matches as before.
* fix(agents): preserve CR fallback for leading edit insertions
* fix(agents): align replacement line-ending boundaries
* fix(agents): preserve edit line-ending provenance
* refactor(agents): isolate edit replacement reconstruction
* test(agents): cover edits through production line endings
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
The claude-cli terminal result envelope carries only the final message text.
On the default draft-preview path (no block streaming, no commentary
classification) the preview streamed every text block, then final delivery
edited it down to just the post-tool closer, silently erasing pre-tool
answer text (#106760).
Both the incremental parser and the transcript reparse now keep the
streamed accumulation when it is connected to the result through tool
splits: boundaries join with a paragraph break, non-tool message
boundaries restart preservation candidacy (superseded drafts still defer
to the envelope), interim results commit their segment without breaking
cumulative delta snapshots, and transcript reparses continue past interim
results like the incremental parser.