From c0e9515545d82c5f8b71c14df1ca4ad6ae989309 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 22:27:53 +0300 Subject: [PATCH 1/6] fix(claude-code): recover missing resumed sessions Co-authored-by: Medulla --- .../src/providers/claude_code/README.md | 5 ++++ .../src/providers/claude_code/driver.rs | 26 +++++++++++++++++++ .../src/providers/claude_code/driver_tests.rs | 9 +++++++ .../providers/claude_code/session_store.rs | 14 ++++++++++ .../claude_code/session_store_tests.rs | 12 +++++++++ 5 files changed, 66 insertions(+) diff --git a/crates/tinyagents-harness/src/providers/claude_code/README.md b/crates/tinyagents-harness/src/providers/claude_code/README.md index c521370b9..ff50d8b34 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/README.md +++ b/crates/tinyagents-harness/src/providers/claude_code/README.md @@ -58,6 +58,11 @@ UUID comes from `session_store.rs`. `cwd` is `project_dir` — the caller's project root, not this provider's own `workspace_dir` — so CC's file tools act on the user's code. +If a resumed session fails with Claude's explicit `No conversation found with +session ID` diagnostic, the driver clears that thread mapping and retries once +as a new session with the full conversation history. Other CLI failures do not +invalidate the mapping or retry. + The thread key that selects a session comes from `thread_key_from_request` in `mod.rs`: it reads `metadata.thread_id` / `conversation_id` / `session_id` off the `ModelRequest`, then `continuation_id`, and falls back to a fresh diff --git a/crates/tinyagents-harness/src/providers/claude_code/driver.rs b/crates/tinyagents-harness/src/providers/claude_code/driver.rs index 7f720c764..f58342f71 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/driver.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/driver.rs @@ -37,6 +37,10 @@ fn parse_error_line_shape(line: &str) -> &'static str { } } +fn is_missing_session_error(message: &str) -> bool { + message.contains("No conversation found with session ID:") +} + fn parse_error_log_line(ev: &ClaudeCodeEvent) -> Option { let ClaudeCodeEvent::ParseError { line, reason } = ev else { return None; @@ -611,6 +615,17 @@ pub(crate) async fn run_turn(ctx: TurnContext<'_>) -> anyhow::Result return Box::pin(run_turn(ctx)).await, + Err(error) => tracing::warn!( + "[claude-code][driver] failed to clear missing session mapping: {error}" + ), + } + } anyhow::bail!( "[claude-code][driver] exit {:?} stderr={}", status.code(), @@ -618,6 +633,17 @@ pub(crate) async fn run_turn(ctx: TurnContext<'_>) -> anyhow::Result return Box::pin(run_turn(ctx)).await, + Err(error) => tracing::warn!( + "[claude-code][driver] failed to clear missing session mapping: {error}" + ), + } + } anyhow::bail!("[claude-code][driver] {}", err); } diff --git a/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs b/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs index 44e5d36ff..affb219cd 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs @@ -3,6 +3,15 @@ use super::*; +#[test] +fn missing_session_error_is_recognized_precisely() { + assert!(is_missing_session_error( + "No conversation found with session ID: 1234" + )); + assert!(!is_missing_session_error("authentication_failed")); + assert!(!is_missing_session_error("permission denied")); +} + #[test] fn write_mcp_http_config_emits_http_url_with_bearer_header() { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/crates/tinyagents-harness/src/providers/claude_code/session_store.rs b/crates/tinyagents-harness/src/providers/claude_code/session_store.rs index e0d80f46e..674e912e1 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/session_store.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/session_store.rs @@ -57,6 +57,20 @@ impl SessionStore { } std::fs::write(&self.path, serialized) } + + /// Forget a thread's session mapping after the CLI confirms that session + /// does not exist. The next turn can then create a replacement session. + pub fn remove(&self, thread_id: &str) -> std::io::Result<()> { + let mut guard = self.inner.lock().expect("session store mutex poisoned"); + if guard.sessions.remove(thread_id).is_none() { + return Ok(()); + } + let serialized = serde_json::to_string_pretty(&*guard).map_err(std::io::Error::other)?; + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&self.path, serialized) + } } /// Random RFC-4122 v4 UUID, formatted lower-case with hyphens. diff --git a/crates/tinyagents-harness/src/providers/claude_code/session_store_tests.rs b/crates/tinyagents-harness/src/providers/claude_code/session_store_tests.rs index cd98d97ff..aded5b338 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/session_store_tests.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/session_store_tests.rs @@ -26,3 +26,15 @@ fn roundtrip_set_and_get() { let reopened = SessionStore::open(dir.path()); assert_eq!(reopened.get("thread_a").as_deref(), Some("abc")); } + +#[test] +fn remove_forgets_mapping_across_reopen() { + let dir = tempdir().unwrap(); + let store = SessionStore::open(dir.path()); + store.set("thread_a", "session-a").unwrap(); + + store.remove("thread_a").unwrap(); + + assert!(store.get("thread_a").is_none()); + assert!(SessionStore::open(dir.path()).get("thread_a").is_none()); +} From 3b02d417fb0f3931c59b7b7ca8bfc2a281936633 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 22:40:02 +0300 Subject: [PATCH 2/6] test(dependency-boundary): shift Claude Code ChatMessage debt lines The missing-session helper added four lines above the two known ChatMessage references in driver.rs, moving them from 71/237 to 75/241. Co-authored-by: Medulla --- .../tinyagents-integration-tests/tests/dependency_boundary.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tinyagents-integration-tests/tests/dependency_boundary.rs b/crates/tinyagents-integration-tests/tests/dependency_boundary.rs index 77e23425e..311b75442 100644 --- a/crates/tinyagents-integration-tests/tests/dependency_boundary.rs +++ b/crates/tinyagents-integration-tests/tests/dependency_boundary.rs @@ -43,11 +43,11 @@ const KNOWN_GENERIC_CLAUDE_CODE_CHAT_MESSAGE_DEBT: &[(&str, usize)] = &[ ), ( "crates/tinyagents-harness/src/providers/claude_code/driver.rs", - 71, + 75, ), ( "crates/tinyagents-harness/src/providers/claude_code/driver.rs", - 237, + 241, ), ( "crates/tinyagents-harness/src/providers/claude_code/input_builder.rs", From 1d8ec42cb12cfe01fce38a56bfe0265494e3ef9f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 07:25:17 +0300 Subject: [PATCH 3/6] chore(vendor): bump vendored submodules and drop sha2 dependency Updated the tinyinference, tinyliveagents, and tinytools submodule pointers to their latest revisions and removed the now-unused sha2 entry from the lockfile. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 1 - vendor/tinyinference | 2 +- vendor/tinyliveagents | 2 +- vendor/tinytools | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2da44ec8..4449d23e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1969,7 +1969,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", "thiserror", "tinyinference-core", "tinytools-agent", diff --git a/vendor/tinyinference b/vendor/tinyinference index a32eec813..cafc5f373 160000 --- a/vendor/tinyinference +++ b/vendor/tinyinference @@ -1 +1 @@ -Subproject commit a32eec8138d7be0978dd232ec9f93af33945dbe6 +Subproject commit cafc5f3730bbe92261db2913484369047b39d940 diff --git a/vendor/tinyliveagents b/vendor/tinyliveagents index 91664e7af..e10e6eb2a 160000 --- a/vendor/tinyliveagents +++ b/vendor/tinyliveagents @@ -1 +1 @@ -Subproject commit 91664e7af86feadac7759e5e5cdc64354b712277 +Subproject commit e10e6eb2a6cd3d6b45f720f2562d4ee88634af78 diff --git a/vendor/tinytools b/vendor/tinytools index bd60b9b52..c3d99e079 160000 --- a/vendor/tinytools +++ b/vendor/tinytools @@ -1 +1 @@ -Subproject commit bd60b9b52f1998b4483b9cf107a72d3338e672f8 +Subproject commit c3d99e079702fb3723b58881f48a97892073143b From a1b09223b33988433b69074dfe20016d7b3a7493 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 07:36:21 +0300 Subject: [PATCH 4/6] chore: files changed crates/tinyagents-harness/src/providers/claude_code/driver.rs,crates/tinyagents Auto-committed-on: dragonfly Co-authored-by: Medulla --- .../src/providers/claude_code/driver.rs | 16 ++++---- .../src/providers/claude_code/driver_tests.rs | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/tinyagents-harness/src/providers/claude_code/driver.rs b/crates/tinyagents-harness/src/providers/claude_code/driver.rs index 5fc162d6f..df141207a 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/driver.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/driver.rs @@ -622,10 +622,12 @@ pub(crate) async fn run_turn(ctx: TurnContext<'_>) -> anyhow::Result) -> anyhow::Result, failed_session_id: &str) -> bool { tracing::warn!( "[claude-code][driver] saved session is missing; clearing mapping and retrying with full history" @@ -674,7 +676,7 @@ fn clear_missing_session(ctx: &TurnContext<'_>, failed_session_id: &str) -> bool .session_store .remove_if(&ctx.thread_id, failed_session_id) { - Ok(_) => true, + Ok(removed) => removed, Err(error) => { tracing::warn!( "[claude-code][driver] failed to clear missing session mapping: {error}" diff --git a/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs b/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs index 18dffb32c..81922758e 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs @@ -444,3 +444,41 @@ fn the_shape_of_the_line_is_reported() { assert!(shape("panic: claude-code crashed").contains("non-json")); assert!(shape(" ").contains("blank")); } + +#[cfg(unix)] +mod structured_exit { + use super::*; + use std::os::unix::fs::PermissionsExt; + + #[tokio::test] + async fn nonzero_exit_with_structured_missing_session_error_retries() { + let dir = tempfile::tempdir().unwrap(); + let saved = "11111111-1111-4111-8111-111111111111"; + let store = Arc::new(SessionStore::open(dir.path())); + store.set("t1", saved).unwrap(); + let bin = dir.path().join("claude"); + let script = format!( + "#!/bin/sh\ncat >/dev/null\ncase \"$*\" in *--resume*) echo '{{\"type\":\"result\",\"subtype\":\"error_during_execution\",\"errors\":[\"No conversation found with session ID: {saved}\"]}}'; exit 1;; esac\necho '{{\"type\":\"result\",\"subtype\":\"success\",\"result\":\"ok\"}}'\n" + ); + std::fs::write(&bin, script).unwrap(); + std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap(); + let messages = vec![ChatMessage::user("hi")]; + run_turn(TurnContext { + bin_path: bin, + workspace_dir: dir.path().to_path_buf(), + project_dir: dir.path().join("project"), + thread_id: "t1".into(), + persist_session: true, + model: "m".into(), + append_system_prompt: None, + messages: &messages, + session_store: store.clone(), + stream: None, + anthropic_api_key: None, + mcp_provider: None, + }) + .await + .expect("retry succeeds"); + assert_ne!(store.get("t1").as_deref(), Some(saved)); + } +} From 9963a4072d4609517f3f3f6f0a0887821d3085d3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 07:38:23 +0300 Subject: [PATCH 5/6] fix(claude_code): retry only after removing the failed mapping; classify structured errors on nonzero exit Co-authored-by: Medulla --- crates/tinyagents-harness/src/providers/claude_code/driver.rs | 3 ++- .../tinyagents-integration-tests/tests/dependency_boundary.rs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/tinyagents-harness/src/providers/claude_code/driver.rs b/crates/tinyagents-harness/src/providers/claude_code/driver.rs index df141207a..8eb91edd1 100644 --- a/crates/tinyagents-harness/src/providers/claude_code/driver.rs +++ b/crates/tinyagents-harness/src/providers/claude_code/driver.rs @@ -667,7 +667,8 @@ pub(crate) async fn run_turn(ctx: TurnContext<'_>) -> anyhow::Result, failed_session_id: &str) -> bool { tracing::warn!( "[claude-code][driver] saved session is missing; clearing mapping and retrying with full history" diff --git a/crates/tinyagents-integration-tests/tests/dependency_boundary.rs b/crates/tinyagents-integration-tests/tests/dependency_boundary.rs index 676877be7..80cb25820 100644 --- a/crates/tinyagents-integration-tests/tests/dependency_boundary.rs +++ b/crates/tinyagents-integration-tests/tests/dependency_boundary.rs @@ -53,6 +53,10 @@ const KNOWN_GENERIC_CLAUDE_CODE_CHAT_MESSAGE_DEBT: &[(&str, usize)] = &[ "crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs", 46, ), + ( + "crates/tinyagents-harness/src/providers/claude_code/driver_tests.rs", + 465, + ), ( "crates/tinyagents-harness/src/providers/claude_code/input_builder.rs", 15, From f89a5e129b53f3baf285553f7ca9087ecb89ffcd Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 07:56:08 +0300 Subject: [PATCH 6/6] chore: restore main's Cargo.lock and tinyinference pin Co-authored-by: Medulla --- Cargo.lock | 10 +++++----- vendor/tinyinference | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4449d23e2..f6eefe133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1917,7 +1917,7 @@ dependencies = [ [[package]] name = "tinyinference-core" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "httpdate", @@ -1927,7 +1927,7 @@ dependencies = [ [[package]] name = "tinyinference-embeddings" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "once_cell", @@ -1944,7 +1944,7 @@ dependencies = [ [[package]] name = "tinyinference-image" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "base64 0.23.1", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "tinyinference-llm" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -1978,7 +1978,7 @@ dependencies = [ [[package]] name = "tinyinference-video" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "serde", diff --git a/vendor/tinyinference b/vendor/tinyinference index cafc5f373..8f55dfd5d 160000 --- a/vendor/tinyinference +++ b/vendor/tinyinference @@ -1 +1 @@ -Subproject commit cafc5f3730bbe92261db2913484369047b39d940 +Subproject commit 8f55dfd5d5e3789a400e4909eb44c86c0cb15c6a