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:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user