|
4 | 4 | "context" |
5 | 5 | "encoding/json" |
6 | 6 | "fmt" |
7 | | - "io" |
8 | 7 | "log/slog" |
9 | 8 | "os" |
10 | 9 | "path/filepath" |
@@ -215,7 +214,6 @@ func (c *PTYConversation) Start(ctx context.Context) { |
215 | 214 | } |
216 | 215 | } |
217 | 216 |
|
218 | | - // Enqueue initial prompt once after agent is ready (and after state is potentially loaded) |
219 | 217 | if c.initialPromptReady && len(c.cfg.InitialPrompt) > 0 && !c.initialPromptSent { |
220 | 218 | c.outboundQueue <- outboundMessage{parts: c.cfg.InitialPrompt, errCh: nil} |
221 | 219 | c.initialPromptSent = true |
@@ -576,27 +574,34 @@ func (c *PTYConversation) SaveState() error { |
576 | 574 | initialPromptStr = buildStringFromMessageParts(c.cfg.InitialPrompt) |
577 | 575 | } |
578 | 576 |
|
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 | | - |
590 | 577 | // Create directory if it doesn't exist |
591 | 578 | dir := filepath.Dir(stateFile) |
592 | 579 | if err := os.MkdirAll(dir, 0o700); err != nil { |
593 | 580 | return xerrors.Errorf("failed to create state directory: %w", err) |
594 | 581 | } |
595 | 582 |
|
596 | | - // Write to temp file |
| 583 | + // Use atomic write: write to temp file, then rename to target path |
597 | 584 | 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) |
600 | 605 | } |
601 | 606 |
|
602 | 607 | // Atomic rename |
@@ -641,10 +646,6 @@ func (c *PTYConversation) loadStateLocked() error { |
641 | 646 | var agentState AgentState |
642 | 647 | decoder := json.NewDecoder(f) |
643 | 648 | 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 | | - } |
648 | 649 | return xerrors.Errorf("failed to unmarshal state (corrupted or invalid JSON): %w", err) |
649 | 650 | } |
650 | 651 |
|
|
0 commit comments