b001aa1eb8d472630ce02a00a789c19fd69b0200
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3652f0fd79 |
fix(agentctl): order workspace stream wg.Add before publish to fix Close race (#1272)
* fix(agentctl): order workspace stream wg.Add before publish to fix Close race Client.StreamWorkspace published the new WorkspaceStream pointer to c.workspaceStream under the lock, then released the lock and called stream.wg.Add(2) afterwards. A concurrent Client.Close captured c.workspaceStream under the same lock and then called ws.Wait() outside of it, racing the subsequent Add(2) on a WaitGroup whose counter was still 0. The race detector flagged this as Add-after-Wait on the same counter address — reproduced in CI in TestInitializeAndPrompt_StreamBeforeInitialize and TestManager_RestartAgentProcess_StopErrorIsNonFatal, both of which use a deferred client.Close after the StreamManager spawns the workspace stream goroutine. Move stream.wg.Add(2) under the same lock that publishes the pointer. The unlock/lock pair now carries the Add into Close's view, so any caller that observes c.workspaceStream is guaranteed to see the counter already bumped before invoking Wait. Adds TestClientClose_StreamWorkspaceAddWaitRace as a deterministic regression: it waits for the server to observe the WS upgrade, then fires Close in parallel with StreamWorkspace — pre-fix this triggers the race within a handful of iterations under -race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(agentctl): defer ctx cancel in StreamWorkspaceAddWaitRace iter Wrap the per-iteration body in a runIter closure so `defer cancel()` covers every exit path (timeout fatals as well as the normal flow), replacing the explicit cancel() calls. Pure test hygiene — addresses Claude review comment on PR #1272. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9aaf3170e0 |
fix(lifecycle): drain StreamManager goroutines in tests (#1227)
* fix(lifecycle): drain StreamManager goroutines in tests Goroutines spawned by `agentctl.Client.StreamUpdates` / `StreamWorkspace` and by `lifecycle.StreamManager.connectWorkspaceStream` could outlive the tests that created them on slow CI runners, causing intermittent `goleak.VerifyTestMain` failures in `internal/agent/runtime/lifecycle` even though every individual test passed. The leak required the test to race: `client.Close()` could return while a workspace dial was still in flight, leaving the just-spawned WS read/write loops with nobody to drain them, and `StreamManager.Wait()` only fired the drain when the external `stopCh` had been closed first. Make the close paths absolute drain barriers: - `Client.Close()` now tracks every stream goroutine it spawns (`streamWG` + a per-stream `WorkspaceStream` reference) and blocks until all of them have exited. A `closed` flag flipped under the client mutex makes subsequent `StreamUpdates` / `StreamWorkspace` calls reject the dial, so a Close that races a dial in flight cannot leave behind a stranded WS connection. `manager_interaction.go` switches the restart path to per-stream Close helpers so it can keep reusing the client after teardown. - `StreamManager.Wait()` closes a new internal `waitCh` that the retry backoff (`sleepOrStop`) and the connected `<-ws.Done() / <-stop>` select also observe. This makes `Wait` an absolute drain barrier regardless of whether the caller ever closes the external `stopCh`. `streamContext` carries both stops through `stopChannelContext` so in-flight WebSocket dials cancel on either signal. Local stress (`make test-lifecycle-goleak LIFECYCLE_GOLEAK_COUNT=20`) passes 20/20 with `-race` after the fix. The new `TestClientClose_DrainsStreamGoroutines` covers the drain barrier and the post-Close rejection path. Added a `test-lifecycle-goleak` Makefile target as the repro hook for the CI-only flake. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(lifecycle): track mergeStops goroutine and guard workspace stream races Address PR review feedback: - stopChannelContext now registers the mergeStops goroutine on an optional *sync.WaitGroup so sm.wg.Wait stays a true drain barrier even for the connectUpdatesStream path (outer goroutine returns immediately after dial). - StreamWorkspace re-checks workspaceStreamConn after dial to seal the concurrent-callers race that could orphan a conn. - readWorkspaceStream's defer now guards the workspaceStreamConn reset by identity so an old read goroutine cannot zero out a newer conn pointer. - Clarify comments on streamWG, Client.Close, and the mock.connected wait in the close-barrier test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(lifecycle): drop agent-stream drain barrier to unblock workflow cascade The previous Close drain barrier for the agent (updates) stream blocked the on_turn_complete cascade flow. completeAndStopSession calls Close synchronously on the current session's client, and Close was waiting on streamWG (which tracked the read goroutine). The wait stalled the cascade long enough that Step2's auto-started session never got its profile name into the tab — surfacing as a flaky "Profile B tab not visible" failure in workflow-agent-switch E2E shard 7. Drain barrier is still in place for the workspace stream (where the original goleak flake lived). Agent stream is closed but not awaited: the cascade legitimately stops + restarts the agent stream on the same client, and the workspace stream's existing barrier is sufficient for the leak coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Kandev Agent <agent@kandev.dev> |