- SwapManager queues user notices when a journal is held by another kte
(this session's edits are then not journaled) and, once per gap, when
journal writes fail. Editor shows them in the status line from its
per-frame hook. Both went only to the error log, so crash recovery was
silently off or incomplete.
- The fallback swap name '<basename>.<hash>.swp' exceeded NAME_MAX for
basenames over ~233 bytes, so such files were never journaled; the
basename is shortened (on a UTF-8 boundary) only in that case.
- Audit doc: sixth-round findings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
After a lost record the journal drops position-based records until a
full checkpoint. That checkpoint was only requested from later edits,
rate-limited to once a second; when the writer handled the failing
record before its edit asked for a checkpoint, the checkpoint failed as
well and consumed the slot, so edits made within the next second were
not recovered and, if the user stopped typing, the journal stayed behind
indefinitely. SwapManager::RetryGapCheckpoints(), called once per frame
from Editor::ProcessPendingOpens, requests it once the interval has
passed. This also made SwapReplay_LostRecord_ResyncsWithCheckpoint flaky
under load; the test now exercises the retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- The writer's periodic fsync now holds a separate io_mtx_, taken also
(before mtx_) by the main-thread paths that close journal fds, instead
of mtx_. Every keystroke takes mtx_ to enqueue its record, so a slow
fsync (writeback pressure, NFS) froze typing.
- A journal's base identity (stat + content CRC) is computed before
taking mtx_ and applied under it; the CRC is skipped above 64 MiB
(size and mtime still identify the file). It re-read the whole file
under the lock on every open and save.
- A buffer that was never journaled (a virtual buffer saved under a
real name) is attached by NotifyFilenameChanged.
- ThreadSanitizer: open_ctx (writer thread) no longer writes ctx.path,
and locked_out is recorded by the caller under mtx_; both raced with
main-thread reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- kte refuses to start when stdin is not a terminal: piped bytes were
executed as editor commands (C-k s plus a path writes a file), and
after EOF the loop spun at full CPU with no way to quit.
- The terminal frontend handles all pending input before drawing (a
paste used to cost a frame and a full redraw per character) and
redraws only after input, a resize or deferred opens, else once a
second. The renderer caches, per row and buffer version, where the
horizontally scrolled part of a line starts and the cursor's render
column, instead of rescanning the line every frame. Idle CPU with the
cursor at the end of a 20 MB line: one full core -> ~1%.
- Buffers with a virtual name (+HELP+) are not journaled and saving one
asks for a file name; it used to write ./+HELP+ and journal under
that path.
- Yank with a repeat count is capped at 256 MiB in total instead of
reserving count x size bytes (C-u 1000000 C-y of a large kill).
- Remove an overload clang reports as unused (-Werror on macOS).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- 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
- A session never unlinks a journal it does not own: Detach, ResetJournal
and NotifyFilenameChanged skip removal when this session was locked
out or another session holds the lock, and PruneSwapDir skips held
journals. A second session used to delete the first session's live
journal on save or close.
- The writer thread's periodic fsync now runs under mtx_. It fsynced fds
that the main thread could close concurrently (or that had been
reused); an in-flight fsync also kept a just-closed journal's flock
alive, so the owner's own cleanup saw its journal as held.
- Compaction locks the compacted file before renaming it into place and
keeps using it, so the journal is never unlocked in between; a failed
lock on the truncating reopen marks the session locked out.
- Unnamed-buffer journals include the pid, so sessions no longer share
(and delete) each other's unnamed-N.swp.
- RowsView keeps only the previous generation of cached rows: keeping
all of them made C-u 2000 M-d on a 1 MiB line use ~2 GB.
- Renderers run regex match highlighting through ForEachRegexMatch,
which uses the large-stack worker for lines over 512 bytes; some
patterns overflowed the main stack well under the previous 10,000
byte limit.
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.
- Added a comprehensive error propagation standardization report detailing dominant patterns, inconsistencies, and recommended remediations (`docs/audits/error-propagation-standardization.md`).
- Integrated `ErrorHandler` into key components, including `main.cc` for robust exception reporting, and added centralized logging to a user state path.
- Introduced EINTR-safe syscall wrappers (`SyscallWrappers.h`, `.cc`) to improve resilience of file and metadata operations.
- Enhanced `DEVELOPER_GUIDE.md` with an error handling conventions section, covering pattern guidelines and best practices.
- Identified gaps in `PieceTable` and internal helpers; deferred fixes with detailed recommendations for improved memory allocation error reporting.
- 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.
Add benchmarks for core operations, migration edge case tests, improved
buffer I/O tests, and developer guide
- Introduced `test_benchmarks.cc` for performance benchmarking of key
operations in `PieceTable` and `Buffer`, including syntax highlighting
and iteration patterns.
- Added `test_migration_coverage.cc` to provide comprehensive tests for
migration of `Buffer::Rows()` to `PieceTable` APIs, with edge cases,
boundary handling, and consistency checks.
- Enhanced `test_buffer_io.cc` with additional cases for save/load
workflows, file handling, and better integration with the core API.
- Documented architectural details and core concepts in a new
`DEVELOPER_GUIDE.md`. Highlighted design principles, code
organization, and contribution workflows.
- 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.