Skip to content

[Bug]: Thread device claims are never released — device_close and thread settle leave simulators "in use" indefinitely #17470

Open

Description

@dorukanc

Before submitting

Area

apps/server

Steps to reproduce

  1. In thread A, call device_open for an iOS Simulator and run any agent-device command with the returned --config/--session flags (e.g. open <bundle-id>).
  2. Call device_close for that device (or just finish / settle / archive thread A without closing).
  3. Run the bundled CLI's agent-device device status (or look in ~/.agent-device/device-claims/).
  4. In thread B, device_open the same simulator and run an agent-device command.

Expected behavior

When a thread is done with a device — device_close, the thread settling/archiving, or a reasonable idle timeout — its agent-device session closes and the host-wide device claim is released, so other threads can use that simulator.

Actual behavior

  • device_close removes the device from the panel but does not close the agent-device session. The claim stays live.
  • Every T3 thread's claim names the long-lived T3 agent-device daemon as its owner. agent-device only treats a claim as stale once the owner process dies, so these claims never expire while T3 Code is running.
  • Settling or archiving the thread doesn't release them either.

Result: after a day of parallel worktree threads, every simulator was claimed. device status showed 4/4 live, held by threads whose PRs had all merged 1–19 h earlier; two of those simulators were even shut down. Agents in new threads then see "in use by another workspace session" (DEVICE_IN_USE), assume a teammate is using the device, and create yet another simulator. That compounds into more stuck claims, more simulators and more RAM.

device_list doesn't expose claim state either, so an agent can't tell an abandoned claim from a real one.

Workaround: from outside the thread, run the bundled CLI close --session <t3-session> --state-dir <T3 agent-device state dir> per stale session (or restart T3 Code). The T3 agent-device wrapper rejects this because it requires --config/--session.

Suggested fixes:

  1. device_close should run agent-device close for the thread's session, keeping the simulator booted unless shutdown: true.
  2. Release a thread's device sessions when the thread settles or is archived, and/or after an idle timeout.
  3. Include claimedBy (thread / workspace / last activity) in device_list, so agents can choose a free device.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Alpha) 0.0.45 · agent-device 0.21.12 · device hub 0.12.0

Environment

macOS (Darwin 25.6), Apple Silicon, Xcode iOS 26.4 simulators, local device host. Several Claude Code threads running in parallel worktrees of one repo.

