fix(chat): use runtime session ID in approval RPCs to prevent 4001 session not found

This commit is contained in:
M57 committed 2026-09-05 13:15:08 +03:00
1 parent 20b57af770
commit aff1b7ed3a
2 files changed
+88 -7

No files matched your search

@@ -947,7 +947,12 @@ class ChatViewModel(
rid != null &&
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
if (!alreadyShown) {
handleApprovalRequest(parseApprovalMap(pendingApproval, _uiState.value.currentSessionId))
handleApprovalRequest(
parseApprovalMap(
pendingApproval,
runtimeSessionId ?: _uiState.value.currentSessionId,
),
)
}
}
}
@@ -1241,10 +1246,16 @@ class ChatViewModel(
rid != null &&
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
if (!alreadyShown) {
handleApprovalRequest(parseApprovalMap(pendingApproval, sessionId))
handleApprovalRequest(
parseApprovalMap(
pendingApproval,
runtimeSessionId ?: sessionId,
),
)
}
}
if (sessionId != null) replayPendingApproval(sessionId)
val activeSessionId = runtimeSessionId ?: sessionId
if (activeSessionId != null) replayPendingApproval(activeSessionId)
}
WsMethods.SESSION_INTERRUPT -> {
@@ -3648,7 +3659,7 @@ class ChatViewModel(
// Desktop parity (`prompts.ts` receiveApprovalRequest): ack the render
// so the backend knows this client holds the prompt. Fire-and-forget.
val requestId = event.requestId
val sessionId = event.sessionId ?: _uiState.value.currentSessionId
val sessionId = runtimeSessionId ?: event.sessionId ?: _uiState.value.currentSessionId
if (requestId != null && sessionId != null) {
viewModelScope.launch(ioDispatcher) {
runCatching {
@@ -3715,10 +3726,11 @@ class ChatViewModel(
* after resume and after each respond (the queue can hold more).
*/
private fun replayPendingApproval(sessionId: String) {
val targetSessionId = runtimeSessionId ?: sessionId
viewModelScope.launch(ioDispatcher) {
wsClient.send(
method = WsMethods.APPROVAL_PENDING,
params = mapOf("session_id" to sessionId),
params = mapOf("session_id" to targetSessionId),
onSent = { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) },
)
}
@@ -3729,7 +3741,7 @@ class ChatViewModel(
val approvals = (result as? Map<*, *>)?.get("approvals") as? List<*>
val first = approvals?.filterIsInstance<Map<*, *>>()?.firstOrNull() ?: return
val requestId = first["request_id"] as? String ?: return
val sessionId = _uiState.value.currentSessionId
val sessionId = runtimeSessionId ?: _uiState.value.currentSessionId
// Don't duplicate a prompt already on screen.
val alreadyShown =
_uiState.value.messages.any { it.approvalInfo?.requestId == requestId }
@@ -3740,7 +3752,7 @@ class ChatViewModel(
fun respondToApproval(action: String) {
val state = _uiState.value
val approvalMsg = state.messages.lastOrNull { it.approvalInfo != null } ?: return
val sessionId = state.currentSessionId ?: return
val sessionId = runtimeSessionId ?: state.currentSessionId ?: return
// Desktop sends `once` for a single run; legacy mobile sent `approve`
// (any non-deny still unblocks, but stay on-spec going forward).
val choice = if (action == "approve") "once" else action
@@ -3196,6 +3196,75 @@ class ChatViewModelTest {
}
}
@Test
fun testApprovalFlow_prefersRuntimeSessionIdOverStorageId() =
runTest {
val viewModel = createViewModel()
advanceUntilIdle()
mockConnectionStatus.value = ConnectionStatus.CONNECTED
mockEventsFlow.emit(WsEvent.GatewayReady(null))
advanceUntilIdle()
mockEventsFlow.emit(
WsEvent.RpcResult(
"req-id-3",
mapOf(
"session_id" to "runtime-sid-999",
"stored_session_id" to "storage-uuid-111",
),
),
)
advanceUntilIdle()
assertEquals("storage-uuid-111", viewModel.uiState.value.currentSessionId)
mockEventsFlow.emit(
WsEvent.ApprovalRequest(
command = "rm -rf /data",
description = "Dangerous operation",
patternKeys = null,
sessionId = null,
requestId = "req-runtime-test",
choices = listOf("once", "deny"),
),
)
advanceUntilIdle()
verify {
HermesWsClient.send(
WsMethods.APPROVAL_RECEIVED,
withArg { params ->
assertEquals("runtime-sid-999", params["session_id"])
assertEquals("req-runtime-test", params["request_id"])
},
any(),
)
}
viewModel.respondToApproval("once")
advanceUntilIdle()
verify {
HermesWsClient.send(
WsMethods.APPROVAL_RESPOND,
withArg { params ->
assertEquals("runtime-sid-999", params["session_id"])
assertEquals("once", params["choice"])
assertEquals("req-runtime-test", params["request_id"])
},
any(),
)
HermesWsClient.send(
WsMethods.APPROVAL_PENDING,
withArg { params ->
assertEquals("runtime-sid-999", params["session_id"])
},
any(),
)
}
}
// ── Sudo / secret prompt flow (issue #524) ───────────────────────────
@Test