* fix(edit): preserve bytes outside fuzzy-match span on matched lines
When an edit tool call falls back to fuzzy matching (e.g. plain space vs
NBSP, straight quotes vs smart quotes), the entire overlapped line was
rebuilt from the NFKC-normalized copy, silently destroying unrelated
bytes on that line — fullwidth punctuation, halfwidth katakana, em-dashes,
and other Unicode characters outside the edited span.
Root cause:
- applyEdits() set replacementBaseContent to
normalizeForFuzzyMatch(normalizedContent) when any edit needed fuzzy
matching.
- applyReplacementsPreservingUnchangedLines then emitted untouched lines
from the original content but rebuilt touched lines wholesale from the
normalized base, applying NFKC folding to bytes the edit never
referenced.
Fix:
- normalizeForFuzzyMatch now optionally returns an offset map that
translates every normalized position back to the original text.
- fuzzyFindText uses this map to translate match indices from fuzzy-
normalized coordinates to original-content coordinates.
- applyEdits keeps replacementBaseContent as the original (LF-normalized)
content and uses direct applyReplacements instead of the line-preserving
variant. Touch-untouched line distinction is no longer needed because
replacements splice at byte granularity in the original content.
Fixes#116459
* fix(edit): map fuzzy spans to stable source boundaries
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(state): converge deterministic legacy runtime-state conflicts instead of wedging startup
Four legacy runtime-state sources still use the pre-4da0eb19c570 pattern when
their JSON conflicts with canonical shared SQLite state: they push a startup
warning and leave the file in place. The conflict is deterministic — every
restart re-reads the same file against the same rows — so the warning feeds
startupMigrationWarnings and the readiness gate refuses to report the gateway
ready on every boot, and "openclaw doctor --fix" runs the identical code path
and cannot clear it. That is the same non-converging failure class #112395
reports for the state-dir source and 4da0eb19c5 already removed for
update-check state.
Mirror the merged update-check precedent at all four sites — voice wake
triggers, voice wake routing, plugin binding approvals, and
current-conversation bindings: on deterministic conflict, keep SQLite
canonical, emit a non-blocking notice, and archive the legacy file (the
archive preserves the conflicting payload for operator inspection; nothing is
deleted). Conflicting legacy entries are never imported over existing rows —
unchanged from before. Read failures and migration failures keep their
blocking warnings; the fail-closed boundary for non-deterministic problems
does not move.
The notices flow through the existing collectNotices plumbing that
update-check already uses; the three affected steps now opt in.
Tests: per site, a conflict case asserts no warnings, the exact notice, SQLite
values preserved, and the legacy file archived; malformed-file cases assert
the blocking warning remains. All new cases fail against the previous
behavior.
Refs #112395; complementary to #114678 (state-dir source, same failure class).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(state): keep legacy imports retryable
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>