Summary
Two concurrency issues in the host-runner:
1. Data race on Runner.drivers and Runner.inputs maps
Runner.drivers and Runner.inputs maps are accessed from two goroutines without synchronization:
- The main loop (
launchOne(), stopDriver())
- The A2A tunnel goroutine (
handleHostExit via host_verbs.go:90-96)
No mutex protects these maps. Under concurrent stop+exit, this is a real data race.
2. input_router.go fire-and-forget goroutines
input_router.go:239-257 (tick() method) launches a goroutine per event dispatch with no lifecycle tracking:
- No
wg.Done() — Detach() returns without waiting for in-flight dispatches
- If
driver.Input() blocks (ACP session/prompt can take up to 120s), the goroutine outlives Detach
- No mechanism to cancel in-flight dispatches on stop
3. StdioDriver/ACPDriver readLoop goroutine leak risk
readLoop() does not select on ctx.Done(). It relies on scanner EOF when the pipe closes. If Closer is nil or the pipe doesn't close, wg.Wait() in Stop() hangs indefinitely.
Summary
Two concurrency issues in the host-runner:
1. Data race on Runner.drivers and Runner.inputs maps
Runner.driversandRunner.inputsmaps are accessed from two goroutines without synchronization:launchOne(),stopDriver())handleHostExitviahost_verbs.go:90-96)No mutex protects these maps. Under concurrent stop+exit, this is a real data race.
2. input_router.go fire-and-forget goroutines
input_router.go:239-257(tick()method) launches a goroutine per event dispatch with no lifecycle tracking:wg.Done()—Detach()returns without waiting for in-flight dispatchesdriver.Input()blocks (ACPsession/promptcan take up to 120s), the goroutine outlivesDetach3. StdioDriver/ACPDriver readLoop goroutine leak risk
readLoop()does not select onctx.Done(). It relies on scanner EOF when the pipe closes. IfCloseris nil or the pipe doesn't close,wg.Wait()inStop()hangs indefinitely.