Skip to content

Commit eef927d

Browse files
committed
feat: address maf's review
1 parent 220d360 commit eef927d

5 files changed

Lines changed: 34 additions & 46 deletions

File tree

cmd/server/server.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
140140
if err := writePIDFile(pidFile, logger); err != nil {
141141
return xerrors.Errorf("failed to write PID file: %w", err)
142142
}
143+
defer cleanupPIDFile(pidFile, logger)
143144
}
144145

145146
printOpenAPI := viper.GetBool(FlagPrintOpenAPI)
@@ -189,30 +190,20 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
189190
// Setup signal handlers (they will call gracefulCancel)
190191
handleSignals(gracefulCtx, gracefulCancel, logger, srv)
191192

192-
// Setup PID file cleanup
193-
if pidFile != "" {
194-
defer cleanupPIDFile(pidFile, logger)
195-
}
196-
197193
logger.Info("Starting server on port", "port", port)
198194

199195
// Monitor process exit
200196
processExitCh := make(chan error, 1)
201197
go func() {
202198
defer close(processExitCh)
199+
defer gracefulCancel()
203200
if err := process.Wait(); err != nil {
204201
if errors.Is(err, termexec.ErrNonZeroExitCode) {
205202
processExitCh <- xerrors.Errorf("========\n%s\n========\n: %w", strings.TrimSpace(process.ReadScreen()), err)
206203
} else {
207204
processExitCh <- xerrors.Errorf("failed to wait for process: %w", err)
208205
}
209206
}
210-
211-
select {
212-
case <-gracefulCtx.Done():
213-
default:
214-
gracefulCancel()
215-
}
216207
}()
217208

218209
// Start the server
@@ -243,17 +234,16 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
243234
logger.Error("Failed to stop HTTP server", "error", err)
244235
}
245236

246-
// Close the process
247-
if err := process.Close(logger, 5*time.Second); err != nil {
248-
logger.Error("Failed to close process cleanly", "error", err)
249-
}
250-
251237
select {
252238
case err := <-processExitCh:
253239
if err != nil {
254240
return xerrors.Errorf("agent exited with error: %w", err)
255241
}
256242
default:
243+
// Close the process
244+
if err := process.Close(logger, 5*time.Second); err != nil {
245+
logger.Error("Failed to close process cleanly", "error", err)
246+
}
257247
}
258248
return nil
259249
}

