Issue: TBD (will be filed alongside this directive, referencing CC#66734 and CC#66486) Upstream:
- anthropics/claude-code#66734 — Session JSONL rewritten in-place to metadata-only stub — user/assistant records lost (2.1.168–2.1.170, since native installer migration)
- anthropics/claude-code#66486 — 2.1.169: interactive sessions write no JSONL transcript (only ai-title stub)
Priority: P1
Branch:
feature/jsonl-session-mirrorStage: directive — round 4 /approved-by-codex-agent(Codex round-2 APPROVE at8dc14b0cleared the round-1 blockers; this commit also fixes the non-blocking metadata nit Codex flagged. Prior rounds addressed Fable round-1, Fable round-2, then Codex round-1 REQUEST_CHANGES.) Labels:directive-stage,schema-change(mirror file is a new contract for CC-transcript-reader interoperability),needs-sim-validation(mandatory merge gate on envelope-shape parity) Milestone: v4.2.0
The proxy mirrors every assistant message and the tool results / user inputs it observes into a per-session JSONL file under user control — independent of CC's own transcript-write path. If CC's in-place stub-rewrite (CC#66734) destroys the canonical transcript, or the transcript-write path is silently disabled (CC#66486), the user retains a complete content record from the proxy side. The mirror is read-only with respect to upstream traffic; no requests or responses are modified.
This is a belt-and-suspenders backup, not a transcript replacement. CC's own JSONL remains canonical when it survives.
CC#66734 documents data-loss across 12 occurrences in two projects since the 2026-06-06 native installer migration; multiple CC versions affected. CC#66486 documents transcript-stub regressions in 2.1.169 specifically. Both are tagged data-loss by Anthropic. Users on affected versions have no recourse once the stub-rewrite fires.
The proxy is downstream of CC's transcript writer. It sees every assistant message and tool result in the response stream as the bytes flow through. If we tee that stream to a side-write JSONL keyed by session, the user retains a complete content record regardless of what happens to CC's canonical file. The mirror is not affected by CC's stub-rewrite because it is a separate file under proxy-controlled storage.
We do not claim the mirror solves CC's bug; CC's bug is upstream and Anthropic's to fix. We claim the mirror reduces the cost of CC's bug from "conversation gone" to "conversation recoverable from mirror" while CC's fix is in flight.
- Size/complexity budget: ~450–550 LOC including stream-event accumulator + envelope shaping + dedup state + retention sweep. Per Fable round 1, the round-1 budget of 200–300 LOC was not credible. Restated honestly.
- Threat model: the mirror writes plaintext conversation content to disk under user-controlled paths. This is the same threat surface as CC's own transcript; no incremental risk. Mirror files inherit the user's umask.
- Activation model:
enabled: trueinextensions.json(always loaded) with internal env-var gateCACHE_FIX_SESSION_MIRROR(on/off). Default-off in v4.2.0 and v4.3.0. Per Fable round 1, flipping plaintext-conversation-persistence to on-by-default is a privacy-posture change that belongs to its own future directive after a real validation cycle, not as a "matured-out" milestone here. - Disk-cost discipline: per-session active-file rotation at
CACHE_FIX_SESSION_MIRROR_MAX_BYTES(default 100 MB) AND a retention sweep atCACHE_FIX_SESSION_MIRROR_RETENTION_DAYS(default 30) — rotated files past the retention window are unlinked by a throttled sweep modeled oncache-telemetry.mjs'ssweepStaleSessions. Worst-case bytes documented as: active sessions × MAX_BYTES × (rotations within retention window). - PII discipline: the
session-mirror-events.jsonlwriter log carries only session id, timestamps, action (open / rotate / sweep / error), rotation count, and byte counts — never image bytes, never prompt content, never auth headers. Rotation at 5 MB single-tier perbootstrap-defense.rotateIfNeededprecedent. - Failure isolation: the pipeline try/catches every hook (
pipeline.mjs:91-96and equivalent), so a thrown mirror writer cannot break the response. The directive states this explicitly and the test plan proves it (B5 follow-through). Async writes inside the writer are.catch-handled internally — un-awaited rejected promises must not escape intounhandledRejection. - Memory tradeoff: the per-message accumulator on
ctx.meta._mirrorAccumulatoris request-scoped, so memory usage is one full in-flight assistant message buffered per concurrent request. At observed CC concurrency this is acceptable; documented for honest accounting (closes Codex round-1 attention). - Load-bearing? Yes. This directive introduces a new persisted file contract intended for CC-transcript-reader interoperability AND defines new env-var / config surfaces that affect plaintext-on-disk privacy posture. By CLAUDE.md's rubric (shared abstraction + wire/schema contract + security-relevant), this qualifies as load-bearing. Per CLAUDE.md, load-bearing changes require Chris human review before merge in addition to the routine Lead + Codex review path.
The pipeline exposes exactly four hooks: onRequest, onResponseStart, onStreamEvent, onResponse (pipeline.mjs:85-141). For streaming traffic — essentially all /v1/messages traffic — runOnResponse does NOT fire (server.mjs:160-188); streamResponse runs onStreamEvent per event and then ends the client response. The previously-named onResponseEnd does not exist.
The mirror runs as a stream-event accumulator with one buffered file write at message_stop — the staged user/tool-result records and the assistant record are concatenated into a single appendFile call (NDJSON, one record per line), so a write-time crash either persists all of them or none. This is the batching boundary; "buffered write" and "flush user records first, then the assistant record" refer to the same one file write, not two:
onRequest— extracts the resolved session id viaresolveSessionId(ctx.headers)(cache-telemetry.mjs:64-72); stashes it onctx.meta._mirrorSessionId. Examinesctx.body.messagesfor new user/tool-result content not already mirrored for this session (per the dedup state machine; closes B3); stages those as pending records onctx.meta._mirrorPendingUserRecordsfor emission alongside the response record. The dedup map is module-scope per session.onStreamEvent— accumulates per-message state acrossmessage_start→content_block_start→content_block_delta→content_block_stop→message_delta→message_stop. Onmessage_stop, builds the complete assistant record in CC's transcript envelope (B2 fix below), writes it + any staged user/tool-result records, and clears the per-message accumulator. The accumulator handlestext_delta,input_json_delta(fortool_useblocks), andthinking_delta(with optional exclusion per env var).onResponse— fires only on the non-streaming branch (rare for/v1/messagesbut possible for some routes). The same envelope shaper handles the already-complete envelope and writes one record.
No pipeline.mjs / stream.mjs / server.mjs modifications. The accumulator pattern is option-1-mechanics + option-2-cadence per Fable round 1's recommendation. The previously-listed "stream tee that buffers SSE chunks per-message-id" claim in the round-1 Scope section was a different framing for the same accumulator; the round-2 directive collapses them.
CC's transcript record shape, verified against ~/.claude/projects/<project>/<session-uuid>.jsonl:
Assistant record top-level keys: cwd, entrypoint, gitBranch, isSidechain, message, parentUuid, requestId, sessionId, timestamp, type, userType, uuid, version.
message nested keys: content, id, model, role, stop_details, stop_reason, stop_sequence, type, usage.
User record top-level keys (verified against 9,410 real user records on 2.1.148): same as assistant, plus isMeta, promptId, permissionMode, slug. User records do NOT carry requestId. isMeta appears only on ~13% of records (meta records, e.g. command output); non-meta user records may omit it. Tool-result user records additionally carry top-level toolUseResult and sourceToolAssistantUUID — these are CC-internal enriched-result objects that the proxy cannot reconstruct from request body alone; the mirror omits both and the CHANGELOG must call this out as a known limitation. message nested keys: content, role.
The mirror record adopts this envelope with proxy-derived substitutions:
{
"type": "assistant",
"uuid": "<sha256(sessionId+timestamp+messageId).slice(0,32) formatted as 8-4-4-4-12 dashed string (e.g. abcd1234-5678-90ab-cdef-1234567890ab) — same 32 hex chars, UUID-shape so shape-validating parsers accept; version/variant bits won't be RFC-valid which is honest about the synthetic origin>",
"parentUuid": "<previous mirror record uuid in this session, or null for first>",
"isSidechain": false,
"sessionId": "<resolved session id>",
"requestId": "<from response request-id header>",
"timestamp": "<ISO8601>",
"cwd": null,
"version": "<proxy version, e.g. \"cache-fix-proxy/4.2.0\">",
"userType": "external",
"entrypoint": "cache-fix-proxy-mirror",
"message": {
"type": "message",
"role": "assistant",
"id": "<from message_start>",
"model": "<from message_start>",
"content": [...],
"stop_reason": "<from message_delta>",
"stop_sequence": null,
"stop_details": "<from message_delta>",
"usage": "<from message_start + message_delta merge>"
},
"source": "cache-fix-proxy-mirror"
}Three load-bearing properties:
- Envelope shape matches CC's exactly — existing transcript readers (including
restore-claude-history-linux) dereferencingrecord.message.contentfind the expected content. The round-1 directive's top-level-fields shape would have returnedundefinedon every dereference. source: "cache-fix-proxy-mirror"is the additive provenance marker — the one field that distinguishes mirror records from canonical CC records.- Synthetic linear
uuid/parentUuidchain — each mirror record'suuidis deterministic from(sessionId, timestamp, messageId); itsparentUuidis the previous mirror record'suuidfor the same session. This produces a linear chain reconstructable in file order. CC's true UUID tree is not reconstructible from the proxy view (no parentUuid signal from the stream), so the linear approximation is an honest best-effort.
Image content blocks are mirrored as references (type: "image" with source.type: "base64" replaced by source: { type: "reference", sha256: "<64-hex>", media_type: "<from-block>" }). The byte content itself is not stored.
The CHANGELOG must call out three known limitations: the mirror's cwd is null (proxy does not know caller cwd), the uuid chain is synthetic (dash-formatted but not RFC-valid version/variant bits), and tool-result user records omit toolUseResult/sourceToolAssistantUUID (CC-internal enriched objects the proxy cannot reconstruct). Consumers depending on those for recovery should verify their behavior against mirror files specifically.
Per-message accumulator residence (closes Fable round-2 nit #5): the accumulator state for the in-flight assistant message lives on ctx.meta._mirrorAccumulator (request-scoped), NOT in module-scope state. This means an aborted stream cleans up for free when ctx is garbage-collected — no cross-request leak, no eviction story needed for interrupted streams.
Partial-flush on stream abort — DEFERRED to follow-up directive (post-0a59247). The round-4 directive listed stream-abort partial-flush (with stop_reason: "interrupted" and any content blocks accumulated so far) as in scope via CACHE_FIX_SESSION_MIRROR_FLUSH_INTERRUPTED (default true). Codex round 1 review on the implementation PR #221 flagged the env var as dead code — the proxy's existing client-close path (server.mjs:113-115) only aborts the upstream stream; there is no hook from which the extension could be invoked on client-abort. Wiring this requires a server.mjs change that this directive earlier claimed was unnecessary. The env var, helper, and partial-flush mechanics are explicitly cut from the v4.2.0 ship. They are tracked for a follow-up directive that takes the server.mjs change in scope.
CC re-sends the full conversation history in every request. Without dedup, the mirror would write O(n²) records over an n-turn session. The fix uses user-ordinal position tracking (not content-hash matching), with state advancement deferred until write time (not stage time) so failed requests do not silently lose user records.
Module-scope state: mirrorState.sessions = Map<sessionId, { mirroredUserMessageCount: number, mirroredToolResultIds: Set<string> }>.
mirroredUserMessageCount is the count of user-role messages already written to the mirror file for this session. This is a count in user-role-message coordinates, not in raw ctx.body.messages indices — the round-2 directive conflated the two coordinate systems, which produced incorrect dedup once assistant turns were interleaved (Codex round-1 blocker on PR #214).
onRequest logic (staging only — does NOT advance state):
- Resolve session id; load (or create) the session entry.
- Filter
ctx.body.messagesto user-role entries, preserving original-array order. Call this filtered listuserMessagesand its lengthuserMessagesLen. - For each
userMessages[k]wherek >= mirroredUserMessageCount, stage it as a pending user record. (The corresponding raw-array index is recorded only for the syntheticparentUuidchain — it is NOT used as a dedup key.) - For each
tool_resultcontent block inside those staged user messages, check itstool_use_idagainstmirroredToolResultIds. If already present, omit that block from the staged record (the surrounding user message may still stage if it has other un-mirrored blocks). Otherwise add the id to apendingToolResultIdsset carried onctx.meta, NOT yet to the session map. - Stage everything on
ctx.meta._mirrorPendingUserRecords(the records) +ctx.meta._mirrorPendingUserMessageCount(the valueuserMessagesLen— the new high-water if these records write successfully) +ctx.meta._mirrorPendingToolResultIds(the new ids).
onStreamEvent (at message_stop, after the assistant record is successfully written):
- Flush the staged user records first, then the assistant record (the linear
parentUuidchain orders correctly). - Only after all writes have succeeded: advance
mirroredUserMessageCountto_mirrorPendingUserMessageCountand unionmirroredToolResultIdswith_mirrorPendingToolResultIds.
If the request never reaches message_stop (upstream 529, mid-stream abort, 4xx, client close) the staged records on ctx.meta are discarded with the request and the session-map state is unchanged. CC's natural retry re-sends the same messages array; the same un-mirrored user-ordinal positions stage again, and the mirror records appear exactly once when the retry succeeds.
Walk-through verifying the coordinate fix (Codex round-1 blocker):
- Request 1
[u1]→userMessages = [u1],userMessagesLen = 1.mirroredUserMessageCountwas 0; stages u1 (k=0). On write success: count → 1. ✓ - Request 2
[u1, a1, u2]→userMessages = [u1, u2],userMessagesLen = 2. count is 1; stages userMessages[1] = u2 only. On write success: count → 2. ✓ - Request 3
[u1, a1, u2, a2, u3]→userMessages = [u1, u2, u3],userMessagesLen = 3. count is 2; stages userMessages[2] = u3 only. On write success: count → 3. 3 user records after 3 turns, not 4. ✓
The round-2 algorithm produced 4 records on request 3 (because raw array index 2 is u2 and index 4 is u3, both ≥ a count-of-2). The round-3 fix uses the same coordinate system end-to-end and yields the acceptance number.
The session map is bounded by CACHE_FIX_SESSION_MIRROR_MAX_SESSIONS (default 1024) with LRU eviction and a periodic 60s throttled sweep removing entries inactive past the retention window.
Why position-based, not hash-based (closes round-2 NB1): a single-value lastMirroredUserMessageHash re-iterating each request's full history will re-stage the entire history starting on turn 3 (u1's hash no longer matches the stored h(u2), so u1 re-stages, then u2 re-stages, etc.) — exactly the O(n²) failure mode this section exists to close. A Set of hashes would dedup more aggressively but wrongly collapse legitimately-repeated user texts (e.g. the literal "yes" sent at turn 2 and again at turn 9 — distinct records in CC's transcripts). Position-based tracking is correct on both axes: distinct positions always mirror, and CC's full-history replay naturally pads the prefix without re-mirroring it.
Why commit at write time, not stage time (closes round-2 NB2): committing state in onRequest while the actual write happens in onStreamEvent creates a silent data-loss path on failed requests — the session-map says "mirrored" while the user record never made it to disk. CC's retry, sending the same history, would then skip the lost message forever. Mirror is a data-loss-defense feature; introducing a data-loss path is incompatible with its mission.
Test fixtures (acceptance):
- 3-turn replay with single-message-per-turn growth: produces exactly 3 user records + 3 assistant records, no duplicates. (Fails with the round-1 algorithm at 5 user records by turn 3.)
- 200-turn replay: produces exactly 200 user + 200 assistant records, not ~20,000.
- Failed-request re-stage: request 3 reaches stage but the upstream fails before
message_stop→ session-map state unchanged. Request 4 (CC's retry) sends the same history → records stage again and the mirror contains them exactly once. Asserts no record loss AND no duplicates. - Legitimately-repeated user text:
"yes"at turns 2 and 9 produces two distinct mirror records (position-based dedup preserves them; hash-set dedup would have collapsed them — this fixture verifies the position-based design choice).
Session id resolution and filesystem encoding both reuse cache-telemetry.mjs:
- Session id resolved at
onRequesttime viaresolveSessionId(ctx.headers)(checks three session-id header variants). - Filesystem directory name is
sessionFilename(sessionId)(safe-charset passthrough,inv-<sha256[:16]>otherwise,unknownon null). This is the established convention for the same threat surface (path traversal, invalid-filename injection). - Sessionless requests bucket to
"unknown"and share a directory; this is best-effort and documented as such.
Mirror storage root: ~/.claude/session-mirrors/ (NOT ~/.cache-fix-proxy/; per Fable round 1, every proxy artifact lives under ~/.claude/ — established convention). Sub-directory is sessionFilename(sessionId); per-rotation file <timestamp>.jsonl.
MAX_BYTES rotation bounds the active file but accumulated files would grow without bound. The retention sweep closes this:
CACHE_FIX_SESSION_MIRROR_RETENTION_DAYS(default 30) — any mirror file (active or rotated) whose mtime is older than the threshold is unlinked. For an inactive session whose active file has not rotated for >RETENTION_DAYS, the active file itself is unlinked — sweep is not "rotated-files-only" (closes Codex round-1 attention).- Sweep runs at a 60s throttled cadence inside the mirror extension's
onResponse/onStreamEventhooks (lazy, not a separate timer) — modeled onsweepStaleSessionsincache-telemetry.mjs:132-156. - Empty directories are pruned after sweep.
- A throttled
sweepevent is logged tosession-mirror-events.jsonlwith the count of files unlinked.
Worst-case disk footprint (documented in docs/disk-usage.md):
active_sessions × MAX_BYTES × (1 + rotations_within_retention_days)
For default config (100 MB cap × 30-day retention × 32 sessions × typical 2 rotations/session/month): ~9.6 GB worst case, more typically <500 MB.
session-mirror-events.jsonl itself rotates at 5 MB single-tier per bootstrap-defense.rotateIfNeeded precedent — the round-1 directive omitted this and event-log growth was unbounded.
CC's own canonical transcript includes full thinking content blocks (verified against 2.1.148 transcripts). Default-include is fidelity-parity with the thing being backed up. This was previously framed in round 1 as "out of scope (deferred)" while describing a shipped default — the round-2 directive moves it into Scope explicitly with the parity argument named.
Opt-out: CACHE_FIX_SESSION_MIRROR_INCLUDE_THINKING=false excludes thinking content blocks from mirror records.
In scope:
- New extension
proxy/extensions/jsonl-session-mirror.mjsat order 720 (afterusage-log650 andrequest-log700; the round-1 directive's order claim was correct but its citation said "after usage-log at 700" — usage-log is 650, request-log is 700). - Session-mirror writer module flat at
proxy/session-mirror-writer.mjs(per the flatproxy/convention reaffirmed in PR #213 round 2; NOTproxy/lib/). - Reusable envelope shaper at
proxy/session-mirror-envelope.mjs(flat). - Per-session dedup state with LRU + throttled sweep.
- Per-message accumulator handling text / input_json / thinking deltas.
- Retention sweep with rotation-count discipline.
- Mirror records in CC's verified envelope shape with synthetic uuid chain.
- Env-vars:
CACHE_FIX_SESSION_MIRROR(on/off),CACHE_FIX_SESSION_MIRROR_DIR(override base path),CACHE_FIX_SESSION_MIRROR_MAX_BYTES(default 100 MB),CACHE_FIX_SESSION_MIRROR_RETENTION_DAYS(default 30),CACHE_FIX_SESSION_MIRROR_MAX_SESSIONS(default 1024),CACHE_FIX_SESSION_MIRROR_INCLUDE_THINKING(defaulttrue). - Failure-isolation test: writer throws (sync and async-rejected) mid-stream; assert client still receives complete SSE response.
- Format-round-trip test: feed a mirror file to
restore-claude-history-linux(or vendored fixture parser); assert successful reconstruction. needs-sim-validationlabel (mandatory merge gate): compare mirror records to CC's own transcript records for the same captured traffic; assert envelope-shape parity.
Out of scope (deferred):
- LRU file-handle cache (per Fable round 1, plain
appendFileper write is adequate at chat-traffic rates; the LRU earns its keep only under per-chunk writes, which are not the chosen design). - Recovery / restore tooling.
- GUI / dashboard.
- Cross-session indexing.
- Default-on flip — split into its own future directive once production validation data exists.
Stream-event accumulator with one buffered write at message_stop. Zero pipeline modifications. The restore-claude-history-linux round-trip test confirms format parity before merge. The needs-sim-validation capture confirms envelope-shape parity against real CC traffic before merge.
- Unit: envelope shaper — produces records matching CC's verified key set for assistant + user records; image-content blocks become references; synthetic uuid chain is deterministic.
- Unit: dedup state — user-ordinal high-water advancement is correct (the round-2 hash-based wording is gone; replaced with position-based); different tool-result ids mirror separately; session entry LRU eviction.
- Unit: retention sweep — files past
RETENTION_DAYSunlinked; empty directories pruned. - Unit:
sessionFilenamereuse — verify integration with the established helper (no re-implementation). - Integration: 3-turn replay — exactly 3 user + 3 assistant records, no duplicates. (Position-based dedup; round-1 hash algorithm produced 5 user records by turn 3.)
- Integration: 200-turn replay — total records = 400, not 20,400.
- Integration: failed-request re-stage — turn 3 stages then upstream fails before
message_stop→mirroredUserMessageCountunchanged. Turn 4 (CC's retry, same history) re-stages and the records appear exactly once. Asserts no record loss AND no duplicates. - Integration: legitimately-repeated user text — same
"yes"user text at turns 2 and 9 produces TWO distinct mirror records (position-based dedup; verifies the design choice over hash-set dedup which would collapse them). Integration: stream-abort partial flush —DEFERRED — partial-flush feature cut from v4.2.0 ship per Codex round-1 finding on PR #221.CACHE_FIX_SESSION_MIRROR_FLUSH_INTERRUPTED=true(default) and stream aborts mid-message → mirror contains a partial assistant record withstop_reason: "interrupted". With env var false, no partial record written.- Integration: writer throws synchronously mid-stream → client still receives complete SSE response.
- Integration: writer rejects asynchronously mid-stream → no
unhandledRejection; client still receives complete SSE response; error logged tosession-mirror-events.jsonl. - Integration: env-var off → no mirror writes occur.
- Integration: thinking exclusion → mirror records contain everything except
thinkingblocks when set. - Integration: rotation at
MAX_BYTES→ next record opens new file; rotation event logged. - Integration: rotation count under retention → old rotated files unlinked when past
RETENTION_DAYS. - Integration: path-traversal session id (
../../etc/passwd) → mirror writes underinv-<hash>directory, not anywhere up the tree. - Format-round-trip: feed a mirror file to
restore-claude-history-linux(or vendored fixture parser); assert successful reconstruction of conversation order and content. - Sim validation: capture real CC traffic on a test session; compare mirror records to CC's canonical transcript records for the same session; assert envelope-shape parity (all expected top-level keys present,
messagenesting correct, content blocks structurally identical).
Created:
proxy/extensions/jsonl-session-mirror.mjsproxy/session-mirror-writer.mjs(flatproxy/)proxy/session-mirror-envelope.mjs(flatproxy/; reusable shaper)test/extensions/jsonl-session-mirror.test.mjstest/session-mirror-writer.test.mjstest/session-mirror-envelope.test.mjstest/fixtures/multi-turn-session-replay.jsontest/fixtures/cc-transcript-shape-snapshot.json(captured from a real CC 2.1.148 transcript for envelope-parity tests)docs/disk-usage.md(mirror disk footprint, configuration, rotation, retention)
Modified:
proxy/extensions.json— register at order 720, default-internal-disabled.CHANGELOG.md— v4.2.0 entry citing CC#66734 + CC#66486.README.md— extension list addition + "session backup" feature callout.docs/extensions.md— extension reference.
Out of scope (no changes):
proxy/pipeline.mjs,proxy/stream.mjs,proxy/server.mjs— no pipeline modifications.- No
proxy/lib/directory introduced.
- Hook surface uses
onRequest+onStreamEvent+onResponseonly; noonResponseEndreferenced. - Envelope shape matches CC's verified key set for assistant + user records (verified by fixture-snapshot test).
-
restore-claude-history-linuxround-trip test passes against a generated mirror file. - Synthetic uuid chain is deterministic from
(sessionId, timestamp, messageId). - Image content blocks mirrored as references; bytes never persisted.
- Dedup uses position-based
mirroredUserMessageCount(user-role-message ordinal coordinates, NOT rawmessages[]indices); advances ONLY atmessage_stopwrite success; failed-request re-stage test passes. - Synthetic
uuidis dash-formatted (8-4-4-4-12) so shape-validating parsers accept it. - Per-message accumulator lives on
ctx.meta._mirrorAccumulator(request-scoped); aborted streams free state via ctx GC.Partial-flush deferred to follow-up directive (cut from v4.2.0 ship per Codex round-1 on PR #221).CACHE_FIX_SESSION_MIRROR_FLUSH_INTERRUPTEDpartial-flush behavior tested both directions. - User record key list matches verified 2.1.148 transcript shape:
isMetaoptional,permissionMode+slugincluded,requestIdabsent on user records, tool-result records omittoolUseResult/sourceToolAssistantUUID(CHANGELOG caveat). -
sessionFilenamefromcache-telemetry.mjsreused for directory name; sessionless requests bucket to"unknown". - Retention sweep unlinks files past
RETENTION_DAYS; empty directories pruned. -
session-mirror-events.jsonlrotates at 5 MB. - Failure-isolation: writer throws sync AND async-reject tests both pass without breaking client response.
- Storage root is
~/.claude/session-mirrors/, NOT~/.cache-fix-proxy/. - No
proxy/lib/directory introduced. - Default-off in v4.2.0 AND v4.3.0 (per the privacy-posture argument).
-
needs-sim-validationlabel present; sim results attached as PR comment before merge. - CHANGELOG cites CC#66734 + CC#66486 explicitly; documents three caveats: dash-formatted-but-not-RFC-valid uuid, null cwd, omitted toolUseResult on tool-result records.
- Replacing CC's canonical transcript. The mirror is a backup; CC's transcript remains primary when it survives.
- Mirroring server-side classifier verdicts, model-routing decisions, or any non-content surface.
- Compression of mirror files.
- Encryption at rest.
- Cross-session indexing.
- Default-on flip — separate future directive.
- LRU file-handle cache.
— AI Team Lead