From fe64186ee3461dc18f554ff9eb9973aaa5b27408 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:47:54 +0000 Subject: [PATCH 1/7] refactor(tinymemory-tools): make EngineResolver::resolve async The EngineResolver trait now exposes an async resolve method, and all call sites await it. This lets resolvers perform asynchronous work such as I/O when locating the configured engine and identity instead of blocking the caller. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinymemory-tools/src/lifecycle/turn.rs | 9 +++++---- .../tinymemory-tools/src/lifecycle/turn_runtime_tests.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/tinymemory-tools/src/lifecycle/turn.rs b/crates/tinymemory-tools/src/lifecycle/turn.rs index d39209b5..2dad6f83 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn.rs @@ -56,13 +56,14 @@ pub trait Clock: Send + Sync { } /// Resolves the configured engine and identity to their lifecycle instance. +#[async_trait] pub trait EngineResolver: Send + Sync { /// Returns the configured memory instance for this host hook. /// /// # Errors /// /// Returns the host's engine or identity resolution failure. - fn resolve(&self) -> tinymemory_api::Result; + async fn resolve(&self) -> tinymemory_api::Result; } /// Receives background jobs returned by a successful post-turn log. @@ -307,7 +308,7 @@ impl TurnHooks { let work_clock = clock.clone(); let model = self.date_hint_model.clone(); let operation = async move { - let mut memory = resolver.resolve().ok()?; + let mut memory = resolver.resolve().await.ok()?; if deadline.is_some_and(|deadline| work_clock.now() >= deadline) { return None; } @@ -427,7 +428,7 @@ impl TurnHooks { if !logging { return; } - let Ok(memory) = self.resolver.resolve() else { + let Ok(memory) = self.resolver.resolve().await else { return; }; let Ok(report) = memory.post_turn(turn).await else { @@ -483,7 +484,7 @@ impl TurnHooks { let resolver = self.resolver.clone(); let clock = self.clock.clone(); let operation = async move { - let memory = resolver.resolve().ok()?; + let memory = resolver.resolve().await.ok()?; if deadline.is_some_and(|deadline| clock.now() >= deadline) { return None; } diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index dee04197..21554ad9 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -139,7 +139,7 @@ struct StaticResolver { } impl EngineResolver for StaticResolver { - fn resolve(&self) -> Result { + async fn resolve(&self) -> Result { self.calls.fetch_add(1, Ordering::SeqCst); self.clock.advance_by(self.advance); if self.fail { From 2583965aca6a6f3adae3daf63531b410033ce27a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:48:16 +0000 Subject: [PATCH 2/7] test(lifecycle): cover pre-turn awaiting async engine resolution Add a DeferredResolver test double whose resolve future stays pending until released, and a test asserting that pre_turn does not complete while engine resolution is still in flight. This pins down the ordering guarantee that citations are not recorded before the engine memory is available. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/lifecycle/turn_runtime_tests.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index 21554ad9..c0eb3130 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -138,6 +138,21 @@ struct StaticResolver { advance: Duration, } +struct DeferredResolver { + memory: AgentMemory, + release: Mutex>>, +} + +#[async_trait] +impl EngineResolver for DeferredResolver { + async fn resolve(&self) -> Result { + if let Some(release) = self.release.lock().unwrap().take() { + let _ = release.await; + } + Ok(self.memory.clone()) + } +} + impl EngineResolver for StaticResolver { async fn resolve(&self) -> Result { self.calls.fetch_add(1, Ordering::SeqCst); @@ -296,6 +311,39 @@ fn fixture_with_memory_and_advance( } } +#[tokio::test] +async fn pre_turn_awaits_async_engine_resolution() { + let engine = Arc::new(ReferenceEngine::new()); + let (release, wait) = oneshot::channel(); + let resolver = Arc::new(DeferredResolver { + memory: memory(engine), + release: Mutex::new(Some(wait)), + }); + let clock = FakeClock::at(Duration::ZERO); + let citations = Arc::new(RecordingCitations::default()); + let hooks = TurnHooks::new( + resolver, + Arc::new(clock.clone()), + None, + Arc::new(RecordingJobs::default()), + citations.clone(), + ); + let mut work = Box::pin(hooks.pre_turn( + PreTurn::new("thread", 0, "remember this"), + options(true, true), + )); + + poll_fn(|cx| match work.as_mut().poll(cx) { + Poll::Pending => Poll::Ready(()), + Poll::Ready(_) => panic!("pre-turn completed before async engine resolution"), + }) + .await; + release.send(()).unwrap(); + + assert!(work.await.is_some()); + assert_eq!(citations.calls.load(Ordering::SeqCst), 0); +} + fn options(logging: bool, recall: bool) -> PreTurnOptions { PreTurnOptions { logging, From 7d8cf40eec4dd42bcc4f099aa48c682b41d56861 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:48:32 +0000 Subject: [PATCH 3/7] test(lifecycle): add async_trait to StaticResolver and scope release lock The StaticResolver implementation of EngineResolver was missing the async_trait attribute, so it now matches the trait's async signature. The DeferredResolver resolve method also drops its mutex guard before awaiting the release, avoiding holding the lock across an await point. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index c0eb3130..61c22d7f 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -146,13 +146,15 @@ struct DeferredResolver { #[async_trait] impl EngineResolver for DeferredResolver { async fn resolve(&self) -> Result { - if let Some(release) = self.release.lock().unwrap().take() { + let release = self.release.lock().unwrap().take(); + if let Some(release) = release { let _ = release.await; } Ok(self.memory.clone()) } } +#[async_trait] impl EngineResolver for StaticResolver { async fn resolve(&self) -> Result { self.calls.fetch_add(1, Ordering::SeqCst); From d9fe2fdc6732f1f22939e645f17430efb4bd7a06 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:49:10 +0000 Subject: [PATCH 4/7] test(lifecycle): assert engine resolution is awaited once Track resolver invocations with an atomic counter so the pre-turn test can verify the async engine resolution is awaited exactly once before proceeding. The fixture now clones the resolver handle to keep it accessible after the turn completes. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../tinymemory-tools/src/lifecycle/turn_runtime_tests.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index 61c22d7f..2aab74fd 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -141,11 +141,13 @@ struct StaticResolver { struct DeferredResolver { memory: AgentMemory, release: Mutex>>, + calls: AtomicUsize, } #[async_trait] impl EngineResolver for DeferredResolver { async fn resolve(&self) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); let release = self.release.lock().unwrap().take(); if let Some(release) = release { let _ = release.await; @@ -304,7 +306,7 @@ fn fixture_with_memory_and_advance( )); Fixture { hooks, - resolver, + resolver.clone(), clock, model, jobs, @@ -320,6 +322,7 @@ async fn pre_turn_awaits_async_engine_resolution() { let resolver = Arc::new(DeferredResolver { memory: memory(engine), release: Mutex::new(Some(wait)), + calls: AtomicUsize::new(0), }); let clock = FakeClock::at(Duration::ZERO); let citations = Arc::new(RecordingCitations::default()); @@ -340,9 +343,10 @@ async fn pre_turn_awaits_async_engine_resolution() { Poll::Ready(_) => panic!("pre-turn completed before async engine resolution"), }) .await; + assert_eq!(resolver.calls.load(Ordering::SeqCst), 1); release.send(()).unwrap(); - assert!(work.await.is_some()); + let _ = work.await; assert_eq!(citations.calls.load(Ordering::SeqCst), 0); } From 67496f622088cda20c3342199d16143af348ac1a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:49:17 +0000 Subject: [PATCH 5/7] test(lifecycle): clone resolver in pre-turn async resolution test The test now clones the resolver before passing it to TurnHooks so the original binding stays available for later assertions. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index 2aab74fd..0ae27690 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -327,7 +327,7 @@ async fn pre_turn_awaits_async_engine_resolution() { let clock = FakeClock::at(Duration::ZERO); let citations = Arc::new(RecordingCitations::default()); let hooks = TurnHooks::new( - resolver, + resolver.clone(), Arc::new(clock.clone()), None, Arc::new(RecordingJobs::default()), From 1098a54c8cde259caaa7c37c6bb93552b10ff356 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:49:28 +0000 Subject: [PATCH 6/7] test(lifecycle): drop redundant clone in turn runtime fixture The fixture builder cloned the resolver before moving it into the struct, which was unnecessary since the value is not used afterwards. Removing the clone keeps the test setup free of a needless allocation. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs index 0ae27690..fb3ce185 100644 --- a/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs +++ b/crates/tinymemory-tools/src/lifecycle/turn_runtime_tests.rs @@ -306,7 +306,7 @@ fn fixture_with_memory_and_advance( )); Fixture { hooks, - resolver.clone(), + resolver, clock, model, jobs, From 54ec2a36a190bbffb1f73b03181442e8cfb58f87 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 02:52:57 +0000 Subject: [PATCH 7/7] chore(deps): update tokio to 1.53.3 and pin windows-sys to 0.52.0 Bumps tokio from 1.53.1 to 1.53.3 and downgrades the windows-sys dependency pulled in by several crates from 0.61.2 to 0.52.0. The lockfile was regenerated to reflect the resolved dependency graph. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc49190b..c1f1b409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,7 +392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1133,7 +1133,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1328,7 +1328,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1652,7 +1652,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1770,9 +1770,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "bytes", "libc", @@ -2182,7 +2182,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]]