aboutsummaryrefslogtreecommitdiff
path: root/mcp/senia/tasks.py
diff options
context:
space:
mode:
Diffstat (limited to 'mcp/senia/tasks.py')
-rw-r--r--mcp/senia/tasks.py93
1 files changed, 93 insertions, 0 deletions
diff --git a/mcp/senia/tasks.py b/mcp/senia/tasks.py
new file mode 100644
index 0000000..1bf901f
--- /dev/null
+++ b/mcp/senia/tasks.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""Live task tracking with owner-based handling.
+
+Completed/cancelled tasks are removed from this table; their history lives in
+the other memory layers.
+"""
+
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent))
+
+from db import get_conn
+
+
+def _now() -> str:
+ return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
+
+
+def _row(r):
+ return dict(r) if r else None
+
+
+_VALID_OWNERS = ("agent", "user")
+
+
+def _validate_owner(owner: str | None) -> dict | None:
+ """Return an error dict if owner is set to an invalid value, else None."""
+ if owner is not None and owner not in _VALID_OWNERS:
+ return {"error": f"invalid owner '{owner}'. Must be one of: {', '.join(_VALID_OWNERS)}"}
+ return None
+
+
+def add_task(owner: str, content: str, due_at: str | None = None,
+ notes: str | None = None) -> dict:
+ if err := _validate_owner(owner):
+ return err
+ with get_conn() as conn:
+ cur = conn.execute(
+ "INSERT INTO tasks (owner, content, due_at, notes) VALUES (?, ?, ?, ?)",
+ (owner, content, due_at, notes),
+ )
+ return _row(conn.execute("SELECT * FROM tasks WHERE id=?", (cur.lastrowid,)).fetchone())
+
+
+def list_tasks(owner: str | None = None, due_before: str | None = None) -> list[dict]:
+ sql = "SELECT * FROM tasks WHERE 1=1"
+ params = []
+ if owner:
+ sql += " AND owner=?"; params.append(owner)
+ if due_before:
+ sql += " AND due_at IS NOT NULL AND due_at < ?"; params.append(due_before)
+ sql += " ORDER BY due_at IS NULL, due_at"
+ with get_conn() as conn:
+ return [dict(r) for r in conn.execute(sql, params).fetchall()]
+
+
+def update_task(task_id: int, content: str | None = None, due_at: str | None = None,
+ owner: str | None = None, notes: str | None = None) -> dict:
+ if err := _validate_owner(owner):
+ return err
+ sets, params = [], []
+ for col, val in (("content", content), ("due_at", due_at),
+ ("owner", owner), ("notes", notes)):
+ if val is not None:
+ sets.append(f"{col}=?"); params.append(val)
+ if not sets:
+ return {"error": "no fields to update"}
+ sets.append("updated_at=?"); params.append(_now())
+ params.append(task_id)
+ with get_conn() as conn:
+ if not conn.execute("SELECT id FROM tasks WHERE id=?", (task_id,)).fetchone():
+ return {"error": f"no task with id {task_id}"}
+ conn.execute(f"UPDATE tasks SET {', '.join(sets)} WHERE id=?", params)
+ return _row(conn.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone())
+
+
+def remove_task(task_id: int) -> dict:
+ with get_conn() as conn:
+ row = conn.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ if not row:
+ return {"error": f"no task with id {task_id}"}
+ conn.execute("DELETE FROM tasks WHERE id=?", (task_id,))
+ return {"deleted": _row(row)}
+
+
+def record_nudge(task_id: int) -> dict:
+ with get_conn() as conn:
+ if not conn.execute("SELECT id FROM tasks WHERE id=?", (task_id,)).fetchone():
+ return {"error": f"no task with id {task_id}"}
+ conn.execute("UPDATE tasks SET last_nudged_at=? WHERE id=?", (_now(), task_id))
+ return _row(conn.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone())