fix(chat): keep restored legacy prompts out of the live tail (#1260)
Co-authored-by: m57 <41645758+Hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
35e6ebc8d5
commit
4d1720ad4d
5 files changed
+295
-13
No files matched your search
@@ -101,6 +101,12 @@ data class ChatMessage(
|
||||
val messageProvenance: MessageProvenance = MessageProvenance.UNKNOWN,
|
||||
/** Read from history, not observed live in this view. Never persisted as delivery state. */
|
||||
val isHistoricalCache: Boolean = false,
|
||||
/**
|
||||
* Legacy USER restored without a canonical identity or a current send receipt.
|
||||
* Placement only: keep it outside the live tail without asserting delivery.
|
||||
* Transient; UNKNOWN provenance and the persisted row remain unchanged.
|
||||
*/
|
||||
val isRestoredUnconfirmed: Boolean = false,
|
||||
)
|
||||
|
||||
/** Cached REST rows already carry their canonical identity in the persisted primary key. */
|
||||
|
||||
@@ -335,6 +335,7 @@ internal fun dedupeCachedMessages(
|
||||
restId = it.canonicalRestId,
|
||||
completionId = message.completionId ?: it.completionId,
|
||||
displayKind = message.displayKind ?: it.displayKind,
|
||||
isRestoredUnconfirmed = false,
|
||||
)
|
||||
} ?: message
|
||||
}
|
||||
@@ -403,6 +404,9 @@ internal fun mergeCachedTranscriptPage(
|
||||
restId = match.canonicalRestId ?: message.canonicalRestId,
|
||||
completionId = match.completionId ?: message.completionId,
|
||||
displayKind = match.displayKind ?: message.displayKind,
|
||||
isRestoredUnconfirmed =
|
||||
match.isRestoredUnconfirmed && message.isRestoredUnconfirmed &&
|
||||
match.canonicalRestId == null && message.canonicalRestId == null,
|
||||
)
|
||||
}.toMap()
|
||||
val resolvedOrders =
|
||||
@@ -437,6 +441,7 @@ internal fun mergeTranscriptWithLive(
|
||||
restId = (message.canonicalRestId ?: match.canonicalRestId).takeUnless { it == match.id },
|
||||
displayKind = message.displayKind ?: match.displayKind,
|
||||
isHistoricalCache = false,
|
||||
isRestoredUnconfirmed = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -464,6 +469,7 @@ internal fun mergeTranscriptWithLive(
|
||||
tokenCount = message.tokenCount ?: match.tokenCount,
|
||||
completionId = message.completionId ?: match.completionId,
|
||||
isHistoricalCache = false,
|
||||
isRestoredUnconfirmed = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -482,6 +488,7 @@ internal fun mergeTranscriptWithLive(
|
||||
content = mergedContent,
|
||||
completionId = message.completionId ?: match?.completionId,
|
||||
isHistoricalCache = false,
|
||||
isRestoredUnconfirmed = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -533,9 +540,10 @@ private fun List<ChatMessage>.inTranscriptOrder(
|
||||
?: latestCanonical
|
||||
}
|
||||
if (hasPendingPredecessor) pendingOrderByLocal[message.id] = pendingLocalOrder ?: Long.MAX_VALUE
|
||||
} else if (message.isHistoricalCache) {
|
||||
} else if (message.isHistoricalCache || message.isRestoredUnconfirmed) {
|
||||
// Room groups UUID-only rows after all confirmed rows; that predecessor is
|
||||
// not a chronological anchor. Keep unresolved history before the server window.
|
||||
// not a chronological anchor. Restored legacy rows stay before the server
|
||||
// window too, without promoting their uncertain delivery to confirmed history.
|
||||
localAnchors[message.id] = beforeCanonical
|
||||
} else {
|
||||
hasPendingPredecessor = true
|
||||
|
||||
@@ -3334,6 +3334,14 @@ class ChatViewModel(
|
||||
role = MessageRole.USER,
|
||||
content = displayContent,
|
||||
tokenCount = TokenEstimator.estimate(displayContent).takeIf { it > 0 },
|
||||
// Follow-up to #1253: stripped /queue text is an unconfirmed prompt,
|
||||
// not a permanently-local command or an ambiguous legacy cache row.
|
||||
messageProvenance =
|
||||
if (result is SlashResult.QueuePrompt && displayContent != command) {
|
||||
MessageProvenance.LOCAL_PENDING
|
||||
} else {
|
||||
MessageProvenance.UNKNOWN
|
||||
},
|
||||
)
|
||||
val sessionId = _uiState.value.currentSessionId
|
||||
|
||||
@@ -4417,15 +4425,24 @@ class ChatViewModel(
|
||||
// Receipt bubbles can be absent after rejection/reconnect. Offer their identities
|
||||
// to the canonical matcher without displaying unconfirmed messages again.
|
||||
val visibleIds = snapshot.messages.map { it.id }.toSet()
|
||||
val currentSendScope = sendScope()
|
||||
val receiptRows =
|
||||
sendStore
|
||||
.all()
|
||||
.filter { it.scope == currentSendScope && it.sessionId == snapshot.currentSessionId }
|
||||
val receiptBackedIds =
|
||||
receiptRows
|
||||
.filter {
|
||||
it.state in
|
||||
setOf(PendingSendState.SENDING, PendingSendState.ACCEPTED, PendingSendState.UNKNOWN)
|
||||
}.mapTo(mutableSetOf()) { it.id }
|
||||
val receiptCandidates =
|
||||
if (cached) {
|
||||
emptyList()
|
||||
} else {
|
||||
sendStore
|
||||
.all()
|
||||
receiptRows
|
||||
.filter {
|
||||
it.scope == sendScope() && it.sessionId == snapshot.currentSessionId &&
|
||||
it.id !in visibleIds &&
|
||||
it.id !in visibleIds &&
|
||||
it.state in
|
||||
setOf(PendingSendState.SENDING, PendingSendState.ACCEPTED, PendingSendState.UNKNOWN)
|
||||
}.map {
|
||||
@@ -4444,8 +4461,8 @@ class ChatViewModel(
|
||||
val mapped = mapPage(snapshot.messages)
|
||||
val currentById = snapshot.messages.associateBy { it.id }
|
||||
// Durable provenance distinguishes new unsent prompts after process death.
|
||||
// Legacy UUID-only USER rows remain conservatively unconfirmed: UNKNOWN is
|
||||
// not evidence that the server delivered them.
|
||||
// Legacy UUID-only USER rows remain unconfirmed, but restored placement is
|
||||
// independent of delivery: UNKNOWN must not pin old prompts after fresh replies.
|
||||
val page =
|
||||
if (cached) {
|
||||
mapped.map { message ->
|
||||
@@ -4459,6 +4476,24 @@ class ChatViewModel(
|
||||
message.role == MessageRole.USER -> false
|
||||
else -> true
|
||||
},
|
||||
isRestoredUnconfirmed =
|
||||
when {
|
||||
message.role != MessageRole.USER ||
|
||||
message.canonicalRestId != null ||
|
||||
message.isPermanentlyLocal() ||
|
||||
message.messageProvenance == MessageProvenance.LOCAL_PENDING ||
|
||||
message.id in receiptBackedIds -> {
|
||||
false
|
||||
}
|
||||
|
||||
currentById[message.id] != null -> {
|
||||
currentById.getValue(message.id).isRestoredUnconfirmed
|
||||
}
|
||||
|
||||
else -> {
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user