refactor(desktop): name the tail entry's route and owner

TranscriptTailState.profile was documented as the owning profile but now
holds the request ROUTE (the owner is the map key). Rename the
recordTranscriptTail parameters to route/owner, fix the field doc, say why
a bare-string scope still defaults to 'default' while an object scope does
not, and correct transcript-tail-cache's claim that it mirrors the
in-memory key (it keys by request scope and the two stores never cross-read).
This commit is contained in:
kshitijk4poor
2026-09-11 11:52:35 +05:30
committed by kshitij
parent a0977b0a3c
commit fd9c52ae1e
3 changed files with 21 additions and 16 deletions
+5 -5
View File
@@ -459,9 +459,9 @@ export function getLatestSessionMessages(
// (ambient, profile string, or explicit pin). Otherwise refreshes create
// duplicate tail entries and "Show earlier" cannot resolve the loaded tail.
// Capture before awaiting: the active gateway may change during the read.
const tailScope = { ...connectionScoped(), ...sessionScoped(profile) }
const route = { ...connectionScoped(), ...sessionScoped(profile) }
// Normalize only the lookup key; backfill must replay the original route.
const ownerScope = { ...tailScope, connectionId: tailScope.connectionId || ambientOwnerConnectionId() }
const owner = { ...route, connectionId: route.connectionId || ambientOwnerConnectionId() }
// includeCompacted: durable display history must include rows preserved by
// in-place compaction (active=0, compacted=1); without them the transcript
@@ -484,11 +484,11 @@ export function getLatestSessionMessages(
// untagged read there lands on the ambient profile (`'default'` when
// none is active). `null` is a custom HERMES_HOME and stays distinct.
const servedProfile = page.profile === undefined ? getApiRequestProfile() || 'default' : page.profile
const resolvedOwnerScope = { ...ownerScope, profile: ownerScope.profile || servedProfile }
recordTranscriptTail(id, page, tailScope, resolvedOwnerScope)
const resolvedOwner = { ...owner, profile: owner.profile || servedProfile }
recordTranscriptTail(id, page, route, resolvedOwner)
if (page.session_id && page.session_id !== id) {
recordTranscriptTail(page.session_id, page, tailScope, resolvedOwnerScope)
recordTranscriptTail(page.session_id, page, route, resolvedOwner)
}
return page
@@ -23,8 +23,10 @@ import { withUniqueToolCallIdsWithinMessage } from '@/lib/chat-messages'
// profile switches in the same window: an unscoped key let profile A's
// tail be painted against profile B's backend, which then retried a
// session id that does not exist there ("session not found") on every
// wake. The scope mirrors the in-memory twin's transcriptTailKey
// (transcript-tail.ts). Entries persisted before scoping shipped (v1,
// wake. The scope is the REQUEST scope the resume path passes (always a
// named profile); the in-memory twin (transcript-tail.ts) keys by the
// resolved owner instead and the two are never cross-read. Entries
// persisted before scoping shipped (v1,
// bare-id keys) carry no owner and are unreachable by construction —
// they are swept once per window so a stale tail can never paint again.
+12 -9
View File
@@ -23,8 +23,10 @@ export interface TranscriptTailState {
/** The last hydration page was exactly the page limit, so older rows
* likely exist beyond what the in-memory store holds. */
possiblyTruncated: boolean
/** Owning profile captured at hydration time, so a later backfill routes
* its REST read to the same backend that served the tail. */
/** The request route captured at hydration time, replayed verbatim by a
* later backfill so it reaches the backend that served the tail. The
* resolved OWNER is the map key, not this field: an ambient read must stay
* ambient even once the server has named its profile. */
profile?: TranscriptProfileScope
}
@@ -43,6 +45,8 @@ let transcriptTailOrder: string[] = []
type TailPage = Pick<SessionMessagesResponse, 'messages' | 'pagination'>
function normalizedScope(profile?: TranscriptProfileScope): { connectionId: string; profile: string } | null {
// A bare string is the legacy "named profile" spelling: it always names a
// profile, so empty means the default one.
if (typeof profile === 'string') {
return { connectionId: '', profile: profile.trim() || 'default' }
}
@@ -115,21 +119,20 @@ function setTranscriptTailEntry(key: string, state: TranscriptTailState): void {
$transcriptTailBySessionId.set(next)
}
/** Record the outcome of a tail hydration (`getLatestSessionMessages`). */
/** Record the outcome of a tail hydration (`getLatestSessionMessages`).
* `route` is what the request was sent with; `owner` is the resolved backend
* the entry is keyed under (defaults to the route when they coincide). */
export function recordTranscriptTail(
storedSessionId: string,
page: TailPage,
profile?: TranscriptProfileScope,
ownerScope: TranscriptProfileScope | undefined = profile
route?: TranscriptProfileScope,
owner: TranscriptProfileScope | undefined = route
): void {
if (!storedSessionId) {
return
}
// Keep request routing separate from the resolved owner used for lookup:
// backfill must replay an ambient read even when the server names its profile.
const key = transcriptTailKey(storedSessionId, ownerScope)
setTranscriptTailEntry(key, tailStateFromPage(page, profile))
setTranscriptTailEntry(transcriptTailKey(storedSessionId, owner), tailStateFromPage(page, route))
}
/** Advance the bookkeeping after one older backfill page landed. */