e2e/echo_test.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,6 @@ func TestE2E(t *testing.T) {
138138
// Step 2: Stop server (triggers state save)
139139
cleanup()
140140

141-
// Give filesystem a moment to sync
142-
time.Sleep(100 * time.Millisecond)
143-
144141
// Verify state file was created
145142
require.FileExists(t, stateFile, "State file should exist after shutdown")
146143

@@ -194,7 +191,6 @@ func TestE2E(t *testing.T) {
194191

195192
// Step 2: Close server
196193
cleanup()
197-
time.Sleep(100 * time.Millisecond)
198194
require.FileExists(t, stateFile, "State file should exist after shutdown")
199195

200196
// Step 3: Restart WITHOUT an initial prompt
@@ -213,7 +209,6 @@ func TestE2E(t *testing.T) {
213209

214210
// Step 5: Close server
215211
cleanup2()
216-
time.Sleep(100 * time.Millisecond)
217212

218213
// Step 6: Restart with same initial prompt
219214
_, apiClient3, cleanup3 := setup(ctx, t, &params{
@@ -256,7 +251,6 @@ func TestE2E(t *testing.T) {
256251

257252
// Step 2: Close server
258253
cleanup()
259-
time.Sleep(100 * time.Millisecond)
260254
require.FileExists(t, stateFile, "State file should exist after shutdown")
261255

262256
// Step 3: Restart with DIFFERENT initial prompt using a different script
@@ -384,7 +378,7 @@ func setup(ctx context.Context, t testing.TB, p *params, waitForStable bool) ([]
384378
// Create cleanup function
385379
cleanup := func() {
386380
if cmd.Process != nil {
387-
// Send SIGTERM to allow graceful shutdown and state save
381+
// Send SIGINT to allow graceful shutdown and state save
388382
_ = cmd.Process.Signal(os.Interrupt)
389383
// Wait for process to exit gracefully (with timeout)
390384
done := make(chan error, 1)
@@ -394,7 +388,7 @@ func setup(ctx context.Context, t testing.TB, p *params, waitForStable bool) ([]
394388
select {
395389
case <-done:
396390
// Process exited gracefully
397-
case <-time.After(5 * time.Second):
391+
case <-time.After(10 * time.Second):
398392
// Timeout, force kill
399393
_ = cmd.Process.Kill()
400394
<-done

lib/httpapi/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/sha256"
66
"encoding/hex"
77
"encoding/json"
8+
"errors"
89
"fmt"
910
"io"
1011
"log/slog"
@@ -615,7 +616,9 @@ func (s *Server) Stop(ctx context.Context) error {
615616
s.cleanupTempDir()
616617

617618
if s.srv != nil {
618-
err = s.srv.Shutdown(ctx)
619+
if err = s.srv.Shutdown(ctx); errors.Is(err, http.ErrServerClosed) {
620+
err = nil
621+
}
619622
}
620623
})
621624
return err

lib/screentracker/pty_conversation.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"io"
87
"log/slog"
98
"os"
109
"path/filepath"
@@ -215,7 +214,6 @@ func (c *PTYConversation) Start(ctx context.Context) {
215214
}
216215
}
217216

218-
// Enqueue initial prompt once after agent is ready (and after state is potentially loaded)
219217
if c.initialPromptReady && len(c.cfg.InitialPrompt) > 0 && !c.initialPromptSent {
220218
c.outboundQueue <- outboundMessage{parts: c.cfg.InitialPrompt, errCh: nil}
221219
c.initialPromptSent = true
@@ -576,27 +574,34 @@ func (c *PTYConversation) SaveState() error {
576574
initialPromptStr = buildStringFromMessageParts(c.cfg.InitialPrompt)
577575
}
578576

579-
// Use atomic write: write to temp file, then rename to target path
580-
data, err := json.MarshalIndent(AgentState{
581-
Version: 1,
582-
Messages: conversation,
583-
InitialPrompt: initialPromptStr,
584-
InitialPromptSent: c.initialPromptSent,
585-
}, "", " ")
586-
if err != nil {
587-
return xerrors.Errorf("failed to marshal state: %w", err)
588-
}
589-
590577
// Create directory if it doesn't exist
591578
dir := filepath.Dir(stateFile)
592579
if err := os.MkdirAll(dir, 0o700); err != nil {
593580
return xerrors.Errorf("failed to create state directory: %w", err)
594581
}
595582

596-
// Write to temp file
583+
// Use atomic write: write to temp file, then rename to target path
597584
tempFile := stateFile + ".tmp"
598-
if err := os.WriteFile(tempFile, data, 0o600); err != nil {
599-
return xerrors.Errorf("failed to write temp state file: %w", err)
585+
f, err := os.OpenFile(tempFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
586+
if err != nil {
587+
return xerrors.Errorf("failed to create temp state file: %w", err)
588+
}
589+
590+
// Encode directly to file to avoid loading entire JSON into memory
591+
encoder := json.NewEncoder(f)
592+
encoder.SetIndent("", " ")
593+
if err := encoder.Encode(AgentState{
594+
Version: 1,
595+
Messages: conversation,
596+
InitialPrompt: initialPromptStr,
597+
InitialPromptSent: c.initialPromptSent,
598+
}); err != nil {
599+
return xerrors.Errorf("failed to encode state: %w", err)
600+
}
601+
602+
// Close file before rename
603+
if err := f.Close(); err != nil {
604+
return xerrors.Errorf("failed to close temp state file: %w", err)
600605
}
601606

602607
// Atomic rename
@@ -641,10 +646,6 @@ func (c *PTYConversation) loadStateLocked() error {
641646
var agentState AgentState
642647
decoder := json.NewDecoder(f)
643648
if err := decoder.Decode(&agentState); err != nil {
644-
if err == io.EOF {
645-
c.cfg.Logger.Info("No previous state to load (file is empty)", "path", stateFile)
646-
return nil
647-
}
648649
return xerrors.Errorf("failed to unmarshal state (corrupted or invalid JSON): %w", err)
649650
}
650651

lib/termexec/termexec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (p *Process) Close(logger *slog.Logger, timeout time.Duration) error {
163163
case err := <-exited:
164164
var pathErr *os.SyscallError
165165
// ECHILD is expected if the process has already exited
166-
if err != nil && !(errors.As(err, &pathErr) && pathErr.Err == syscall.ECHILD) {
166+
if err != nil && !(errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ECHILD)) {
167167
exitErr = xerrors.Errorf("process exited with error: %w", err)
168168
}
169169
}

0 commit comments

Comments
 (0)