code-puppy 0.0.194__py3-none-any.whl → 0.0.196__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/command_line/command_handler.py +71 -82
- code_puppy/config.py +72 -98
- code_puppy/main.py +9 -0
- code_puppy/session_storage.py +250 -0
- {code_puppy-0.0.194.dist-info → code_puppy-0.0.196.dist-info}/METADATA +1 -1
- {code_puppy-0.0.194.dist-info → code_puppy-0.0.196.dist-info}/RECORD +10 -9
- {code_puppy-0.0.194.data → code_puppy-0.0.196.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.194.dist-info → code_puppy-0.0.196.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.194.dist-info → code_puppy-0.0.196.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.194.dist-info → code_puppy-0.0.196.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,12 @@
|
|
1
1
|
import os
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
2
4
|
|
3
5
|
from code_puppy.command_line.model_picker_completion import update_model_in_input
|
4
6
|
from code_puppy.command_line.motd import print_motd
|
5
7
|
from code_puppy.command_line.utils import make_directory_table
|
6
|
-
from code_puppy.config import get_config_keys
|
8
|
+
from code_puppy.config import CONTEXTS_DIR, get_config_keys
|
9
|
+
from code_puppy.session_storage import list_sessions, load_session, save_session
|
7
10
|
from code_puppy.tools.tools_content import tools_content
|
8
11
|
|
9
12
|
|
@@ -76,22 +79,10 @@ def get_commands_help():
|
|
76
79
|
Text("/load_context", style="cyan")
|
77
80
|
+ Text(" <name> Load message history from file")
|
78
81
|
)
|
79
|
-
help_lines.append(
|
80
|
-
Text("", style="cyan")
|
81
|
-
+ Text("Session Management:", style="bold yellow")
|
82
|
-
)
|
83
|
-
help_lines.append(
|
84
|
-
Text("auto_save_session", style="cyan")
|
85
|
-
+ Text(" Auto-save session after each response (true/false)")
|
86
|
-
)
|
87
|
-
help_lines.append(
|
88
|
-
Text("max_saved_sessions", style="cyan")
|
89
|
-
+ Text(" Maximum number of sessions to keep (default: 20, 0 = unlimited)")
|
90
|
-
)
|
91
82
|
help_lines.append(
|
92
83
|
Text("/set", style="cyan")
|
93
84
|
+ Text(
|
94
|
-
" Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true
|
85
|
+
" Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true)"
|
95
86
|
)
|
96
87
|
)
|
97
88
|
help_lines.append(
|
@@ -345,6 +336,30 @@ def handle_command(command: str):
|
|
345
336
|
)
|
346
337
|
return True
|
347
338
|
|
339
|
+
if command.startswith("/session"):
|
340
|
+
# /session id -> show current autosave id
|
341
|
+
# /session new -> rotate autosave id
|
342
|
+
tokens = command.split()
|
343
|
+
from code_puppy.config import (
|
344
|
+
AUTOSAVE_DIR,
|
345
|
+
get_current_autosave_id,
|
346
|
+
get_current_autosave_session_name,
|
347
|
+
rotate_autosave_id,
|
348
|
+
)
|
349
|
+
if len(tokens) == 1 or tokens[1] == "id":
|
350
|
+
sid = get_current_autosave_id()
|
351
|
+
emit_info(
|
352
|
+
f"[bold magenta]Autosave Session[/bold magenta]: {sid}\n"
|
353
|
+
f"Files prefix: {Path(AUTOSAVE_DIR) / get_current_autosave_session_name()}"
|
354
|
+
)
|
355
|
+
return True
|
356
|
+
if tokens[1] == "new":
|
357
|
+
new_sid = rotate_autosave_id()
|
358
|
+
emit_success(f"New autosave session id: {new_sid}")
|
359
|
+
return True
|
360
|
+
emit_warning("Usage: /session [id|new]")
|
361
|
+
return True
|
362
|
+
|
348
363
|
if command.startswith("/set"):
|
349
364
|
# Syntax: /set KEY=VALUE or /set KEY VALUE
|
350
365
|
from code_puppy.config import set_config_value
|
@@ -367,8 +382,12 @@ def handle_command(command: str):
|
|
367
382
|
config_keys = get_config_keys()
|
368
383
|
if "compaction_strategy" not in config_keys:
|
369
384
|
config_keys.append("compaction_strategy")
|
385
|
+
session_help = (
|
386
|
+
"\n[yellow]Session Management[/yellow]"
|
387
|
+
"\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
|
388
|
+
)
|
370
389
|
emit_warning(
|
371
|
-
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]"
|
390
|
+
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
|
372
391
|
)
|
373
392
|
return True
|
374
393
|
if key:
|
@@ -655,14 +674,7 @@ def handle_command(command: str):
|
|
655
674
|
return pr_prompt
|
656
675
|
|
657
676
|
if command.startswith("/dump_context"):
|
658
|
-
import json
|
659
|
-
import pickle
|
660
|
-
from datetime import datetime
|
661
|
-
from pathlib import Path
|
662
|
-
|
663
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
664
677
|
from code_puppy.agents.agent_manager import get_current_agent
|
665
|
-
from code_puppy.config import CONFIG_DIR
|
666
678
|
|
667
679
|
tokens = command.split()
|
668
680
|
if len(tokens) != 2:
|
@@ -677,49 +689,26 @@ def handle_command(command: str):
|
|
677
689
|
emit_warning("No message history to dump!")
|
678
690
|
return True
|
679
691
|
|
680
|
-
# Create contexts directory inside CONFIG_DIR if it doesn't exist
|
681
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
682
|
-
contexts_dir.mkdir(parents=True, exist_ok=True)
|
683
|
-
|
684
692
|
try:
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
current_agent = get_current_agent()
|
693
|
-
metadata = {
|
694
|
-
"session_name": session_name,
|
695
|
-
"timestamp": datetime.now().isoformat(),
|
696
|
-
"message_count": len(history),
|
697
|
-
"total_tokens": sum(
|
698
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
699
|
-
),
|
700
|
-
"file_path": str(pickle_file),
|
701
|
-
}
|
702
|
-
|
703
|
-
with open(meta_file, "w") as f:
|
704
|
-
json.dump(metadata, f, indent=2)
|
705
|
-
|
693
|
+
metadata = save_session(
|
694
|
+
history=history,
|
695
|
+
session_name=session_name,
|
696
|
+
base_dir=Path(CONTEXTS_DIR),
|
697
|
+
timestamp=datetime.now().isoformat(),
|
698
|
+
token_estimator=agent.estimate_tokens_for_message,
|
699
|
+
)
|
706
700
|
emit_success(
|
707
|
-
f"✅ Context saved: {
|
708
|
-
f"📁 Files: {
|
701
|
+
f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
|
702
|
+
f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
|
709
703
|
)
|
710
704
|
return True
|
711
705
|
|
712
|
-
except Exception as
|
713
|
-
emit_error(f"Failed to dump context: {
|
706
|
+
except Exception as exc:
|
707
|
+
emit_error(f"Failed to dump context: {exc}")
|
714
708
|
return True
|
715
709
|
|
716
710
|
if command.startswith("/load_context"):
|
717
|
-
import pickle
|
718
|
-
from pathlib import Path
|
719
|
-
|
720
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
721
711
|
from code_puppy.agents.agent_manager import get_current_agent
|
722
|
-
from code_puppy.config import CONFIG_DIR
|
723
712
|
|
724
713
|
tokens = command.split()
|
725
714
|
if len(tokens) != 2:
|
@@ -727,38 +716,38 @@ def handle_command(command: str):
|
|
727
716
|
return True
|
728
717
|
|
729
718
|
session_name = tokens[1]
|
730
|
-
contexts_dir = Path(
|
731
|
-
|
719
|
+
contexts_dir = Path(CONTEXTS_DIR)
|
720
|
+
session_path = contexts_dir / f"{session_name}.pkl"
|
732
721
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
722
|
+
try:
|
723
|
+
history = load_session(session_name, contexts_dir)
|
724
|
+
except FileNotFoundError:
|
725
|
+
emit_error(f"Context file not found: {session_path}")
|
726
|
+
available = list_sessions(contexts_dir)
|
737
727
|
if available:
|
738
|
-
|
739
|
-
|
728
|
+
emit_info(f"Available contexts: {', '.join(available)}")
|
729
|
+
return True
|
730
|
+
except Exception as exc:
|
731
|
+
emit_error(f"Failed to load context: {exc}")
|
740
732
|
return True
|
741
733
|
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
agent = get_current_agent()
|
747
|
-
agent.set_message_history(history)
|
748
|
-
current_agent = get_current_agent()
|
749
|
-
total_tokens = sum(
|
750
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
751
|
-
)
|
734
|
+
agent = get_current_agent()
|
735
|
+
agent.set_message_history(history)
|
736
|
+
total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
|
752
737
|
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
)
|
757
|
-
|
738
|
+
# Rotate autosave id to avoid overwriting any existing autosave
|
739
|
+
try:
|
740
|
+
from code_puppy.config import rotate_autosave_id
|
741
|
+
new_id = rotate_autosave_id()
|
742
|
+
autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
|
743
|
+
except Exception:
|
744
|
+
autosave_info = ""
|
758
745
|
|
759
|
-
|
760
|
-
|
761
|
-
|
746
|
+
emit_success(
|
747
|
+
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
748
|
+
f"📁 From: {session_path}{autosave_info}"
|
749
|
+
)
|
750
|
+
return True
|
762
751
|
|
763
752
|
if command.startswith("/truncate"):
|
764
753
|
from code_puppy.agents.agent_manager import get_current_agent
|
code_puppy/config.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
import configparser
|
2
|
+
import datetime
|
2
3
|
import json
|
3
4
|
import os
|
4
5
|
import pathlib
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
from code_puppy.session_storage import save_session
|
5
9
|
|
6
10
|
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".code_puppy")
|
7
11
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
|
@@ -10,10 +14,15 @@ COMMAND_HISTORY_FILE = os.path.join(CONFIG_DIR, "command_history.txt")
|
|
10
14
|
MODELS_FILE = os.path.join(CONFIG_DIR, "models.json")
|
11
15
|
EXTRA_MODELS_FILE = os.path.join(CONFIG_DIR, "extra_models.json")
|
12
16
|
AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
|
17
|
+
CONTEXTS_DIR = os.path.join(CONFIG_DIR, "contexts")
|
18
|
+
AUTOSAVE_DIR = os.path.join(CONFIG_DIR, "autosaves")
|
13
19
|
|
14
20
|
DEFAULT_SECTION = "puppy"
|
15
21
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
16
22
|
|
23
|
+
# Runtime-only autosave session ID (per-process)
|
24
|
+
_CURRENT_AUTOSAVE_ID: Optional[str] = None
|
25
|
+
|
17
26
|
# Cache containers for model validation and defaults
|
18
27
|
_model_validation_cache = {}
|
19
28
|
_default_model_cache = None
|
@@ -697,116 +706,81 @@ def set_max_saved_sessions(max_sessions: int):
|
|
697
706
|
set_config_value("max_saved_sessions", str(max_sessions))
|
698
707
|
|
699
708
|
|
700
|
-
def
|
701
|
-
"""
|
702
|
-
|
703
|
-
if
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
709
|
-
if not contexts_dir.exists():
|
710
|
-
return
|
711
|
-
|
712
|
-
# Get all .pkl files (session files) and sort by modification time
|
713
|
-
session_files = []
|
714
|
-
for pkl_file in contexts_dir.glob("*.pkl"):
|
715
|
-
try:
|
716
|
-
session_files.append((pkl_file.stat().st_mtime, pkl_file))
|
717
|
-
except OSError:
|
718
|
-
continue
|
719
|
-
|
720
|
-
# Sort by modification time (oldest first)
|
721
|
-
session_files.sort(key=lambda x: x[0])
|
722
|
-
|
723
|
-
# If we have more than max_sessions, remove the oldest ones
|
724
|
-
if len(session_files) > max_sessions:
|
725
|
-
files_to_remove = session_files[:-max_sessions] # All except the last max_sessions
|
726
|
-
|
727
|
-
from rich.console import Console
|
728
|
-
console = Console()
|
729
|
-
|
730
|
-
for _, old_file in files_to_remove:
|
731
|
-
try:
|
732
|
-
# Remove the .pkl file
|
733
|
-
old_file.unlink()
|
734
|
-
|
735
|
-
# Also remove the corresponding _meta.json file if it exists
|
736
|
-
meta_file = contexts_dir / f"{old_file.stem}_meta.json"
|
737
|
-
if meta_file.exists():
|
738
|
-
meta_file.unlink()
|
739
|
-
|
740
|
-
console.print(f"[dim]🗑️ Removed old session: {old_file.name}[/dim]")
|
741
|
-
|
742
|
-
except OSError as e:
|
743
|
-
console.print(f"[dim]❌ Failed to remove {old_file.name}: {e}[/dim]")
|
709
|
+
def get_current_autosave_id() -> str:
|
710
|
+
"""Get or create the current autosave session ID for this process."""
|
711
|
+
global _CURRENT_AUTOSAVE_ID
|
712
|
+
if not _CURRENT_AUTOSAVE_ID:
|
713
|
+
# Use a full timestamp so tests and UX can predict the name if needed
|
714
|
+
_CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
715
|
+
return _CURRENT_AUTOSAVE_ID
|
744
716
|
|
745
717
|
|
746
|
-
def
|
747
|
-
"""
|
748
|
-
|
749
|
-
|
750
|
-
|
718
|
+
def rotate_autosave_id() -> str:
|
719
|
+
"""Force a new autosave session ID and return it."""
|
720
|
+
global _CURRENT_AUTOSAVE_ID
|
721
|
+
_CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
722
|
+
return _CURRENT_AUTOSAVE_ID
|
723
|
+
|
724
|
+
|
725
|
+
def get_current_autosave_session_name() -> str:
|
726
|
+
"""Return the full session name used for autosaves (no file extension)."""
|
727
|
+
return f"auto_session_{get_current_autosave_id()}"
|
728
|
+
|
729
|
+
|
730
|
+
def set_current_autosave_from_session_name(session_name: str) -> str:
|
731
|
+
"""Set the current autosave ID based on a full session name.
|
732
|
+
|
733
|
+
Accepts names like 'auto_session_YYYYMMDD_HHMMSS' and extracts the ID part.
|
734
|
+
Returns the ID that was set.
|
751
735
|
"""
|
736
|
+
global _CURRENT_AUTOSAVE_ID
|
737
|
+
prefix = "auto_session_"
|
738
|
+
if session_name.startswith(prefix):
|
739
|
+
_CURRENT_AUTOSAVE_ID = session_name[len(prefix):]
|
740
|
+
else:
|
741
|
+
_CURRENT_AUTOSAVE_ID = session_name
|
742
|
+
return _CURRENT_AUTOSAVE_ID
|
743
|
+
|
744
|
+
|
745
|
+
def auto_save_session_if_enabled() -> bool:
|
746
|
+
"""Automatically save the current session if auto_save_session is enabled."""
|
752
747
|
if not get_auto_save_session():
|
753
748
|
return False
|
754
|
-
|
749
|
+
|
755
750
|
try:
|
756
|
-
import
|
757
|
-
import
|
758
|
-
|
759
|
-
from pathlib import Path
|
751
|
+
import pathlib
|
752
|
+
from rich.console import Console
|
753
|
+
|
760
754
|
from code_puppy.agents.agent_manager import get_current_agent
|
761
|
-
|
762
|
-
|
755
|
+
|
756
|
+
console = Console()
|
757
|
+
|
763
758
|
current_agent = get_current_agent()
|
764
759
|
history = current_agent.get_message_history()
|
765
|
-
|
766
760
|
if not history:
|
767
|
-
return False
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
# Also save metadata as JSON for readability
|
783
|
-
meta_file = contexts_dir / f"{session_name}_meta.json"
|
784
|
-
metadata = {
|
785
|
-
"session_name": session_name,
|
786
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
787
|
-
"message_count": len(history),
|
788
|
-
"total_tokens": sum(
|
789
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
790
|
-
),
|
791
|
-
"file_path": str(pickle_file),
|
792
|
-
"auto_saved": True,
|
793
|
-
}
|
794
|
-
|
795
|
-
with open(meta_file, "w") as f:
|
796
|
-
json.dump(metadata, f, indent=2)
|
797
|
-
|
798
|
-
from rich.console import Console
|
799
|
-
console = Console()
|
761
|
+
return False
|
762
|
+
|
763
|
+
now = datetime.datetime.now()
|
764
|
+
session_name = get_current_autosave_session_name()
|
765
|
+
autosave_dir = pathlib.Path(AUTOSAVE_DIR)
|
766
|
+
|
767
|
+
metadata = save_session(
|
768
|
+
history=history,
|
769
|
+
session_name=session_name,
|
770
|
+
base_dir=autosave_dir,
|
771
|
+
timestamp=now.isoformat(),
|
772
|
+
token_estimator=current_agent.estimate_tokens_for_message,
|
773
|
+
auto_saved=True,
|
774
|
+
)
|
775
|
+
|
800
776
|
console.print(
|
801
|
-
f"🐾 [dim]Auto-saved session: {
|
777
|
+
f"🐾 [dim]Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)[/dim]"
|
802
778
|
)
|
803
|
-
|
804
|
-
# Cleanup old sessions if limit is set
|
805
|
-
_cleanup_old_sessions()
|
779
|
+
|
806
780
|
return True
|
807
|
-
|
808
|
-
except Exception as
|
781
|
+
|
782
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
809
783
|
from rich.console import Console
|
810
|
-
|
811
|
-
|
784
|
+
|
785
|
+
Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
|
812
786
|
return False
|
code_puppy/main.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
import argparse
|
2
2
|
import asyncio
|
3
|
+
import json
|
3
4
|
import os
|
4
5
|
import subprocess
|
5
6
|
import sys
|
6
7
|
import time
|
7
8
|
import webbrowser
|
9
|
+
from datetime import datetime
|
10
|
+
from pathlib import Path
|
8
11
|
|
9
12
|
from rich.console import Console, ConsoleOptions, RenderResult
|
10
13
|
from rich.markdown import CodeBlock, Markdown
|
@@ -18,11 +21,13 @@ from code_puppy.command_line.prompt_toolkit_completion import (
|
|
18
21
|
get_prompt_with_active_model,
|
19
22
|
)
|
20
23
|
from code_puppy.config import (
|
24
|
+
AUTOSAVE_DIR,
|
21
25
|
COMMAND_HISTORY_FILE,
|
22
26
|
ensure_config_exists,
|
23
27
|
initialize_command_history_file,
|
24
28
|
save_command_to_history,
|
25
29
|
)
|
30
|
+
from code_puppy.session_storage import list_sessions, load_session, restore_autosave_interactively
|
26
31
|
from code_puppy.http_utils import find_available_port
|
27
32
|
from code_puppy.tools.common import console
|
28
33
|
|
@@ -288,6 +293,8 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
288
293
|
from code_puppy.messaging import emit_info
|
289
294
|
|
290
295
|
emit_info("[bold cyan]Initializing agent...[/bold cyan]")
|
296
|
+
|
297
|
+
|
291
298
|
# Initialize the runtime agent manager
|
292
299
|
if initial_command:
|
293
300
|
from code_puppy.agents import get_current_agent
|
@@ -367,6 +374,8 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
367
374
|
emit_error(f"Error installing prompt_toolkit: {e}")
|
368
375
|
emit_warning("Falling back to basic input without tab completion")
|
369
376
|
|
377
|
+
await restore_autosave_interactively(Path(AUTOSAVE_DIR))
|
378
|
+
|
370
379
|
while True:
|
371
380
|
from code_puppy.agents.agent_manager import get_current_agent
|
372
381
|
from code_puppy.messaging import emit_info
|
@@ -0,0 +1,250 @@
|
|
1
|
+
"""Shared helpers for persisting and restoring chat sessions.
|
2
|
+
|
3
|
+
This module centralises the pickle + metadata handling that used to live in
|
4
|
+
both the CLI command handler and the auto-save feature. Keeping it here helps
|
5
|
+
us avoid duplication while staying inside the Zen-of-Python sweet spot: simple
|
6
|
+
is better than complex, nested side effects are worse than deliberate helpers.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import json
|
12
|
+
import pickle
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any, Callable, List
|
16
|
+
|
17
|
+
SessionHistory = List[Any]
|
18
|
+
TokenEstimator = Callable[[Any], int]
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass(slots=True)
|
22
|
+
class SessionPaths:
|
23
|
+
pickle_path: Path
|
24
|
+
metadata_path: Path
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass(slots=True)
|
28
|
+
class SessionMetadata:
|
29
|
+
session_name: str
|
30
|
+
timestamp: str
|
31
|
+
message_count: int
|
32
|
+
total_tokens: int
|
33
|
+
pickle_path: Path
|
34
|
+
metadata_path: Path
|
35
|
+
auto_saved: bool = False
|
36
|
+
|
37
|
+
def as_serialisable(self) -> dict[str, Any]:
|
38
|
+
return {
|
39
|
+
"session_name": self.session_name,
|
40
|
+
"timestamp": self.timestamp,
|
41
|
+
"message_count": self.message_count,
|
42
|
+
"total_tokens": self.total_tokens,
|
43
|
+
"file_path": str(self.pickle_path),
|
44
|
+
"auto_saved": self.auto_saved,
|
45
|
+
}
|
46
|
+
|
47
|
+
|
48
|
+
def ensure_directory(path: Path) -> Path:
|
49
|
+
path.mkdir(parents=True, exist_ok=True)
|
50
|
+
return path
|
51
|
+
|
52
|
+
|
53
|
+
def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
|
54
|
+
pickle_path = base_dir / f"{session_name}.pkl"
|
55
|
+
metadata_path = base_dir / f"{session_name}_meta.json"
|
56
|
+
return SessionPaths(pickle_path=pickle_path, metadata_path=metadata_path)
|
57
|
+
|
58
|
+
|
59
|
+
def save_session(
|
60
|
+
*,
|
61
|
+
history: SessionHistory,
|
62
|
+
session_name: str,
|
63
|
+
base_dir: Path,
|
64
|
+
timestamp: str,
|
65
|
+
token_estimator: TokenEstimator,
|
66
|
+
auto_saved: bool = False,
|
67
|
+
) -> SessionMetadata:
|
68
|
+
ensure_directory(base_dir)
|
69
|
+
paths = build_session_paths(base_dir, session_name)
|
70
|
+
|
71
|
+
with paths.pickle_path.open("wb") as pickle_file:
|
72
|
+
pickle.dump(history, pickle_file)
|
73
|
+
|
74
|
+
total_tokens = sum(token_estimator(message) for message in history)
|
75
|
+
metadata = SessionMetadata(
|
76
|
+
session_name=session_name,
|
77
|
+
timestamp=timestamp,
|
78
|
+
message_count=len(history),
|
79
|
+
total_tokens=total_tokens,
|
80
|
+
pickle_path=paths.pickle_path,
|
81
|
+
metadata_path=paths.metadata_path,
|
82
|
+
auto_saved=auto_saved,
|
83
|
+
)
|
84
|
+
|
85
|
+
with paths.metadata_path.open("w", encoding="utf-8") as metadata_file:
|
86
|
+
json.dump(metadata.as_serialisable(), metadata_file, indent=2)
|
87
|
+
|
88
|
+
return metadata
|
89
|
+
|
90
|
+
|
91
|
+
def load_session(session_name: str, base_dir: Path) -> SessionHistory:
|
92
|
+
paths = build_session_paths(base_dir, session_name)
|
93
|
+
if not paths.pickle_path.exists():
|
94
|
+
raise FileNotFoundError(paths.pickle_path)
|
95
|
+
with paths.pickle_path.open("rb") as pickle_file:
|
96
|
+
return pickle.load(pickle_file)
|
97
|
+
|
98
|
+
|
99
|
+
def list_sessions(base_dir: Path) -> List[str]:
|
100
|
+
if not base_dir.exists():
|
101
|
+
return []
|
102
|
+
return sorted(path.stem for path in base_dir.glob("*.pkl"))
|
103
|
+
|
104
|
+
|
105
|
+
def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
|
106
|
+
if max_sessions <= 0:
|
107
|
+
return []
|
108
|
+
|
109
|
+
if not base_dir.exists():
|
110
|
+
return []
|
111
|
+
|
112
|
+
candidate_paths = list(base_dir.glob("*.pkl"))
|
113
|
+
if len(candidate_paths) <= max_sessions:
|
114
|
+
return []
|
115
|
+
|
116
|
+
sorted_candidates = sorted(
|
117
|
+
((path.stat().st_mtime, path) for path in candidate_paths),
|
118
|
+
key=lambda item: item[0],
|
119
|
+
)
|
120
|
+
|
121
|
+
stale_entries = sorted_candidates[:-max_sessions]
|
122
|
+
removed_sessions: List[str] = []
|
123
|
+
for _, pickle_path in stale_entries:
|
124
|
+
metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
|
125
|
+
try:
|
126
|
+
pickle_path.unlink(missing_ok=True)
|
127
|
+
metadata_path.unlink(missing_ok=True)
|
128
|
+
removed_sessions.append(pickle_path.stem)
|
129
|
+
except OSError:
|
130
|
+
continue
|
131
|
+
|
132
|
+
return removed_sessions
|
133
|
+
|
134
|
+
|
135
|
+
async def restore_autosave_interactively(base_dir: Path) -> None:
|
136
|
+
"""Prompt the user to load an autosave session from base_dir, if any exist.
|
137
|
+
|
138
|
+
This helper is deliberately placed in session_storage to keep autosave
|
139
|
+
restoration close to the persistence layer. It uses the same public APIs
|
140
|
+
(list_sessions, load_session) and mirrors the interactive behaviours from
|
141
|
+
the command handler.
|
142
|
+
"""
|
143
|
+
sessions = list_sessions(base_dir)
|
144
|
+
if not sessions:
|
145
|
+
return
|
146
|
+
|
147
|
+
# Import locally to avoid pulling the messaging layer into storage modules
|
148
|
+
from datetime import datetime
|
149
|
+
from prompt_toolkit.formatted_text import FormattedText
|
150
|
+
|
151
|
+
from code_puppy.agents.agent_manager import get_current_agent
|
152
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
153
|
+
get_input_with_combined_completion,
|
154
|
+
)
|
155
|
+
from code_puppy.messaging import emit_success, emit_system_message, emit_warning
|
156
|
+
|
157
|
+
entries = []
|
158
|
+
for name in sessions:
|
159
|
+
meta_path = base_dir / f"{name}_meta.json"
|
160
|
+
try:
|
161
|
+
with meta_path.open("r", encoding="utf-8") as meta_file:
|
162
|
+
data = json.load(meta_file)
|
163
|
+
timestamp = data.get("timestamp")
|
164
|
+
message_count = data.get("message_count")
|
165
|
+
except Exception:
|
166
|
+
timestamp = None
|
167
|
+
message_count = None
|
168
|
+
entries.append((name, timestamp, message_count))
|
169
|
+
|
170
|
+
def sort_key(entry):
|
171
|
+
_, timestamp, _ = entry
|
172
|
+
if timestamp:
|
173
|
+
try:
|
174
|
+
return datetime.fromisoformat(timestamp)
|
175
|
+
except ValueError:
|
176
|
+
return datetime.min
|
177
|
+
return datetime.min
|
178
|
+
|
179
|
+
entries.sort(key=sort_key, reverse=True)
|
180
|
+
top_entries = entries[:5]
|
181
|
+
|
182
|
+
emit_system_message("[bold magenta]Autosave Sessions Available:[/bold magenta]")
|
183
|
+
for index, (name, timestamp, message_count) in enumerate(top_entries, start=1):
|
184
|
+
timestamp_display = timestamp or "unknown time"
|
185
|
+
message_display = (
|
186
|
+
f"{message_count} messages" if message_count is not None else "unknown size"
|
187
|
+
)
|
188
|
+
emit_system_message(
|
189
|
+
f" [{index}] {name} ({message_display}, saved at {timestamp_display})"
|
190
|
+
)
|
191
|
+
|
192
|
+
if len(entries) > len(top_entries):
|
193
|
+
emit_system_message(
|
194
|
+
f" [dim]...and {len(entries) - len(top_entries)} more autosaves[/dim]"
|
195
|
+
)
|
196
|
+
|
197
|
+
try:
|
198
|
+
selection = await get_input_with_combined_completion(
|
199
|
+
FormattedText([("class:prompt", "Load autosave (number, name, or Enter to skip): ")])
|
200
|
+
)
|
201
|
+
except (KeyboardInterrupt, EOFError):
|
202
|
+
emit_warning("Autosave selection cancelled")
|
203
|
+
return
|
204
|
+
|
205
|
+
selection = selection.strip()
|
206
|
+
if not selection:
|
207
|
+
return
|
208
|
+
|
209
|
+
chosen_name = None
|
210
|
+
if selection.isdigit():
|
211
|
+
idx = int(selection) - 1
|
212
|
+
if 0 <= idx < len(top_entries):
|
213
|
+
chosen_name = top_entries[idx][0]
|
214
|
+
else:
|
215
|
+
for name, _, _ in entries:
|
216
|
+
if name == selection:
|
217
|
+
chosen_name = name
|
218
|
+
break
|
219
|
+
|
220
|
+
if not chosen_name:
|
221
|
+
emit_warning("No autosave loaded (invalid selection)")
|
222
|
+
return
|
223
|
+
|
224
|
+
try:
|
225
|
+
history = load_session(chosen_name, base_dir)
|
226
|
+
except FileNotFoundError:
|
227
|
+
emit_warning(f"Autosave '{chosen_name}' could not be found")
|
228
|
+
return
|
229
|
+
except Exception as exc:
|
230
|
+
emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
|
231
|
+
return
|
232
|
+
|
233
|
+
agent = get_current_agent()
|
234
|
+
agent.set_message_history(history)
|
235
|
+
|
236
|
+
# Set current autosave session id so subsequent autosaves overwrite this session
|
237
|
+
try:
|
238
|
+
from code_puppy.config import set_current_autosave_from_session_name
|
239
|
+
|
240
|
+
set_current_autosave_from_session_name(chosen_name)
|
241
|
+
except Exception:
|
242
|
+
pass
|
243
|
+
|
244
|
+
total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
|
245
|
+
|
246
|
+
session_path = base_dir / f"{chosen_name}.pkl"
|
247
|
+
emit_success(
|
248
|
+
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
249
|
+
f"📁 From: {session_path}"
|
250
|
+
)
|
@@ -1,13 +1,14 @@
|
|
1
1
|
code_puppy/__init__.py,sha256=ehbM1-wMjNmOXk_DBhhJECFyBv2dRHwwo7ucjHeM68E,107
|
2
2
|
code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
|
3
3
|
code_puppy/callbacks.py,sha256=ukSgVFaEO68o6J09qFwDrnmNanrVv3toTLQhS504Meo,6162
|
4
|
-
code_puppy/config.py,sha256=
|
4
|
+
code_puppy/config.py,sha256=S-VcC97Syz8j-bVNBr85DlhOr6exxAHQQR9JdHYOODs,25877
|
5
5
|
code_puppy/http_utils.py,sha256=YLd8Y16idbI32JGeBXG8n5rT4o4X_zxk9FgUvK9XFo8,8248
|
6
|
-
code_puppy/main.py,sha256=
|
6
|
+
code_puppy/main.py,sha256=Csdkoufbgt3aTQBopQqq29XDyHIy84Ink6M7H5NBrXs,22076
|
7
7
|
code_puppy/model_factory.py,sha256=ZbIAJWMNKNdTCEMQK8Ig6TDDZlVNyGO9hOLHoLLPMYw,15397
|
8
8
|
code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
|
9
9
|
code_puppy/reopenable_async_client.py,sha256=4UJRaMp5np8cbef9F0zKQ7TPKOfyf5U-Kv-0zYUWDho,8274
|
10
10
|
code_puppy/round_robin_model.py,sha256=UEfw-Ix7GpNRWSxxuJtA-EE4_A46KXjMgFRciprfLmg,5634
|
11
|
+
code_puppy/session_storage.py,sha256=40RAOCrh7MpR5cWWoatjj2bQMikduIvSzFV2yI8v2Yk,7928
|
11
12
|
code_puppy/status_display.py,sha256=F6eEAkGePDp4StM2BWj-uLLQTDGtJrf0IufzCeP1rRg,8336
|
12
13
|
code_puppy/summarization_agent.py,sha256=LnObgtLmM6N4z2553XXQlXAOf8R1BPSNmFSfXkjpivg,3211
|
13
14
|
code_puppy/tui_state.py,sha256=TT76XBVapKj6fKjFzz6oxCONeN_BZwcMILxxZcxu6-Y,1171
|
@@ -29,7 +30,7 @@ code_puppy/agents/agent_typescript_reviewer.py,sha256=EDY1mFkVpuJ1BPXsJFu2wQ2pfA
|
|
29
30
|
code_puppy/agents/base_agent.py,sha256=rJm0xA9kLT_NU9MSZIrN-Z_T5O4Q-QuUmQM8paGZKHQ,39066
|
30
31
|
code_puppy/agents/json_agent.py,sha256=KPS1q-Rr3b5ekem4i3wtu8eLJRDd5nSPiZ8duJ_tn0U,4630
|
31
32
|
code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
|
32
|
-
code_puppy/command_line/command_handler.py,sha256=
|
33
|
+
code_puppy/command_line/command_handler.py,sha256=1oR2pvTX7-3ciHqb3kVTfpVfsZdExmN8CfbyWiATkOQ,31274
|
33
34
|
code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
|
34
35
|
code_puppy/command_line/load_context_completion.py,sha256=6eZxV6Bs-EFwZjN93V8ZDZUC-6RaWxvtZk-04Wtikyw,2240
|
35
36
|
code_puppy/command_line/model_picker_completion.py,sha256=vYNCZS1QWu6fxF__hTwpc7jwH7h_48wUxrnITawc83E,4140
|
@@ -120,9 +121,9 @@ code_puppy/tui/screens/help.py,sha256=eJuPaOOCp7ZSUlecearqsuX6caxWv7NQszUh0tZJjB
|
|
120
121
|
code_puppy/tui/screens/mcp_install_wizard.py,sha256=vObpQwLbXjQsxmSg-WCasoev1usEi0pollKnL0SHu9U,27693
|
121
122
|
code_puppy/tui/screens/settings.py,sha256=-WLldnKyWVKUYVPJcfOn1UU6eP9t8lLPUAVI317SOOM,10685
|
122
123
|
code_puppy/tui/screens/tools.py,sha256=3pr2Xkpa9Js6Yhf1A3_wQVRzFOui-KDB82LwrsdBtyk,1715
|
123
|
-
code_puppy-0.0.
|
124
|
-
code_puppy-0.0.
|
125
|
-
code_puppy-0.0.
|
126
|
-
code_puppy-0.0.
|
127
|
-
code_puppy-0.0.
|
128
|
-
code_puppy-0.0.
|
124
|
+
code_puppy-0.0.196.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
|
125
|
+
code_puppy-0.0.196.dist-info/METADATA,sha256=o-3kdJ2IzyMxkaEa4_XOH7StuqfcZX3YwjZF3Sy4Yv8,19987
|
126
|
+
code_puppy-0.0.196.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
127
|
+
code_puppy-0.0.196.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
|
128
|
+
code_puppy-0.0.196.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
|
129
|
+
code_puppy-0.0.196.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|