refactor(desktop): one live-tail vocabulary for transcript reconciliation

Two fixes landed overlapping helpers on the same statement: the mid-turn
reply guard grew `isLiveProjectionRow` / `hasStreamedContent`, while the
inflight-dump guard grew `isLiveTailRow` / `hasStructuralParts`. Two
definitions of "is this row live" and "does it carry content" in one
function is how the next change silently reshapes one of them.

Collapse to a single module-level pair. `isLiveTailRow` now covers pending,
stream ids, inflight projections and sealed interim rows, so the reply guard
also stops treating an interim row as committed history; `hasStreamedContent`
is defined in terms of `hasStructuralParts`. Both text-extension checks route
through `isStrictAnswerTextExtension` rather than a bare `startsWith`.

Also hoists the live-tail lookup out of an inline IIFE and fixes the lint
warnings it carried.

Co-authored-by: 686f6c61 <github@00b.tech>
This commit is contained in:
Brooklyn Nicholson
2026-08-03 08:13:57 -05:00
parent dbefd27ffb
commit 7a4d047e37
@@ -52,6 +52,20 @@ function hasStructuralParts(message: ChatMessage): boolean {
return message.parts.some(part => part.type === 'reasoning' || part.type === 'tool-call')
}
/**
* A live-turn row — the gateway's text-only `inflight` projection, a
* still-streaming local bubble, or an interim row sealed inside the running
* turn — as opposed to a committed transcript row.
*/
function isLiveTailRow(message: ChatMessage): boolean {
return (
message.pending === true ||
message.id.startsWith('assistant-stream-') ||
message.id.startsWith('inflight-assistant-') ||
message.interim === true
)
}
/**
* True when `next` is a pure forward extension of the previous *answer* text.
* Empty previous answer never accepts a dump as an extension — that is how the
@@ -301,11 +315,6 @@ export function reconcileResumeMessages(nextMessages: ChatMessage[], previousMes
// live is not enough — after compression a new live assistant can share a
// role ordinal with an unrelated historical structured row and must not
// inherit its reasoning/tool parts (#76444 review / salvage).
const isLiveTailRow = (row: ChatMessage): boolean =>
Boolean(row.pending) ||
row.id.startsWith('assistant-stream-') ||
Boolean(row.interim)
const sameTurn =
sameText ||
(nextText.length > 0 && previousTrimmed.length > 0 && isStrictAnswerTextExtension(nextText, previousTrimmed)) ||
@@ -394,19 +403,12 @@ const isGatewaySystemMarker = (message: ChatMessage): boolean =>
message.role === 'user' && chatMessageText(message).trimStart().startsWith('[System:')
/**
* Does the local pending row carry anything a viewer would miss — streamed
* text, reasoning, or tool calls? An empty inflight shell carries none of it.
* Does the row carry anything a viewer would miss — streamed answer text, or
* the reasoning / tool-call structure the gateway's flat dump cannot express?
* An empty inflight shell carries none of it.
*/
const hasStreamedContent = (message: ChatMessage): boolean =>
chatMessageText(message).trim().length > 0 ||
message.parts.some(part => part.type === 'tool-call' || part.type === 'reasoning')
/**
* A live-turn projection row — the gateway's text-only `inflight` snapshot or a
* still-streaming local bubble — as opposed to a committed transcript row.
*/
const isLiveProjectionRow = (message: ChatMessage): boolean =>
message.pending === true || message.id.startsWith('assistant-stream-') || message.id.startsWith('inflight-assistant-')
chatMessageText(message).trim().length > 0 || hasStructuralParts(message)
/**
* May the cached local row stand in for this authoritative assistant?
@@ -419,11 +421,11 @@ const isLiveProjectionRow = (message: ChatMessage): boolean =>
* from the local partial would hide the error and mark the turn healthy again.
*/
const localPendingSupersedes = (local: ChatMessage, authoritative: ChatMessage): boolean => {
if (local.role !== 'assistant' || !isLiveProjectionRow(local)) {
if (local.role !== 'assistant' || !isLiveTailRow(local)) {
return false
}
if (!isLiveProjectionRow(authoritative) || authoritative.error) {
if (!isLiveTailRow(authoritative) || authoritative.error) {
return false
}
@@ -435,7 +437,7 @@ const localPendingSupersedes = (local: ChatMessage, authoritative: ChatMessage):
const localText = chatMessageText(local).trim()
return localText.length > authoritativeText.length && localText.startsWith(authoritativeText)
return localText.length > authoritativeText.length && isStrictAnswerTextExtension(localText, authoritativeText)
}
/**
@@ -705,28 +707,32 @@ export function appendLiveSessionProjection(
// Only inspect the live tail after the latest user run — never a completed
// historical tool-bearing reply earlier in the transcript (review feedback).
const liveStreamId = `assistant-stream-${sessionId}`
const liveAssistantOfCurrentTurn = (() => {
const liveAssistantOfCurrentTurn = ((): ChatMessage | null => {
const byStreamId = messages.find(message => message.id === liveStreamId)
if (byStreamId) {
return byStreamId
}
// Assistants after the latest user row belong to this turn's tail.
if (latestUserIndex < 0) {
return null
}
for (let index = messages.length - 1; index > latestUserIndex; index -= 1) {
if (messages[index].role === 'assistant') {
return messages[index]
}
}
return null
})()
const turnAlreadyStructured = Boolean(
liveAssistantOfCurrentTurn &&
hasStructuralParts(liveAssistantOfCurrentTurn) &&
(liveAssistantOfCurrentTurn.pending ||
liveAssistantOfCurrentTurn.id === liveStreamId ||
liveAssistantOfCurrentTurn.interim)
hasStructuralParts(liveAssistantOfCurrentTurn) &&
isLiveTailRow(liveAssistantOfCurrentTurn)
)
if (inflightAssistant || inflightStreaming || inflightError || (inflightUser && queuedUser)) {