Skip to content

fix: resolve_thread adopts existing session threads by UUID#377

Merged
henrypark133 merged 2 commits intomainfrom
worktree-fix-thread-switch
Feb 26, 2026
Merged

fix: resolve_thread adopts existing session threads by UUID#377
henrypark133 merged 2 commits intomainfrom
worktree-fix-thread-switch

Conversation

@henrypark133
Copy link
Copy Markdown
Collaborator

Summary

  • Fix thread ID mismatch that caused empty threads when switching back after mid-execution and orphaned duplicate tabs in the thread list
  • chat_new_thread_handler creates threads directly in the session without registering them in SessionManager.thread_map
  • On first message, resolve_thread couldn't find the mapping → created a duplicate thread with a different UUID
  • The agent processed on the duplicate UUID while the frontend tracked the original → loadHistory returned empty

Fix

In resolve_thread, before creating a new thread, check if the external_thread_id is itself a UUID that already exists as a thread in the session. If so, adopt it and register the mapping for future lookups. A mapped_elsewhere guard ensures channel-scoped isolation is preserved.

Changes

File What
src/agent/session_manager.rs Add UUID-based thread lookup fallback in resolve_thread + new test

Test plan

  • cargo fmt — clean
  • cargo clippy --all --all-features — zero warnings
  • cargo test — 1,598 passed, 0 failed
  • New test: test_resolve_thread_finds_existing_session_thread_by_uuid
  • Existing test: test_register_then_resolve_different_channel_creates_new (channel isolation preserved)
  • Manual: create new thread → send message → response appears correctly
  • Manual: switch threads mid-execution → switch back → response appears
  • Manual: no orphaned empty threads in sidebar

🤖 Generated with Claude Code

When chat_new_thread_handler creates a thread directly in the session,
it doesn't register a thread_map entry. On the first message,
resolve_thread would create a duplicate thread with a different UUID,
causing:

- Thread appears empty when switching back (loadHistory queries the
  original UUID but turns live on the duplicate)
- Orphaned tabs in the thread list (both the original and duplicate
  appear)

Fix: before creating a new thread, check if the external_thread_id is
itself a UUID that exists as a thread in the session. If so, adopt it
and register the mapping. A mapped_elsewhere guard preserves channel
scope isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added scope: agent Agent core (agent loop, router, scheduler) size: M 50-199 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: experienced 6-19 merged PRs labels Feb 25, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @henrypark133, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses critical thread management issues within the session manager, specifically preventing the creation of duplicate threads and ensuring proper continuity when switching between existing threads. By enhancing the resolve_thread function to intelligently identify and adopt already existing session threads based on their UUIDs, it eliminates scenarios leading to empty thread states and orphaned entries in the thread list, significantly improving the user experience and system stability.

Highlights

  • Thread Resolution Fix: Resolved issues causing empty threads and orphaned duplicate tabs by correcting thread ID mismatches in the session manager.
  • UUID-based Thread Adoption: Implemented a mechanism in resolve_thread to adopt existing session threads by their UUID if they are not already registered, preventing duplicate thread creation.
  • Channel Isolation Preserved: Ensured that channel-scoped isolation is maintained during the thread adoption process, preventing unintended cross-channel thread mapping.
Changelog
  • src/agent/session_manager.rs
    • Added logic to resolve_thread to check for and adopt existing session threads by UUID before creating a new one.
    • Included a new test case to verify the UUID-based thread adoption functionality.
Activity
  • All cargo fmt checks passed.
  • cargo clippy reported zero warnings.
  • All 1,598 cargo test cases passed successfully.
  • A new test, test_resolve_thread_finds_existing_session_thread_by_uuid, was added and passed.
  • An existing test, test_register_then_resolve_different_channel_creates_new, was verified to ensure channel isolation.
  • Manual tests confirmed correct behavior for new thread creation, switching threads mid-execution, and the absence of orphaned empty threads.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request aims to fix a thread ID mismatch by adopting existing session threads by their UUID in resolve_thread. The logic seems correct and is accompanied by a new test case. The identified race condition is a critical issue that has been retained as it does not contradict any existing rules and provides valuable feedback.

Comment on lines +143 to +158
if !mapped_elsewhere {
let sess = session.lock().await;
if sess.threads.contains_key(&ext_uuid) {
drop(sess);
// Register mapping so future lookups are fast
let mut thread_map = self.thread_map.write().await;
thread_map.insert(key, ext_uuid);
drop(thread_map);
// Ensure undo manager exists
let mut undo_managers = self.undo_managers.write().await;
undo_managers
.entry(ext_uuid)
.or_insert_with(|| Arc::new(Mutex::new(UndoManager::new())));
return (session, ext_uuid);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential race condition in this block that could violate the intended channel-scoped thread isolation. The check for mapped_elsewhere is done under a read lock, which is then released. If another thread acquires a write lock and maps the same ext_uuid before this thread acquires its write lock for insertion, this thread will proceed to create a duplicate mapping. This would break the rule that a thread UUID should only be mapped from one channel.

To fix this, you should re-check if the ext_uuid is mapped after acquiring the write lock on thread_map but before inserting the new key. This double-checked locking approach ensures the check and insertion are effectively atomic with respect to other threads trying to map the same UUID.

            if !mapped_elsewhere {
                let sess = session.lock().await;
                if sess.threads.contains_key(&ext_uuid) {
                    drop(sess);

                    let mut thread_map = self.thread_map.write().await;
                    // Re-check after acquiring write lock to prevent race condition.
                    if !thread_map.values().any(|&v| v == ext_uuid) {
                        // Register mapping so future lookups are fast
                        thread_map.insert(key, ext_uuid);
                        drop(thread_map);
                        // Ensure undo manager exists
                        let mut undo_managers = self.undo_managers.write().await;
                        undo_managers
                            .entry(ext_uuid)
                            .or_insert_with(|| Arc::new(Mutex::new(UndoManager::new())));
                        return (session, ext_uuid);
                    }
                    // If it was mapped elsewhere while we were unlocked, we fall through
                    // to create a new thread, preserving channel isolation.
                }
            }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this is a real TOCTOU race. Applied double-checked locking: re-check thread_map.values().any(|&v| v == ext_uuid) after acquiring the write lock. If the UUID was mapped by another task in the interim, we fall through to create a new thread instead. All 8 resolve_thread tests pass. See ecc2378.

Re-check mapped_elsewhere after acquiring the write lock to prevent
a TOCTOU race where another task could map the same UUID between
the read lock check and write lock insertion, breaking channel
isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@henrypark133 henrypark133 merged commit 2477923 into main Feb 26, 2026
4 checks passed
@henrypark133 henrypark133 deleted the worktree-fix-thread-switch branch February 26, 2026 00:01
@github-actions github-actions Bot mentioned this pull request Feb 26, 2026
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
* fix: resolve_thread adopts existing session threads by UUID

When chat_new_thread_handler creates a thread directly in the session,
it doesn't register a thread_map entry. On the first message,
resolve_thread would create a duplicate thread with a different UUID,
causing:

- Thread appears empty when switching back (loadHistory queries the
  original UUID but turns live on the duplicate)
- Orphaned tabs in the thread list (both the original and duplicate
  appear)

Fix: before creating a new thread, check if the external_thread_id is
itself a UUID that exists as a thread in the session. If so, adopt it
and register the mapping. A mapped_elsewhere guard preserves channel
scope isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: double-checked locking in resolve_thread UUID adoption

Re-check mapped_elsewhere after acquiring the write lock to prevent
a TOCTOU race where another task could map the same UUID between
the read lock check and write lock insertion, breaking channel
isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: experienced 6-19 merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants