()
| 616 | } |
| 617 | |
| 618 | func (c *PTYConversation) SaveState() error { |
| 619 | c.lock.Lock() |
| 620 | defer c.lock.Unlock() |
| 621 | |
| 622 | stateFile := c.cfg.StatePersistenceConfig.StateFile |
| 623 | saveState := c.cfg.StatePersistenceConfig.SaveState |
| 624 | |
| 625 | if !saveState { |
| 626 | c.cfg.Logger.Info("State persistence is disabled") |
| 627 | return nil |
| 628 | } |
| 629 | |
| 630 | // Skip if not dirty |
| 631 | if !c.dirty { |
| 632 | c.cfg.Logger.Info("Skipping state save: no changes since last save") |
| 633 | return nil |
| 634 | } |
| 635 | |
| 636 | conversation := c.messagesLocked() |
| 637 | |
| 638 | // Serialize initial prompt from message parts |
| 639 | var initialPromptStr string |
| 640 | if len(c.cfg.InitialPrompt) > 0 { |
| 641 | initialPromptStr = buildStringFromMessageParts(c.cfg.InitialPrompt) |
| 642 | } |
| 643 | |
| 644 | // Create directory if it doesn't exist |
| 645 | dir := filepath.Dir(stateFile) |
| 646 | if err := os.MkdirAll(dir, 0o700); err != nil { |
| 647 | return xerrors.Errorf("failed to create state directory: %w", err) |
| 648 | } |
| 649 | |
| 650 | // Use atomic write: write to temp file, then rename to target path |
| 651 | tempFile := stateFile + ".tmp" |
| 652 | f, err := os.OpenFile(tempFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) |
| 653 | if err != nil { |
| 654 | return xerrors.Errorf("failed to create temp state file: %w", err) |
| 655 | } |
| 656 | |
| 657 | // Clean up temp file on error (before successful rename) |
| 658 | var renamed bool |
| 659 | defer func() { |
| 660 | if !renamed { |
| 661 | if removeErr := os.Remove(tempFile); removeErr != nil && !os.IsNotExist(removeErr) { |
| 662 | c.cfg.Logger.Warn("Failed to clean up temp state file", "path", tempFile, "err", removeErr) |
| 663 | } |
| 664 | } |
| 665 | }() |
| 666 | |
| 667 | // Encode directly to file to avoid loading entire JSON into memory |
| 668 | encoder := json.NewEncoder(f) |
| 669 | if err := encoder.Encode(AgentState{ |
| 670 | Version: 1, |
| 671 | Messages: conversation, |
| 672 | InitialPrompt: initialPromptStr, |
| 673 | InitialPromptSent: c.initialPromptSent, |
| 674 | }); err != nil { |
| 675 | _ = f.Close() |
nothing calls this directly
no test coverage detected