fix(chat): use runtime session ID in approval RPCs to prevent 4001 session not found
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user