fix(history): resolve session resume profile scoping and history pagination (#1040)
This commit is contained in:
5 files changed
+64
-10
No files matched your search
@@ -8,6 +8,7 @@ import android.content.Intent
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
@@ -124,11 +125,20 @@ open class NotificationReplyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resumeSession(storedSessionId: String): String {
|
private suspend fun resumeSession(storedSessionId: String): String {
|
||||||
|
val profile = AuthManager.activeProfileId.value
|
||||||
|
val params =
|
||||||
|
mutableMapOf<String, Any>(
|
||||||
|
"session_id" to storedSessionId,
|
||||||
|
"omit_messages" to true,
|
||||||
|
)
|
||||||
|
if (!profile.isNullOrBlank()) {
|
||||||
|
params["profile"] = profile
|
||||||
|
}
|
||||||
val result =
|
val result =
|
||||||
HermesWsClient
|
HermesWsClient
|
||||||
.request(
|
.request(
|
||||||
WsMethods.SESSION_RESUME,
|
WsMethods.SESSION_RESUME,
|
||||||
mapOf("session_id" to storedSessionId, "omit_messages" to true),
|
params,
|
||||||
timeoutMs = REPLY_TIMEOUT_MS,
|
timeoutMs = REPLY_TIMEOUT_MS,
|
||||||
).await() as? Map<*, *>
|
).await() as? Map<*, *>
|
||||||
val runtimeSessionId =
|
val runtimeSessionId =
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ fun ChatScreen(
|
|||||||
.collectLatest { firstVisibleIndex ->
|
.collectLatest { firstVisibleIndex ->
|
||||||
if (firstVisibleIndex > 2) {
|
if (firstVisibleIndex > 2) {
|
||||||
isOlderPagingArmed = true
|
isOlderPagingArmed = true
|
||||||
} else if (isOlderPagingArmed) {
|
} else if (isOlderPagingArmed || state.messages.size <= 3) {
|
||||||
val anchorId = state.messages.getOrNull(firstVisibleIndex)?.id
|
val anchorId = state.messages.getOrNull(firstVisibleIndex)?.id
|
||||||
val offset = scrollController.captureAnchorOffset()
|
val offset = scrollController.captureAnchorOffset()
|
||||||
pagingAnchor.value = if (anchorId != null) anchorId to offset else null
|
pagingAnchor.value = if (anchorId != null) anchorId to offset else null
|
||||||
|
|||||||
@@ -647,6 +647,8 @@ class ChatViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ProfileSwitchCoordinator.switched
|
ProfileSwitchCoordinator.switched
|
||||||
.collect { _ ->
|
.collect { _ ->
|
||||||
|
pendingGoneSessionNotice = false
|
||||||
|
sessionHasServerPresence = false
|
||||||
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,6 +659,8 @@ class ChatViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ProfileSwitchCoordinator.connectionSwitched
|
ProfileSwitchCoordinator.connectionSwitched
|
||||||
.collect { _ ->
|
.collect { _ ->
|
||||||
|
pendingGoneSessionNotice = false
|
||||||
|
sessionHasServerPresence = false
|
||||||
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2469,6 +2473,27 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
|
val errorMsg = result.error.message
|
||||||
|
val is404 =
|
||||||
|
errorMsg.contains("404", ignoreCase = true) ||
|
||||||
|
errorMsg.contains("not found", ignoreCase = true)
|
||||||
|
if (is404) {
|
||||||
|
// A 404 from GET /api/sessions/{id}/messages indicates the session has no
|
||||||
|
// persisted rows in state.db (e.g. newly created/lazy unprompted session, or empty history).
|
||||||
|
// It is NOT a fatal error: keep the session open with empty transcript, clear loading,
|
||||||
|
// and mark hydrated cleanly.
|
||||||
|
_uiState.update {
|
||||||
|
if (!isCurrentHydration(sessionId, generation, requestSequence)) return@update it
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isLoadingOlder = false,
|
||||||
|
hasOlderMessages = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hydratedGeneration = generation
|
||||||
|
finishResumeWhenHydrated(generation)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
if (hydratedGeneration == generation) hydratedGeneration = -1L
|
if (hydratedGeneration == generation) hydratedGeneration = -1L
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
if (!isCurrentHydration(sessionId, generation, requestSequence)) return@update it
|
if (!isCurrentHydration(sessionId, generation, requestSequence)) return@update it
|
||||||
@@ -2576,10 +2601,19 @@ class ChatViewModel(
|
|||||||
) {
|
) {
|
||||||
val requestSequence = ++resumeRequestSequence
|
val requestSequence = ++resumeRequestSequence
|
||||||
activeResumeRequestSequence = requestSequence
|
activeResumeRequestSequence = requestSequence
|
||||||
|
val profile = AuthManager.activeProfileId.value
|
||||||
|
val params =
|
||||||
|
mutableMapOf<String, Any>(
|
||||||
|
"session_id" to sessionId,
|
||||||
|
"omit_messages" to true,
|
||||||
|
)
|
||||||
|
if (!profile.isNullOrBlank()) {
|
||||||
|
params["profile"] = profile
|
||||||
|
}
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
wsClient.send(
|
wsClient.send(
|
||||||
WsMethods.SESSION_RESUME,
|
WsMethods.SESSION_RESUME,
|
||||||
mapOf("session_id" to sessionId, "omit_messages" to true),
|
params,
|
||||||
onSent = { id ->
|
onSent = { id ->
|
||||||
trackSessionRequest(
|
trackSessionRequest(
|
||||||
id = id,
|
id = id,
|
||||||
@@ -2653,6 +2687,17 @@ class ChatViewModel(
|
|||||||
private fun recoverGoneSession(sessionId: String) {
|
private fun recoverGoneSession(sessionId: String) {
|
||||||
if (_uiState.value.currentSessionId != sessionId) return
|
if (_uiState.value.currentSessionId != sessionId) return
|
||||||
if (sessionGoneRecoveryInFlight) return
|
if (sessionGoneRecoveryInFlight) return
|
||||||
|
if (_uiState.value.messages.isNotEmpty()) {
|
||||||
|
cancelResumeRetry()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isResumeRetrying = false,
|
||||||
|
resumeError = "Session not found on server (displaying cached messages)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
sessionGoneRecoveryInFlight = true
|
sessionGoneRecoveryInFlight = true
|
||||||
cancelResumeRetry()
|
cancelResumeRetry()
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
|
|||||||
@@ -804,9 +804,13 @@ fun SessionsScreen(
|
|||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val info = listState.layoutInfo
|
val info = listState.layoutInfo
|
||||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
lastVisible >= 0 &&
|
if (lastVisible < 0 || info.totalItemsCount == 0) {
|
||||||
info.totalItemsCount >= AUTO_LOAD_THRESHOLD &&
|
false
|
||||||
|
} else if (info.totalItemsCount <= AUTO_LOAD_THRESHOLD) {
|
||||||
|
lastVisible >= info.totalItemsCount - 1
|
||||||
|
} else {
|
||||||
lastVisible >= info.totalItemsCount - AUTO_LOAD_THRESHOLD
|
lastVisible >= info.totalItemsCount - AUTO_LOAD_THRESHOLD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
|
|||||||
@@ -216,11 +216,6 @@ class SessionsViewModel :
|
|||||||
val hasMore = receivedFullPage && addedNewItems && data.total > newSessions.size
|
val hasMore = receivedFullPage && addedNewItems && data.total > newSessions.size
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoadingMore = false,
|
isLoadingMore = false,
|
||||||
// distinctBy guards offset-pagination churn: if a new
|
|
||||||
// session lands on top between page loads, offsets
|
|
||||||
// shift and the next page can repeat an id we already
|
|
||||||
// have — LazyColumn would crash on the duplicate key.
|
|
||||||
// pinnedFirst keeps pinned sessions above the rest.
|
|
||||||
sessions = newSessions,
|
sessions = newSessions,
|
||||||
total = if (hasMore) data.total else newSessions.size,
|
total = if (hasMore) data.total else newSessions.size,
|
||||||
hasMore = hasMore,
|
hasMore = hasMore,
|
||||||
|
|||||||
Reference in new issue
Block a user