fix(group-chat): full desktop parity for turn timeout (180s base + 20m adaptive cap) and multi-profile REST fallback

- Match desktop GROUP_TURN_TIMEOUT_MS (180s) and GROUP_TURN_HARD_CAP_MS (20m)
- Implement adaptive deadline extension: actively streaming tokens or running tools within 60s keeps the turn slot alive up to 20m, preventing long research/terminal runs from getting discarded
- Update LEASE_STEP_TTL_MS to 180s
- Add explicit `@Query("profile") profile: String?` support to `HermesApiService.getSessionMessages`
- Pass `bot.name` explicitly in `GroupChatViewModel` REST fallback so multi-profile bot sessions do not hit 404 under `ProfileScopeInterceptor`

Co-authored-by: Luna <luna.m57@agentmail.to>
This commit is contained in:
M57andLuna committed 2026-09-04 21:11:00 +03:00
1 parent 31e292d7fe
commit c34701f4e2
2 files changed
+41 -5

No files matched your search

@@ -194,6 +194,7 @@ interface HermesApiService {
// legacy backends that don't know the param — they ignore it.
@Query("order") order: String? = null,
@Query("include_compacted") includeCompacted: Boolean? = true,
@Query("profile") profile: String? = null,
): Response<SessionMessagesResponse>
@GET("api/sessions/stats")
@@ -56,8 +56,9 @@ const val MAX_ALLOWED_BOT_MESSAGES = 50
const val MAX_ALLOWED_CONTINUATION_PASSES = 20
const val WARN_MAX_BOT_MESSAGES = 20
const val WARN_MAX_CONTINUATION_PASSES = 6
private const val LEASE_STEP_TTL_MS = 45_000L
private const val TURN_TIMEOUT_MS = 45_000L
private const val LEASE_STEP_TTL_MS = 180_000L
private const val GROUP_TURN_TIMEOUT_MS = 180_000L
private const val GROUP_TURN_HARD_CAP_MS = 20 * 60_000L
private const val HISTORY_LIMIT = 10
class GroupChatViewModel(
@@ -86,6 +87,9 @@ class GroupChatViewModel(
// Map of session_id -> bot profile
private val sessionToBot = mutableMapOf<String, ProfileInfo>()
// Map of session_id -> last activity epoch ms (streaming token, tool call, thinking)
private val sessionLastActivity = mutableMapOf<String, Long>()
init {
if (groupName.isNotBlank()) {
loadGroup()
@@ -106,6 +110,7 @@ class GroupChatViewModel(
inFlightTurns.clear()
activeStreamMsgId.clear()
sessionToBot.clear()
sessionLastActivity.clear()
_uiState.value = GroupChatUiState(groupName = name)
loadGroup()
}
@@ -145,6 +150,7 @@ class GroupChatViewModel(
is WsEvent.MessageToken -> {
val sid = event.sessionId?.trim('\"', ' ')
if (sid != null) {
sessionLastActivity[sid] = System.currentTimeMillis()
val streamId = activeStreamMsgId[sid]
val bot = sessionToBot[sid]
if (streamId != null && bot != null && event.token.isNotEmpty()) {
@@ -180,6 +186,7 @@ class GroupChatViewModel(
is WsEvent.ToolStart -> {
val sid = event.sessionId?.trim('\"', ' ')
if (sid != null) {
sessionLastActivity[sid] = System.currentTimeMillis()
val streamId = activeStreamMsgId[sid]
val bot = sessionToBot[sid]
val toolName = event.name ?: "tool"
@@ -230,6 +237,7 @@ class GroupChatViewModel(
is WsEvent.ToolComplete -> {
val sid = event.sessionId?.trim('\"', ' ')
if (sid != null) {
sessionLastActivity[sid] = System.currentTimeMillis()
val streamId = activeStreamMsgId[sid]
val toolId = event.data?.get("tool_id") as? String
val toolName = event.name
@@ -719,10 +727,34 @@ class GroupChatViewModel(
}
}
var replyText =
withTimeoutOrNull(TURN_TIMEOUT_MS) {
turnDeferred.await()
var replyText: String? = null
var deadline = turnStartTime + GROUP_TURN_TIMEOUT_MS
val hardCapDeadline = turnStartTime + GROUP_TURN_HARD_CAP_MS
while (System.currentTimeMillis() < deadline) {
val pollResult =
withTimeoutOrNull(2000L) {
turnDeferred.await()
}
if (pollResult != null) {
replyText = pollResult
break
}
if (activeEpoch != epoch) break
// Adaptive extension: if bot is actively working (tokens/tools streamed in last 60s), extend deadline
val lastActivity =
listOfNotNull(
activeRuntimeId?.let { sessionLastActivity[it] },
activeStoredId?.let { sessionLastActivity[it] },
).maxOrNull() ?: turnStartTime
val isBusy = (System.currentTimeMillis() - lastActivity) < 60_000L
if (isBusy) {
deadline =
minOf(hardCapDeadline, maxOf(deadline, System.currentTimeMillis() + GROUP_TURN_TIMEOUT_MS))
}
}
inFlightTurns.remove(runtimeId)
storedId?.let { inFlightTurns.remove(it) }
@@ -736,6 +768,7 @@ class GroupChatViewModel(
sessionId = tid,
limit = 10,
order = "latest",
profile = bot.name,
)
}
if (res is NetworkResult.Success) {
@@ -828,11 +861,13 @@ class GroupChatViewModel(
activeStreamMsgId.remove(it)
sessionToBot.remove(it)
inFlightTurns.remove(it)
sessionLastActivity.remove(it)
}
activeStoredId?.let {
activeStreamMsgId.remove(it)
sessionToBot.remove(it)
inFlightTurns.remove(it)
sessionLastActivity.remove(it)
}
}
}