139 lines
4.1 KiB
Python
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
|