fix(agent): fix data races, add tests, and fix raw protocol detection
- Fix data race on output strings.Builder in codex backend by adding mutex and waiting for reader goroutine before reading final output - Fix data race on onTurnDone by initializing it before reader starts - Fix bug where notificationProtocol zero value "" never matched "unknown", silently dropping all raw v2 notifications from codex - Add round-robin polling to prevent runtime starvation in poll loop - Log errors in claude handleControlRequest instead of silently dropping - Add 35 tests for pkg/agent covering claude parsing, codex JSON-RPC, protocol detection, event handling, and helper functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d9b687d92
commit
96cfdc2e27
6 changed files with 864 additions and 15 deletions
|
|
@ -60,23 +60,38 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti
|
|||
msgCh := make(chan Message, 256)
|
||||
resCh := make(chan Result, 1)
|
||||
|
||||
var outputMu sync.Mutex
|
||||
var output strings.Builder
|
||||
|
||||
// turnDone is set before starting the reader goroutine so there is no
|
||||
// race between the lifecycle goroutine writing and the reader reading.
|
||||
turnDone := make(chan bool, 1) // true = aborted
|
||||
|
||||
c := &codexClient{
|
||||
cfg: b.cfg,
|
||||
stdin: stdin,
|
||||
pending: make(map[int]*pendingRPC),
|
||||
// Set onMessage before starting the reader goroutine to avoid a race.
|
||||
cfg: b.cfg,
|
||||
stdin: stdin,
|
||||
pending: make(map[int]*pendingRPC),
|
||||
notificationProtocol: "unknown",
|
||||
onMessage: func(msg Message) {
|
||||
if msg.Type == MessageText {
|
||||
outputMu.Lock()
|
||||
output.WriteString(msg.Content)
|
||||
outputMu.Unlock()
|
||||
}
|
||||
trySend(msgCh, msg)
|
||||
},
|
||||
onTurnDone: func(aborted bool) {
|
||||
select {
|
||||
case turnDone <- aborted:
|
||||
default:
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Start reading stdout in background
|
||||
readerDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readerDone)
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
|
||||
for scanner.Scan() {
|
||||
|
|
@ -156,14 +171,6 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti
|
|||
b.cfg.Logger.Printf("[codex] thread started: %s", threadID)
|
||||
|
||||
// 3. Send turn and wait for completion
|
||||
turnDone := make(chan bool, 1) // true = aborted
|
||||
c.onTurnDone = func(aborted bool) {
|
||||
select {
|
||||
case turnDone <- aborted:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
_, err = c.request(runCtx, "turn/start", map[string]any{
|
||||
"threadId": threadID,
|
||||
"input": []map[string]any{
|
||||
|
|
@ -198,9 +205,16 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti
|
|||
b.cfg.Logger.Printf("[codex] finished pid=%d status=%s duration=%s",
|
||||
cmd.Process.Pid, finalStatus, duration.Round(time.Millisecond))
|
||||
|
||||
// Wait for the reader goroutine to finish so all output is accumulated.
|
||||
<-readerDone
|
||||
|
||||
outputMu.Lock()
|
||||
finalOutput := output.String()
|
||||
outputMu.Unlock()
|
||||
|
||||
resCh <- Result{
|
||||
Status: finalStatus,
|
||||
Output: output.String(),
|
||||
Output: finalOutput,
|
||||
Error: finalError,
|
||||
DurationMs: duration.Milliseconds(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue