CheddahBot/docs/ARCHITECTURE_REVIEW.md

285 lines
13 KiB
Markdown

# CheddahBot Architecture Review: Data Consistency and Robustness
**Date:** 2026-02-19
**Scope:** Full codebase review focusing on ClickUp data consistency, failure modes, and thread safety
---
## 1. Data Flow Map: Complete Task Lifecycle
### 1.1 ClickUp Auto-Execution Path (Scheduler)
```
ClickUp API (source of truth for task creation)
|
v
Scheduler._poll_clickup() [scheduler.py:235]
|-- get_tasks_from_space() [traverses all folders/lists]
|-- For each task:
| |-- Check KV store for existing state (executing/completed/failed -> skip)
| |-- Verify task_type in skill_map
| |-- Verify auto_execute == true
| |-- Verify due_date within 3-week window
| v
| Scheduler._execute_task() [scheduler.py:313]
| |-- Set KV state to "executing"
| |-- Update ClickUp status to "in progress"
| |-- Call ToolRegistry.execute() with mapped tool
| |-- On success:
| | |-- If tool handled sync ("## ClickUp Sync" in output):
| | | state = "completed" in KV
| | |-- Else (fallback path):
| | | extract docx paths -> upload attachments
| | | state = "completed" in KV
| | | ClickUp status -> "internal review"
| | | Post completion comment
| |-- On failure:
| | state = "failed" in KV
| | ClickUp status -> "to do"
| | Post error comment
```
### 1.2 Press Release Pipeline (Tool-Internal)
```
write_press_releases() [tools/press_release.py:489]
|-- If no clickup_task_id passed, auto-lookup via _find_clickup_task()
|-- Set ClickUp to "in progress", post starting comment
|-- Step 1: Generate 7 headlines (chat brain)
|-- Step 2: AI judge picks best 2 (chat brain)
|-- Step 3: Write 2 PRs (execution brain x2)
|-- Upload docx attachments to ClickUp mid-pipeline
|-- Step 4: Generate 2 JSON-LD schemas (execution brain x2)
|-- Set ClickUp to "internal review"
|-- Update KV state to "completed"
|-- Output includes "## ClickUp Sync" marker
```
### 1.3 Link Building Pipeline (Tool-Internal)
```
run_cora_backlinks() [tools/linkbuilding.py:455]
|-- If no clickup_task_id, auto-lookup via _find_clickup_task()
|-- Step 1: subprocess call to Big-Link-Man ingest-cora
|-- Step 2: subprocess call to Big-Link-Man generate-batch
|-- On success: _complete_clickup_task() -> status "complete", KV "completed"
|-- On failure: _fail_clickup_task() -> status "internal review", KV "failed"
```
### 1.4 Folder Watcher Path
```
Scheduler._folder_watch_loop() [scheduler.py:461]
|-- Scan watch folder for *.xlsx
|-- For each file:
| |-- Check KV "linkbuilding:watched:{filename}" (skip completed/processing/failed)
| |-- Match filename to ClickUp task via fuzzy keyword match
| |-- Set KV to "processing"
| |-- Call run_cora_backlinks via tool registry
| |-- On success: move file to processed/, KV "completed"
| |-- On failure: KV "failed"
```
---
## 2. Single Points of Failure
### 2.1 CRITICAL: Crash During Execution Leaves Task in "executing" Limbo
**File:** `scheduler.py:328-344`
When `_execute_task()` sets the KV state to `"executing"` and updates ClickUp to `"in progress"`, then the process crashes before the tool completes, the task ends up in a permanent dead state:
- **KV store** says `"executing"` -- the scheduler will never pick it up again (skips `executing`, `completed`, `failed`)
- **ClickUp** says `"in progress"` -- the poll only queries `["to do"]` status
- **Result:** The task is permanently orphaned. No automatic recovery. No human alert.
**Severity:** CRITICAL. A crashed process could silently drop a paid client deliverable with no notification.
**Mitigation today:** The `clickup_reset_task` and `clickup_reset_all` tools exist (`clickup_tool.py`), but they require someone to manually notice and intervene.
### 2.2 CRITICAL: ClickUp API Down During Status Update
**File:** `scheduler.py:343`
If `update_task_status()` fails (returns `False`), the code continues execution anyway. The ClickUp status stays at `"to do"` while the KV says `"executing"`. On next poll cycle, the task is skipped in KV but still visible as `"to do"` in ClickUp.
The `update_task_status` method in `clickup.py` catches all exceptions and returns `False` on failure, but the caller at `scheduler.py:343` discards this return value.
**Severity:** CRITICAL. Silent data inconsistency between ClickUp and KV store.
### 2.3 HIGH: Execution Brain 5-Minute Hard Timeout
**File:** `llm.py:193`
The press release pipeline involves 4+ sequential execution brain calls. Each has a 300-second timeout, but none of the callers in `press_release.py` handle `TimeoutExpired`. If the execution brain times out mid-pipeline, intermediate files may have been written and partial docx uploaded to ClickUp.
The link building tool **does** handle `subprocess.TimeoutExpired` properly with cleanup.
**Severity:** HIGH. Partial deliverables uploaded to ClickUp with a failed state could confuse client review.
### 2.4 HIGH: SQLite DB Corruption Recovery
The SQLite database uses WAL mode, which is robust. However:
- There is **no backup mechanism**
- There is **no integrity check on startup**
- If the DB file is corrupted, the entire KV store and all task state history is lost
**Severity:** HIGH. For a production system managing paid deliverables, a single SQLite file with no backups is fragile.
### 2.5 MEDIUM: Tool Execution Has No Timeout
**File:** `tools/__init__.py:175`
`ToolRegistry.execute()` calls tool functions synchronously with no timeout wrapper. If a tool hangs, the scheduler thread blocks forever. Since there is only one ClickUp polling thread, this halts all future ClickUp processing.
### 2.6 MEDIUM: Memory Files Can Be Silently Truncated
**File:** `memory.py:78-85`
The memory system does unsynchronized read-modify-write on markdown files. If two threads both try to log at the same time, one write can overwrite the other's data.
---
## 3. ClickUp Sync Robustness
### 3.1 Source of Truth: It is Ambiguous
The system has **two** sources of truth that can disagree:
1. **KV store** (`clickup:task:{id}:state`) -- the scheduler's decision engine
2. **ClickUp API** (`task.status`) -- the human-facing project management system
Neither is authoritative:
- A task can be `"completed"` in KV but still `"in progress"` on ClickUp if the status update API call failed
- A task can be `"to do"` on ClickUp but `"executing"` in KV if the process crashed
- A human can manually move a task back to `"to do"` on ClickUp, but if KV says `"completed"`, the scheduler will never pick it up
### 3.2 Tasks Can Get Lost
**Scenario 1:** Someone moves a task to `"in progress"` manually on ClickUp -- it will never be polled (only `"to do"` is polled).
**Scenario 2:** Tasks with no due date are skipped by the scheduler. Tasks without a due date that are never assigned one are permanently invisible.
**Scenario 3:** The `DUE_DATE_WINDOW_WEEKS = 3` filter means tasks with due dates more than 3 weeks out are invisible.
### 3.3 Duplicate Executions CAN Happen
If the same tool is invoked both from the scheduler AND from the chat UI (via `_find_clickup_task` auto-lookup), both paths can set up KV state independently. Two press releases for the same client could be generated simultaneously.
**Severity:** HIGH.
### 3.4 Failed Task Retry Mechanism
The retry path works but is manual:
1. On failure, scheduler sets KV to `"failed"` and ClickUp back to `"to do"`
2. But since KV says `"failed"`, the scheduler skips it on next poll
3. To retry, someone must call `clickup_reset_task(task_id)` via chat
**Discrepancy:** The scheduler sets failed tasks to `"to do"`, but the link building tool sets them to `"internal review"`. This inconsistency means retry behavior depends on which code path handles the failure.
### 3.5 Phantom Completion
The `"## ClickUp Sync"` string check (`scheduler.py:385`) is fragile -- it could false-positive on debug output. Also, in the fallback path, a task is marked `"completed"` even if zero docx files were uploaded.
---
## 4. Thread Safety
### 4.1 Threads in Play
| Thread | Name | Purpose |
|--------|------|---------|
| Main | uvicorn | Gradio UI + FastAPI |
| Daemon | `scheduler` | Cron-based scheduled tasks |
| Daemon | `heartbeat` | Periodic checklist check |
| Daemon | `clickup` | ClickUp polling + execution |
| Daemon | `folder-watch` | CORA file watcher |
### 4.2 Shared ClickUp Client Instance
The scheduler lazy-initializes `self._clickup_client` once and reuses it across the ClickUp thread. The folder watcher also calls `self._get_clickup_client()`, which shares the same instance. If both threads are active simultaneously, this could cause request interleaving bugs in httpx.
Tool functions create their **own** ClickUpClient instances, so tool-level calls are isolated.
### 4.3 KV Store Race Conditions
Individual `kv_set` and `kv_get` calls are safe (atomic SQL). But the **read-check-modify-write** pattern in the scheduler is NOT atomic. The real risk is the chat-vs-scheduler race described in section 3.3.
### 4.4 Memory System File Writes
`remember()` and `log_daily()` both do unsynchronized read-modify-write on markdown files with no file locking. Concurrent calls can lose data.
---
## 5. Error Recovery After Crash and Restart
### 5.1 What Survives a Restart
- KV store, ClickUp task statuses, scheduled tasks, conversation history, memory files, notification history -- all persist.
### 5.2 What Does NOT Recover
- **In-flight task execution:** KV says `"executing"`, ClickUp says `"in progress"`. Task is stuck forever.
- **Folder watcher "processing" state:** KV says `"processing"`. File is stuck forever.
- There is no startup reconciliation, no stale-task detector, no watchdog for stuck states.
---
## 6. Recommendations
### CRITICAL Priority
| # | Issue | Fix | Effort |
|---|-------|-----|--------|
| C1 | Stuck "executing" after crash | On startup, scan KV for stale `"executing"` states (>1h old), move to `"failed"`, push notification | 30-50 lines |
| C2 | Ignored ClickUp API failures | Check `update_task_status()` return value before proceeding with execution | 10-20 lines |
| C3 | Chat-vs-scheduler duplicate execution | Check KV store in `_find_clickup_task()` before creating new entries | 15-25 lines/file |
### HIGH Priority
| # | Issue | Fix | Effort |
|---|-------|-----|--------|
| H1 | No DB backup | Copy DB to `.bak` on startup; periodic backups from heartbeat | 5-10 lines |
| H2 | Inconsistent failure status | Unify: scheduler and tools both use same failure status | 10-15 lines |
| H3 | Fragile "## ClickUp Sync" detection | Use structured return type or unique token instead of string parsing | 20-40 lines |
| H4 | No KV-ClickUp reconciliation | Add heartbeat check comparing ClickUp `"in progress"` vs KV states | 40-60 lines |
### MEDIUM Priority
| # | Issue | Fix | Effort |
|---|-------|-----|--------|
| M1 | Memory file race condition | Add `threading.Lock()` to MemorySystem for file writes | 10-15 lines |
| M2 | Shared httpx client across threads | Make `_get_clickup_client()` return thread-local instances | 10-15 lines |
| M3 | No tool execution timeout | Wrap tool execution in a thread with timeout | 15-25 lines |
| M4 | Poor auto-flush summary quality | Use chat brain for summarization instead of truncated concat | 20-30 lines |
| M5 | Unbounded KV growth | Add periodic cleanup of completed/failed states older than N days | 20-30 lines |
### LOW Priority
| # | Issue | Fix | Effort |
|---|-------|-----|--------|
| L1 | No programmatic health check | Add `/health` endpoint checking thread liveness and DB access | 20-30 lines |
| L2 | Unstructured state transition logging | Log every state transition as structured JSON event | 20-40 lines |
| L3 | Test coverage gaps | No tests for crash recovery, concurrent execution, folder watcher | 200+ lines |
---
## Risk Matrix
| # | Issue | Severity | Likelihood | Impact |
|---|-------|----------|------------|--------|
| C1 | Stuck "executing" after crash | CRITICAL | Medium | Task permanently lost |
| C2 | Ignored ClickUp API failures | CRITICAL | Medium | Silent inconsistency |
| C3 | Chat-vs-scheduler duplicate | CRITICAL | Low | Duplicate deliverables |
| H1 | No DB backup | HIGH | Low | Total state loss |
| H2 | Inconsistent failure status | HIGH | Medium | Confusing retry behavior |
| H3 | Fragile sync detection | HIGH | Low | Phantom completion |
| H4 | No KV-ClickUp reconciliation | HIGH | Medium | Undetected inconsistency |
| M1 | Memory file race condition | MEDIUM | Low | Lost memory entries |
| M2 | Shared httpx client | MEDIUM | Low | Corrupted HTTP requests |
| M3 | No tool execution timeout | MEDIUM | Low | Thread deadlock |
| M4 | Poor auto-flush summaries | MEDIUM | High | Lost context |
| M5 | Unbounded KV growth | MEDIUM | Low | Performance degradation |