fix(core): isolate sessions to prevent old/new interference#28
Merged
Conversation
When a new session starts while the old one is still running (e.g. finalizing ASR or LLM correction), they previously shared the same cancelled flag and session Arc, causing the old task's cleanup to wipe the new session's state. Rust side: create fresh per-session Arcs in sp_core_session_begin instead of reusing the global ones. The old task keeps its own Arc clones and exits via the cancellation signal without affecting the new session. Add an extra cancellation check before the LLM call so aborted sessions exit faster. ObjC side: use cancellable dispatch blocks for the 300ms delayed endSession calls. When a new session starts, cancel any pending block from the previous hotkey cycle so a stale endSession cannot drop the new session's audio channel.
The previous commit only checked cancellation before the LLM call. If a new session started while the old one was awaiting LLM correction, the old session would still paste its stale result into the new session's window. Add a post-LLM cancellation check.
Even with per-session Arcs and Rust-side cancellation checks, stale callbacks from an old session can still reach the ObjC main thread via dispatch_async after a new session has started, overwriting the new session's UI state. Add a u64 session_token to SPSessionContext and all session-scoped callbacks (ready, error, warning, final_text, state_changed, interim). ObjC increments the token on each beginSession and discards any callback whose token does not match the current one.
The paste completion (simulatePasteWithCompletion) and error reset (dispatch_after 2s) blocks run asynchronously on the main thread without going through the Rust callback chain, so the session token filter in SPRustBridge did not cover them. If a new session starts during the delay, the stale block would reset the UI to idle. Expose currentSessionToken on SPRustBridge and capture it in both blocks. Skip the idle transition when the token no longer matches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Sessions share global
cancelledandsessionArcs in theCorestruct. When a new session starts while the old one is still finalizing (ASR/LLM/paste):cleanup_sessionwipes the new session's state (same Arc)endSessionfrom the old hotkey cycle kills the new session's audio channeldispatch_asyncSolution
Three coordinated layers:
1. Per-session Arcs (Rust)
sp_core_session_begincreates freshcancelledandsessionArcs each time and signals the old task to cancel. The old task holds its own Arc clones — its cleanup cannot affect the new session. Cancellation checks are added both before and after the LLM call.2. Cancellable dispatch blocks (ObjC)
The 300ms delayed
endSessionuses a cancellabledispatch_block_t. NewholdStart/tapStartcancels any pending block so a staleendSessioncannot kill the new session's audio channel.3. Session token on all callbacks
A monotonic
u64 session_tokenis added toSPSessionContextand threaded through all 6 session-scoped callbacks. ObjC increments the token on eachbeginSessionand discards any callback whose token doesn't match — eliminating thedispatch_asyncrace. The token is also exposed viaSPRustBridge.currentSessionTokenand used to guard local async blocks (paste completion, error reset, audio error recovery).Files changed
koe-core/src/ffi.rs— session token inSPSessionContextand all callback signatureskoe-core/src/lib.rs— per-session Arcs, cancellation checks, token threadingKoeApp/Koe/Bridge/SPRustBridge.h—currentSessionTokenpropertyKoeApp/Koe/Bridge/SPRustBridge.m— token increment, stale callback filteringKoeApp/Koe/AppDelegate/SPAppDelegate.m— cancellable dispatch blocks, token guards on async blocksTest plan
make buildpasses