Operating system: Windows
Orca version: 1.4.143
Short summary
Since #8862 (2026-07-15), saving a .md file in the rich markdown editor throws an uncaught Failed to determine byte offset and the save is silently dropped — no error reaches the UI. It only triggers on documents containing multi-byte UTF-8 characters, and whether a given keystroke triggers it is essentially a coin flip on the byte alignment of the edit position. So for Korean/Japanese/Chinese users, saving works sometimes, which reads as flaky behavior rather than a bug.
What happened?
Editing a Korean .md file and pressing Ctrl+S sometimes writes the file and sometimes does nothing at all — no error, no toast, no dirty-state change. Reduced to a deterministic case on one real file:
Line under edit (an H2 heading):
## 터미널 1: 텔레그램 (터미널 1로 통합 실행)
- Append one
- → Ctrl+S → saves
- Append a second
- → Ctrl+S → silently does not save
Same file, same line, one byte apart. main.trace.ndjson shows the corresponding uncaught error each time a save is dropped:
Uncaught Error: Failed to determine byte offset
at advanceTo
at adjustIndiciesToUcs2
at apply
at reconcileSerializedMarkdown
Root cause
reconcileSerializedMarkdown() calls @sanity/diff-match-patch like this:
const patches = make(baseLf, diffs); // makePatches
const [reconciledLf, results] = apply(patches, originalSourceLf); // applyPatches
That is a documented, type-correct use of the public API — makePatches returns Patch[] and applyPatches accepts Patch[].
But makePatches populates two coordinate systems on each patch:
start1 = 114 // UCS-2 code unit offset
utf8Start1 = 210 // UTF-8 byte offset
and applyPatches → adjustIndiciesToUcs2(patches, text) reads patch.start1 / patch.start2 and treats them as UTF-8 byte offsets, walking the base string by utf8len(codePoint) until it lands exactly on the target:
if (!options.allowExceedingIndices && byteOffset !== target)
throw new Error("Failed to determine byte offset");
For ASCII, 1 char = 1 byte = 1 code unit, so start1 === utf8Start1 and the misread is a harmless identity — this can never throw. For Korean (3 bytes/char) the two diverge (114 vs 210), and the walk can only stop on real character boundaries. If the misread offset happens to coincide with a boundary the call survives; otherwise it overshoots and throws.
That is exactly the one-dash/two-dash split above:
one dash : start1=113 → is a byte boundary in the base → saves
two dashes: start1=114 → is not a byte boundary → throws
The throw escapes #8862's safety net
The reconcile has a fallback for a failed patch:
const [reconciledLf, results] = apply(patches, originalSourceLf);
if (results.some((applied) => !applied)) {
return restoreEol(editedLf, eol); // never reached
}
but apply() calls adjustIndiciesToUcs2 on its first statement, before results exists. There is no try/catch, so a thrown patch is not a failed patch and the whole save unwinds instead of falling back. #8862 states:
On any mismatch, failed patch, oversize input, or no-op → fall back to today's canonical output. So it can never corrupt or relocate content — worst case is exactly today's behavior.
The invariant holds for content correctness, but not for liveness: the save never happens at all.
It then disappears completely, because handleSaveForFile swallows the rejection:
try {
await requestEditorFileSave({ fileId: saveTargetFile.id, fallbackContent: content });
} catch {
}
How can we reproduce it?
Standalone, no Orca needed (@sanity/diff-match-patch@3.2.0):
import { makePatches, applyPatches } from '@sanity/diff-match-patch'
const base = '## 터미널 1: 텔레그램 (터미널 1로 통합 실행)-\n'
const edited = '## 터미널 1: 텔레그램 (터미널 1로 통합 실행)--\n'
applyPatches(makePatches(base, edited), base)
// => Error: Failed to determine byte offset
// identical edit, ASCII document:
const enBase = '## Terminal 1: Telegram (unified run)-\n'
const enEdited = '## Terminal 1: Telegram (unified run)--\n'
applyPatches(makePatches(enBase, enEdited), enBase)
// => fine, always
In the app:
- Open any
.md containing Korean/Japanese/Chinese text in the rich editor (the default for .md).
- Type single characters at the end of a line, pressing Ctrl+S after each.
- Some keystrokes save; others silently do not. The error appears only in
main.trace.ndjson.
Suggested fix
Three options, all verified locally against the repro above:
// 1. round-trip through the serialized patch format, whose offsets are UTF-8
// (the coordinate system applyPatches actually expects)
apply(parsePatch(stringifyPatches(patches)), originalSourceLf) // works
// 2. disable the exactness assertion
apply(patches, originalSourceLf, { allowExceedingIndices: true }) // works
// 3. make the existing fallback reachable
let reconciledLf, results;
try {
[reconciledLf, results] = apply(patches, originalSourceLf);
} catch {
return restoreEol(editedLf, eol); // same path as a failed patch
}
Both (1) and (2) produce the correct reconciled output for the Korean case. (3) is the smallest safe change and routes through already-reviewed behavior; (1) is the most correct if the reconcile should keep working for CJK rather than degrading to canonical output on every CJK edit.
This also looks like an upstream footgun in @sanity/diff-match-patch — applyPatches(makePatches(a, b), a) type-checks and is the obvious usage, but is only correct for ASCII. I could not find an existing issue on either side.
Anything else that might help
- The empty
catch {} in handleSaveForFile turns every save rejection into silence. Even with this bug fixed, a failed write deserves surfacing — it took reading main.trace.ndjson to discover the file simply was not being written.
- Reconcile tests presumably all pass today because ASCII makes the offset misread an accidental identity. A CJK fixture would have caught this immediately.
- Severity reads higher than a typical editor bug: from the user's side this is silent data loss. The editor accepts the edit, Ctrl+S gives no feedback, the file never changes, and closing the tab loses the work with no warning.
Operating system: Windows
Orca version: 1.4.143
Short summary
Since #8862 (2026-07-15), saving a
.mdfile in the rich markdown editor throws an uncaughtFailed to determine byte offsetand the save is silently dropped — no error reaches the UI. It only triggers on documents containing multi-byte UTF-8 characters, and whether a given keystroke triggers it is essentially a coin flip on the byte alignment of the edit position. So for Korean/Japanese/Chinese users, saving works sometimes, which reads as flaky behavior rather than a bug.What happened?
Editing a Korean
.mdfile and pressing Ctrl+S sometimes writes the file and sometimes does nothing at all — no error, no toast, no dirty-state change. Reduced to a deterministic case on one real file:Line under edit (an H2 heading):
-→ Ctrl+S → saves-→ Ctrl+S → silently does not saveSame file, same line, one byte apart.
main.trace.ndjsonshows the corresponding uncaught error each time a save is dropped:Root cause
reconcileSerializedMarkdown()calls@sanity/diff-match-patchlike this:That is a documented, type-correct use of the public API —
makePatchesreturnsPatch[]andapplyPatchesacceptsPatch[].But
makePatchespopulates two coordinate systems on each patch:and
applyPatches→adjustIndiciesToUcs2(patches, text)readspatch.start1/patch.start2and treats them as UTF-8 byte offsets, walking the base string byutf8len(codePoint)until it lands exactly on the target:For ASCII, 1 char = 1 byte = 1 code unit, so
start1 === utf8Start1and the misread is a harmless identity — this can never throw. For Korean (3 bytes/char) the two diverge (114 vs 210), and the walk can only stop on real character boundaries. If the misread offset happens to coincide with a boundary the call survives; otherwise it overshoots and throws.That is exactly the one-dash/two-dash split above:
The throw escapes #8862's safety net
The reconcile has a fallback for a failed patch:
but
apply()callsadjustIndiciesToUcs2on its first statement, beforeresultsexists. There is notry/catch, so a thrown patch is not a failed patch and the whole save unwinds instead of falling back. #8862 states:The invariant holds for content correctness, but not for liveness: the save never happens at all.
It then disappears completely, because
handleSaveForFileswallows the rejection:How can we reproduce it?
Standalone, no Orca needed (
@sanity/diff-match-patch@3.2.0):In the app:
.mdcontaining Korean/Japanese/Chinese text in the rich editor (the default for.md).main.trace.ndjson.Suggested fix
Three options, all verified locally against the repro above:
Both (1) and (2) produce the correct reconciled output for the Korean case. (3) is the smallest safe change and routes through already-reviewed behavior; (1) is the most correct if the reconcile should keep working for CJK rather than degrading to canonical output on every CJK edit.
This also looks like an upstream footgun in
@sanity/diff-match-patch—applyPatches(makePatches(a, b), a)type-checks and is the obvious usage, but is only correct for ASCII. I could not find an existing issue on either side.Anything else that might help
catch {}inhandleSaveForFileturns every save rejection into silence. Even with this bug fixed, a failed write deserves surfacing — it took readingmain.trace.ndjsonto discover the file simply was not being written.