ADR-178: Orphan Task Detection and Context Vacuum
Status
Accepted - 2026-02-12
Context
The Problem
CODITECT has 1,216 unchecked tasks across 14 TRACK files (pilot tracks alone), plus hundreds more in 30+ extension tracks. Sessions die, contexts compact, projects get parked — and the work those sessions were doing becomes invisible.
Current orphan sources:
| Source | Scale | How tasks get orphaned |
|---|---|---|
| Dead session claims | 2 active claims / 36 historical sessions | Session PID dies, task_claims row persists until stale timeout (10 min) — but the work context is lost forever |
| Compacted contexts | 1000+ sessions, ~512K messages | Session hits token limit, compacts, loses awareness of in-progress task. Next /orient starts fresh with no memory of partial work |
| Parked projects | Unknown | Human switches focus. Tasks left [ ] in TRACK files with no indicator they were ever started |
| Cross-session conflicts | 37 stale alerts in messaging.db | Multiple sessions touched same task. One finished, one didn't. Which subtasks are actually done? |
Why existing mechanisms don't catch this:
| Mechanism | What it does | What it misses |
|---|---|---|
_cleanup_stale_sessions() | Marks sessions stale after 10 min, deletes task_claims | Doesn't check if the task work was completed — just releases the lock |
TRACK file [ ] / [x] | Manual checkbox tracking | No timestamp, no session attribution. Can't distinguish "never started" from "started and abandoned" |
| Session logs | Chronological record of work | Buried in daily logs. No structured query for "what was in-progress when this session died?" |
/session-rescue | Recovers hung sessions | Reactive (user must notice). Doesn't scan for orphaned tasks, only hung processes |
The Scale
Pilot tracks (TRACK-A through TRACK-N):
| Track | Unchecked | Done | Orphan Risk |
|---|---|---|---|
| H (Framework) | 256 | 320 | High — most active, many session rollovers |
| J (Memory) | 145 | 653 | High — complex multi-session work |
| N (GTM) | 47 | 105 | Medium — recent financial model work |
| C (DevOps) | 45 | 43 | Medium — infrastructure tasks |
| K (Workflow) | 75 | 27 | Low — mostly planned, not started |
| F (Docs) | 8 | 65 | Low — stable |
Extension tracks (track-aa through track-ak): ~800+ unchecked tasks, mostly 0% complete (planned, not orphaned).
What "Orphan" Means
A task is orphaned when any of these are true:
- Claimed but owner is dead —
task_claimsrow exists, PID no longer running - Partially completed — session log shows work started, but
[ ]still unchecked in TRACK - Context-lost — session that worked on it compacted/died, no handoff note exists
- Stale in-progress — last session log mention is >7 days old, still
[ ] - Conflict-abandoned — operator alert fired (task_conflict), neither session completed it
Decision
Implement a Context Vacuum — a periodic sweep that cross-references TRACK files, messaging.db, session logs, and session history to detect orphaned tasks and surface them for reactivation.
Architecture
┌─────────────────────────────────────────────────────┐
│ Context Vacuum │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ TRACK │ │messaging │ │ Session Logs │ │
│ │ Files │ │ .db │ │ (JSONL + .md) │ │
│ │ [ ] / [x]│ │claims, │ │ task mentions, │ │
│ │ │ │lifecycle │ │ timestamps │ │
│ └────┬─────┘ └────┬─────┘ └────────┬──────────┘ │
│ │ │ │ │
│ └──────────────┼─────────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Orphan Detector │ │
│ │ (correlator) │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Orphan Report │ │
│ │ (actionable) │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌────────────┴────────────┐ │
│ ▼ ▼ │
│ /orient surfaces /next-task │
│ "3 orphaned tasks" prioritizes orphans │
│ │
└───────────────────────────────────────────────────────┘
Three Sweep Modes
1. Quick Sweep (on every /orient)
Cost: <2 seconds. Queries only messaging.db.
| Check | Query | Orphan Signal |
|---|---|---|
| Dead claims | task_claims WHERE PID not in ps output | Claimed task, dead owner |
| Stale claims | task_claims WHERE claimed_at > 24h ago | Long-running claim, possibly abandoned |
| Unread alerts | session_messages WHERE message_type = 'task_conflict' AND status = 'pending' | Conflict never resolved |
Output in /orient:
Orphaned Tasks: 2
H.13.9.7 — claimed by claude-55631 (dead PID), last activity 2h ago
N.6.10.5 — task_conflict alert unresolved (claude-9958 vs claude-46833)
Run /vacuum for full analysis
2. Standard Sweep (/vacuum command)
Cost: 10-30 seconds. Cross-references TRACK files + messaging.db + recent session logs.
| Data Source | What it provides |
|---|---|
TRACK files [ ] | All unchecked tasks with IDs |
messaging.db task_claims | Which tasks were claimed, by whom, when |
messaging.db task_broadcast | Last activity timestamp per task |
| Session logs (last 30 days) | Task ID mentions with timestamps |
sessions.db messages | Task IDs in conversation history |
Correlation logic:
for task_id in unchecked_track_tasks:
last_claim = query_last_claim(task_id) # messaging.db
last_broadcast = query_last_broadcast(task_id) # messaging.db
last_log_mention = grep_session_logs(task_id) # session logs
last_message = query_sessions_db(task_id) # sessions.db
last_activity = max(last_claim, last_broadcast, last_log_mention, last_message)
if last_activity is None:
status = "never_started"
elif last_activity > 7_days_ago:
status = "stale_in_progress" # ORPHAN
elif claim_exists and pid_dead:
status = "dead_claim" # ORPHAN
elif conflict_unresolved:
status = "conflict_abandoned" # ORPHAN
else:
status = "active" # Not orphaned
Output:
Context Vacuum Report — 2026-02-12
═══════════════════════════════════
ORPHANED TASKS (action required):
[DEAD_CLAIM] H.13.9.7 Documentation updates Last: 2026-02-11 (claude-55631, dead)
[STALE 14d] J.29.3.2 Dashboard data refresh Last: 2026-01-29
[CONFLICT] N.6.10.5 Test validation Conflict: claude-9958 vs claude-46833
PARKED PROJECTS (no activity >30 days):
Track K (Workflow) 75 tasks, 0% done, last activity: never
Track G (DMS) 64 tasks, 0% done, last activity: never
RECENTLY ACTIVE (no action needed):
H.13.9 95% done Last: 2026-02-12 (this session)
N.6.10 100% done Last: 2026-02-12 (completed)
J.31 In progress Last: 2026-02-11
Summary: 3 orphans, 2 parked tracks, 1216 total unchecked
3. Deep Sweep (/vacuum --deep)
Cost: 2-5 minutes. Full historical analysis.
Adds to Standard Sweep:
- Scans ALL session logs (not just 30 days)
- Checks
sessions.dbfor task ID mentions in conversation messages - Cross-references git commit messages (
feat(H.13.9):pattern) - Builds a task lifecycle timeline per task ID
- Identifies tasks that were worked on but never marked
[x]
Storage
Vacuum results are stored in messaging.db (new table):
CREATE TABLE IF NOT EXISTS vacuum_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sweep_type TEXT NOT NULL, -- 'quick', 'standard', 'deep'
created_at TEXT NOT NULL,
task_id TEXT NOT NULL,
orphan_type TEXT NOT NULL, -- 'dead_claim', 'stale_in_progress',
-- 'conflict_abandoned', 'never_started',
-- 'parked_project'
last_activity TEXT, -- ISO 8601 timestamp
last_session_id TEXT, -- Session that last touched it
track TEXT, -- Track letter
evidence TEXT, -- JSON: {claim_at, broadcast_at, log_mention, commit}
resolution TEXT, -- NULL, 'adopted', 'completed', 'deferred', 'cancelled'
resolved_at TEXT,
resolved_by TEXT
);
CREATE INDEX idx_vacuum_task ON vacuum_reports(task_id);
CREATE INDEX idx_vacuum_orphan ON vacuum_reports(orphan_type);
CREATE INDEX idx_vacuum_unresolved ON vacuum_reports(resolution) WHERE resolution IS NULL;
Commands
| Command | Purpose | Frequency |
|---|---|---|
/vacuum | Standard sweep — orphan report | Weekly or on-demand |
/vacuum --deep | Deep sweep — full history correlation | Monthly or when resuming parked project |
/vacuum --quick | Quick sweep — dead claims only | Built into /orient |
/vacuum --adopt <task_id> | Claim an orphaned task for this session | On-demand |
/vacuum --defer <task_id> | Mark orphan as intentionally deferred | On-demand |
/vacuum --cancel <task_id> | Mark orphan as no longer needed | On-demand (requires confirmation) |
Integration Points
| System | Integration |
|---|---|
/orient | Quick sweep runs automatically. Shows orphan count. |
/next-task | Prioritizes orphaned tasks over new work (dead_claim > stale > never_started) |
/session-end | Checks if current session has claimed tasks, warns if uncompleted |
/session-gc | Cleans up dead claims, triggers quick sweep |
| Session log | /vacuum results logged as standalone analysis document |
Parked Project Detection
A project/track is "parked" when:
- All tasks are
[ ](0% complete), OR - Last activity across all tasks is >30 days ago, AND
- No active session claims for any task in the track
Parked projects are surfaced separately from individual orphans — they represent strategic decisions (intentional deferral) not accidental abandonment.
Consequences
Positive
- No more invisible abandoned work — orphans are surfaced automatically
- Faster session startup —
/orientshows what needs attention without manual searching - Parked project awareness — tracks that haven't been touched in months become visible
- Task lifecycle visibility — first time we can answer "when was this task last worked on?"
- Builds on existing infrastructure — uses messaging.db, session logs, TRACK files. No new databases.
Negative
- Standard sweep reads TRACK files on disk — I/O cost, but TRACK files are small (<100KB each)
- Deep sweep is slow — 2-5 min for full history. Acceptable for monthly use.
- False positives on "parked" — Track K and Track G are intentionally deferred, not orphaned. Need
--deferto suppress.
Risks
| Risk | Mitigation |
|---|---|
| Vacuum report noise (too many orphans) | Filter by track, priority, age. Default to high-activity tracks only. |
sessions.db query performance | Use FTS5 index on messages table. Already indexed. |
| Stale TRACK file state | Vacuum detects this — if git shows task [x] but no commit evidence, flag it. |
Alternatives Considered
| Alternative | Rejected Because |
|---|---|
Manual /grep for task IDs | Doesn't correlate across sources. Can't distinguish "never started" from "started and abandoned." |
| Automatic task reaping (auto-cancel after N days) | Too aggressive. Parked projects are intentional. Human must decide. |
| Real-time task lifecycle tracking (event sourcing) | Over-engineered for current scale. The vacuum approach is batch-oriented and simpler. Reconsider if task volume reaches 10K+. |
Extend /session-status with orphan detection | /session-status is already overloaded (v4.1). Separate command keeps concerns clean. |
Success Criteria
- Quick sweep runs in <2s on
/orient - Standard sweep identifies orphans across TRACK + messaging.db + session logs
- Deep sweep correlates with git commits and full session history
-
/vacuum --adoptclaims orphaned task and broadcasts to bus - Parked projects are distinguished from individual orphans
- Vacuum results stored in
messaging.dbfor historical tracking -
/next-taskprioritizes orphaned tasks over fresh work - False positive rate <10% (validated on current TRACK files)
Implementation Plan
| Phase | Scope | Effort |
|---|---|---|
| 1 | Quick sweep + /orient integration | 1 session |
| 2 | Standard sweep (/vacuum command) | 1-2 sessions |
| 3 | Storage (vacuum_reports table) + adopt/defer/cancel | 1 session |
| 4 | Deep sweep + /next-task integration | 1-2 sessions |
| 5 | /session-end warning + /session-gc integration | 1 session |
Version: 1.0.0 Created: 2026-02-12 Author: Claude (Opus 4.6) Track: H (Framework Autonomy) Task: H.13.10
Changelog
- v1.1.0 (2026-02-12) — Accepted. Full implementation complete (context_vacuum.py, 1277 lines). All 5 phases implemented. Integration with /orient, /next-task, /session-end, /session-gc command specs.
- v1.0.0 (2026-02-12) — Initial proposal. Three sweep modes, vacuum_reports table, 5-phase implementation plan.