Skip to content

Move chat-history persistence to append-only JSONL - #2431

Draft
SasinduDilshara wants to merge 4 commits into
wso2:feat/copilot-changesfrom
SasinduDilshara:fix/1861-jsonl-chat-history
Draft

Move chat-history persistence to append-only JSONL#2431
SasinduDilshara wants to merge 4 commits into
wso2:feat/copilot-changesfrom
SasinduDilshara:fix/1861-jsonl-chat-history

Conversation

@SasinduDilshara

Copy link
Copy Markdown
Contributor

Solution

Moves Copilot chat-history persistence from full-file JSON rewrites to an append-only JSONL log in copilot-utilities.

  • Each new message/step is appended as a single record instead of re-serializing and rewriting the whole thread on every update, so persisting a turn costs O(new messages) instead of O(total history).
  • On load the log is replayed to rebuild the thread; the file is compacted periodically (size-aware, amortized).
  • The chat-state store is rewired to the incremental API (appendGeneration / removeGenerationRecord / truncateFromGeneration); full rewrites are kept only for thread creation and one-time legacy migration.

Fixes wso2/product-integrator#1861

🤖 Generated with Claude Code

SasinduDilshara and others added 4 commits July 14, 2026 09:55
Threads previously persisted as a single whole-file JSON snapshot
(thread.json) that was re-serialized and rewritten on every mutation.
Because the file grows with the conversation and the write is
synchronous, per-turn cost scaled with total history (O(n²) over a
session), causing noticeable lag in long chats.

Switch to an append-only log (thread.jsonl): each mutation appends one
record instead of rewriting everything, so adding a turn costs the same
whether the conversation is 2 or 200 messages long. On load the records
are replayed to rebuild the thread; the log is compacted (amortized, and
on load) to reclaim superseded records.

- file-utils: appendLineSync + readJsonlSync (tolerant of a torn
  trailing line for crash safety)
- types: ThreadLogRecord union (head/meta/gen/del/trunc)
- schema-migration: bump thread schema to v2 with an identity v1->v2
  migration (data shape unchanged, storage format differs)
- persistence-store: append API (appendGeneration/removeGenerationRecord/
  truncateFromGeneration/updateThreadMeta), replay-based loadThread with
  one-time thread.json -> thread.jsonl migration + legacy delete,
  compacted saveThread, compactThread, amortized + on-load compaction
- tests: full coverage of append/replay/tombstone/truncate/torn-line/
  migration/compaction; existing suite updated for the new format

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…writes (wso2#1861)

Adversarial self-review follow-ups:

- Amortized compaction is now size-proportional (compact after
  max(64, 3*liveGens) appends, not a fixed 100), so per-append cost stays
  genuinely O(1) amortized instead of growing with thread size — the
  O(n²) shape the issue targets is fully removed, not just reduced.
- On-load compaction (compactOnLoadIfNeeded) and append-time compaction
  (noteAppend) now swallow write errors. loadThread became a writer on the
  happy path; an unguarded transient disk error there would have aborted
  restore of the entire workspace even though the thread was already
  replayed in memory. Compaction is a pure optimization and must never
  fail a load or an append.
- loadThread falls back to a legacy thread.json when the jsonl exists but
  is empty/unrecoverable, instead of reporting the thread as gone.

Tests: +3 (size-aware threshold, read-only-dir load resilience, empty-jsonl
legacy fallback); 72 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

Follow-up to reviewer feedback: the previous design re-appended the whole
generation (its entire, growing modelMessages) on every agent step and
relied on compaction to clean up the duplicates — roughly O(S^2) bytes
written per S-step turn, all synchronous.

Switch to message-level records so each message is written exactly once:

- `gen`  — a lightweight generation header (everything except modelMessages),
           appended only when a header field actually changes.
- `msg`  — one model message, appended as it arrives.
- `msgs` — replace a generation's whole message list (only when the messages
           are rewritten rather than extended, e.g. server-side compaction;
           detected cheaply via length/last-message change).

appendGeneration now diffs against per-generation tracking and writes only
the delta (new messages + header-if-changed), so a turn costs O(messages)
to persist instead of O(messages^2). Replay groups `msg` records by genId
in order; compaction/save emit one `msg` per message. The consumer
(ChatStateStorage) is unchanged — it still calls appendGeneration with the
full generation; the delta logic lives entirely in the store.

Tests: +4 (message-once / no-duplication, header-only-on-change, msgs reset,
headless message reconstruction); 75 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (4)
  • main
  • feature/.*
  • release/.*
  • hotfix/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a0cbb0d-0b18-423d-ae2e-1dee8ea7a8b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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