Skip to content

fix(local): recover truncated JSONL transcript tails - #3590

Open
GautamSharma99 wants to merge 1 commit into
letta-ai:mainfrom
GautamSharma99:fix/local-transcript-jsonl-recovery
Open

fix(local): recover truncated JSONL transcript tails#3590
GautamSharma99 wants to merge 1 commit into
letta-ai:mainfrom
GautamSharma99:fix/local-transcript-jsonl-recovery

Conversation

@GautamSharma99

Copy link
Copy Markdown

Summary

Fixes #3580.

Local transcript loading, suffix reads, migration, and search each parsed messages.jsonl independently. A single interrupted final append caused full history loading to throw, while search caught the same parse failure at file scope and silently returned no messages at all.

This PR gives all local transcript consumers one byte-aware JSONL implementation with deliberately narrow recovery semantics:

  • an invalid, non-newline-terminated final physical row is treated as an interrupted append;
  • all valid preceding rows remain readable and searchable;
  • any malformed newline-committed row still throws an actionable corruption error;
  • the next append preserves the original file before removing the incomplete tail.

What changed

Shared transcript JSONL owner

Added src/backend/local/transcript-jsonl.ts and routed these consumers through it:

  • full transcript loads in LocalStore;
  • bounded suffix reads used by descending history and direct message lookup;
  • local transcript search;
  • transcript migration.

The parser operates on Buffer byte ranges rather than splitting a decoded string. This keeps byte offsets accurate and lets suffix reads discard a clipped initial row before UTF-8 decoding, including when the byte window begins inside a multibyte character.

Narrow corruption handling

Rows terminated by a newline are considered committed. If one cannot be parsed, LocalTranscriptJsonlCorruptionError includes:

  • the transcript path;
  • the one-based line number when the read starts at the file beginning;
  • the exact byte offset;
  • the underlying JSON parse message.

Search no longer wraps the entire JSONL parse in a blanket catch, so committed corruption is surfaced instead of being converted into an empty conversation.

Only an invalid final row without a terminating newline is recoverable. The reader returns preceding records and emits a deduplicated warning describing the file and recovery behavior. A complete final JSON value without a newline remains readable and is terminated before a later append.

Safe append recovery

Before appending a transcript entry, the store examines only the final physical row, avoiding an O(file size) parse on every message append.

For an incomplete tail it:

  1. copies the complete original messages.jsonl to a uniquely named messages.jsonl.corrupt-tail-backup-* sidecar;
  2. truncates the active file to the exact starting byte of the damaged row;
  3. appends the new record normally.

The copy must succeed before truncation occurs. The backup therefore preserves both every durable row and the exact damaged bytes for inspection or manual recovery.

Repository maintenance

Extracting the duplicated readers reduced local-store.ts from 3,594 to 3,558 lines, so the source-size baseline is lowered in the same change. The new tests live in responsibility-sized adjacent files rather than growing the already grandfathered local-backend.test.ts.

Tests

Added coverage for:

  • full reads preserving valid rows before a partial final row;
  • committed middle-row corruption reporting path, line, and byte offset;
  • suffix reads that start inside a UTF-8 sequence and end with a partial record;
  • exact original-byte backup before tail truncation;
  • complete final JSON without a newline being preserved and safely delimited;
  • local history remaining available after a partial append;
  • the next append repairing the file, creating the backup, and remaining readable after another reload;
  • search finding durable messages before a partial tail;
  • search surfacing a committed malformed row instead of returning no results.

Validation completed:

  • focused JSONL, integration, and search tests: 11 passed;
  • existing local-backend transcript suite: 38 passed;
  • total focused/regression tests: 49 passed;
  • bun run check: all 12 repository checks passed.

Scope

This does not attempt to guess around committed mid-file corruption or silently skip arbitrary bad rows. Those cases remain failures because continuing across them could hide lost ordering, parent links, or transcript state. Recovery is limited to the crash-consistent case produced by an interrupted final append.

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.

A single malformed JSONL row makes local transcript history and search unavailable

2 participants