In windowed mode the frontend only holds the tail window of the chat,
so /hide could never reach messages stored before the loaded window:
the existing patch channel (RewriteFromIndex) physically starts at the
window cursor.
Add a hide_chat_payload_before_cursor backend command (character +
group) that stream-rewrites the JSONL lines before the cursor, flipping
is_system in place with an optional exact-match name filter, and returns
the recomputed cursor. Lines already in the desired state, blank lines,
and non-object lines are copied through byte-identical. The command has
no force bypass: a stale cursor signature aborts before anything is
written.
On the frontend, /hide and /unhide now accept "all" (whole chat) and
"before" (only the unloaded messages before the window). The in-window
part reuses the existing in-memory path; the out-of-window part flushes
pending saves first, then calls the new command and swaps the windowed
cursor only if the chat and cursor offset are still the ones the rewrite
started from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildWindowedPayloadPatch now returns expectedWindowLineCount; assert it
on every patch branch (append/rewrite/truncate/full/empty) plus an
invariant test pinning it to the PRE-write savedMessageCount. If it ever
drifts to the post-write message count, every stale-cursor write would
be silently accepted, so the invariant is the real regression guard.
Replace the fixed-character window in the patch-commit guard test with
brace-balanced function-body extraction (the fixed window already broke
once when the function grew) and assert the patch call forwards
expectedWindowLineCount end to end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The max(20, 50%) line-count tolerance was a guess about which diffs are
legitimate edits: stale-cursor writes inside the tolerance still
overwrote history, and legitimate bulk deletions could be false-flagged.
Callers now declare the window baseline - how many message lines their
last successful load/save left between cursor.offset and EOF
(savedMessageCount on the frontend) - and every windowed write verifies
the file still matches exactly before touching it.
The Append path previously ran no offset validation at all and re-signed
whatever offset it was given into the returned cursor, arming the next
RewriteFromIndex to truncate at the wrong place. Both patch ops and the
windowed save now share the same line-boundary + baseline checks ahead
of any write.
The two mode-switch repro tests gained assertions and are included; a
new test covers the Append-with-stale-offset case that previously passed
silently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Translations for the new windowed-hide command surface: helpStrings for
/hide and /unhide, the unnamed-argument description, the all/before
autocomplete enum descriptions, and the three toast messages. Restores
the zh-cn/zh-tw coverage for the reworded help keys and adds net-new
entries for the other 14 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When switching between full and windowed chat loading modes, the cursor
signature check (size + modified_millis) passes because the file hasn't
changed, but cursor.offset points to the wrong position. This causes
set_len(cursor.offset) to silently truncate messages.
Add verify_window_line_consistency() that counts lines in the window
region on disk and rejects saves where the written line count diverges
significantly from the existing window. This catches both directions:
- Full->Windowed: cursor.offset near header, writing only tail messages
- Windowed->Full: cursor.offset near EOF, writing all messages
Relates to #87
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>