""" task_store.py — Persistent task record store. Writes task state to disk after each status change so that an interrupted session can be reviewed and continued. Each session produces one JSON file under data/tasks/ named by session ID. File layout: data/tasks/session_.json Record structure: { "session_id": "20260402_143022", "session_start": "2026-04-02T14:30:22", "tasks": { "T-20260402-001-A": { "task_id": "T-20260402-001-A", "directive": "...", # the Principal's original directive "assigned_to": "atlas", "brief": "...", "status": "complete", # pending | in_progress | complete | error "output": "...", "error": "", "created_at": "2026-04-02T14:30:22", "updated_at": "2026-04-02T14:31:05" }, ... } } """ import json from datetime import datetime from pathlib import Path import config # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _path(session_id: str) -> Path: return config.TASKS_DIR / f"session_{session_id}.json" def _load(session_id: str) -> dict: path = _path(session_id) if path.exists(): return json.loads(path.read_text(encoding="utf-8")) return { "session_id": session_id, "session_start": datetime.now().isoformat(timespec="seconds"), "tasks": {}, } def _save(session_id: str, data: dict) -> None: _path(session_id).write_text( json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8", ) def _now() -> str: return datetime.now().isoformat(timespec="seconds") # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def record_tasks(tasks: list, session_id: str, directive: str) -> None: """ Persist initial records for a set of newly parsed tasks. Call this immediately after parse_task_briefs() returns. """ now = _now() data = _load(session_id) for task in tasks: data["tasks"][task.task_id] = { "task_id": task.task_id, "directive": directive, "assigned_to": task.assigned_to, "brief": task.brief, "status": task.status, "output": task.output, "error": task.error, "created_at": now, "updated_at": now, } _save(session_id, data) def update_task(task, session_id: str) -> None: """ Update the persisted status, output, and error for a single task. Call this whenever task.status changes. """ data = _load(session_id) rec = data["tasks"].get(task.task_id) if rec is None: return rec["status"] = task.status rec["output"] = task.output rec["error"] = task.error rec["updated_at"] = _now() _save(session_id, data) def load_session(session_id: str) -> dict: """Return the full session record, or an empty structure if not found.""" return _load(session_id) def list_sessions() -> list[dict]: """ Return summary metadata for all recorded sessions, newest first. Each entry: {session_id, session_start, task_count, incomplete_count}. """ summaries = [] for path in sorted(config.TASKS_DIR.glob("session_*.json"), reverse=True): try: data = json.loads(path.read_text(encoding="utf-8")) tasks = data.get("tasks", {}) incomplete = sum( 1 for t in tasks.values() if t.get("status") not in ("complete", "error") ) summaries.append({ "session_id": data.get("session_id", path.stem), "session_start": data.get("session_start", ""), "task_count": len(tasks), "incomplete_count": incomplete, }) except (json.JSONDecodeError, KeyError): continue return summaries