mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(agent): resolve race conditions and resource leaks in SubTurn
Critical fixes (5): - Fix turnState hierarchy corruption in nested SubTurns by checking context before creating new root turnState in runAgentLoop - Fix deadlock risk in deliverSubTurnResult by separating lock and channel ops - Fix session rollback race in HardAbort by calling Finish() before rollback - Fix resource leak by closing pendingResults channel in Finish() with recovery - Add thread-safety docs for childTurnIDs and isFinished fields Medium priority fixes (5): - Move globalTurnCounter to AgentLoop.subTurnCounter to prevent ID conflicts - Improve semaphore acquisition to ensure release even on early validation failures - Document design choice: ephemeral sessions start empty for complete isolation - Add final poll before Finish() to capture late-arriving SubTurn results - Remove duplicate channel registration in spawnSubTurn to fix timing issues Testing: - Add 6 new tests covering hierarchy, deadlock, ordering, channel lifecycle, final poll, and semaphore behavior - All 12 SubTurn tests passing with race detector This resolves 10 critical and medium issues (5 race conditions, 2 resource leaks, 3 timing issues) identified in code review, bringing SubTurn to production-ready state.
This commit is contained in:
@@ -721,3 +721,34 @@ func TestFinishClosesChannel(t *testing.T) {
|
||||
// This should not panic - it should recover and emit OrphanResultEvent
|
||||
deliverSubTurnResult(ts, "child-1", result)
|
||||
}
|
||||
|
||||
// TestFinalPollCapturesLateResults verifies that the final poll before Finish()
|
||||
// captures results that arrive after the last iteration poll.
|
||||
func TestFinalPollCapturesLateResults(t *testing.T) {
|
||||
al, _, _, _, cleanup := newTestAgentLoop(t)
|
||||
defer cleanup()
|
||||
|
||||
sessionKey := "test-session-final-poll"
|
||||
ch := make(chan *tools.ToolResult, 4)
|
||||
|
||||
// Register the channel
|
||||
al.registerSubTurnResultChannel(sessionKey, ch)
|
||||
defer al.unregisterSubTurnResultChannel(sessionKey)
|
||||
|
||||
// Simulate results arriving after last iteration poll
|
||||
ch <- &tools.ToolResult{ForLLM: "result 1"}
|
||||
ch <- &tools.ToolResult{ForLLM: "result 2"}
|
||||
|
||||
// Dequeue should capture both results
|
||||
results := al.dequeuePendingSubTurnResults(sessionKey)
|
||||
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 results, got %d", len(results))
|
||||
}
|
||||
|
||||
// Verify channel is now empty
|
||||
results = al.dequeuePendingSubTurnResults(sessionKey)
|
||||
if len(results) != 0 {
|
||||
t.Errorf("expected 0 results on second poll, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user