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.RemoteInput
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
@@ -124,11 +125,20 @@ open class NotificationReplyReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
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 =
|
||||
HermesWsClient
|
||||
.request(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to storedSessionId, "omit_messages" to true),
|
||||
params,
|
||||
timeoutMs = REPLY_TIMEOUT_MS,
|
||||
).await() as? Map<*, *>
|
||||
val runtimeSessionId =
|
||||
|
||||
@@ -231,7 +231,7 @@ fun ChatScreen(
|
||||
.collectLatest { firstVisibleIndex ->
|
||||
if (firstVisibleIndex > 2) {
|
||||
isOlderPagingArmed = true
|
||||
} else if (isOlderPagingArmed) {
|
||||
} else if (isOlderPagingArmed || state.messages.size <= 3) {
|
||||
val anchorId = state.messages.getOrNull(firstVisibleIndex)?.id
|
||||
val offset = scrollController.captureAnchorOffset()
|
||||
pagingAnchor.value = if (anchorId != null) anchorId to offset else null
|
||||
|
||||
@@ -647,6 +647,8 @@ class ChatViewModel(
|
||||
viewModelScope.launch {
|
||||
ProfileSwitchCoordinator.switched
|
||||
.collect { _ ->
|
||||
pendingGoneSessionNotice = false
|
||||
sessionHasServerPresence = false
|
||||
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
||||
}
|
||||
}
|
||||
@@ -657,6 +659,8 @@ class ChatViewModel(
|
||||
viewModelScope.launch {
|
||||
ProfileSwitchCoordinator.connectionSwitched
|
||||
.collect { _ ->
|
||||
pendingGoneSessionNotice = false
|
||||
sessionHasServerPresence = false
|
||||
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
||||
}
|
||||
}
|
||||
@@ -2469,6 +2473,27 @@ class ChatViewModel(
|
||||
}
|
||||
|
||||
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
|
||||
_uiState.update {
|
||||
if (!isCurrentHydration(sessionId, generation, requestSequence)) return@update it
|
||||
@@ -2576,10 +2601,19 @@ class ChatViewModel(
|
||||
) {
|
||||
val requestSequence = ++resumeRequestSequence
|
||||
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) {
|
||||
wsClient.send(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to sessionId, "omit_messages" to true),
|
||||
params,
|
||||
onSent = { id ->
|
||||
trackSessionRequest(
|
||||
id = id,
|
||||
@@ -2653,6 +2687,17 @@ class ChatViewModel(
|
||||
private fun recoverGoneSession(sessionId: String) {
|
||||
if (_uiState.value.currentSessionId != sessionId) 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
|
||||
cancelResumeRetry()
|
||||
_uiState.update {
|
||||
|
||||
@@ -804,9 +804,13 @@ fun SessionsScreen(
|
||||
derivedStateOf {
|
||||
val info = listState.layoutInfo
|
||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
lastVisible >= 0 &&
|
||||
info.totalItemsCount >= AUTO_LOAD_THRESHOLD &&
|
||||
if (lastVisible < 0 || info.totalItemsCount == 0) {
|
||||
false
|
||||
} else if (info.totalItemsCount <= AUTO_LOAD_THRESHOLD) {
|
||||
lastVisible >= info.totalItemsCount - 1
|
||||
} else {
|
||||
lastVisible >= info.totalItemsCount - AUTO_LOAD_THRESHOLD
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(
|
||||
|
||||
@@ -216,11 +216,6 @@ class SessionsViewModel :
|
||||
val hasMore = receivedFullPage && addedNewItems && data.total > newSessions.size
|
||||
it.copy(
|
||||
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,
|
||||
total = if (hasMore) data.total else newSessions.size,
|
||||
hasMore = hasMore,
|
||||
|
||||
Reference in new issue
Block a user