Files
2026-04-02 22:01:07 -07:00

139 lines
4.1 KiB
Python

"""
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_<session_id>.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