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:
1 parent
31e292d7fe
commit
c34701f4e2
2 files changed
+40
-4
No files matched your search
@@ -194,6 +194,7 @@ interface HermesApiService {
|
|||||||
// legacy backends that don't know the param — they ignore it.
|
// legacy backends that don't know the param — they ignore it.
|
||||||
@Query("order") order: String? = null,
|
@Query("order") order: String? = null,
|
||||||
@Query("include_compacted") includeCompacted: Boolean? = true,
|
@Query("include_compacted") includeCompacted: Boolean? = true,
|
||||||
|
@Query("profile") profile: String? = null,
|
||||||
): Response<SessionMessagesResponse>
|
): Response<SessionMessagesResponse>
|
||||||
|
|
||||||
@GET("api/sessions/stats")
|
@GET("api/sessions/stats")
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ const val MAX_ALLOWED_BOT_MESSAGES = 50
|
|||||||
const val MAX_ALLOWED_CONTINUATION_PASSES = 20
|
const val MAX_ALLOWED_CONTINUATION_PASSES = 20
|
||||||
const val WARN_MAX_BOT_MESSAGES = 20
|
const val WARN_MAX_BOT_MESSAGES = 20
|
||||||
const val WARN_MAX_CONTINUATION_PASSES = 6
|
const val WARN_MAX_CONTINUATION_PASSES = 6
|
||||||
private const val LEASE_STEP_TTL_MS = 45_000L
|
private const val LEASE_STEP_TTL_MS = 180_000L
|
||||||
private const val TURN_TIMEOUT_MS = 45_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
|
private const val HISTORY_LIMIT = 10
|
||||||
|
|
||||||
class GroupChatViewModel(
|
class GroupChatViewModel(
|
||||||
@@ -86,6 +87,9 @@ class GroupChatViewModel(
|
|||||||
// Map of session_id -> bot profile
|
// Map of session_id -> bot profile
|
||||||
private val sessionToBot = mutableMapOf<String, ProfileInfo>()
|
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 {
|
init {
|
||||||
if (groupName.isNotBlank()) {
|
if (groupName.isNotBlank()) {
|
||||||
loadGroup()
|
loadGroup()
|
||||||
@@ -106,6 +110,7 @@ class GroupChatViewModel(
|
|||||||
inFlightTurns.clear()
|
inFlightTurns.clear()
|
||||||
activeStreamMsgId.clear()
|
activeStreamMsgId.clear()
|
||||||
sessionToBot.clear()
|
sessionToBot.clear()
|
||||||
|
sessionLastActivity.clear()
|
||||||
_uiState.value = GroupChatUiState(groupName = name)
|
_uiState.value = GroupChatUiState(groupName = name)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
}
|
}
|
||||||
@@ -145,6 +150,7 @@ class GroupChatViewModel(
|
|||||||
is WsEvent.MessageToken -> {
|
is WsEvent.MessageToken -> {
|
||||||
val sid = event.sessionId?.trim('\"', ' ')
|
val sid = event.sessionId?.trim('\"', ' ')
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
|
sessionLastActivity[sid] = System.currentTimeMillis()
|
||||||
val streamId = activeStreamMsgId[sid]
|
val streamId = activeStreamMsgId[sid]
|
||||||
val bot = sessionToBot[sid]
|
val bot = sessionToBot[sid]
|
||||||
if (streamId != null && bot != null && event.token.isNotEmpty()) {
|
if (streamId != null && bot != null && event.token.isNotEmpty()) {
|
||||||
@@ -180,6 +186,7 @@ class GroupChatViewModel(
|
|||||||
is WsEvent.ToolStart -> {
|
is WsEvent.ToolStart -> {
|
||||||
val sid = event.sessionId?.trim('\"', ' ')
|
val sid = event.sessionId?.trim('\"', ' ')
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
|
sessionLastActivity[sid] = System.currentTimeMillis()
|
||||||
val streamId = activeStreamMsgId[sid]
|
val streamId = activeStreamMsgId[sid]
|
||||||
val bot = sessionToBot[sid]
|
val bot = sessionToBot[sid]
|
||||||
val toolName = event.name ?: "tool"
|
val toolName = event.name ?: "tool"
|
||||||
@@ -230,6 +237,7 @@ class GroupChatViewModel(
|
|||||||
is WsEvent.ToolComplete -> {
|
is WsEvent.ToolComplete -> {
|
||||||
val sid = event.sessionId?.trim('\"', ' ')
|
val sid = event.sessionId?.trim('\"', ' ')
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
|
sessionLastActivity[sid] = System.currentTimeMillis()
|
||||||
val streamId = activeStreamMsgId[sid]
|
val streamId = activeStreamMsgId[sid]
|
||||||
val toolId = event.data?.get("tool_id") as? String
|
val toolId = event.data?.get("tool_id") as? String
|
||||||
val toolName = event.name
|
val toolName = event.name
|
||||||
@@ -719,10 +727,34 @@ class GroupChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyText =
|
var replyText: String? = null
|
||||||
withTimeoutOrNull(TURN_TIMEOUT_MS) {
|
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()
|
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)
|
inFlightTurns.remove(runtimeId)
|
||||||
storedId?.let { inFlightTurns.remove(it) }
|
storedId?.let { inFlightTurns.remove(it) }
|
||||||
|
|
||||||
@@ -736,6 +768,7 @@ class GroupChatViewModel(
|
|||||||
sessionId = tid,
|
sessionId = tid,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
order = "latest",
|
order = "latest",
|
||||||
|
profile = bot.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (res is NetworkResult.Success) {
|
if (res is NetworkResult.Success) {
|
||||||
@@ -828,11 +861,13 @@ class GroupChatViewModel(
|
|||||||
activeStreamMsgId.remove(it)
|
activeStreamMsgId.remove(it)
|
||||||
sessionToBot.remove(it)
|
sessionToBot.remove(it)
|
||||||
inFlightTurns.remove(it)
|
inFlightTurns.remove(it)
|
||||||
|
sessionLastActivity.remove(it)
|
||||||
}
|
}
|
||||||
activeStoredId?.let {
|
activeStoredId?.let {
|
||||||
activeStreamMsgId.remove(it)
|
activeStreamMsgId.remove(it)
|
||||||
sessionToBot.remove(it)
|
sessionToBot.remove(it)
|
||||||
inFlightTurns.remove(it)
|
inFlightTurns.remove(it)
|
||||||
|
sessionLastActivity.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user