refactor: consolidate run state layout

This commit is contained in:
0xallam
2026-04-26 15:01:35 -07:00
parent 0a5be6be3f
commit af826e1281
14 changed files with 223 additions and 133 deletions
+1
View File
@@ -142,6 +142,7 @@ hiddenimports = [
'strix.core.agents',
'strix.core.execution',
'strix.core.inputs',
'strix.core.paths',
'strix.core.runner',
'strix.core.sessions',
'strix.report',
+15 -9
View File
@@ -120,15 +120,20 @@ class AgentCoordinator:
session = runtime.session
stream = runtime.stream
interrupt = runtime.interrupt_on_message
if session is not None:
try:
await session.add_items([self._message_to_session_item(message)])
except Exception:
logger.exception(
"agent.send failed to append to SDK session target=%s",
target_agent_id,
)
return False
if session is None:
logger.warning(
"agent.send dropped target=%s because its SDK session is not attached",
target_agent_id,
)
return False
try:
await session.add_items([self._message_to_session_item(message)])
except Exception:
logger.exception(
"agent.send failed to append to SDK session target=%s",
target_agent_id,
)
return False
async with self._lock:
self.pending_counts[target_agent_id] = self.pending_counts.get(target_agent_id, 0) + 1
self.runtimes.setdefault(target_agent_id, AgentRuntime()).wake.set()
@@ -158,6 +163,7 @@ class AgentCoordinator:
session = self.runtimes.get(agent_id, AgentRuntime()).session
if count <= 0:
return 0, []
await self._maybe_snapshot()
if not include_items or session is None:
return count, []
items = await session.get_items()
+23
View File
@@ -0,0 +1,23 @@
"""Run directory path helpers."""
from __future__ import annotations
from pathlib import Path
RUNS_DIR_NAME = "strix_runs"
RUNTIME_STATE_DIR_NAME = ".state"
RUN_RECORD_FILENAME = "run.json"
def run_dir_for(run_name: str, *, cwd: Path | None = None) -> Path:
base = cwd or Path.cwd()
return base / RUNS_DIR_NAME / run_name
def runtime_state_dir(run_dir: Path) -> Path:
return run_dir / RUNTIME_STATE_DIR_NAME
def run_record_path(run_dir: Path) -> Path:
return run_dir / RUN_RECORD_FILENAME
+9 -7
View File
@@ -12,7 +12,6 @@ import json
import logging
import uuid
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any
from agents import RunConfig
@@ -35,6 +34,7 @@ from strix.core.inputs import (
build_scope_context,
make_model_settings,
)
from strix.core.paths import run_dir_for, runtime_state_dir
from strix.core.sessions import open_agent_session
from strix.runtime import session_manager
from strix.telemetry.logging import set_scan_id, setup_scan_logging
@@ -69,13 +69,15 @@ async def run_strix_scan(
# Resolve run_dir before any heavy bring-up so the log file captures
# everything from sandbox start onwards.
run_dir = Path.cwd() / "strix_runs" / scan_id
run_dir = run_dir_for(scan_id)
run_dir.mkdir(parents=True, exist_ok=True)
state_dir = runtime_state_dir(run_dir)
state_dir.mkdir(parents=True, exist_ok=True)
teardown_logging = setup_scan_logging(run_dir)
set_scan_id(scan_id)
agents_path = run_dir / "agents.json"
agents_db = run_dir / "agents.db"
agents_path = state_dir / "agents.json"
agents_db = state_dir / "agents.db"
is_resume = agents_path.exists()
logger.info(
@@ -103,14 +105,14 @@ async def run_strix_scan(
coordinator = AgentCoordinator()
coordinator.set_snapshot_path(agents_path)
# Wire the per-agent todo store to ``{run_dir}/todos.json`` (mirrored
# Wire the per-agent todo store to ``{run_dir}/.state/todos.json`` (mirrored
# on every CRUD) and reload any prior todos so respawned subagents
# find their lists intact. Same for the shared notes store.
from strix.tools.notes.tools import hydrate_notes_from_disk
from strix.tools.todo.tools import hydrate_todos_from_disk
hydrate_todos_from_disk(run_dir)
hydrate_notes_from_disk(run_dir)
hydrate_todos_from_disk(state_dir)
hydrate_notes_from_disk(state_dir)
root_id: str | None = None
if is_resume:
+7 -2
View File
@@ -89,6 +89,10 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
"run_name": args.run_name,
"diff_scope": getattr(args, "diff_scope", {"active": False}),
"scan_mode": scan_mode,
"non_interactive": bool(getattr(args, "non_interactive", False)),
"local_sources": getattr(args, "local_sources", None) or [],
"scope_mode": getattr(args, "scope_mode", "auto"),
"diff_base": getattr(args, "diff_base", None),
# Forward the new --instruction (if any) to the resume path so it
# can deliver it as a fresh user message after SDK session replay.
# Empty string when the user didn't pass one on resume — no-op.
@@ -96,8 +100,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
}
report_state = ReportState(args.run_name)
report_state.set_scan_config(scan_config)
report_state.hydrate_from_run_dir()
report_state.set_scan_config(scan_config)
report_state.save_run_data()
def display_vulnerability(report: dict[str, Any]) -> None:
report_id = report.get("id", "unknown")
@@ -121,7 +126,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
report_state.cleanup()
def signal_handler(_signum: int, _frame: Any) -> None:
report_state.cleanup()
report_state.cleanup(status="interrupted")
sys.exit(1)
atexit.register(cleanup_on_exit)
+31 -35
View File
@@ -5,9 +5,9 @@ Strix Agent Interface
import argparse
import asyncio
import json
import shutil
import sys
from datetime import UTC, datetime
from pathlib import Path
from agents.model_settings import ModelSettings
@@ -24,6 +24,7 @@ from strix.config import (
persist_current,
)
from strix.config.models import configure_sdk_model_defaults, normalize_model_name
from strix.core.paths import run_dir_for, runtime_state_dir
from strix.interface.cli import run_cli
from strix.interface.tui import run_tui
from strix.interface.utils import (
@@ -42,6 +43,7 @@ from strix.interface.utils import (
validate_config_file,
)
from strix.report.state import get_global_report_state
from strix.report.writer import read_run_record, write_run_record
from strix.telemetry import posthog
from strix.telemetry.logging import configure_dependency_logging
@@ -433,7 +435,7 @@ Examples:
"the prior run left off, including the original target list."
)
_load_resume_state(args, parser)
agents_path = Path("strix_runs") / args.resume / "agents.json"
agents_path = runtime_state_dir(run_dir_for(args.resume)) / "agents.json"
if not agents_path.exists():
parser.error(
f"--resume {args.resume}: missing {agents_path}. The run was "
@@ -469,17 +471,16 @@ Examples:
return args
def _persist_scan_state(args: argparse.Namespace) -> None:
"""Dump the resolved scan inputs to ``{run_dir}/scan_state.json``.
Called once at the end of fresh-run setup. ``--resume <run_name>`` on
a future invocation reads this file to repopulate targets, scan_mode,
instruction, local_sources, and diff_scope without the user having to
retype them.
"""
run_dir = Path("strix_runs") / args.run_name
def _persist_run_record(args: argparse.Namespace) -> None:
"""Write the single public run descriptor used by resume and reporting."""
run_dir = run_dir_for(args.run_name)
run_dir.mkdir(parents=True, exist_ok=True)
state = {
run_record = {
"run_id": args.run_name,
"run_name": args.run_name,
"status": "running",
"start_time": datetime.now(UTC).isoformat(),
"end_time": None,
"targets_info": args.targets_info,
"scan_mode": args.scan_mode,
"instruction": args.instruction,
@@ -489,34 +490,26 @@ def _persist_scan_state(args: argparse.Namespace) -> None:
"scope_mode": args.scope_mode,
"diff_base": args.diff_base,
}
(run_dir / "scan_state.json").write_text(
json.dumps(state, ensure_ascii=False, indent=2, default=str),
encoding="utf-8",
)
write_run_record(run_dir, run_record)
def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
"""Populate ``args.targets_info`` and friends from a prior run's scan state.
Reads ``strix_runs/<run_name>/scan_state.json`` written at the end of the
fresh-run setup in ``main()``. Only fields the user did not explicitly
set on this invocation are restored — passing ``--instruction`` on
resume, for example, overrides the persisted instruction.
"""
state_path = Path("strix_runs") / args.resume / "scan_state.json"
"""Populate ``args.targets_info`` and friends from a prior run's run.json."""
run_dir = run_dir_for(args.resume)
state_path = run_dir / "run.json"
if not state_path.exists():
parser.error(
f"--resume {args.resume}: no such run "
f"(missing {state_path}; remove --resume for a fresh start)"
)
try:
state = json.loads(state_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
parser.error(f"--resume {args.resume}: scan_state.json unreadable: {exc}")
state = read_run_record(run_dir)
except RuntimeError as exc:
parser.error(f"--resume {args.resume}: run.json unreadable: {exc}")
args.targets_info = state.get("targets_info") or []
if not args.targets_info:
parser.error(f"--resume {args.resume}: scan_state.json has no targets_info")
parser.error(f"--resume {args.resume}: run.json has no targets_info")
# Validate any persisted ``cloned_repo_path`` still exists on disk.
# The resume path skips re-cloning, so a missing dir would mean the
@@ -540,8 +533,6 @@ def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser
if args.instruction is None:
args.instruction = state.get("instruction")
if state.get("instruction_file") and args.instruction_file is None:
args.instruction_file = state.get("instruction_file")
if state.get("local_sources"):
args.local_sources = state.get("local_sources")
if state.get("diff_scope"):
@@ -561,8 +552,8 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
report_state = get_global_report_state()
scan_completed = False
if report_state and report_state.scan_results:
scan_completed = report_state.scan_results.get("scan_completed", False)
if report_state:
scan_completed = report_state.run_record.get("status") == "completed"
completion_text = Text()
if scan_completed:
@@ -739,10 +730,10 @@ def main() -> None:
else:
args.instruction = diff_scope.instruction_block
# Persist the fully-resolved scan state so a future
# Persist the fully-resolved run descriptor so a future
# ``--resume <run_name>`` invocation can pick up without
# re-supplying targets / instructions / scope.
_persist_scan_state(args)
_persist_run_record(args)
posthog.start(
model=load_settings().llm.model,
@@ -767,9 +758,14 @@ def main() -> None:
finally:
report_state = get_global_report_state()
if report_state:
status = {"interrupted": "interrupted", "error": "failed"}.get(
exit_reason,
"stopped",
)
report_state.cleanup(status=status)
posthog.end(report_state, exit_reason=exit_reason)
results_path = Path("strix_runs") / args.run_name
results_path = run_dir_for(args.run_name)
display_completion_message(args, results_path)
if args.non_interactive:
+11 -3
View File
@@ -708,8 +708,9 @@ class StrixTUIApp(App): # type: ignore[misc]
self.scan_config = self._build_scan_config(args)
self.report_state = ReportState(self.scan_config["run_name"])
self.report_state.set_scan_config(self.scan_config)
self.report_state.hydrate_from_run_dir()
self.report_state.set_scan_config(self.scan_config)
self.report_state.save_run_data()
set_global_report_state(self.report_state)
self.live_view = TuiLiveView()
self.live_view.hydrate_from_run_dir(self.report_state.get_run_dir())
@@ -759,6 +760,10 @@ class StrixTUIApp(App): # type: ignore[misc]
"run_name": args.run_name,
"diff_scope": getattr(args, "diff_scope", {"active": False}),
"scan_mode": getattr(args, "scan_mode", "deep"),
"non_interactive": bool(getattr(args, "non_interactive", False)),
"local_sources": getattr(args, "local_sources", None) or [],
"scope_mode": getattr(args, "scope_mode", "auto"),
"diff_base": getattr(args, "diff_base", None),
# Forward the new --instruction (if any) so the resume path
# can deliver it as a fresh user message after session replay.
"resume_instruction": getattr(args, "user_explicit_instruction", None) or "",
@@ -769,7 +774,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self.report_state.cleanup()
def signal_handler(_signum: int, _frame: Any) -> None:
self.report_state.cleanup()
self.report_state.cleanup(status="interrupted")
sys.exit(0)
atexit.register(cleanup_on_exit)
@@ -1611,13 +1616,16 @@ class StrixTUIApp(App): # type: ignore[misc]
)
target_agent_id = self.selected_agent_id
send_user_message_to_agent(
submitted = send_user_message_to_agent(
coordinator=self.coordinator,
loop=self._scan_loop,
live_view=self.live_view,
target_agent_id=target_agent_id,
message=message,
)
if not submitted:
self.notify("Scan loop is not ready; message was not sent", severity="warning")
return
self._displayed_events.clear()
self._update_chat_view()
+3 -1
View File
@@ -8,6 +8,8 @@ import sqlite3
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from strix.core.paths import runtime_state_dir
if TYPE_CHECKING:
from pathlib import Path
@@ -17,7 +19,7 @@ logger = logging.getLogger(__name__)
def load_session_history(run_dir: Path, agent_ids: Any) -> list[tuple[str, dict[str, Any], str]]:
agents_db = run_dir / "agents.db"
agents_db = runtime_state_dir(run_dir) / "agents.db"
session_ids = [aid for aid in agent_ids if isinstance(aid, str)]
if not agents_db.exists() or not session_ids:
return []
+3 -1
View File
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pathlib import Path
from strix.core.paths import runtime_state_dir
from strix.interface.tui.history import load_session_history
@@ -24,7 +25,8 @@ class TuiLiveView:
self._tool_event_by_call_id: dict[str, dict[str, Any]] = {}
def hydrate_from_run_dir(self, run_dir: Path) -> None:
agents_path = run_dir / "agents.json"
state_dir = runtime_state_dir(run_dir)
agents_path = state_dir / "agents.json"
if not agents_path.exists():
return
try:
+17 -3
View File
@@ -3,9 +3,13 @@
from __future__ import annotations
import asyncio
import logging
from typing import Any
logger = logging.getLogger(__name__)
def send_user_message_to_agent(
*,
coordinator: Any,
@@ -15,16 +19,26 @@ def send_user_message_to_agent(
message: str,
) -> bool:
"""Record a local user message and enqueue it into the target SDK session."""
live_view.record_user_message(target_agent_id, message)
if loop is None or loop.is_closed():
return False
asyncio.run_coroutine_threadsafe(
live_view.record_user_message(target_agent_id, message)
future = asyncio.run_coroutine_threadsafe(
coordinator.send(
target_agent_id,
{"from": "user", "content": message, "type": "instruction"},
),
loop,
)
future.add_done_callback(_log_delivery_failure)
return True
def _log_delivery_failure(future: Any) -> None:
try:
delivered = bool(future.result())
except Exception:
logger.exception("TUI user message delivery failed")
return
if not delivered:
logger.warning("TUI user message was not persisted to the SDK session")
+75 -59
View File
@@ -6,7 +6,13 @@ from pathlib import Path
from typing import Any, Optional
from uuid import uuid4
from strix.report.writer import write_executive_report, write_run_metadata, write_vulnerabilities
from strix.core.paths import run_dir_for
from strix.report.writer import (
read_run_record,
write_executive_report,
write_run_record,
write_vulnerabilities,
)
from strix.telemetry import posthog
@@ -45,13 +51,13 @@ class ReportState:
self.scan_results: dict[str, Any] | None = None
self.scan_config: dict[str, Any] | None = None
self.run_metadata: dict[str, Any] = {
self.run_record: dict[str, Any] = {
"run_id": self.run_id,
"run_name": self.run_name,
"start_time": self.start_time,
"end_time": None,
"targets": [],
"status": "running",
"targets_info": [],
}
self._run_dir: Path | None = None
self._saved_vuln_ids: set[str] = set()
@@ -61,28 +67,22 @@ class ReportState:
def get_run_dir(self) -> Path:
if self._run_dir is None:
runs_dir = Path.cwd() / "strix_runs"
runs_dir.mkdir(exist_ok=True)
run_dir_name = self.run_name if self.run_name else self.run_id
self._run_dir = runs_dir / run_dir_name
self._run_dir.mkdir(exist_ok=True)
self._run_dir = run_dir_for(run_dir_name)
self._run_dir.mkdir(parents=True, exist_ok=True)
return self._run_dir
def hydrate_from_run_dir(self) -> None:
"""Reload prior-scan state from ``{run_dir}/`` for resume.
Called by :func:`run_strix_scan` before any new agent runs.
Restores:
- ``vulnerability_reports`` from ``vulnerabilities.json`` so
:meth:`add_vulnerability_report` doesn't allocate a colliding
``vuln-0001`` and overwrite the prior on-disk MD.
- ``run_metadata`` (start_time, run_id, targets, status) from
``run_metadata.json`` so audit-trail timestamps + the final
report's duration calc reflect the original scan, not just
this resume segment.
- ``run_record`` from ``run.json`` so timestamps, run inputs,
status, and final report state have one public source of truth.
Idempotent on missing files (fresh runs land here too via the
same code path). **Raises on corruption** — silently swallowing
@@ -93,25 +93,18 @@ class ReportState:
"""
run_dir = self.get_run_dir()
meta_path = run_dir / "run_metadata.json"
if meta_path.exists():
try:
data = json.loads(meta_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(
f"run_metadata.json at {meta_path} is unreadable: {exc}",
) from exc
if isinstance(data, dict):
if isinstance(data.get("start_time"), str):
self.start_time = data["start_time"]
self.run_metadata.update(
{
k: v
for k, v in data.items()
if k in {"run_id", "run_name", "start_time", "targets", "status"}
},
)
logger.info("scan store hydrated run_metadata from %s", meta_path)
data = read_run_record(run_dir)
if data:
self.run_record.update(data)
if isinstance(data.get("start_time"), str):
self.start_time = data["start_time"]
if isinstance(data.get("end_time"), str):
self.end_time = data["end_time"]
scan_results = data.get("scan_results")
if isinstance(scan_results, dict):
self.scan_results = scan_results
self.final_scan_result = self._format_final_scan_result(scan_results)
logger.info("report state hydrated run.json from %s", run_dir)
json_path = run_dir / "vulnerabilities.json"
if json_path.exists():
@@ -133,7 +126,8 @@ class ReportState:
if isinstance(rid, str):
self._saved_vuln_ids.add(rid)
logger.info(
"scan store hydrated %d vulnerability report(s)", len(self.vulnerability_reports)
"report state hydrated %d vulnerability report(s)",
len(self.vulnerability_reports),
)
def add_vulnerability_report(
@@ -228,22 +222,8 @@ class ReportState:
"success": True,
}
self.final_scan_result = f"""# Executive Summary
{executive_summary.strip()}
# Methodology
{methodology.strip()}
# Technical Analysis
{technical_analysis.strip()}
# Recommendations
{recommendations.strip()}
"""
self.final_scan_result = self._format_final_scan_result(self.scan_results)
self.run_record["scan_results"] = self.scan_results
logger.info("Updated scan final fields")
self.save_run_data(mark_complete=True)
@@ -251,25 +231,61 @@ class ReportState:
def set_scan_config(self, config: dict[str, Any]) -> None:
self.scan_config = config
self.run_metadata.update(
self.run_record["status"] = "running"
self.run_record["end_time"] = None
self.run_record.pop("scan_results", None)
self.end_time = None
self.scan_results = None
self.final_scan_result = None
self.run_record.update(
{
"targets": config.get("targets", []),
"user_instructions": config.get("user_instructions", ""),
"max_iterations": config.get("max_iterations", 200),
"targets_info": config.get("targets", []),
"instruction": config.get("user_instructions", ""),
"scan_mode": config.get("scan_mode", "deep"),
"diff_scope": config.get("diff_scope", {"active": False}),
"non_interactive": bool(config.get("non_interactive", False)),
"local_sources": config.get("local_sources", []),
"scope_mode": config.get("scope_mode", "auto"),
"diff_base": config.get("diff_base"),
}
)
def save_run_data(self, mark_complete: bool = False) -> None:
def save_run_data(self, mark_complete: bool = False, status: str | None = None) -> None:
if mark_complete:
self.end_time = datetime.now(UTC).isoformat()
self.run_record["end_time"] = self.end_time
self.run_record["status"] = "completed"
elif status and self.run_record.get("status") != "completed":
current_status = self.run_record.get("status")
if status == "stopped" and current_status in {"failed", "interrupted"}:
status = str(current_status)
if self.end_time is None:
self.end_time = datetime.now(UTC).isoformat()
self.run_metadata["end_time"] = self.end_time
self.run_metadata["status"] = "completed"
self.run_record["end_time"] = self.end_time
self.run_record["status"] = status
self._save_artifacts()
def cleanup(self) -> None:
self.save_run_data(mark_complete=True)
def cleanup(self, status: str = "stopped") -> None:
self.save_run_data(status=status)
def _format_final_scan_result(self, scan_results: dict[str, Any]) -> str:
return f"""# Executive Summary
{str(scan_results.get("executive_summary", "")).strip()}
# Methodology
{str(scan_results.get("methodology", "")).strip()}
# Technical Analysis
{str(scan_results.get("technical_analysis", "")).strip()}
# Recommendations
{str(scan_results.get("recommendations", "")).strip()}
"""
def _save_artifacts(self) -> None:
"""Write scan artifacts under ``run_dir``."""
@@ -283,7 +299,7 @@ class ReportState:
if self.vulnerability_reports:
write_vulnerabilities(run_dir, self.vulnerability_reports, self._saved_vuln_ids)
write_run_metadata(run_dir, self.run_metadata)
write_run_record(run_dir, self.run_record)
logger.info("Essential scan data saved to: %s", run_dir)
except (OSError, RuntimeError):
+18 -3
View File
@@ -10,16 +10,31 @@ from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from strix.core.paths import run_record_path
logger = logging.getLogger(__name__)
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
def write_run_metadata(run_dir: Path, run_metadata: dict[str, Any]) -> None:
def read_run_record(run_dir: Path) -> dict[str, Any]:
path = run_record_path(run_dir)
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"run.json at {path} is unreadable: {exc}") from exc
if not isinstance(data, dict):
raise RuntimeError(f"run.json at {path} is not an object")
return data
def write_run_record(run_dir: Path, run_record: dict[str, Any]) -> None:
_atomic_write_text(
run_dir / "run_metadata.json",
json.dumps(run_metadata, ensure_ascii=False, indent=2, default=str),
run_record_path(run_dir),
json.dumps(run_record, ensure_ascii=False, indent=2, default=str),
)
+5 -5
View File
@@ -1,7 +1,7 @@
"""Per-run notes (shared across agents).
Module-level dict shared across every agent in the same scan process.
Mirrored to ``{run_dir}/notes.json`` after every CRUD via :func:`_persist`
Mirrored to ``{state_dir}/notes.json`` after every CRUD via :func:`_persist`
so a process restart can :func:`hydrate_notes_from_disk` and the resumed
scan picks up exactly where it left off. Concurrent writers are
serialised by ``_notes_lock`` since each tool entry-point dispatches
@@ -37,8 +37,8 @@ _DEFAULT_CONTENT_PREVIEW_CHARS = 280
_notes_path: Path | None = None
def hydrate_notes_from_disk(run_dir: Path) -> None:
"""Wire the on-disk mirror at ``{run_dir}/notes.json`` and reload it.
def hydrate_notes_from_disk(state_dir: Path) -> None:
"""Wire the on-disk mirror at ``{state_dir}/notes.json`` and reload it.
Called by :func:`run_strix_scan` once at scan setup. Subsequent CRUD
calls auto-persist after every mutation. Idempotent on missing file.
@@ -46,7 +46,7 @@ def hydrate_notes_from_disk(run_dir: Path) -> None:
the scan over a broken sidecar artifact.
"""
global _notes_path # noqa: PLW0603
_notes_path = run_dir / "notes.json"
_notes_path = state_dir / "notes.json"
with _notes_lock:
_notes_storage.clear()
if not _notes_path.exists():
@@ -76,7 +76,7 @@ def hydrate_notes_from_disk(run_dir: Path) -> None:
def _persist() -> None:
"""Atomic-rename mirror of ``_notes_storage`` → ``{run_dir}/notes.json``.
"""Atomic-rename mirror of ``_notes_storage`` → ``{state_dir}/notes.json``.
No-op when ``_notes_path`` isn't wired (tests). Errors are logged
and swallowed — a disk hiccup must never tear down the agent's call.
+5 -5
View File
@@ -1,7 +1,7 @@
"""Per-agent todo tools.
Per-agent in-memory dict, scoped via ``ctx.context['agent_id']``. The
table is mirrored to ``{run_dir}/todos.json`` after every mutation so a
table is mirrored to ``{state_dir}/todos.json`` after every mutation so a
process restart can ``hydrate_todos_from_disk`` and each respawned
agent finds its prior list intact. The persistence is best-effort —
errors are logged and swallowed so a disk failure can't kill the agent
@@ -54,8 +54,8 @@ _todos_path: Path | None = None
_todos_io_lock = threading.RLock()
def hydrate_todos_from_disk(run_dir: Path) -> None:
"""Wire the on-disk mirror at ``{run_dir}/todos.json`` and reload it.
def hydrate_todos_from_disk(state_dir: Path) -> None:
"""Wire the on-disk mirror at ``{state_dir}/todos.json`` and reload it.
Called by :func:`run_strix_scan` once at scan setup. Subsequent CRUD
calls auto-persist after every mutation. Idempotent on missing file.
@@ -63,7 +63,7 @@ def hydrate_todos_from_disk(run_dir: Path) -> None:
the scan over a broken sidecar artifact.
"""
global _todos_path # noqa: PLW0603
_todos_path = run_dir / "todos.json"
_todos_path = state_dir / "todos.json"
with _todos_io_lock:
_todos_storage.clear()
if not _todos_path.exists():
@@ -99,7 +99,7 @@ def hydrate_todos_from_disk(run_dir: Path) -> None:
def _persist() -> None:
"""Atomic-rename mirror of ``_todos_storage`` → ``{run_dir}/todos.json``.
"""Atomic-rename mirror of ``_todos_storage`` → ``{state_dir}/todos.json``.
No-op when ``_todos_path`` isn't wired (tests). Errors are logged
and swallowed.