Split Conversation storage from API view#231
Conversation
This file was checked in by accident. It is a per-user local config and lives in the global gitignore now.
Compaction used to destroy pre-compaction messages on push(): the store
clobbered history in place, and ConversationStore.trimToLastCompaction
did the same on disk load. Once compaction fired there was no way back;
if the compaction itself errored, the session died with it.
This split fixes that. Conversation now stores the full history forever.
Compaction messages are appended like any other message. For API calls
the caller uses cloneForRequest(), which deep-clones the slice from the
last compaction forward. The clone is caller-owned and RequestBuilder
mutates it directly: no more defensive spread-copy dance to keep the
send-time cache_control and system-reminder blocks out of stored state.
Three follow-ons fall out for free:
- compaction rollback works (remove the compaction block, next
cloneForRequest slices further back)
- the full history is visible to TUI replay, audit, and inspection
- on-disk format stays pristine (no cache_control leakage)
AgentRun switches from this.#history.messages to cloneForRequest() at
the send site. ConversationStore adds a thin passthrough and stops
trimming on load. The addCacheControlToLastBlock and cacheLastUserMessage
helpers become void functions that mutate in place.
Test updates reflect the new semantics: 4 old compaction-clear tests
rewritten to expect retention; 6 new cloneForRequest tests cover the
deep-clone guarantee, the slice boundary, and the empty case. The
'does not mutate input' test on RequestBuilder was contract-based on
the old shape and is removed.
bananabot9000
left a comment
There was a problem hiding this comment.
Exactly the right refactor. Storage and API view separated cleanly. The spread-copy dance is dead, structuredClone handles deep isolation, cloneForRequest() is the single point where "what the API sees" diverges from "what we store."
Tests are thorough — deep-clone proven at array, message, and content-block levels. Compaction-clear tests correctly rewritten as retention tests.
One minor observation: trimToLastCompaction does an O(n) reverse scan which is fine. The structuredClone only runs on the post-compaction slice, so even with large pre-compaction histories the clone cost stays bounded by the active conversation size.
Clean, narrow, well-scoped. The session log's design discussion is excellent context for PR 2.
🍌 Approved.
Why
Conversation.push()used to clear the entire item array when it saw a compaction block, andConversationStoredid the same on disk load viatrimToLastCompaction. That was destructive on two axes:RequestBuilderalso carried a stack of defensive spread-copies ([...messages],{ ...block, cache_control }) whose only job was to keep send-time cache_control and system-reminder blocks from leaking back into stored state, because the same array served both purposes.What
Storage and API view are now separate concerns.
Conversationstores the full message history forever. Compaction messages are appended like any other message; no clearing.Conversation.cloneForRequest()returns a deep clone (structuredClone) of the slice from the last compaction forward. The returned array is caller-owned and may be mutated freely.RequestBuilder's helpers (addCacheControlToLastBlock,cacheLastUserMessage, systemReminder injection) become void functions that mutate in place. The defensive spread-copy dance is gone.AgentRunswapsthis.#history.messagesforthis.#history.cloneForRequest()at the send site.ConversationStoreadds a thincloneForRequest()passthrough and stops trimming on load.Payoffs
remove(id)the compaction block and the nextcloneForRequest()slices further back.Scope
This is PR 1 of a larger SDK reshape. Deferred to a later PR: extracting
AnthropicClientfromAnthropicAgent, theconfigure(partial)/run()split, removingConversationStore/historyFilefrom the SDK so persistence lives fully in the CLI, and collapsingrunAgent.ts. Full design notes in.claude/sessions/2026-04-09.mdunder "Session 2026-04-09 (SDK refactor planning)".Tests
cloneForRequesttests cover the empty case, no-compaction case, post-compaction slice, deep-clone guarantee (mutating the clone leaves the source untouched at array, message, and content-block levels).does not mutate inputtest onRequestBuilderremoved — it encoded the old contract.Related: #232