Review follow-ups on the salvage (#94914): the delete path derives its
scope from the removed row while saves derive from ownerRoute — a shape
drift orphaned the entry until LRU eviction. Deletes now sweep every
scope of the stored id (safe: deleted ids are never reused), while the
failed-resume path keeps the exact-scope drop (a same-id twin in another
profile must keep its tail). purgeLegacyV1 latches only after a
completed sweep so a mid-sweep throw retries next touch.
Stored session ids are only unique within one profile's state.db, and
localStorage survives profile switches in the same window. The durable
transcript-tail cache keyed entries by bare stored id, so a tail cached
while working in profile A was painted against profile B's backend after
a switch; that backend never held the session and retried it on every
wake ('session not found'), matching the desktop.log pattern of repeated
session.reclaimed (ws_orphan_reap) events for ids that live in another
profile's state.db (#94828).
Scope every entry by its owner — the same {connectionId, profile} shape
the REST layer already threads through getLatestSessionMessages and the
in-memory twin stores in TranscriptTailState:
- key entries under v2:[connectionId, profile, storedId]; loads only
return a tail saved under the SAME scope
- thread the session's owning scope through all call sites
(resumeSession's warm-path save + cold-paint load/rollback drop,
final save, removeSession's delete drop)
- sweep pre-scoping v1 entries once per window: a bare-id key cannot be
attributed to an owner, so it must never paint again
- dropTranscriptTail with a scope leaves other backends' same-id tails
intact
Two follow-up layers on top of the salvaged runtime-boundary guard (#87871):
- coalesceToolOnlyAssistants now folds via concatToolPartsUnique, dropping an
incoming tool-call part whose toolCallId the predecessor already carries.
Two individually-clean rows sharing an id (structural carry-over re-attaching
a cached row's calls) no longer become one crashing message — and no longer
render the same call twice. Root-cause analysis by @marketing2981 (#87857).
- loadTranscriptTail repairs a poisoned persisted tail on read; installs
already carrying a duplicate in hermes.transcript-tail.v1:* stop
crash-looping after upgrade instead of re-deriving the same collision
every launch.
- Regression tests for all three layers, incl. the end-to-end repository link
test (from #92093 by @RasputinKaiser) and the cross-message ids-stay-
untouched contract (per-response tool numbering, e.g. Kimi — #90545 by
@M7MMAD-OMAR). Each test sabotage-verified against its reverted layer.
Second layer of the "make hydration feel instant" work (on top of the
paint-first wait): persist a bounded tail (40 msgs / 256KB / 50-session
LRU) of every reconciled transcript in localStorage, keyed by durable
stored-session id.
- Cold resume paints the cached tail immediately — the wake is visually
complete before any network I/O; the REST prefetch / runtime resume
reconcile the authoritative transcript over it when they land.
- The cached paint is DISPLAY-ONLY: reconciliation treats the view as
empty (viewMessagesForReconcile), so authoritative content replaces the
provisional paint wholesale — no grafting onto stale rows. Failure
latches also treat it as empty, so a cached paint can never mask a
genuinely stranded resume; a resume that proves the session empty rolls
the paint back and drops the poisoned entry.
- Saves happen only post-reconcile (cold path + warm activate path);
deletes drop the entry; a gateway/mode re-home wipes the cache (another
backend can recycle stored ids).
Sabotage-proven: disabling the cache load fails 5/7 cache tests; the
display-only contract is covered by the existing resume reconciliation
suite (663 tests green).