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 &&
|
rid != null &&
|
||||||
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
|
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
|
||||||
if (!alreadyShown) {
|
if (!alreadyShown) {
|
||||||
handleApprovalRequest(parseApprovalMap(pendingApproval, _uiState.value.currentSessionId))
|
handleApprovalRequest(
|
||||||
|
parseApprovalMap(
|
||||||
|
pendingApproval,
|
||||||
|
runtimeSessionId ?: _uiState.value.currentSessionId,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1241,10 +1246,16 @@ class ChatViewModel(
|
|||||||
rid != null &&
|
rid != null &&
|
||||||
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
|
_uiState.value.messages.any { it.approvalInfo?.requestId == rid }
|
||||||
if (!alreadyShown) {
|
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 -> {
|
WsMethods.SESSION_INTERRUPT -> {
|
||||||
@@ -3648,7 +3659,7 @@ class ChatViewModel(
|
|||||||
// Desktop parity (`prompts.ts` receiveApprovalRequest): ack the render
|
// Desktop parity (`prompts.ts` receiveApprovalRequest): ack the render
|
||||||
// so the backend knows this client holds the prompt. Fire-and-forget.
|
// so the backend knows this client holds the prompt. Fire-and-forget.
|
||||||
val requestId = event.requestId
|
val requestId = event.requestId
|
||||||
val sessionId = event.sessionId ?: _uiState.value.currentSessionId
|
val sessionId = runtimeSessionId ?: event.sessionId ?: _uiState.value.currentSessionId
|
||||||
if (requestId != null && sessionId != null) {
|
if (requestId != null && sessionId != null) {
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -3715,10 +3726,11 @@ class ChatViewModel(
|
|||||||
* after resume and after each respond (the queue can hold more).
|
* after resume and after each respond (the queue can hold more).
|
||||||
*/
|
*/
|
||||||
private fun replayPendingApproval(sessionId: String) {
|
private fun replayPendingApproval(sessionId: String) {
|
||||||
|
val targetSessionId = runtimeSessionId ?: sessionId
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
wsClient.send(
|
wsClient.send(
|
||||||
method = WsMethods.APPROVAL_PENDING,
|
method = WsMethods.APPROVAL_PENDING,
|
||||||
params = mapOf("session_id" to sessionId),
|
params = mapOf("session_id" to targetSessionId),
|
||||||
onSent = { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) },
|
onSent = { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3729,7 +3741,7 @@ class ChatViewModel(
|
|||||||
val approvals = (result as? Map<*, *>)?.get("approvals") as? List<*>
|
val approvals = (result as? Map<*, *>)?.get("approvals") as? List<*>
|
||||||
val first = approvals?.filterIsInstance<Map<*, *>>()?.firstOrNull() ?: return
|
val first = approvals?.filterIsInstance<Map<*, *>>()?.firstOrNull() ?: return
|
||||||
val requestId = first["request_id"] as? String ?: 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.
|
// Don't duplicate a prompt already on screen.
|
||||||
val alreadyShown =
|
val alreadyShown =
|
||||||
_uiState.value.messages.any { it.approvalInfo?.requestId == requestId }
|
_uiState.value.messages.any { it.approvalInfo?.requestId == requestId }
|
||||||
@@ -3740,7 +3752,7 @@ class ChatViewModel(
|
|||||||
fun respondToApproval(action: String) {
|
fun respondToApproval(action: String) {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val approvalMsg = state.messages.lastOrNull { it.approvalInfo != null } ?: return
|
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`
|
// Desktop sends `once` for a single run; legacy mobile sent `approve`
|
||||||
// (any non-deny still unblocks, but stay on-spec going forward).
|
// (any non-deny still unblocks, but stay on-spec going forward).
|
||||||
val choice = if (action == "approve") "once" else action
|
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) ───────────────────────────
|
// ── Sudo / secret prompt flow (issue #524) ───────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in new issue
Block a user