Activity

  1. juliusmarminge commented on Oct 9, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed report and the workaround. Reading current main (101f8b2f5), the T3 side looks consistent with what you describe. I haven't reproduced it.

    device_close only drops T3's own session record

    • The MCP handler calls DeviceService.close (handlers.ts#L241-L253).
    • close filters the thread's entries out of the in-memory sessions list and calls the hub only when shutdown: true (DeviceService.ts#L803-L825). I don't see an agent-device close for the thread's --session anywhere in apps/server/src/device or the device toolkit.

    How sessions are created

    No release on settle, archive, or delete, and no idle timeout

    device_list has no claim state

    • It returns hosts, devices, and the calling thread's own open entries (handlers.ts#L123-L148). DeviceSession has no claim or last-activity field (device.ts#L121-L127), so another thread's agent-device claim isn't visible.

    Related, but not duplicates

    A fix would likely touch DeviceService.close (run agent-device close --session … against the T3 state dir) and add a device cleanup effect next to the terminal and preview ones. Your third suggestion, claim info in device_list, looks like a separate enhancement.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 9, 2026
  3. added
    needs more infoInitial triage showed no bug. Awaiting more info
    and removed
    needs more infoInitial triage showed no bug. Awaiting more info
    on Oct 21, 2026
  4. Artic0din commented on Oct 23, 2026

    @Artic0din

    Another data point from a t3 triage investigation, on a newer build than the original report: 0.1.3-nightly.20261023.3553 (agent-device 0.21.25, expo-device-hub 0.15.3), macOS 27.2.0 arm64, 16 GB RAM, local device host.

    Here the problem is the number of simulators booted at once, not stale claims (the device-claims directory was empty when checked).

    • Two iOS 27.0 simulators were booted at once, about 2 minutes after the service started. Several delegated agent tasks drive simulators through agent-device. SimulatorTrampoline showed 231% CPU in the user's menu-bar monitor.
    • Swap was 5830 of 6144 MB used. After xcrun simctl shutdown all, swap fell to 1836 MB of 3072 MB and the 1-minute load average went from 173 to 22 within about 2 minutes.
    • One agent-device open from a T3 session failed after 41 s with a simctl error while the machine was overloaded.
    • In apps/server/src/device/LocalDeviceHost.ts at this tag I found no cap on concurrent sessions. Its only semaphore (startLock) serializes hub start. EasDeviceHost.ts enforces config.maxConcurrentSessions, so a similar limit for local simulators may be worth considering.

    That overload then repeated at every login because restart continuation resumed all in-flight runs together: #24059.

  5. Gieveppix commented on Oct 30, 2026

    @Gieveppix
    Contributor

    Update: #21478 has merged and adds host-owned agent-session release before ordinary device_close removes its panel record.

    #27569 has been reconciled with that change. It focuses on the remaining launcher-cleanup retry path: failed or timed-out CLI closes must not discard tracking, and partial release batches or failed power-off must remain retryable. It also retains failed background-cleanup entries. The updated branch passes 55 focused tests and the changed-file checks.

    Julius's #25832 separately proposes the broader archive/delete lifecycle cleanup. The failure-handling changes and tests in #27569 can be considered alongside that work or incorporated there if preferred; they do not add archive/delete cleanup or change settle/idle policy.

  6. elannash commented on Oct 31, 2026

    @elannash

    Note

    🤖 Claude Opus 5.5 responding on behalf of Elan

    Another data point, from the desktop app on macOS 27.0.1 arm64 (local device host, agent-device 0.21.31). A delegated task's device claim survived both an app update restart and a task_cancel. The claim dates from an earlier nightly whose version I didn't record. The update to 0.1.6-nightly.20261030.3917 installed partway through, and the cancel and both conflicts below ran on 3917.

    What happened (2026-10-30, UTC)

    • 15:29: a delegated child thread (delegate_task) called device_open for an iOS Simulator. Its agent-device open succeeded at 15:41 under session t3-68add828201501de1b7eec10, which is agentDeviceSession(childThreadId, "local", udid).
    • About 16:05: the desktop app installed the update and restarted its server. The agent-device daemon, running since 15:29, stayed up and kept the claim. The child's run ended cancelled at 16:07, and T3 resumed it as a restart continuation at 16:08. That continuation made no further device_open call.
    • 16:15: the parent stopped the task with task_cancel. The child's run went interrupted. Nothing archived the child thread.
    • 16:57 and 17:11: install on that simulator from two other threads failed with DEVICE_IN_USE ("owned by session t3-68add828…"). Both requests logged device_claim_conflict with classification: "live".
    • 17:17: another thread released the claim with the workaround from the issue body, running agent-device close for that session against T3's state dir.

    Why, at the v0.1.6-nightly.20261030.3917 tag

    At this tag, a session that retires from DeviceService (for example through device_close) ends its agent-device session through its bound launcher (DeviceService.ts#L311-L339). This incident fell outside that path in two ways:

    1. Cancelling a delegated task doesn't retire its device session. task_cancel dispatches thread.stop on the child, then stops the child's own tasks and its queued turn (OrchestratorMcpService.ts#L2457-L2510). Neither that code nor dispatchThreadStop (Orchestrator.ts#L11461-L11546) has a device step. fix(server): closing, archiving or deleting a thread releases its devices #25832 adds device.cleanup on archive and delete of the thread itself, and leaves settle alone on purpose. Nothing archives a delegated child automatically, so those hooks don't run when a device worker is cancelled.
    2. A server restart drops T3's record of the session, but the daemon and its claim survive. DeviceService keeps its sessions only in memory (DeviceService.ts#L303), and the launcher map it releases through is in memory too (DeviceService.ts#L312-L313). close({ threadId }) returns early when there is no matching session (DeviceService.ts#L846-L855), and fix(server): closing, archiving or deleting a thread releases its devices #25832's cleanupDevices goes through that close. CodeRabbit's open thread on #25832 raises this for a cleanup replayed after server loss. This incident shows it after an ordinary update restart, for a session opened before the restart. fix(server): device_close shutdown no longer reports success after a restart forgot the device #25167 describes the same in-memory gap for device_close with shutdown: true.

    The incident involved both the restart and the cancel, so it doesn't isolate the cancel path on its own. Point 1 comes from reading the source at the tag.

    Written by Claude Opus 5.5 with a hand-run t3 triage flow in T3 Code Nightly.

  7. jaksiri commented on Nov 4, 2026

    @jaksiri

    Another data point from a t3 triage session, on v0.1.6 (stable desktop app, "T3 Code (Beta)"), macOS 27.0 arm64, local device host.

    What happened. Thread A opened an iPhone 17 Pro with device_open on 2026-11-02 at 12:05 UTC and drove the app until 2026-11-03 at 06:01 UTC. It never called device_close. T3 Code auto-settled thread A at 08:10 UTC that day. About 24h later, thread B's agent-device open failed with DEVICE_IN_USE ("held by session t3-8c71…"), including after a 30s wait. Thread B's agent correctly refused to close another thread's session and told the user to close it "from the owning T3 thread".

    Two gaps on v0.1.6:

    • Settling doesn't release device sessions. cleanUpSettledThread closes idle terminals and runs the settle script, and never touches DeviceService.
    • The UI has no way to release a device. The user found the settled thread and closed its device preview window, but the claim stayed, and the session's events.ndjson has no close. In the source, devices.close is only called from the device_close MCP tool, which acts as the owning thread.

    It's also hard to find the owner. The error names an agent-device session, not a thread, and the owning thread had dropped out of the active list when it settled.

    Workaround that worked: run the stale session's own launcher, <state>/device/targets/<session>/agent-device close. That's the same call DeviceService makes in release. Afterwards session list was empty.

    I checked the v0.1.7 notes and found nothing that releases devices on settle.

    Posted by Claude Code (Claude Opus 5.5) via t3 triage.

  8. ViaxCo commented on Nov 4, 2026

    @ViaxCo
    Contributor

    Another data point, from T3 Code Nightly (now 0.1.7-nightly.20261104.4151; the events below were on Nov 3), macOS 26 arm64 (Darwin 25.6), 18 GB RAM, local device host, agent-device 0.21.31.

    Leftover sessions boot simulators again after a shutdown

    • On Nov 3, two iOS simulators (iPhone 17 Pro and iPhone Air) were still booted after the threads that opened them had finished their work. Those threads never called device_close. They were idle, not settled or archived.
    • A cleanup thread ran xcrun simctl shutdown on both. Both booted again within seconds.
    • The cause was two agent-device XCUITest runners, one for each simulator, that still ran for the old sessions. After the agent stopped the runners and shut the simulators down again, they stayed off. Free memory went from 31% to 51%.
    • device_close from the cleanup thread did nothing, because the devices were not in that thread's Device panel. A second thread can free them only with device_open and then device_close with shutdown: true. Agents do not find this on their own, and T3 does not tell them if the first thread still needs the device.

    Effect over a few days

    • Agents correctly leave alone the devices that other threads opened. But they cannot tell a finished thread from a working one, so devices from finished threads stay on.
    • At one point on Nov 3, 3 simulators and 1 emulator were on together, about 7 GB of 18 GB RAM. The device tools then timed out because the Mac was busy.
    • To fix this, I started 5 threads only to find and shut down leftover devices. One of those cleanups also stopped a flutter run session that another thread still used.

    About the open fix

    #25832 releases devices on close, archive and delete. The threads in this report were only idle, so that would not have freed their devices. A release after a period with no device commands (for example 30 minutes) would have.

  9. spadaval commented on Nov 5, 2026

    @spadaval

    Related, but the opposite problem: #31303. A held device didn't stop another thread from tapping on it. The fix in #31304 doesn't change when devices are released.

    Suggestion 3 here (show who holds a device in device_list) would help with both.

  10. pbayer8 commented on Nov 5, 2026

    @pbayer8

    Additional current-version evidence from a main-checkout workflow, plus a limitation of the available settlement-hook workaround.

    Environment

    • T3 Code Nightly 0.1.8-nightly.20261104.4192, packaged commit 7f6f47b27b44.
    • macOS 26.5.2 (25F84), Apple Silicon.
    • Local iOS 26.5 simulator; agent-device 0.21.31; device hub 0.19.1.
    • Codex / gpt-6.1-sol. Both threads use the project's main checkout, without separate worktrees.

    Observed incident

    1. Thread A opened the simulator through device_open, drove its app through the returned bound launcher, finished testing, and returned its final response without closing its automation session.
    2. A subsequent thread B opened the same simulator through its own device tools. Its bound launcher failed on open <bundle-id> with:
      Error (DEVICE_IN_USE): Device is already in use by session "<thread-A-session>".
      
    3. Retrying with --wait 1000 failed again after approximately 1002 ms, naming the same owner. B correctly did not take over another thread's session and left its visual/tap checks unverified.
    4. Returning to A and running its own bound launcher with close succeeded. The simulator remained booted and the local recordings remained intact.

    This confirms the completed/idle-turn case on this version. We did not perform a new settle/archive reproduction, and this incident does not establish that ordinary device_close is still broken after the fixes discussed above.

    Why a repository settlement hook cannot cover this workflow

    We looked for a native cleanup hook before falling back to instructions in AGENTS.md. The current thread documentation says runOnSettle runs only in a thread's separate worktree and skips the project's main checkout.

    Consequently, main-checkout users cannot use that hook to release their own device session on settlement. Our present workaround is prompting agents to run their own bound agent-device close after testing and before handing back/settling, without --shutdown. This depends on the agent remembering cleanup.

    Requested outcome: a native option to release only the settling thread's device ownership while retaining the booted simulator and its data, or a separately scoped, opt-in per-thread settlement hook that also supports main-checkout threads. This is not a request to run existing potentially destructive worktree-cleanup scripts unconditionally in main.

    Posted by Codex (gpt-6.1-sol) at the owner's request, based on the actual thread error output and local version inspection.

  11. vcode-sh commented on Nov 5, 2026

    @vcode-sh

    Hi! Adding a Linux + Android data point here, plus a small confession.

    I've been using T3 Code Nightly for about a month now (currently 0.1.7-nightly.20261104.4151, Ubuntu 26.04, local device host, expo-device-hub 0.19.1, agent-device 0.21.31, Android emulator 37.2.12). Today my machine started swapping like crazy, so I went looking for the culprit.

    Confession first: my own agents had left two 8 GB repo clones in /tmp, which on my box is tmpfs. So yes, I was eating my own RAM. I deleted them, swap went from 23.7 GB down to 8 GB, and I felt very smart for about a minute.

    Then I found the other 23 GB. Around 13:00, agents in a few parallel threads opened four AVDs with device_open (emulator-5554 through 5560). One thread called device_close on 5554, without shutdown. The other three never closed anything. At 22:00, nine hours later, all four emulators were still up, each qemu process sitting on 5.3 to 6.4 GB. (About 4 GB of each is guest RAM from my own quickboot snapshots, so that part is on me too.)

    I don't think #25832 would catch this. It frees the claim on close, archive and delete and leaves the device running, which makes sense for claims. My threads weren't archived though. They were just done and sitting there, and nothing powers off an emulator nobody has touched in hours. Right now the idle timeout is me, and I only notice once swap is full.

    Things that would have saved me today:

    1. An opt-in idle shutdown for devices the hub booted itself. Something like "no device commands and no viewer for 30 minutes, power it off", keeping the AVD data. Only for devices the hub actually owns, so it doesn't step on other tools (Android: the hub can adopt or kill an emulator it didn't start, because it never checks which AVD answers on a serial expo/expo-device-hub#551 covers the ownership side).
    2. Showing who holds a device and when it was last used in device_list (suggestion 3 in the original report), so a cleanup agent can tell "abandoned" from "busy".
    3. A per-host limit like feat(hub): cap booted simulators and emulators per host account with an opt-in policy expo/expo-device-hub#545. My fourth emulator would have been politely told no.

    I'm glad to test any of this on Linux and Android. The Device panel is honestly the first thing I show people when I talk about T3 Code. I'd just love it to give the RAM back when it's done.

  12. vinicius741 commented on Nov 6, 2026

    @vinicius741

    Another data point, on stable 0.1.7 (agent-device 0.21.31, expo-device-hub 0.19.1). Linux (Fedora Atomic), local Android emulator (API 36). This build includes #21478, so device_close would have released the session. The thread never called it, though, and nothing else released it.

    Timeline (UTC)

    • Nov 5, 22:45: thread A (Claude Code) called device_open on emulator-5554. agent-device open succeeded under session t3-e6e32ac5…. Its last device command ran at 22:47.
    • Nov 5, 23:11: thread A's last run completed and the thread settled. Its provider log shows 12 device_open calls, 4 device_screenshot calls and no device_close.
    • Nov 6, 02:32: thread B's open failed with DEVICE_IN_USE ("Device is already in use by session t3-e6e32ac5…"). The rest of that thread is on [Bug]: A thread refused with DEVICE_IN_USE can still tap on the device #31303.
    • Nov 6, 12:07: thread C's open failed with DEVICE_IN_USE twice, the second time with --wait 10000. Its manual QA was blocked.
    • Nov 6, 12:25: the user opened thread A and chose Power off device in the Device panel. Both stale sessions logged close (ok) at that moment, so the claim was held for about 13.5 hours after thread A settled. Freeing it meant powering the emulator off.

    What made this hard to recover from

    • The DEVICE_IN_USE error names a session, not a thread. Thread C had to grep the T3 provider logs for the session id to find thread A, which had dropped out of the active list when it settled.
    • The device instructions T3 adds to the system prompt cover device_list, device_open and device_screenshot. They never say to call device_close when the work is done, so finishing a turn with the device still open looks normal to the agent. Telling agents to close the device before their final reply would be a cheap mitigation until settle or idle release lands.
    • fix(server): closing, archiving or deleting a thread releases its devices #25832 releases devices on close, archive and delete, but deliberately not on settle. In this case the thread only completed and settled, so that PR wouldn't have freed the device. Releasing on settle, or after a stretch with no device commands, would have.

    Posted by Claude Code (Claude Opus 5.5) on behalf of @vinicius741.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions