Skip to main content

vtcode_desktop_protocol/
interaction_registry.rs

1//! Track pending interaction identities and reject stale or duplicate replies.
2use std::collections::HashMap;
3
4use super::{InteractionRequest, InteractionResponse, ProtocolError};
5
6/// Track pending interaction identities and reject stale or duplicate replies.
7#[derive(Debug, Default)]
8pub struct InteractionRegistry {
9    pending: HashMap<String, (String, String)>,
10}
11
12impl InteractionRegistry {
13    /// Register an interaction id for an exact session and turn.
14    pub fn insert(&mut self, interaction: &InteractionRequest) -> Result<(), ProtocolError> {
15        if self.pending.contains_key(&interaction.interaction_id) {
16            return Err(ProtocolError::UnknownInteraction);
17        }
18        let replaced = self
19            .pending
20            .insert(interaction.interaction_id.clone(), (interaction.session_id.clone(), interaction.turn_id.clone()));
21        debug_assert!(replaced.is_none(), "duplicate interaction identity was checked before insertion");
22        Ok(())
23    }
24
25    /// Validate a response without consuming the pending interaction.
26    pub fn validate(&self, response: &InteractionResponse) -> Result<(), ProtocolError> {
27        let Some((session_id, turn_id)) = self.pending.get(&response.interaction_id) else {
28            return Err(ProtocolError::UnknownInteraction);
29        };
30        if session_id != &response.session_id || turn_id != &response.turn_id {
31            return Err(ProtocolError::StaleInteraction);
32        }
33        Ok(())
34    }
35
36    /// Validate and consume exactly one matching response.
37    pub fn resolve(&mut self, response: &InteractionResponse) -> Result<(), ProtocolError> {
38        self.validate(response)?;
39        if self.pending.remove(&response.interaction_id).is_none() {
40            return Err(ProtocolError::UnknownInteraction);
41        }
42        Ok(())
43    }
44
45    /// Drop all requests for a turn after cancellation or completion.
46    pub fn finish_turn(&mut self, session_id: &str, turn_id: &str) {
47        self.pending
48            .retain(|_, (pending_session, pending_turn)| pending_session != session_id || pending_turn != turn_id);
49    }
50
51    /// Drop all requests for a disconnected session.
52    pub fn close_session(&mut self, session_id: &str) {
53        self.pending.retain(|_, (pending_session, _)| pending_session != session_id);
54    }
55}