# Daemon Startup Reconciliation (#43)

## Problem

The daemon tracks file processing progress in `~/.qc-trace/state.json`. If Postgres loses data (container restart, volume wipe, crash), the daemon doesn't know — it skips files it already "processed" even though the server has nothing. Previously this required a manual `rm ~/.qc-trace/state.json` to recover.

## Solution

On startup, the daemon queries the server for what it actually has and resets local state for any files where data is missing.

### How It Works

```
Daemon startup
    │
    ▼
Load state.json
    │
    ▼
GET /api/sync ──► Server queries Postgres:
    │               SELECT raw_file_path, source,
    │                      count(*) AS message_count,
    │                      max(raw_line_number) AS max_line
    │               FROM messages GROUP BY raw_file_path, source
    │
    ▼
Compare local state vs server response
    │
    ├─ Server has nothing for a tracked file → reset (full re-process)
    ├─ JSONL source, server max_line < local → rewind to server's line
    ├─ Hash source, server message_count = 0 → reset (full re-process)
    └─ Server unreachable → skip reconciliation, continue normally
    │
    ▼
Enter poll loop
```

### Reconciliation Rules

| Source Type | Condition | Action |
|-------------|-----------|--------|
| JSONL (claude_code, codex_cli) | Server has no data | Remove from state.json → full re-ingest |
| JSONL (claude_code, codex_cli) | Server max_line < local | Rewind `last_line_processed` to server's position → partial re-ingest |
| Hash-based (gemini_cli, cursor) | Server has no data | Remove from state.json → full re-ingest |
| Hash-based (gemini_cli, cursor) | Server message_count = 0 | Remove from state.json → full re-ingest |
| Any | Server unreachable | Skip reconciliation entirely (non-fatal) |

JSONL sources get partial recovery because line numbers allow rewinding to exactly where the server left off. Hash-based sources are all-or-nothing since they don't have line-level granularity.

## Files Changed

| File | Change |
|------|--------|
| `qc_trace/db/reader.py` | Added `get_file_sync_state()` — query returning per-file message counts and max line numbers |
| `qc_trace/server/handlers.py` | Added `handle_api_sync()` handler |
| `qc_trace/server/app.py` | Registered `GET /api/sync` route |
| `qc_trace/daemon/main.py` | Added `_reconcile()` and `_fetch_server_state()`, called on startup after state load |
| `qc_trace/daemon/state.py` | Added `reset_state()` method |

## API

### `GET /api/sync`

Returns a JSON array of per-file sync state from Postgres:

```json
[
  {
    "raw_file_path": "/home/user/.claude/projects/foo/abc123.jsonl",
    "source": "claude_code",
    "message_count": 42,
    "max_line": 150
  }
]
```

## Verification

1. Start full stack: Postgres + server + daemon
2. Verify sessions appear on dashboard
3. Wipe Postgres: `docker compose down -v && docker compose up -d`, re-init schema
4. Restart daemon — logs should show `"Reconciliation: reset N file(s) for re-processing"`
5. Wait one poll cycle (5s) — data should re-appear on dashboard automatically
6. No manual `rm state.json` needed
