commit d5ae0ae2c0b51c9c1063a38ca5f1e9fbaa049223
parent 308193e2794fb5b05317a0949637bd946c1c938f
Author: William Casarin <jb55@jb55.com>
Date: Sun, 25 Jan 2026 23:07:13 -0800
dave: fix Claude subprocess cleanup on session delete
Store the tokio JoinHandle in ChatSession and abort it when the session
is dropped. This ensures the Claude subprocess is properly terminated
when a user deletes a session, preventing orphaned processes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
5 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs
@@ -84,7 +84,10 @@ impl AiBackend for ClaudeBackend {
_user_id: String,
_session_id: String, // TODO: Currently unused - --continue resumes last conversation globally
ctx: egui::Context,
- ) -> mpsc::Receiver<DaveApiResponse> {
+ ) -> (
+ mpsc::Receiver<DaveApiResponse>,
+ Option<tokio::task::JoinHandle<()>>,
+ ) {
let (tx, rx) = mpsc::channel();
let _api_key = self.api_key.clone();
@@ -102,7 +105,7 @@ impl AiBackend for ClaudeBackend {
.count()
== 1;
- tokio::spawn(async move {
+ let handle = tokio::spawn(async move {
// For first message, send full prompt; for continuation, just the latest message
let prompt = if is_first_message {
Self::messages_to_prompt(&messages)
@@ -270,6 +273,6 @@ impl AiBackend for ClaudeBackend {
tracing::debug!("Claude stream closed");
});
- rx
+ (rx, Some(handle))
}
}
diff --git a/crates/notedeck_dave/src/backend/openai.rs b/crates/notedeck_dave/src/backend/openai.rs
@@ -33,7 +33,10 @@ impl AiBackend for OpenAiBackend {
user_id: String,
_session_id: String,
ctx: egui::Context,
- ) -> mpsc::Receiver<DaveApiResponse> {
+ ) -> (
+ mpsc::Receiver<DaveApiResponse>,
+ Option<tokio::task::JoinHandle<()>>,
+ ) {
let (tx, rx) = mpsc::channel();
let api_messages: Vec<ChatCompletionRequestMessage> = {
@@ -47,7 +50,7 @@ impl AiBackend for OpenAiBackend {
let client = self.client.clone();
let tool_list: Vec<_> = tools.values().map(|t| t.to_api()).collect();
- tokio::spawn(async move {
+ let handle = tokio::spawn(async move {
let mut token_stream = match client
.chat()
.create_stream(CreateChatCompletionRequest {
@@ -156,6 +159,6 @@ impl AiBackend for OpenAiBackend {
tracing::debug!("stream closed");
});
- rx
+ (rx, Some(handle))
}
}
diff --git a/crates/notedeck_dave/src/backend/traits.rs b/crates/notedeck_dave/src/backend/traits.rs
@@ -15,7 +15,8 @@ pub enum BackendType {
pub trait AiBackend: Send + Sync {
/// Stream a request to the AI backend
///
- /// Returns a receiver that will receive tokens and tool calls as they arrive
+ /// Returns a receiver that will receive tokens and tool calls as they arrive,
+ /// plus an optional JoinHandle to the spawned task for cleanup on session deletion.
fn stream_request(
&self,
messages: Vec<crate::Message>,
@@ -24,5 +25,8 @@ pub trait AiBackend: Send + Sync {
user_id: String,
session_id: String,
ctx: egui::Context,
- ) -> mpsc::Receiver<DaveApiResponse>;
+ ) -> (
+ mpsc::Receiver<DaveApiResponse>,
+ Option<tokio::task::JoinHandle<()>>,
+ );
}
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -371,10 +371,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
let ctx = ctx.clone();
// Use backend to stream request
- let rx = self
+ let (rx, task_handle) = self
.backend
.stream_request(messages, tools, model_name, user_id, session_id, ctx);
session.incoming_tokens = Some(rx);
+ session.task_handle = task_handle;
}
}
diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs
@@ -17,6 +17,17 @@ pub struct ChatSession {
pub incoming_tokens: Option<Receiver<DaveApiResponse>>,
/// Pending permission requests waiting for user response
pub pending_permissions: HashMap<Uuid, oneshot::Sender<PermissionResponse>>,
+ /// Handle to the background task processing this session's AI requests.
+ /// Aborted on drop to clean up the subprocess.
+ pub task_handle: Option<tokio::task::JoinHandle<()>>,
+}
+
+impl Drop for ChatSession {
+ fn drop(&mut self) {
+ if let Some(handle) = self.task_handle.take() {
+ handle.abort();
+ }
+ }
}
impl ChatSession {
@@ -28,6 +39,7 @@ impl ChatSession {
input: String::new(),
incoming_tokens: None,
pending_permissions: HashMap::new(),
+ task_handle: None,
}
}