Skip to content

Keep compacted turns out of the session row - #1153

Draft
sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1271-trim-session-turns
Draft

sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1271-trim-session-turns

Conversation

@sosidudku1

@sosidudku1 sosidudku1 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Long sessions re-serialized their whole transcript on every write: each step's checkpoint and every turn's end turned all of SessionState.turns into one JSON string, including the part a compaction had already summarized. Peak memory per write grew with session age, the likely cause of the 0xC000009A crashes on multi-hour sessions and overnight runs (ATO-1271).

What changes:

  • Turns covered by a compaction checkpoint move once into a new session_turn_archive table, one row per turn, in the same transaction as the payload. The payload keeps only the turns after them, plus an archivedTurns marker (count and first user message).
  • load, listRecent and listByWorkingDir put the archived turns back, so the in-memory transcript and all indices into it (coveredThrough, macroTurnStarts, pack start) are unchanged.
  • If a write's transcript no longer starts with the archived turns (an older copy, a replaced history), the archive is dropped and the transcript is stored in full, so a load always returns what was saved.
  • listChatLines and the session list's first prompt read across the archive. delete, the retention pass and the desktop importer handle it.
  • Old rows load as before and migrate on their next write. Sessions without a checkpoint are stored exactly as before. If the table cannot be created, the store keeps storing in full.

If the archive table cannot be created at open (for example the shared database is write-locked by another process), the store retries before each load or save. While it is unavailable, a compacted session loads as its tail with the marker kept, is written back as a tail without touching the archive rows, and any save that would replace archived history is skipped with a warning (ATOMIC_SESSION_ARCHIVE_UNAVAILABLE), so archived turns are never deleted.

Not in this PR: the in-memory transcript is still full, and load still parses all archived turns (row by row). Bounding memory itself needs a tail-only in-memory model and index rebasing across about 30 readers.

Known risk, needs a decision before merge: the desktop app and the terminal share ~/.atomic-agent. An older binary (for example a desktop build without this change) opening a compacted session sees only the uncovered tail, drops the checkpoint as invalid, and if it saves, writes macroTurnStarts relative to the tail. Sessions never compacted are unaffected. Mixed versions on the same folder (old desktop + new terminal) drop the checkpoint and bring back the whole uncompacted history. The terminal and the desktop must ship this change together, in the same release.

Not yet checked on the second Mac.

Every write of a session (each step's checkpoint, every turn's end)
serialized the whole transcript into one JSON string, even the part a
compaction had already summarized. On long sessions and overnight runs
that string grows without bound, and so does the peak memory of each
write.

Turns covered by a compaction checkpoint now move once into a new
session_turn_archive table, one row per turn. The row's payload keeps
only the turns after them, plus an archivedTurns marker with the count
and the first user message (for the session list). SessionStore.load,
listRecent and listByWorkingDir put the archived turns back in front, so
the in-memory transcript and every absolute index into it
(coveredThrough, macroTurnStarts, pack start) are unchanged.

- Archive rows are append-only and written in the same transaction as
  the payload. A write whose transcript no longer starts with the
  archived turns (an older copy, a replaced history) drops the archive
  and stores the transcript in full, so a load always returns what was
  saved.
- Rows written before this change load as before and are migrated on
  their next write. Sessions without a checkpoint are stored exactly as
  before.
- listChatLines and the summary page read across the archive.
- delete, the retention pass and the desktop importer handle the
  archive. If the table cannot be created (read-only file, lock held),
  the store keeps storing transcripts in full.

Refs ATO-1271
If the store could not create the turn archive table at open (a lock
held past the busy timeout, a read-only file), it ran without the
archive for its whole lifetime. Loading a compacted session then
stripped the archivedTurns marker and kept only the tail, the next save
wrote that tail as the full transcript, and a later process with the
archive saw a transcript unrelated to the archived rows and deleted
them.

- The archive is now retried lazily before each load, list and save
  (a table another process created is picked up at once; a failed
  create is retried after 30 seconds) instead of being disabled for
  the store's lifetime.
- Without the archive, a loaded payload keeps its marker. Such a
  tail-only state is written back as it is, marker included, and never
  compared with the archived rows, so they are never cleared. Its
  stored checkpoint and rolling summary are kept as well.
- A write that would still lose history is skipped with a process
  warning: a whole transcript over a row whose prefix is archived while
  the archive is unavailable, or a tail-only state over a row that no
  longer counts archived turns.
- Tests: a store without the archive loads and saves a compacted
  session and a normal reopen still has every turn; a tail copy saved
  once the archive is back; checkpointTurn rolls archive rows back when
  the owner guard matches no row; the desktop importer reads a source's
  archived turns.

Refs ATO-1271

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant