Two safe-integer timestamps can differ by more than 2^53-1 (e.g.
MIN_SAFE_INTEGER to MAX_SAFE_INTEGER-1), so the dt subtraction rounds
and the packed row decodes to a timestamp one off the original --
violating the codec's lossless contract. Unreachable from a real clock
(the gap needs ~285k years) but reachable from hand-written fixtures,
and the decoder accepts hand-written rows.
continues() now refuses to extend a run across such a gap (the check is
exact both ways: an in-range true gap subtracts without rounding and
passes; an out-of-range one rounds to an out-of-range value and fails),
splitting the run instead -- whitelist philosophy, compression lost,
data never. The decoder tightens to match the encoder's image: time0
and dt must be safe integers, and reconstructed member seqs/times must
stay in safe range, so float reconstruction is exact wherever
validation passes.
The round-trip property now draws times from the full safe-integer
range (it previously generated only small gaps, which is how this
escaped); the bot's counterexample is pinned as an example test.
Found by ds-review-bot on #338.
Providers stream token-sized deltas, so a session log stores hundreds of
near-identical assistant/chunk lines whose JSON envelopes dwarf their
payloads (~56x measured on a real DeepSeek session, 73% of file bytes).
Add a lossless storage codec to dsh-session: packChunkRuns() folds each
run of >=3 consecutive same-block delta chunks into one storage row --
text-chunks / reasoning-chunks / tool-call-chunks, bare slash-less tags
like the header line's 'session' so rows cannot be confused with session
events -- and decodeStorageRecord() expands rows back to the exact
original events (seq0/time0 + dt gap array reconstruct every member's
seq/time; tool-call rows carry the run-constant id/name). The encoder
whitelists exact shapes and stores anything unrecognized verbatim; the
decoder validates row-tagged values and fails loud on malformation.
The JSONL backend gains a packChunks config (default false). Writing
packs only when enabled -- default-off output stays byte-identical to
the previous layout, so snapshot goldens are untouched. Reading is
layout-blind: scanLog always decodes rows and now checks seq contiguity
with a cursor instead of the line index, so packed, unpacked, and mixed
files all load identically. Fixture readers (llm-replay parseSessionLog,
acp-snapshot normalizeSessionLog) share the codec; the normalizer zeroes
a row's time0/dt exactly like an event's time. The two demo bundles
plumb packChunks from cordis.yml to the backend.
Measured on a real coding session: 105 KB -> 42 KB (-60%), 475 lines ->
74, with reasoning/tool-call heavy sessions saving the most. Covered by
example + fast-check round-trip codec tests, backend packed/mixed/torn-
tail specs, and an end-to-end demo run loading a packed log through a
default-config backend.