| name | task-persistence |
| description | Cross-session tasks via task-db.py or task_* MCP tools. v2: story/slices, paused, switch, bus-as-tasks, Telegram events. |
Task Persistence — Cross-Session Task Tracking (tasks schema)
Why This Exists
The Hermes todo() tool is per-session — items set in one session vanish
when the next starts. This skill documents the durable task system on the
tasks schema of each host's mycortex-postgres (party-reviewed design 2026-08-06:
docs/design/task-workflow.md). Task Lifecycle v2 (2026-08-06,
docs/design/task-lifecycle-v2.md) adds story/slices, paused, switch,
bus-commands-as-tasks, and Telegram visibility.
History: the old system (bus.todos + todo-db.py) was retired 2026-08-06 —
bus.todos never existed on workers (bus schema is orchestrator-only), and
todo-db.py silently no-op'd (stdin-mode psql rc=0). The tasks schema is
applied on EVERY host by cortex-update.sh via the version-gated runner.
Architecture
Agent Session
│
├── todo() ──► ephemeral (Hermes tool, per-session)
│
└── task-db.py ──► tasks.tasks (per-host mycortex-postgres)
│ │
├─ CLI ├─ RLS per-profile isolation (fail-closed)
└─ task-mcp.py├─ CRUD as mycortex_reader_<profile> (never superuser)
(MCP tools) └─ Survives crashes, restarts, disconnects
└─ task_events → telegram_notify → Luke's DM
Database
- Schema:
ops/services/tasks/schema/v00X__*.sql (version-gated by
tasks.schema_version; applied by ops/services/tasks/migrate.py).
Current: v007 — v005 added hierarchy/paused/correlation/events,
v006 added reader SELECT on schema_version, v007 fixed task_upsert
partial-update preservation.
- Table:
tasks.tasks (active), tasks.task_archive (historical),
tasks.task_events (append-only audit + notify source of truth)
- Functions:
tasks.task_upsert(), tasks.task_list(),
tasks.task_archive_old(), tasks.task_prune(), tasks.task_log_event(),
tasks.transition_allowed()
- RLS: personal rows visible to own profile only; fleet rows visible to
all, writable only by fleet writers (orchestrator profiles); fleet ×
client-project and path/email/IP content banned (PII scrub gate)
Identity — NEVER hostname (Luke 2026-08-10)
Agent identity (PROFILE, CRUD_ROLE) resolves from, in order:
HERMES_PROFILE env → AGENT_NAME env → AGENT_NAME in
~/.hermes-cortex/agent.env → ~/hermes-cortex/.env → ~/.hermes-cortex/.env.
There is NO hostname/whoami fallback — if no agent variable is found,
task-db.py exits 1 with a clear error. A misconfigured host fails loudly
instead of silently writing rows as the machine name. Same rule in
commands.py (handler dispatch) — it raises RuntimeError.
CLI + MCP
- Deployed to:
~/.hermes-cortex/scripts/task-db.py (register in cortex-update.sh)
- MCP server:
mcp-servers/task-mcp.py, registered as todos
(tools: task_add, task_list, task_pending, task_update, task_switch,
task_save_end, task_prune). All agents — not orchestrator-only.
Commands
task-db.py list [--status pending|in_progress|paused|completed|cancelled]
task-db.py list --project hermes-cortex --repo hermes-cortex
task-db.py add "Fix dashboard health check" --priority 2 \
--project hermes-cortex --repo hermes-cortex --scope personal --source manual
task-db.py add "Story: fleet observability" --kind story
task-db.py add "Slice: bus task wiring" --kind slice --parent <story-uuid>
task-db.py add "EXEC: cortex-doctor.py" --source inbox --correlation-id <corr>
task-db.py update <id> --status in_progress
task-db.py update <id> --status completed
task-db.py update <id> --status paused
task-db.py update <id> --status in_progress --reason reopen
task-db.py update --by-correlation <corr> --status completed
task-db.py switch <target-id>
task-db.py pending
task-db.py restore <pending.json>
task-db.py restore <pending.json> --include-inbox
task-db.py save-end
task-db.py prune --older-than 90d
task-db.py --apply-schema
task-db.py update <id> --status completed --no-notify
Session Protocol
Session Start
todo() — load in-memory state (usually empty)
task-db.py pending — query DB for your profile's pending items (JSON).
Paused rows are surfaced but NEVER auto-resumed (M-9). Inbox-derived
rows are marked "untrusted": true and are excluded from restore by
default (R-4 — bus content is not trusted as agent instructions).
- Restore with
task-db.py restore <pending.json> (skips untrusted).
Throughout Session
- Before each
begin_change(): task-db.py update <id> --status in_progress
- When switching tasks:
task-db.py switch <target-id> (atomic pause+resume)
- After each
end_change(): task-db.py update <id> --status completed
- Reopen a completed task deliberately:
--reason reopen (never casual)
Session End
task-db.py save-end — archives completed/cancelled items
- Remaining
pending/in_progress/paused items persist in DB for next session
Bus Commands as Tasks (v2 S4/S5)
The fleet handler (agent-message-handler.py) creates a tasks.tasks row
(source='inbox', linked by correlation_id) for tracked subjects:
EXEC, UPDATE_REQUEST, TASK_REQUEST (Task: prefix), PROPOSAL,
ISSUES, IMPROVEMENTS. Lifecycle:
| Command class | Consumer | Transitions |
|---|
| EXEC / UPDATE_REQUEST | handler (no_agent) | created(pending) on receipt → in_progress at dispatch → completed at Result-receipt (EXEC_RESULT handler path) |
| ISSUES / PROPOSAL / IMPROVEMENTS / Task: | LLM inbox_read session | created(pending) on handler receipt → in_progress when the orchestrator processes it → completed when handled — the session calls task-db.py update --by-correlation <corr> --status completed |
- Stale sweep: the handler pauses inbox tasks stuck
in_progress > 1h
(TASKS_STALE_HOURS) with reason='stale' on every tick.
- Telegram: entry + completed events notify Luke's DM via
lib/telegram_notify.py ([agent] story → slice: status (id)). Mute via
TASKS_NOTIFY_MUTE=in_progress,paused; quiet hours via
TASKS_NOTIFY_QUIET=22:00-07:00.
- Never silently drop: a handled ISSUES/PROPOSAL must be transitioned to
completed (or paused if deferred). See
cortex-bus-automation skill.
Honest Fleet Semantics (party B-3)
--scope fleet stores the row locally on this host only — it is NOT
visible fleet-wide until transport ships (roadmap: git-backed, private repo).
The CLI prints this warning. Never claim fleet-wide visibility in reports.
Security Invariants (party B-1/B-2)
- Identifier-ish values (agent/project/repo/target/assignee/scope/status/
source/column/kind) are allowlist-validated BEFORE use; all DML funnels
through the parameterized
tasks.task_upsert() — never string-built WHERE
clauses.
- Free text is quote-doubled into string literals only.
- psql runs with
ON_ERROR_STOP=1; query flows via STDIN, never embedded in
a shell command string (the old sg-embedding path was shell-RCE).
- CRUD connects as
mycortex_reader_<profile> — NEVER superuser.
- v007 partial-update fix: status-only updates preserve source/scope/
priority/project/created_by (a bus task stays
source='inbox' through its
lifecycle). Do not revert to COALESCE(EXCLUDED.x, ...) in task_upsert.
Anti-Patterns
- ❌ Relying only on
todo() tool — per-session. Items vanish.
- ❌ Using a local file for persistence — the DB is already there.
- ❌ Letting completed items accumulate — run
task-db.py save-end.
- ❌ Forgetting to update status — stale pending items clutter restore.
- ❌ Hardcoding UUIDs — retrieve via
task-db.py pending/list.
- ❌ Using bus. or todo- nomenclature** — the tasks system is
tasks.*,
task-db.py, task_* tools everywhere (AC-1).
- ❌ Hostname identity fallback — identity comes from AGENT_NAME env /
.env, never platform.node() (Luke 2026-08-10).
Doctor Checks (v2 S7)
The doctor's check_task_lifecycle_v2 FAILs/WARNs on: schema_version < 5
(FAIL), task_events RLS forgeable grants (FAIL), telegram notify failures
(WARN), stale inbox tasks (WARN), .env perms != 600 (WARN). Run the doctor
after any schema/deploy change.
Known Issues
✅ FIXED 2026-08-06 — silent failure class (rc=0 stdin bug)
The old todo-db.py printed ✅ while rows vanished (stdin-mode psql returns
rc=0 on SQL error; bus.todos never existed on the migrated DB). The rewrite
uses -c/ON_ERROR_STOP + stderr scan; errors exit 1 with evidence. The
doctor now runs a write-probe (seed → list → archive) to catch the
"valid JSON but dead table" class — see check_task_db.
✅ FIXED 2026-08-10 — v007 partial-update clobber (found live in S3 e2e)
v005's task_upsert ON CONFLICT used COALESCE(EXCLUDED.x, existing) but
EXCLUDED.x is never NULL (INSERT defaults) → every status-only update
clobbered source (inbox→manual), scope (fleet→personal), priority (→0),
project (→hermes-cortex), created_by. v007 fixes the DO UPDATE clause to
coalesce against the function parameter. Regression-locked in
test-tasks-schema.sh.
Reference
- Design:
~/hermes-cortex/docs/design/task-workflow.md
- Lifecycle v2:
~/hermes-cortex/docs/design/task-lifecycle-v2.md
- CLI:
~/hermes-cortex/ops/scripts/manage/task-db.py
- MCP:
~/hermes-cortex/mcp-servers/task-mcp.py
- Migration runner:
~/hermes-cortex/ops/services/tasks/migrate.py
- Notify:
~/hermes-cortex/ops/scripts/lib/telegram_notify.py
- psql pitfalls: skill
psql-automation
- Session restore: skill
session-start-discipline