mirror of
https://github.com/usestrix/strix.git
synced 2026-09-23 00:41:50 +00:00
Fix checkpoint not updating on second resume
Root cause: checkpoint and tracer run directories used CWD-relative
paths (Path("strix_runs") and Path.cwd() / "strix_runs"). Launching
strix from different directories across sessions created separate
checkpoint files that never updated each other, so the third session
always resumed from the first session's iteration.
Fix: use Path.home() / "strix_runs" as the canonical absolute path in
both tracer.py and main.py so all sessions write to the same location
regardless of CWD.
Also includes earlier serialization robustness fixes (mode="json" +
_json_default fallback) and explicit checkpoint save in action_custom_quit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
daecf22290
commit
9cba355b54
5 changed files with 47 additions and 12 deletions
|
|
@ -314,7 +314,8 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||
results_text = Text()
|
||||
results_text.append("Output", style="dim")
|
||||
results_text.append(" ")
|
||||
results_text.append(f"strix_runs/{args.run_name}", style="#60a5fa")
|
||||
from pathlib import Path
|
||||
results_text.append(str(Path.home() / "strix_runs" / args.run_name), style="#60a5fa")
|
||||
|
||||
note_text = Text()
|
||||
note_text.append("\n\n", style="dim")
|
||||
|
|
|
|||
|
|
@ -606,20 +606,22 @@ def _setup_checkpoint_on_args(args: argparse.Namespace) -> None:
|
|||
args._checkpoint_data = None
|
||||
args.resume_from_checkpoint = False
|
||||
|
||||
strix_runs = Path.home() / "strix_runs"
|
||||
|
||||
if args.force_new:
|
||||
# Delete any checkpoint for the current run name (if explicit) and start fresh
|
||||
if args.run_name:
|
||||
run_dir = Path("strix_runs") / args.run_name
|
||||
run_dir = strix_runs / args.run_name
|
||||
CheckpointManager(args.run_name, run_dir).delete()
|
||||
if not args.run_name:
|
||||
args.run_name = generate_run_name(args.targets_info)
|
||||
run_dir = Path("strix_runs") / args.run_name
|
||||
run_dir = strix_runs / args.run_name
|
||||
args._checkpoint_manager = CheckpointManager(args.run_name, run_dir)
|
||||
return
|
||||
|
||||
# If no explicit run name, try auto-detect by target hash
|
||||
if not args.run_name:
|
||||
found = _find_checkpoint_by_target_hash(Path("strix_runs"), target_hash)
|
||||
found = _find_checkpoint_by_target_hash(strix_runs, target_hash)
|
||||
if found:
|
||||
run_name, checkpoint = found
|
||||
console = Console()
|
||||
|
|
@ -629,7 +631,7 @@ def _setup_checkpoint_on_args(args: argparse.Namespace) -> None:
|
|||
f"[dim]Use --new to start fresh.[/]"
|
||||
)
|
||||
args.run_name = run_name
|
||||
run_dir = Path("strix_runs") / run_name
|
||||
run_dir = strix_runs / run_name
|
||||
args._checkpoint_manager = CheckpointManager(run_name, run_dir)
|
||||
args._checkpoint_data = checkpoint
|
||||
args.resume_from_checkpoint = True
|
||||
|
|
@ -638,7 +640,7 @@ def _setup_checkpoint_on_args(args: argparse.Namespace) -> None:
|
|||
args.run_name = generate_run_name(args.targets_info)
|
||||
|
||||
# Explicit run name (or freshly generated) — look for its checkpoint
|
||||
run_dir = Path("strix_runs") / args.run_name
|
||||
run_dir = strix_runs / args.run_name
|
||||
mgr = CheckpointManager(args.run_name, run_dir)
|
||||
args._checkpoint_manager = mgr
|
||||
|
||||
|
|
@ -727,7 +729,7 @@ def main() -> None:
|
|||
if tracer:
|
||||
posthog.end(tracer, exit_reason=exit_reason)
|
||||
|
||||
results_path = Path("strix_runs") / args.run_name
|
||||
results_path = Path.home() / "strix_runs" / args.run_name
|
||||
display_completion_message(args, results_path)
|
||||
|
||||
if args.non_interactive:
|
||||
|
|
|
|||
|
|
@ -2139,6 +2139,22 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||
|
||||
self._scan_thread.join(timeout=1.0)
|
||||
|
||||
# Explicitly save checkpoint here so it is always persisted before the
|
||||
# TUI exits, regardless of whether atexit handlers run reliably after
|
||||
# Textual's exit() call.
|
||||
_mgr = self.agent_config.get("checkpoint_manager")
|
||||
_agent = getattr(self, "_current_agent", None)
|
||||
if _mgr and _agent:
|
||||
import contextlib
|
||||
with contextlib.suppress(Exception):
|
||||
_mgr.save(
|
||||
_agent.state,
|
||||
self.tracer,
|
||||
self.scan_config,
|
||||
self.agent_config.get("target_hash", ""),
|
||||
_agent.max_iterations,
|
||||
)
|
||||
|
||||
self.tracer.cleanup()
|
||||
|
||||
self.exit()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,21 @@ from datetime import UTC, datetime
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _json_default(obj: Any) -> Any:
|
||||
"""Fallback serialiser for objects json.dumps cannot handle natively."""
|
||||
if hasattr(obj, "isoformat"):
|
||||
return obj.isoformat()
|
||||
if hasattr(obj, "model_dump"):
|
||||
return obj.model_dump(mode="json")
|
||||
return str(obj)
|
||||
|
||||
|
||||
CHECKPOINT_VERSION = "1.0"
|
||||
|
||||
|
||||
|
|
@ -106,7 +117,7 @@ class CheckpointManager:
|
|||
self.run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
state_dict: dict[str, Any] = (
|
||||
agent_state.model_dump() if hasattr(agent_state, "model_dump") else {}
|
||||
agent_state.model_dump(mode="json") if hasattr(agent_state, "model_dump") else {}
|
||||
)
|
||||
|
||||
tracer_chat_messages: list[dict[str, Any]] = []
|
||||
|
|
@ -137,7 +148,7 @@ class CheckpointManager:
|
|||
s = getattr(inst, "state", None)
|
||||
if s is not None and s.parent_id is not None:
|
||||
sub_agent_states[sid] = (
|
||||
s.model_dump() if hasattr(s, "model_dump") else {}
|
||||
s.model_dump(mode="json") if hasattr(s, "model_dump") else {}
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
|
@ -157,8 +168,13 @@ class CheckpointManager:
|
|||
sub_agent_states=sub_agent_states,
|
||||
)
|
||||
|
||||
# Atomic write: .tmp → rename
|
||||
self._tmp_path.write_text(checkpoint.model_dump_json(indent=2), encoding="utf-8")
|
||||
# Atomic write: .tmp → rename. Use json.dumps with a fallback
|
||||
# serialiser so non-standard objects (datetime, Pydantic models,
|
||||
# etc.) never silently abort the save.
|
||||
json_str = json.dumps(
|
||||
checkpoint.model_dump(mode="json"), indent=2, default=_json_default
|
||||
)
|
||||
self._tmp_path.write_text(json_str, encoding="utf-8")
|
||||
os.rename(self._tmp_path, self.checkpoint_path)
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ class Tracer:
|
|||
|
||||
def get_run_dir(self) -> Path:
|
||||
if self._run_dir is None:
|
||||
runs_dir = Path.cwd() / "strix_runs"
|
||||
runs_dir = Path.home() / "strix_runs"
|
||||
runs_dir.mkdir(exist_ok=True)
|
||||
|
||||
run_dir_name = self.run_name if self.run_name else self.run_id
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue