Skip to content

Commit bcf5823

Browse files
serghei-devsergeyklay
authored andcommitted
fix(opencode): address review feedback
Fix the OpenCode session handle and teardown behavior after PR review. - return the resumed session ID instead of leaking workspace paths via Session.ID - honor StopSession(ctx) with a bounded graceful shutdown path - correct the managed-environment warning text in export usage recovery - remove the dead errors import placeholder and add regression coverage for stop deadlines
1 parent c6c7624 commit bcf5823

4 files changed

Lines changed: 102 additions & 18 deletions

File tree

internal/agent/opencode/command_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package opencode
22

33
import (
44
"encoding/json"
5-
"errors"
65
"strings"
76
"testing"
87

@@ -482,6 +481,3 @@ func TestSSHRemoteCommand(t *testing.T) {
482481
}
483482
})
484483
}
485-
486-
// Compile-time interface check.
487-
var _ = errors.New // keep errors imported if needed in future

internal/agent/opencode/opencode.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (a *OpenCodeAdapter) StartSession(_ context.Context, params domain.StartSes
100100
}
101101

102102
return domain.Session{
103-
ID: params.WorkspacePath,
103+
ID: state.sessionID,
104104
AgentPID: "",
105105
Internal: state,
106106
}, nil
@@ -449,7 +449,7 @@ func (a *OpenCodeAdapter) RunTurn(ctx context.Context, session domain.Session, p
449449
}
450450

451451
// StopSession marks the session closed and terminates any active subprocess.
452-
func (a *OpenCodeAdapter) StopSession(_ context.Context, session domain.Session) error {
452+
func (a *OpenCodeAdapter) StopSession(ctx context.Context, session domain.Session) error {
453453
state, ok := session.Internal.(*sessionState)
454454
if !ok {
455455
return &domain.AgentError{
@@ -461,19 +461,10 @@ func (a *OpenCodeAdapter) StopSession(_ context.Context, session domain.Session)
461461
state.mu.Lock()
462462
state.closed = true
463463
active := state.active
464+
state.active = nil
464465
state.mu.Unlock()
465466

466-
if active == nil {
467-
return nil
468-
}
469-
470-
closeStop(active)
471-
killTurnProcess(active)
472-
<-active.readerDone
473-
_ = waitForProcess(active)
474-
clearActive(state, active)
475-
476-
return nil
467+
return stopActiveTurn(ctx, active)
477468
}
478469

479470
// EventStream returns nil because OpenCode events are delivered via the
@@ -695,6 +686,33 @@ func closeStop(runtime *turnRuntime) {
695686
})
696687
}
697688

689+
func stopActiveTurn(ctx context.Context, runtime *turnRuntime) error {
690+
if runtime == nil {
691+
return nil
692+
}
693+
694+
closeStop(runtime)
695+
if runtime.proc == nil {
696+
return nil
697+
}
698+
699+
_ = procutil.SignalGraceful(runtime.proc.Pid) //nolint:errcheck // best-effort signal; process may already be dead
700+
701+
graceTimer := time.NewTimer(5 * time.Second)
702+
defer stopTimer(graceTimer)
703+
704+
select {
705+
case <-runtime.waitCh:
706+
return nil
707+
case <-graceTimer.C:
708+
killTurnProcess(runtime)
709+
return nil
710+
case <-ctx.Done():
711+
killTurnProcess(runtime)
712+
return ctx.Err()
713+
}
714+
}
715+
698716
func killTurnProcess(runtime *turnRuntime) {
699717
if runtime == nil || runtime.proc == nil {
700718
return

internal/agent/opencode/opencode_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ func TestStartSession_ResumeSession(t *testing.T) {
164164
if state.sessionID != resumeID {
165165
t.Errorf("sessionID = %q, want %q", state.sessionID, resumeID)
166166
}
167+
if session.ID != resumeID {
168+
t.Errorf("session.ID = %q, want %q", session.ID, resumeID)
169+
}
167170
}
168171

169172
func TestRunTurn_WrongInternalType(t *testing.T) {
@@ -314,6 +317,73 @@ func TestStopSession_NoActiveTurn(t *testing.T) {
314317
}
315318
}
316319

320+
func TestStopSession_ContextDeadline(t *testing.T) {
321+
t.Parallel()
322+
323+
testCtx, testCancel := context.WithTimeout(context.Background(), 30*time.Second)
324+
defer testCancel()
325+
326+
tmpDir := t.TempDir()
327+
script := writeOpenCodeScript(t, tmpDir, `case "$1" in
328+
export) echo '{"messages":[]}'; exit 0;;
329+
esac
330+
trap '' TERM
331+
printf '{"type":"step_start","timestamp":1000,"sessionID":"ses_abc123","part":{"id":"p1","messageID":"m1","sessionID":"ses_abc123","snapshot":"","type":"step-start"}}\n'
332+
while :; do sleep 1; done`)
333+
334+
a, _ := NewOpenCodeAdapter(map[string]any{})
335+
session, err := a.StartSession(testCtx, domain.StartSessionParams{
336+
WorkspacePath: tmpDir,
337+
AgentConfig: domain.AgentConfig{Command: script},
338+
})
339+
if err != nil {
340+
t.Fatalf("StartSession() error = %v", err)
341+
}
342+
343+
gotEvent := make(chan struct{}, 1)
344+
resultCh := make(chan domain.TurnResult, 1)
345+
errCh := make(chan error, 1)
346+
go func() {
347+
result, runErr := a.RunTurn(context.Background(), session, domain.RunTurnParams{
348+
Prompt: "work",
349+
OnEvent: func(_ domain.AgentEvent) {
350+
select {
351+
case gotEvent <- struct{}{}:
352+
default:
353+
}
354+
},
355+
})
356+
resultCh <- result
357+
errCh <- runErr
358+
}()
359+
360+
select {
361+
case <-gotEvent:
362+
case <-testCtx.Done():
363+
t.Fatal("timed out waiting for first event")
364+
}
365+
366+
stopCtx, stopCancel := context.WithTimeout(testCtx, 50*time.Millisecond)
367+
defer stopCancel()
368+
369+
err = a.StopSession(stopCtx, session)
370+
if !errors.Is(err, context.DeadlineExceeded) {
371+
t.Fatalf("StopSession() error = %v, want %v", err, context.DeadlineExceeded)
372+
}
373+
374+
select {
375+
case result := <-resultCh:
376+
if result.ExitReason != domain.EventTurnCancelled {
377+
t.Errorf("ExitReason = %q, want %q", result.ExitReason, domain.EventTurnCancelled)
378+
}
379+
if runErr := <-errCh; runErr != nil {
380+
t.Errorf("RunTurn() error = %v, want nil", runErr)
381+
}
382+
case <-testCtx.Done():
383+
t.Fatal("RunTurn did not return after StopSession timeout")
384+
}
385+
}
386+
317387
func TestStopSession_WrongInternalType(t *testing.T) {
318388
t.Parallel()
319389

internal/agent/opencode/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func queryExportUsage(ctx context.Context, state *sessionState) exportUsage {
178178

179179
managedEnv, err := buildManagedEnv(state.passthrough)
180180
if err != nil {
181-
state.logger().Warn("failed to build opencode export command", slog.Any("error", err))
181+
state.logger().Warn("failed to build opencode managed environment", slog.Any("error", err))
182182
return exportUsage{}
183183
}
184184

0 commit comments

Comments
 (0)