Merge pull request #1242 from Hy4ri/fix/issue-1241-midturn-steer-ordering
fix(chat): preserve midturn steer and session marker ordering (#1241)
This commit is contained in:
@@ -53,7 +53,12 @@ interface ChatMessageDao {
|
|||||||
suspend fun upsert(message: ChatMessageEntity) {
|
suspend fun upsert(message: ChatMessageEntity) {
|
||||||
val existing = getMessage(message.id)
|
val existing = getMessage(message.id)
|
||||||
val restId = message.restId ?: existing?.restId
|
val restId = message.restId ?: existing?.restId
|
||||||
val canonicalOrder = canonicalMessageOrder(restId ?: message.id, message.sessionId)
|
val canonicalOrder =
|
||||||
|
if (message.isSessionStartMarker()) {
|
||||||
|
-1L
|
||||||
|
} else {
|
||||||
|
canonicalMessageOrder(restId ?: message.id, message.sessionId)
|
||||||
|
}
|
||||||
writeMessage(
|
writeMessage(
|
||||||
message.copy(
|
message.copy(
|
||||||
restId = restId,
|
restId = restId,
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ data class ChatMessageEntity(
|
|||||||
val sortOrder: Long = 0,
|
val sortOrder: Long = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun ChatMessageEntity.isSessionStartMarker(): Boolean =
|
||||||
|
role == "SYSTEM" && (content == "Session created" || content == "Session branched")
|
||||||
|
|
||||||
/** Only the exact session prefix and a nonnegative decimal suffix identify a canonical row. */
|
/** Only the exact session prefix and a nonnegative decimal suffix identify a canonical row. */
|
||||||
internal fun canonicalMessageOrder(
|
internal fun canonicalMessageOrder(
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ fun ChatMessageEntity.toUiModel(): ChatMessage =
|
|||||||
tps = tps,
|
tps = tps,
|
||||||
completionId = completionId,
|
completionId = completionId,
|
||||||
restId = restId,
|
restId = restId,
|
||||||
localOrder = sortOrder.takeIf { sortGroup == 1 && it > 0 },
|
localOrder = sortOrder.takeIf { sortGroup == 1 && it > 0 && !isSessionStartMarker() },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ChatMessage.toEntity(sessionId: String): ChatMessageEntity =
|
fun ChatMessage.toEntity(sessionId: String): ChatMessageEntity =
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ private const val MAX_ITERATIONS_SYSTEM_MARKER =
|
|||||||
*/
|
*/
|
||||||
internal fun ChatMessage.isUserTurnBoundary(): Boolean =
|
internal fun ChatMessage.isUserTurnBoundary(): Boolean =
|
||||||
role == MessageRole.USER &&
|
role == MessageRole.USER &&
|
||||||
displayKind == null &&
|
(displayKind == null || displayKind == "steer") &&
|
||||||
!content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
!content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -164,7 +164,11 @@ internal fun mapServerMessages(
|
|||||||
// Keep the actual @file:/@image: token here: unlike the dedupe path,
|
// Keep the actual @file:/@image: token here: unlike the dedupe path,
|
||||||
// REST-only hydration cannot prove that reference was not user-authored.
|
// REST-only hydration cannot prove that reference was not user-authored.
|
||||||
var finalContent =
|
var finalContent =
|
||||||
if (role == MessageRole.USER) stripGatewayAttachedContext(rawContent) else rawContent
|
if (role == MessageRole.USER) {
|
||||||
|
stripGatewayAttachedContext(stripGatewaySteerWrapper(rawContent))
|
||||||
|
} else {
|
||||||
|
rawContent
|
||||||
|
}
|
||||||
var attachments: List<Attachment>? = null
|
var attachments: List<Attachment>? = null
|
||||||
if (role == MessageRole.ASSISTANT && rawContent.contains("MEDIA:")) {
|
if (role == MessageRole.ASSISTANT && rawContent.contains("MEDIA:")) {
|
||||||
val items = HostMediaExtractor.extract(rawContent)
|
val items = HostMediaExtractor.extract(rawContent)
|
||||||
|
|||||||
@@ -14,6 +14,30 @@ private val CONTEXT_REF_RE =
|
|||||||
"""@(file|folder|url|image|tool|terminal):(?:"[^"\n]+"|'[^'\n]+'|`[^`\n]+`|\S+)""",
|
"""@(file|folder|url|image|tool|terminal):(?:"[^"\n]+"|'[^'\n]+'|`[^`\n]+`|\S+)""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val OUT_OF_BAND_START_RE =
|
||||||
|
Regex("""\[OUT-OF-BAND USER MESSAGE[^\n\]]*\]\s*""", RegexOption.IGNORE_CASE)
|
||||||
|
private val OUT_OF_BAND_END_RE =
|
||||||
|
Regex("""\s*\[/OUT-OF-BAND USER MESSAGE\]""", RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a gateway-generated `[OUT-OF-BAND USER MESSAGE ...]` wrapper from
|
||||||
|
* mid-turn steering messages while preserving the user-authored prompt.
|
||||||
|
*/
|
||||||
|
internal fun stripGatewaySteerWrapper(content: String): String {
|
||||||
|
val trimmed = content.trimStart()
|
||||||
|
val startMatch = OUT_OF_BAND_START_RE.find(trimmed) ?: return content
|
||||||
|
if (startMatch.range.first != 0) return content
|
||||||
|
val afterStart = trimmed.substring(startMatch.range.last + 1)
|
||||||
|
val endMatch = OUT_OF_BAND_END_RE.find(afterStart)
|
||||||
|
val unwrapped =
|
||||||
|
if (endMatch != null) {
|
||||||
|
afterStart.substring(0, endMatch.range.first)
|
||||||
|
} else {
|
||||||
|
afterStart
|
||||||
|
}
|
||||||
|
return unwrapped.trim()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a gateway-generated `--- Attached Context ---` suffix while
|
* Remove a gateway-generated `--- Attached Context ---` suffix while
|
||||||
* preserving the user-authored portion of the message.
|
* preserving the user-authored portion of the message.
|
||||||
@@ -231,7 +255,7 @@ internal fun matchTranscriptMessages(
|
|||||||
* run_agent). Blank lines left behind are dropped too.
|
* run_agent). Blank lines left behind are dropped too.
|
||||||
*/
|
*/
|
||||||
internal fun stripAttachmentRefLines(content: String): String =
|
internal fun stripAttachmentRefLines(content: String): String =
|
||||||
stripGatewayAttachedContext(content)
|
stripGatewayAttachedContext(stripGatewaySteerWrapper(content))
|
||||||
.lines()
|
.lines()
|
||||||
.map { it.trim() }
|
.map { it.trim() }
|
||||||
.filterNot { line ->
|
.filterNot { line ->
|
||||||
@@ -276,7 +300,11 @@ internal fun dedupeCachedMessages(
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
return unique.filterNot { it.id in echoes }.map { message ->
|
return unique.filterNot { it.id in echoes }.map { message ->
|
||||||
aliases[message.id]?.let {
|
aliases[message.id]?.let {
|
||||||
message.copy(restId = it.canonicalRestId, completionId = message.completionId ?: it.completionId)
|
message.copy(
|
||||||
|
restId = it.canonicalRestId,
|
||||||
|
completionId = message.completionId ?: it.completionId,
|
||||||
|
displayKind = message.displayKind ?: it.displayKind,
|
||||||
|
)
|
||||||
} ?: message
|
} ?: message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,6 +334,7 @@ internal fun mergeCachedTranscriptPage(
|
|||||||
id = match.id,
|
id = match.id,
|
||||||
restId = message.canonicalRestId ?: match.canonicalRestId,
|
restId = message.canonicalRestId ?: match.canonicalRestId,
|
||||||
completionId = match.completionId ?: message.completionId,
|
completionId = match.completionId ?: message.completionId,
|
||||||
|
displayKind = message.displayKind ?: match.displayKind,
|
||||||
)
|
)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
return (
|
return (
|
||||||
@@ -333,6 +362,7 @@ internal fun mergeTranscriptWithLive(
|
|||||||
match?.role == MessageRole.USER -> {
|
match?.role == MessageRole.USER -> {
|
||||||
match.copy(
|
match.copy(
|
||||||
restId = (message.canonicalRestId ?: match.canonicalRestId).takeUnless { it == match.id },
|
restId = (message.canonicalRestId ?: match.canonicalRestId).takeUnless { it == match.id },
|
||||||
|
displayKind = message.displayKind ?: match.displayKind,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +408,16 @@ private fun List<ChatMessage>.inTranscriptOrder(): List<ChatMessage> =
|
|||||||
.thenBy { it.canonicalOrder ?: it.localOrder ?: Long.MAX_VALUE },
|
.thenBy { it.canonicalOrder ?: it.localOrder ?: Long.MAX_VALUE },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun ChatMessage.isSessionStartMarker(): Boolean =
|
||||||
|
role == MessageRole.SYSTEM && (content == "Session created" || content == "Session branched")
|
||||||
|
|
||||||
private val ChatMessage.canonicalOrder: Long?
|
private val ChatMessage.canonicalOrder: Long?
|
||||||
get() = if (localOrder != null && restId == null) null else canonicalRestId?.substringAfterLast('-')?.toLongOrNull()
|
get() =
|
||||||
|
when {
|
||||||
|
isSessionStartMarker() -> -1L
|
||||||
|
localOrder != null && restId == null -> null
|
||||||
|
else -> canonicalRestId?.substringAfterLast('-')?.toLongOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
/** Retain the canonical trace once, without copying it across a user/system/tool boundary. */
|
/** Retain the canonical trace once, without copying it across a user/system/tool boundary. */
|
||||||
private fun List<ChatMessage>.reconcileReasoningRows(): List<ChatMessage> =
|
private fun List<ChatMessage>.reconcileReasoningRows(): List<ChatMessage> =
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ private fun ChatMessage.isSyntheticSystemRow(): Boolean =
|
|||||||
displayKind == null &&
|
displayKind == null &&
|
||||||
content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
||||||
|
|
||||||
|
internal fun ChatMessage.isTimelineMarker(): Boolean = displayKind != null && displayKind != "steer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a flat message list into turns for the full-bleed renderer.
|
* Split a flat message list into turns for the full-bleed renderer.
|
||||||
*
|
*
|
||||||
@@ -80,11 +82,12 @@ fun groupIntoTurns(messages: List<ChatMessage>): List<ChatTurn> {
|
|||||||
// Timeline markers (display_kind) ride as role=user rows but are
|
// Timeline markers (display_kind) ride as role=user rows but are
|
||||||
// NOT user turns — group them as system-style timeline entries so
|
// NOT user turns — group them as system-style timeline entries so
|
||||||
// they render as centered chips, not fake user bubbles (issue #904).
|
// they render as centered chips, not fake user bubbles (issue #904).
|
||||||
|
// Steer messages (display_kind == "steer") are genuine user turns.
|
||||||
// The backend's max-iterations nudge is a role=user row with NO
|
// The backend's max-iterations nudge is a role=user row with NO
|
||||||
// display_kind (it's stripped on persistence); detect it by its
|
// display_kind (it's stripped on persistence); detect it by its
|
||||||
// stable content prefix and route it as a system event too, tagging
|
// stable content prefix and route it as a system event too, tagging
|
||||||
// it so the timeline chip can name it.
|
// it so the timeline chip can name it.
|
||||||
message.displayKind != null -> {
|
message.isTimelineMarker() -> {
|
||||||
agentEntries += AgentEntry.SystemEvent(message)
|
agentEntries += AgentEntry.SystemEvent(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,4 +201,24 @@ class ChatMessageMapperTest {
|
|||||||
|
|
||||||
assertNull(roundTripped.displayKind)
|
assertNull(roundTripped.displayKind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionStartMarkerDoesNotAcquireLocalOrder() {
|
||||||
|
val entity =
|
||||||
|
ChatMessageEntity(
|
||||||
|
id = "uuid-session-created",
|
||||||
|
sessionId = "s",
|
||||||
|
role = "SYSTEM",
|
||||||
|
content = "Session created",
|
||||||
|
timestamp = 1000L,
|
||||||
|
sortGroup = 1,
|
||||||
|
sortOrder = 5L,
|
||||||
|
)
|
||||||
|
|
||||||
|
val ui = entity.toUiModel()
|
||||||
|
|
||||||
|
assertNull(ui.localOrder)
|
||||||
|
assertEquals(MessageRole.SYSTEM, ui.role)
|
||||||
|
assertEquals("Session created", ui.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,28 @@ class ChatListEntriesTest {
|
|||||||
assertEquals(mapOf(6 to 5), toolCallMilestoneIndices(messages))
|
assertEquals(mapOf(6 to 5), toolCallMilestoneIndices(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun steerMessageResetsToolCounterAsUserTurnBoundary() {
|
||||||
|
val steer =
|
||||||
|
ChatMessage(
|
||||||
|
role = MessageRole.USER,
|
||||||
|
content = "Stop and run tests",
|
||||||
|
displayKind = "steer",
|
||||||
|
)
|
||||||
|
val messages =
|
||||||
|
listOf(
|
||||||
|
userMessage(),
|
||||||
|
toolMessage(),
|
||||||
|
toolMessage(),
|
||||||
|
steer,
|
||||||
|
toolMessage(),
|
||||||
|
toolMessage(),
|
||||||
|
toolMessage(),
|
||||||
|
)
|
||||||
|
// Only 3 tool calls after the steer boundary, so no 5-call milestone reached
|
||||||
|
assertTrue(toolCallMilestones(messages).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun syntheticMaxIterationsNudgeDoesNotResetToolCounter() {
|
fun syntheticMaxIterationsNudgeDoesNotResetToolCounter() {
|
||||||
val nudge =
|
val nudge =
|
||||||
|
|||||||
@@ -581,6 +581,133 @@ class ChatPagingMergeTest {
|
|||||||
assertTrue(sameLogicalMessage(canonical.copy(reasoningText = "new trace"), live))
|
assertTrue(sameLogicalMessage(canonical.copy(reasoningText = "new trace"), live))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stripGatewaySteerWrapperRemovesEnvelopesSafely() {
|
||||||
|
val standard =
|
||||||
|
"[OUT-OF-BAND USER MESSAGE — a direct message from the user, delivered once at this position; " +
|
||||||
|
"not tool output and not a new delivery when replayed from conversation history]\n" +
|
||||||
|
"Please use Python instead.\n" +
|
||||||
|
"[/OUT-OF-BAND USER MESSAGE]"
|
||||||
|
assertEquals("Please use Python instead.", stripGatewaySteerWrapper(standard))
|
||||||
|
|
||||||
|
val noDescription =
|
||||||
|
"[OUT-OF-BAND USER MESSAGE]\n" +
|
||||||
|
"Halt.\n" +
|
||||||
|
"[/OUT-OF-BAND USER MESSAGE]"
|
||||||
|
assertEquals("Halt.", stripGatewaySteerWrapper(noDescription))
|
||||||
|
|
||||||
|
val multiline =
|
||||||
|
"[OUT-OF-BAND USER MESSAGE — direct steer]\n" +
|
||||||
|
"First line\n" +
|
||||||
|
"Second line\n" +
|
||||||
|
"[/OUT-OF-BAND USER MESSAGE]"
|
||||||
|
assertEquals("First line\nSecond line", stripGatewaySteerWrapper(multiline))
|
||||||
|
|
||||||
|
val plain = "Normal user message without wrapper"
|
||||||
|
assertEquals(plain, stripGatewaySteerWrapper(plain))
|
||||||
|
|
||||||
|
val interior = "Mentions [OUT-OF-BAND USER MESSAGE] in passing"
|
||||||
|
assertEquals(interior, stripGatewaySteerWrapper(interior))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun midTurnSteerMessageReconcilesAndSortsChronologically() {
|
||||||
|
val userPrompt = ChatMessage(id = "rest-session-0", role = MessageRole.USER, content = "Create a script")
|
||||||
|
val liveSteer =
|
||||||
|
ChatMessage(
|
||||||
|
id = "uuid-steer",
|
||||||
|
role = MessageRole.USER,
|
||||||
|
content = "Use Python instead",
|
||||||
|
)
|
||||||
|
val inFlightAssistant =
|
||||||
|
ChatMessage(
|
||||||
|
id = "uuid-assistant",
|
||||||
|
role = MessageRole.ASSISTANT,
|
||||||
|
content = "Here is the Python script",
|
||||||
|
isStreaming = false,
|
||||||
|
)
|
||||||
|
val current = listOf(userPrompt, inFlightAssistant, liveSteer)
|
||||||
|
|
||||||
|
val serverRows =
|
||||||
|
listOf(
|
||||||
|
SessionMessage(
|
||||||
|
id = 0,
|
||||||
|
role = "user",
|
||||||
|
content = JsonPrimitive("Create a script"),
|
||||||
|
timestamp = JsonPrimitive(0),
|
||||||
|
),
|
||||||
|
SessionMessage(
|
||||||
|
id = 1,
|
||||||
|
role = "user",
|
||||||
|
content =
|
||||||
|
JsonPrimitive(
|
||||||
|
"[OUT-OF-BAND USER MESSAGE — a direct message from the user, " +
|
||||||
|
"delivered once at this position; not tool output and not a new " +
|
||||||
|
"delivery when replayed from conversation history]\n" +
|
||||||
|
"Use Python instead\n" +
|
||||||
|
"[/OUT-OF-BAND USER MESSAGE]",
|
||||||
|
),
|
||||||
|
display_kind = "steer",
|
||||||
|
timestamp = JsonPrimitive(1),
|
||||||
|
),
|
||||||
|
SessionMessage(
|
||||||
|
id = 2,
|
||||||
|
role = "assistant",
|
||||||
|
content = JsonPrimitive("Here is the Python script"),
|
||||||
|
timestamp = JsonPrimitive(2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val merged = applyServerPage(current, serverRows)
|
||||||
|
|
||||||
|
assertEquals(3, merged.size)
|
||||||
|
assertEquals("rest-session-0", merged[0].id)
|
||||||
|
assertEquals("Create a script", merged[0].content)
|
||||||
|
|
||||||
|
assertEquals("uuid-steer", merged[1].id)
|
||||||
|
assertEquals("rest-session-1", merged[1].canonicalRestId)
|
||||||
|
assertEquals("Use Python instead", merged[1].content)
|
||||||
|
assertEquals("steer", merged[1].displayKind)
|
||||||
|
|
||||||
|
assertEquals("rest-session-2", merged[2].canonicalRestId)
|
||||||
|
assertEquals("Here is the Python script", merged[2].content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionCreatedMarkerStaysAtStartOfTranscript() {
|
||||||
|
val sessionCreated =
|
||||||
|
ChatMessage(
|
||||||
|
id = "uuid-sys-start",
|
||||||
|
role = MessageRole.SYSTEM,
|
||||||
|
content = "Session created",
|
||||||
|
)
|
||||||
|
val current = listOf(sessionCreated)
|
||||||
|
|
||||||
|
val serverRows =
|
||||||
|
listOf(
|
||||||
|
SessionMessage(
|
||||||
|
id = 0,
|
||||||
|
role = "user",
|
||||||
|
content = JsonPrimitive("Hello"),
|
||||||
|
timestamp = JsonPrimitive(1),
|
||||||
|
),
|
||||||
|
SessionMessage(
|
||||||
|
id = 1,
|
||||||
|
role = "assistant",
|
||||||
|
content = JsonPrimitive("Hi there!"),
|
||||||
|
timestamp = JsonPrimitive(2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val merged = applyServerPage(current, serverRows)
|
||||||
|
|
||||||
|
assertEquals(3, merged.size)
|
||||||
|
assertEquals("uuid-sys-start", merged[0].id)
|
||||||
|
assertEquals("Session created", merged[0].content)
|
||||||
|
assertEquals("rest-session-0", merged[1].canonicalRestId)
|
||||||
|
assertEquals("rest-session-1", merged[2].canonicalRestId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun assertBoundaryTranscript(messages: List<ChatMessage>) {
|
private fun assertBoundaryTranscript(messages: List<ChatMessage>) {
|
||||||
assertEquals(listOf("rest-session-100", "rest-session-200"), messages.map { it.canonicalRestId })
|
assertEquals(listOf("rest-session-100", "rest-session-200"), messages.map { it.canonicalRestId })
|
||||||
assertEquals(listOf("boundary trace", ""), messages.map { it.reasoningText })
|
assertEquals(listOf("boundary trace", ""), messages.map { it.reasoningText })
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ class FullBleedTurnsTest {
|
|||||||
assertEquals(listOf<ChatTurn>(entries(AgentEntry.SystemEvent(marker))), result)
|
assertEquals(listOf<ChatTurn>(entries(AgentEntry.SystemEvent(marker))), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `steer message with display_kind steer is a User turn not a SystemEvent entry`() {
|
||||||
|
val steer =
|
||||||
|
msg("u1", MessageRole.USER, content = "Stop and run tests")
|
||||||
|
.copy(displayKind = "steer")
|
||||||
|
val result = groupIntoTurns(listOf(steer))
|
||||||
|
assertEquals(listOf<ChatTurn>(ChatTurn.User(steer)), result)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `max-iterations nudge is a SystemEvent not a user bubble`() {
|
fun `max-iterations nudge is a SystemEvent not a user bubble`() {
|
||||||
// Backend persists the max-iterations notice as a plain role=user row
|
// Backend persists the max-iterations notice as a plain role=user row
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.ui.chat.fakes
|
|||||||
import com.m57.hermescontrol.data.local.ChatMessageDao
|
import com.m57.hermescontrol.data.local.ChatMessageDao
|
||||||
import com.m57.hermescontrol.data.local.ChatMessageEntity
|
import com.m57.hermescontrol.data.local.ChatMessageEntity
|
||||||
import com.m57.hermescontrol.data.local.canonicalMessageOrder
|
import com.m57.hermescontrol.data.local.canonicalMessageOrder
|
||||||
|
import com.m57.hermescontrol.data.local.isSessionStartMarker
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
|
||||||
@@ -86,7 +87,12 @@ class FakeChatMessageDao : ChatMessageDao {
|
|||||||
fun addMessageDirect(message: ChatMessageEntity) {
|
fun addMessageDirect(message: ChatMessageEntity) {
|
||||||
val existing = messages[message.id]
|
val existing = messages[message.id]
|
||||||
if (existing == null) insertionSequence++
|
if (existing == null) insertionSequence++
|
||||||
val order = canonicalMessageOrder(message.restId ?: message.id, message.sessionId)
|
val order =
|
||||||
|
if (message.isSessionStartMarker()) {
|
||||||
|
-1L
|
||||||
|
} else {
|
||||||
|
canonicalMessageOrder(message.restId ?: message.id, message.sessionId)
|
||||||
|
}
|
||||||
messages[message.id] =
|
messages[message.id] =
|
||||||
message.copy(
|
message.copy(
|
||||||
sortGroup = if (order != null) 0 else 1,
|
sortGroup = if (order != null) 0 else 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user