fix(session): archive all off-branch history once a session passes 8 MiB

The #6842 autosave bound counts entries, not bytes: it archives off-branch
entries only once more than 4,096 sit in the document and then keeps 1,024,
which at a couple of kilobytes each can hold far more than the 8 MiB
full-history bound. On the autosave path (save_session_bounded), when the
file being replaced is already over MAX_CANONICAL_HISTORY_BYTES, every
off-branch entry is now archived instead, through the same fsynced
archive-before-remove step; below the bound the 4,096/1,024 window is
unchanged. The live journal follows through the archived-id registry, and
`/branch` can still restore an archived entry.

Known limits: the trigger stats the previous file, so it lags one save. It
does not bound the active branch, which after compaction still keeps every
pre-compaction ancestor, and the archive sidecar still grows without bound.
Other save paths (plain save_session) never prune.

Verification: checked once on the final batch tree (parent db89057d84):
cargo fmt --check clean; tui lib nextest across route_budget,
provider_catalog_live, provider_lake, mounted_history_tests,
session_manager, client, core::engine, compaction, tools::subagent,
runtime_api, purge, commands::contract, tui::model_picker and whats_new
2757 passed, 0 failed; tui integration exec_turn_usage 11 passed, 0
failed; feature_registry 2 passed, 0 failed; clippy -p codewhale-tui
--lib --tests -D warnings with the CI allow-list clean; check-blocking-
calls-budget 540 sites, within budget. Not run: hosted CI and the
loopback product captures (those follow on the release build).

Refs #6842
Refs #6955

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-21 17:07:06 -07:00
1 parent 4567b79241
commit e20aaedf19
3 files changed
+37 -19

No files matched your search

+5
View File
@@ -143,6 +143,11 @@ guidance.
fits, and any other request fails with a context-length error. Other
routes are unchanged.
- Once a session file passes 8 MiB, autosave moves all of its abandoned
branch history to the session's journal archive, instead of keeping up to
4,096 off-branch entries in the document. `/branch` can still restore
archived entries. Refs #6842. Thanks @7jrxt42BxFZo4iAnN4CX.
- A window that cannot save its queued messages because it holds no queue
lease for the current session now logs both session ids, and failed
session, checkpoint and queue writes log the raw OS error code. Refs
+5
View File
@@ -143,6 +143,11 @@ guidance.
fits, and any other request fails with a context-length error. Other
routes are unchanged.
- Once a session file passes 8 MiB, autosave moves all of its abandoned
branch history to the session's journal archive, instead of keeping up to
4,096 off-branch entries in the document. `/branch` can still restore
archived entries. Refs #6842. Thanks @7jrxt42BxFZo4iAnN4CX.
- A window that cannot save its queued messages because it holds no queue
lease for the current session now logs both session ids, and failed
session, checkpoint and queue writes log the raw OS error code. Refs
+27 -19
View File
@@ -50,7 +50,9 @@ pub(crate) const WORK_GRAPH_IMPORT_ARCHIVE_DIR: &str = ".work-graph-import-archi
pub(crate) const JOURNAL_ARCHIVE_DIR: &str = ".journal-archive";
/// Off-branch entries a session document may carry before an autosave
/// archives the excess. Together with [`JOURNAL_RETAINED_DEAD_ENTRIES`] this
/// keeps a compacted journal far below `MAX_CANONICAL_HISTORY_ENTRIES`.
/// keeps a compacted journal far below `MAX_CANONICAL_HISTORY_ENTRIES`. A
/// document whose file is already over `MAX_CANONICAL_HISTORY_BYTES` archives
/// every off-branch entry instead.
const JOURNAL_PRUNE_DEAD_THRESHOLD: usize = 4_096;
/// Recent off-branch entries kept in the document after archiving, so `/tree`
/// and `/branch` stay cheap for recent history.
@@ -2635,10 +2637,12 @@ impl SessionManager {
/// The autosave path (#6842): like [`Self::save_session_owned`], but when
/// the journal carries more than `JOURNAL_PRUNE_DEAD_THRESHOLD`
/// off-branch entries, first append the excess to the session's journal
/// archive (fsynced), then write the document without them. If the
/// archive cannot be written nothing is pruned. Runs on the persistence
/// actor (`tui/persistence_actor.rs` `flush_inner`), never the UI loop.
/// off-branch entries, or the file being replaced is over
/// `MAX_CANONICAL_HISTORY_BYTES` and carries any, first append the excess
/// (all of them in the second case) to the session's journal archive
/// (fsynced), then write the document without them. If the archive cannot
/// be written nothing is pruned. Runs on the persistence actor
/// (`tui/persistence_actor.rs` `flush_inner`), never the UI loop.
pub(crate) fn save_session_bounded(&self, session: SavedSession) -> std::io::Result<PathBuf> {
self.save_session_inner(session, JournalPrune::Excess)
}
@@ -2753,20 +2757,24 @@ impl SessionManager {
let mut durable_session = session;
let archived = match prune {
JournalPrune::Keep => Vec::new(),
JournalPrune::Excess => self
.archive_dead_journal_entries(
&mut durable_session,
JOURNAL_PRUNE_DEAD_THRESHOLD,
JOURNAL_RETAINED_DEAD_ENTRIES,
)
.unwrap_or_else(|error| {
tracing::warn!(
%session_id,
%error,
"journal archive step failed; keeping the full journal"
);
Vec::new()
}),
JournalPrune::Excess => {
let (threshold, retained) = if fs::symlink_metadata(&path).is_ok_and(|meta| {
meta.len() > codewhale_protocol::MAX_CANONICAL_HISTORY_BYTES as u64
}) {
(0, 0)
} else {
(JOURNAL_PRUNE_DEAD_THRESHOLD, JOURNAL_RETAINED_DEAD_ENTRIES)
};
self.archive_dead_journal_entries(&mut durable_session, threshold, retained)
.unwrap_or_else(|error| {
tracing::warn!(
%session_id,
%error,
"journal archive step failed; keeping the full journal"
);
Vec::new()
})
}
JournalPrune::AllOffBranch => {
self.archive_dead_journal_entries(&mut durable_session, 0, 0)?
}