Skip to content

Commit 1d7aaed

Browse files
committed
wip: address comments
1 parent 7e389d2 commit 1d7aaed

6 files changed

Lines changed: 45 additions & 26 deletions

File tree

cmd/server/server.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,6 @@ 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-
// Ensure PID file is cleaned up on exit
144-
defer cleanupPIDFile(pidFile, logger)
145143
}
146144

147145
printOpenAPI := viper.GetBool(FlagPrintOpenAPI)
@@ -183,7 +181,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
183181
fmt.Println(srv.GetOpenAPI())
184182
return nil
185183
}
186-
handleSignals(ctx, logger, srv, process)
184+
handleSignals(ctx, logger, srv, process, pidFile)
187185
logger.Info("Starting server on port", "port", port)
188186
processExitCh := make(chan error, 1)
189187
go func() {

cmd/server/signals.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
// performGracefulShutdown handles the common shutdown logic for all platforms.
1414
// It saves state, stops the HTTP server, closes the process, and exits.
15-
func performGracefulShutdown(sig os.Signal, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process) {
15+
func performGracefulShutdown(sig os.Signal, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process, pidFile string) {
1616
logger.Info("Received shutdown signal, initiating graceful shutdown", "signal", sig)
1717

1818
// Save state
@@ -31,6 +31,11 @@ func performGracefulShutdown(sig os.Signal, logger *slog.Logger, srv *httpapi.Se
3131
logger.Error("Failed to close process cleanly", "signal", sig, "error", err)
3232
}
3333

34+
// Clean up PID file before exit
35+
if pidFile != "" {
36+
cleanupPIDFile(pidFile, logger)
37+
}
38+
3439
// Exit cleanly
3540
os.Exit(0)
3641
}

cmd/server/signals_unix.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import (
1616
// handleSignals sets up signal handlers for:
1717
// - SIGTERM, SIGINT, SIGHUP: save conversation state, stop server, then close the process
1818
// - SIGUSR1: save conversation state without exiting
19-
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process) {
19+
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process, pidFile string) {
2020
// Handle shutdown signals (SIGTERM, SIGINT, SIGHUP)
2121
shutdownCh := make(chan os.Signal, 1)
2222
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT)
2323
go func() {
2424
defer signal.Stop(shutdownCh)
2525
sig := <-shutdownCh
26-
performGracefulShutdown(sig, logger, srv, process)
26+
performGracefulShutdown(sig, logger, srv, process, pidFile)
2727
}()
2828

2929
// Handle SIGUSR1 for save without exit

cmd/server/signals_windows.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import (
1515

1616
// handleSignals sets up signal handlers for Windows.
1717
// Only handles SIGTERM and SIGINT (SIGHUP and SIGUSR1 don't exist on Windows).
18-
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process) {
18+
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process, pidFile string) {
1919
// Handle shutdown signals (SIGTERM, SIGINT only on Windows)
2020
shutdownCh := make(chan os.Signal, 1)
2121
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
2222
go func() {
2323
defer signal.Stop(shutdownCh)
2424
sig := <-shutdownCh
25-
performGracefulShutdown(sig, logger, srv, process)
25+
performGracefulShutdown(sig, logger, srv, process, pidFile)
2626
}()
2727
}

lib/httpapi/events.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (e *EventEmitter) notifyChannels(eventType EventType, payload any) {
137137
}
138138
}
139139

140-
// UpdateMessagesAndEmitChanges assumes that only the last message can change or new messages can be added.
140+
// EmitMessages assumes that only the last message can change or new messages can be added.
141141
// If a new message is injected between existing messages (identified by Id), the behavior is undefined.
142142
func (e *EventEmitter) EmitMessages(newMessages []st.ConversationMessage) {
143143
e.mu.Lock()

lib/screentracker/pty_conversation.go

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"log/slog"
89
"os"
910
"path/filepath"
@@ -193,7 +194,17 @@ func (c *PTYConversation) Start(ctx context.Context) {
193194
}
194195

195196
if c.initialPromptReady && !c.loadStateSuccessful && c.cfg.StatePersistenceConfig.LoadState {
196-
_ = c.loadState()
197+
if err := c.loadStateLocked(); err != nil {
198+
// Add error message to conversation so user is aware
199+
errorMsg := fmt.Sprintf("AgentAPI state restoration failed, the conversation history may be missing: %v", err)
200+
c.messages = append(c.messages, ConversationMessage{
201+
Id: len(c.messages),
202+
Message: errorMsg,
203+
Role: ConversationRoleAgent,
204+
Time: c.cfg.Clock.Now(),
205+
})
206+
c.cfg.Logger.Error("Failed to load state", "error", err)
207+
}
197208
c.loadStateSuccessful = true
198209
}
199210

@@ -534,16 +545,14 @@ func (c *PTYConversation) Text() string {
534545
}
535546

536547
func (c *PTYConversation) SaveState() error {
537-
conversation := c.Messages()
538-
539548
c.lock.Lock()
540549
defer c.lock.Unlock()
541550

542551
stateFile := c.cfg.StatePersistenceConfig.StateFile
543552
saveState := c.cfg.StatePersistenceConfig.SaveState
544553

545554
if !saveState {
546-
c.cfg.Logger.Info("")
555+
c.cfg.Logger.Info("State persistence is disabled")
547556
return nil
548557
}
549558

@@ -553,6 +562,8 @@ func (c *PTYConversation) SaveState() error {
553562
return nil
554563
}
555564

565+
conversation := c.messagesLocked()
566+
556567
// Serialize initial prompt from message parts
557568
var initialPromptStr string
558569
if len(c.cfg.InitialPrompt) > 0 {
@@ -598,8 +609,8 @@ func (c *PTYConversation) SaveState() error {
598609
return nil
599610
}
600611

601-
// LoadState loads the state, this method assumes that caller holds the Lock
602-
func (c *PTYConversation) loadState() error {
612+
// loadStateLocked loads the state, this method assumes that caller holds the Lock
613+
func (c *PTYConversation) loadStateLocked() error {
603614
stateFile := c.cfg.StatePersistenceConfig.StateFile
604615
loadState := c.cfg.StatePersistenceConfig.LoadState
605616

@@ -613,20 +624,25 @@ func (c *PTYConversation) loadState() error {
613624
return nil
614625
}
615626

616-
// Read state file
617-
data, err := os.ReadFile(stateFile)
627+
// Open state file
628+
f, err := os.Open(stateFile)
618629
if err != nil {
619-
c.cfg.Logger.Warn("Failed to load state file", "path", stateFile, "err", err)
620-
return xerrors.Errorf("failed to read state file: %w", err)
621-
}
622-
623-
if len(data) == 0 {
624-
c.cfg.Logger.Info("No previous state to load (file is empty)", "path", stateFile)
625-
return nil
630+
c.cfg.Logger.Warn("Failed to open state file", "path", stateFile, "err", err)
631+
return xerrors.Errorf("failed to open state file: %w", err)
626632
}
633+
defer func() {
634+
if closeErr := f.Close(); closeErr != nil {
635+
c.cfg.Logger.Warn("Failed to close state file", "path", stateFile, "err", closeErr)
636+
}
637+
}()
627638

628639
var agentState AgentState
629-
if err := json.Unmarshal(data, &agentState); err != nil {
640+
decoder := json.NewDecoder(f)
641+
if err := decoder.Decode(&agentState); err != nil {
642+
if err == io.EOF {
643+
c.cfg.Logger.Info("No previous state to load (file is empty)", "path", stateFile)
644+
return nil
645+
}
630646
c.cfg.Logger.Warn("Failed to load state file (corrupted or invalid JSON)", "path", stateFile, "err", err)
631647
return xerrors.Errorf("failed to unmarshal state (corrupted or invalid JSON): %w", err)
632648
}
@@ -663,7 +679,7 @@ func (c *PTYConversation) adjustScreenAfterStateLoad(screen string) string {
663679
// Before the first user message after loading state, return the last message from the loaded state.
664680
// This prevents computing incorrect diffs from the restored screen, as the agent's message should
665681
// remain stable until the user continues the conversation.
666-
if c.userSentMessageAfterLoadState == false {
682+
if !c.userSentMessageAfterLoadState {
667683
newScreen = "\n" + c.messages[len(c.messages)-1].Message
668684
}
669685

0 commit comments

Comments
 (0)