Skip to content

Split ConversationHistory into Conversation and ConversationStore#183

Merged
shellicar merged 1 commit intomainfrom
feature/conversation-split
Apr 6, 2026
Merged

Split ConversationHistory into Conversation and ConversationStore#183
shellicar merged 1 commit intomainfrom
feature/conversation-split

Conversation

@shellicar
Copy link
Copy Markdown
Owner

What

Step 1a of the architecture refactor plan (.claude/plans/architecture-refactor.md).

Splits ConversationHistory into two files:

  • Conversation.ts — pure in-memory data. Owns the business rules: role-alternation merge, compaction-triggered clear, id tagging/remove. No node:fs.
  • ConversationStore.ts — file-backed wrapper. Reads the JSONL file on construction (applying trimToLastCompaction), delegates to Conversation for mutations, and persists after every push/remove. No-file construction works as a plain in-memory store.

AgentRun and AnthropicAgent swap ConversationHistoryConversationStore. Public interface is identical so call sites are unchanged.

Tests

ConversationHistory.spec.tsConversation.spec.ts. Rewritten to project convention:

  • One expect per it
  • const expected = …; const actual = …; expect(actual).toBe(expected)
  • New load() section covering the raw-initialization path that ConversationStore uses

21 tests, all green.

Why AgentRun gets ConversationStore (not bare Conversation)

AgentRun.push() calls must trigger disk saves. Decoupling AgentRun from the persistence layer entirely is a future step; passing it a ConversationStore achieves the structural separation (pure data vs I/O) without introducing a callback or hook mechanism here.

@shellicar shellicar added this to the 1.0 milestone Apr 6, 2026
@shellicar shellicar added the enhancement New feature or request label Apr 6, 2026
@shellicar shellicar self-assigned this Apr 6, 2026
@shellicar shellicar requested a review from bananabot9000 April 6, 2026 07:57
…ionStore (I/O)

ConversationHistory was doing two jobs: managing message state and persisting
it to disk. This split pulls them apart.

Conversation holds the items array and owns the business logic — role-alternation
merge, compaction-triggered clear, id tagging/remove. No imports from node:fs.

ConversationStore wraps Conversation and handles the JSONL file: reads on
construction (applying trimToLastCompaction), writes after every push/remove.
When constructed without a historyFile it behaves as an in-memory store with
no I/O side effects.

AgentRun and AnthropicAgent switch from ConversationHistory to ConversationStore
— same public interface, so call sites are unchanged.

Conversation.spec.ts replaces ConversationHistory.spec.ts. Tests are rewritten
to the project convention: one expect per it, const expected / const actual
naming, and a load() section covering the raw-initialization path that
ConversationStore uses during construction.
@shellicar shellicar force-pushed the feature/conversation-split branch from 9f6cf27 to 70c6922 Compare April 6, 2026 08:36
Copy link
Copy Markdown
Collaborator

@bananabot9000 bananabot9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean split. Conversation has zero I/O -- node:fs import gone, no #save() calls remain. ConversationStore wraps and delegates correctly: both push() and remove() call #save() after delegation.

Callers (AgentRun, AnthropicAgent) are straight type swaps -- 3+3 lines each, external API unchanged (historyFile option still works).

Tests rewritten to project convention (one expect per it, expected/actual). The load() section correctly verifies that raw initialization bypasses merge logic (two consecutive user messages stay separate after load), which is the key behavioral distinction between the two classes.

One minor note: ConversationStore.remove() calls #save() unconditionally, including when remove() returns false (no-op write). Harmless -- the file content is identical -- but could short-circuit with if (result) this.#save() if you wanted to avoid the disk write on miss. Not blocking.

Exported helpers (HistoryItem, hasCompactionBlock, trimToLastCompaction) are the minimum needed for ConversationStore to do its job. No leaky abstractions.

LGTM. Step 1a done. 🍌

@shellicar shellicar merged commit fd36163 into main Apr 6, 2026
4 checks passed
@shellicar shellicar deleted the feature/conversation-split branch April 6, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants