Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate. Related but different: [Bug]: Expo Device Hub retains simulator capture sessions without clients and uses ~200% CPU #16614 (Device Hub keeps capture sessions alive), [Bug]: Settling a thread leaves its preview browser renderers running, leaking ~250 MB each until restart #16371 (settling a thread leaks preview renderers).
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- In thread A, call
device_openfor an iOS Simulator and run anyagent-devicecommand with the returned--config/--sessionflags (e.g.open <bundle-id>). - Call
device_closefor that device (or just finish / settle / archive thread A without closing). - Run the bundled CLI's
agent-device device status(or look in~/.agent-device/device-claims/). - In thread B,
device_openthe same simulator and run anagent-devicecommand.
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_closeremoves the device from the panel but does not close the agent-device session. The claim stayslive.- 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:
device_closeshould runagent-device closefor the thread's session, keeping the simulator booted unlessshutdown: true.- Release a thread's device sessions when the thread settles or is archived, and/or after an idle timeout.
- Include
claimedBy(thread / workspace / last activity) indevice_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
juliusmarminge commentedon Oct 9, 2026
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_closeonly drops T3's own session recordDeviceService.close(handlers.ts#L241-L253).closefilters the thread's entries out of the in-memorysessionslist and calls the hub only whenshutdown: true(DeviceService.ts#L803-L825). I don't see anagent-device closefor the thread's--sessionanywhere inapps/server/src/deviceor the device toolkit.How sessions are created
device_openreturns--config <stateDir>/device/hosts/<hash>.json --session t3-<hash(thread, host, device)>(DeviceService.ts#L947-L953, AgentDeviceTarget.ts#L25-L29). The shim refuses commands without both flags (AgentDeviceShim.ts#L34-L35).AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS: "0"(LocalDeviceHost.ts#L502-L516). The daemon is stopped only when the host shuts down (LocalDeviceHost.ts#L566-L585). If agent-device ties claim staleness to the owner process, as you describe, that would explain why claims staylivefor as long as T3 runs. That part lives in agent-device, so I'm going on your description of it.No release on settle, archive, or delete, and no idle timeout
terminal.cleanupandpreview.cleanup(ThreadDeletion.ts#L216-L227).ResourceCleanupServicehas no device step (ResourceCleanupService.ts#L38-L61). I don't see a device equivalent for settle or archive either.device_listhas no claim stateopenentries (handlers.ts#L123-L148).DeviceSessionhas 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(runagent-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 indevice_list, looks like a separate enhancement.Artic0din commentedon Oct 23, 2026
Another data point from a
t3 triageinvestigation, 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).
agent-device.SimulatorTrampolineshowed 231% CPU in the user's menu-bar monitor.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.agent-device openfrom a T3 session failed after 41 s with asimctlerror while the machine was overloaded.apps/server/src/device/LocalDeviceHost.tsat this tag I found no cap on concurrent sessions. Its only semaphore (startLock) serializes hub start.EasDeviceHost.tsenforcesconfig.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.
Gieveppix commentedon Oct 30, 2026
Update: #21478 has merged and adds host-owned agent-session release before ordinary
device_closeremoves 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.
elannash commentedon Oct 31, 2026
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)
delegate_task) calleddevice_openfor an iOS Simulator. Its agent-deviceopensucceeded at 15:41 under sessiont3-68add828201501de1b7eec10, which isagentDeviceSession(childThreadId, "local", udid).cancelledat 16:07, and T3 resumed it as a restart continuation at 16:08. That continuation made no furtherdevice_opencall.task_cancel. The child's run wentinterrupted. Nothing archived the child thread.installon that simulator from two other threads failed withDEVICE_IN_USE("owned by session t3-68add828…"). Both requests loggeddevice_claim_conflictwithclassification: "live".closefor that session against T3's state dir.Why, at the
v0.1.6-nightly.20261030.3917tagAt this tag, a session that retires from
DeviceService(for example throughdevice_close) ends its agent-device session through its bound launcher (DeviceService.ts#L311-L339). This incident fell outside that path in two ways:task_canceldispatchesthread.stopon the child, then stops the child's own tasks and its queued turn (OrchestratorMcpService.ts#L2457-L2510). Neither that code nordispatchThreadStop(Orchestrator.ts#L11461-L11546) has a device step. fix(server): closing, archiving or deleting a thread releases its devices #25832 addsdevice.cleanupon 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.DeviceServicekeeps 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'scleanupDevicesgoes through thatclose. 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 fordevice_closewithshutdown: 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 triageflow in T3 Code Nightly.jaksiri commentedon Nov 4, 2026
Another data point from a
t3 triagesession, 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_openon 2026-11-02 at 12:05 UTC and drove the app until 2026-11-03 at 06:01 UTC. It never calleddevice_close. T3 Code auto-settled thread A at 08:10 UTC that day. About 24h later, thread B'sagent-device openfailed withDEVICE_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:
cleanUpSettledThreadcloses idle terminals and runs the settle script, and never touchesDeviceService.events.ndjsonhas noclose. In the source,devices.closeis only called from thedevice_closeMCP 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 callDeviceServicemakes inrelease. Afterwardssession listwas 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.ViaxCo commentedon Nov 4, 2026
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
device_close. They were idle, not settled or archived.xcrun simctl shutdownon both. Both booted again within seconds.device_closefrom the cleanup thread did nothing, because the devices were not in that thread's Device panel. A second thread can free them only withdevice_openand thendevice_closewithshutdown: 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
flutter runsession 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.
spadaval commentedon Nov 5, 2026
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.pbayer8 commentedon Nov 5, 2026
Additional current-version evidence from a main-checkout workflow, plus a limitation of the available settlement-hook workaround.
Environment
0.1.8-nightly.20261104.4192, packaged commit7f6f47b27b44.Observed incident
device_open, drove its app through the returned bound launcher, finished testing, and returned its final response without closing its automation session.open <bundle-id>with:--wait 1000failed 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.closesucceeded. 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_closeis 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 saysrunOnSettleruns 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 closeafter 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.
vcode-sh commentedon Nov 5, 2026
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-hub0.19.1, agent-device0.21.31, Android emulator37.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-5554through5560). One thread calleddevice_closeon 5554, withoutshutdown. 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:
device_list(suggestion 3 in the original report), so a cleanup agent can tell "abandoned" from "busy".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.
vinicius741 commentedon Nov 6, 2026
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_closewould have released the session. The thread never called it, though, and nothing else released it.Timeline (UTC)
device_openonemulator-5554. agent-deviceopensucceeded under sessiont3-e6e32ac5…. Its last device command ran at 22:47.device_opencalls, 4device_screenshotcalls and nodevice_close.openfailed withDEVICE_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.openfailed withDEVICE_IN_USEtwice, the second time with--wait 10000. Its manual QA was blocked.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
DEVICE_IN_USEerror 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.device_list,device_openanddevice_screenshot. They never say to calldevice_closewhen 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.Posted by Claude Code (Claude Opus 5.5) on behalf of @vinicius741.