- Ownership is applied before permissions: chown clears setuid/setgid,
so saving a 06755 file left it 0755.
- The umask is read once during static initialisation. Reading it at
save time needed umask(0)/umask(old), briefly changing it for the swap
writer thread creating files.
- Before rewriting a hard-linked file in place, the new content is
written and synced to a copy beside it; if the in-place write fails
(ENOSPC, EIO) the copy is kept and named in the error.
- Journal headers also record a CRC-32 of the base file (flags bit 1,
bytes 40..43). A changed mtime with the same size and content
(touch, checkout back and forth) no longer marks a journal stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- open_ctx takes a non-blocking flock on the journal. A journal locked
by another kte process is left alone: this session reports it once
and does not write (records from two sessions used to interleave in
one file). Opening a file whose journal is live in another session
skips recovery instead of replaying or removing that journal.
- The header records the size and mtime of the file the journal's
records apply to (flags bit 0, bytes 24..39; previously reserved, so
no version change). Before offering recovery, a journal whose base no
longer matches the file on disk (changed after a crash, or the crash
fell between a save's rename and the journal reset) is treated as
stale and offered for deletion, instead of replaying its edits onto
different text. Journals from older kte (flag clear) replay as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Recovery works when another named buffer is current. On that path
OpenFile's NotifyFilenameChanged deleted the journal (old path ==
new path) that was about to be replayed; the swap was lost and the
file opened without the edits (pre-existing).
- After recovering from a journal with a torn final record, the torn
bytes are truncated before the session appends to it (ReplayFile now
reports the valid prefix length). A journal that recovers nothing is
removed instead of appended to.
- Reload restarts the journal; recovery used to replay the edits the
reload discarded.
- Opening a file that is already open switches to its buffer (in
OpenFile and before the recovery check). Two buffers for one file
interleaved records in one journal, and closing either unlinked it.
- Swap file names escape '!' and '%', so distinct paths no longer
share a journal ('/x/a!b' vs '/x/a/b'). Swap files for paths that
contain those characters get new names.
- The gap flag is cleared when the journal restarts, and a gap that
cannot be closed (buffer too large to checkpoint) is reported once.
- crc32's table is initialised thread-safely; writer-thread error
reports identify the buffer by journal path instead of racing on
Buffer::Filename(); Close() no longer retries on EINTR (the fd is
already released and may have been reused).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Replay recovers every complete record before a torn final record (the
normal result of a crash mid-append) and reports it through err,
instead of rejecting the whole journal. SwapReplay_TruncatedLog is
updated to the new behaviour.
- A record that cannot be written (open/write failure, breaker open)
marks the journal as having a gap. Later position-based records are
dropped until a checkpoint re-establishes the content; recorders
request one on the next edit (at most once per second).
- A failed append is truncated back off the file, and a failed header
write closes the file so the next attempt starts fresh.
- Inserts over the 24-bit record limit are split into consecutive
records; buffers too large to checkpoint are no longer snapshotted
and rejected on every checkpoint.
- Declining to delete an unreadable swap moves it to .corrupt so new
edits are not appended behind the bad data.
- NotifyFilenameChanged no longer writes a full checkpoint into the
journal it is about to delete.
Audit findings D7, P5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
Undo/redo:
- cmd_newline recorded the undo node's position after the cursor moved past
the split point, so undoing Enter joined the wrong pair of lines.
- Backspace/Delete line-joins recorded UndoType::Newline, whose apply()
semantics are inverted for a join (forward=split); add UndoType::JoinLines
with correct forward/backward behavior.
- Regex replace-all, indent/unindent region, kill-to-eol, kill-line,
kill-region, and delete-word-prev/next mutated the buffer via the
no-undo-recording raw APIs; they now record undo, grouped atomically via a
shared UndoGroupGuard.
- Plain-text replace-all with an empty replacement advanced the scan
position one character too far, skipping adjacent/overlapping matches.
Crash recovery / memory safety:
- Buffer's move ctor/assignment never carried over swap_rec_,
on_disk_identity_, or the visual-line-mode fields, so the crash-recovery
journal silently stopped tracking a buffer whenever Editor's
std::vector<Buffer> reallocated or shifted (e.g. opening/closing files).
Added SwapManager::Rehome() plus Flush()-before-mutate in
Editor::AddBuffer/CloseBuffer so the journal's Buffer* key and the
background writer thread never reference a stale address.
- UndoTree leaked its entire node graph (including all edit text) on every
buffer close/reload; added ~UndoTree() to free it.
- main.cc didn't restore the terminal if an exception escaped the run loop;
added an RAII guard around Frontend::Shutdown().
Input handling:
- Terminal frontend used getch() instead of get_wch(), silently dropping
non-ASCII keyboard input despite linking wide-char ncurses.
- KKeymap's C-k lookup switches on an already-lowercased key, making
`case 'E'` unreachable dead code; C-k Shift-E silently aliased to C-k e.
- ImGui file picker wasn't modal: keystrokes typed while it was open still
reached the buffer underneath as edit commands, and Escape didn't close it.
- Gated ImGuiInputHandler's unconditional fprintf/fflush diagnostics behind
an IMGUI_IH_DEBUG macro, matching QtInputHandler's existing convention.
Syntax highlighting:
- Go/Rust/SQL highlighters weren't stateful, so multi-line /* */ comments
mis-highlighted as code starting on the second line.
- Python's triple-quoted-string handler didn't re-scan the remainder of a
line after a string closed mid-line, missing a same-line reopen.
- HighlighterEngine's state_last_contig_ field was declared but unused,
forcing an O(n) scan of state_cache_ on every stateful lookup; wired it up
as a real fast path and fixed InvalidateFrom() to keep it in sync.
- kge's :syntax off was silently undone the next frame because
apply_syntax_to_buffer() re-applied the config default unconditionally
every frame; added a user-override flag mirroring the existing
edit-mode-detection guard.
Added regression tests for all of the above (test_undo.cc, test_kkeymap.cc,
test_command_semantics.cc, test_search_replace_flow.cc, and a new
test_syntax_highlighting.cc). Full suite (163 tests) passes, including under
AddressSanitizer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Added `ErrorRecovery.cc` and `ErrorRecovery.h` for retry and circuit breaker implementations.
- Enhanced swap file handling with transient error retries and exponential backoff (e.g., ENOSPC, EDQUOT).
- Integrated circuit breaker into SwapManager to gracefully handle repeated failures, prevent system overload, and enable automatic recovery.
- Updated `DEVELOPER_GUIDE.md` with comprehensive documentation on error recovery patterns and graceful degradation strategies.
- Refined fsync, temp file creation, and swap file logic with retry-on-failure mechanisms for improved resilience.
- Introduced `test_swap_edge_cases.cc` with extensive tests for minimum payload sizes, truncated payloads, data overflows, unsupported encoding versions, CRC mismatches, and mixed valid/invalid records to ensure reliability under complex scenarios.
- Enhanced `main.cc` with a top-level exception handler to prevent data loss and ensure cleanup during unexpected failures.
- Added detailed journaling system (`SwapManager`) for crash recovery, including edit recording and replay.
- Integrated recovery prompts for handling swap files during file open flows.
- Implemented swap file cleanup, checkpointing, and compaction mechanisms.
- Added extensive unit tests for swap-related behaviors such as recovery prompts, file pruning, and corruption handling.
- Updated CMake to include new test files.
- Introduced SwapManager for sidecar journaling of buffer mutations, with a safe recovery mechanism.
- Added group undo/redo functionality, allowing atomic grouping of related edits.
- Implemented `SwapRecorder` and integrated it as a callback interface for mutations.
- Added unit tests for swap journaling (save/load/replay) and undo grouping.
- Refactored undo to support group tracking and ID management.
- Updated CMake to include the new tests and swap journaling logic.