* 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.
* feat(cli): run agent exec against the ambient config, composed in memory
Exec previously ignored the operator's config entirely, so a one-shot turn
could not reach configured providers, credentials, or agentRuntime harness
selection. It now layers config the way other folder-scoped coding CLIs do.
The composed config is published as this process's runtime snapshot rather
than serialized to a temp file and re-read through OPENCLAW_CONFIG_PATH. The
snapshot is the only in-process config cache, so the file only ever fed it --
while writing env-substituted provider keys to disk where the run's own exec
tool could read them.
* fix(cli): resolve exec stored credentials from the configured agent dir
* chore(scripts): allow agent exec the file-scoped config loader at its process boundary
* test(cli): cover the exec credential default and pinned-config flags
* fix: diagnose prepared model owner config drift
* fix(gateway): bind replies to published model owner
* fix(gateway): bind durable replay to published owner
* test: preserve requested workspace in reply mock