# Plan: qc-traced — Silent Trace Daemon with Postgres Backend

## Context

**Problem**: qc-trace has schemas and transforms for 4 coding agents (Claude Code, Codex CLI, Gemini CLI, Cursor IDE) that normalize session data into `NormalizedMessage`. We need a **silent background daemon** running on a developer's laptop that continuously watches for new session data and pushes it to a **Postgres** central server.

**Approach**: Build from scratch (ignoring prior `feat/traced-daemon` branch). Three-tier architecture: **daemon** (file watcher + transform) → **ingest server** (lightweight HTTP API with retry/batching) → **Postgres**. The ingest server decouples the daemon from the database, handles batching, retries, and connection pooling.

**Build method**: Parallel git worktrees with coordinating agents, GitHub issues for tracking, progress JSON files for inter-agent communication.

---

## Architecture

```
Developer Laptop
┌──────────────────────────────────────────────┐
│  ~/.claude/projects/*.jsonl                  │
│  ~/.codex/sessions/**/*.jsonl                │
│  ~/.gemini/tmp/*/chats/*.json                │
│  ~/.cursor/projects/*/agent-transcripts/*.txt │
└──────────────┬───────────────────────────────┘
               │ poll (5s interval)
               ▼
      ┌─────────────────┐
      │   qc-traced      │  (daemon process)
      │   ─────────────  │
      │   FileWatcher    │  discovers session files
      │   Collectors     │  reads + transforms per source
      │   RetryQueue     │  buffers on failure, retries with backoff
      │   HTTPPusher     │  POST /ingest with batch of NormalizedMessages
      └────────┬────────┘
               │ HTTP POST (localhost:7777)
               ▼
      ┌─────────────────┐
      │  Ingest Server   │  (separate process, Docker)
      │  ─────────────── │
      │  POST /ingest    │  accepts message batches
      │  POST /sessions  │  upsert sessions
      │  GET  /health    │  health check
      │  Batch Writer    │  psycopg3 async COPY
      │  Connection Pool │  AsyncConnectionPool
      └────────┬────────┘
               │ COPY / INSERT
               ▼
      ┌─────────────────┐
      │   PostgreSQL 16  │  (Docker)
      │  ───────────────  │
      │  sessions        │
      │  messages        │
      │  tool_calls      │
      │  tool_results    │
      │  token_usage     │
      └─────────────────┘
```

---

## Worktrees & Agent Assignments

### Worktree 1: `worktrees/infra-db`
**Branch**: `feat/traced-infra`
**Agent**: infra-db
**Purpose**: Docker setup, Postgres schema, database utilities

**Deliverables**:
- `docker-compose.yml` — Postgres 16 + ingest server
- `qc_trace/db/schema.sql` — full Postgres schema
- `qc_trace/db/connection.py` — async connection pool wrapper
- `qc_trace/db/writer.py` — batch COPY writer for NormalizedMessage
- `qc_trace/db/migrations.py` — schema version tracking
- `scripts/dev-db.sh` — start/stop dev database
- `tests/test_db_writer.py`
- Update `pyproject.toml` with optional `[daemon]` deps

### Worktree 2: `worktrees/ingest-server`
**Branch**: `feat/traced-ingest`
**Agent**: ingest-server
**Purpose**: HTTP ingest API that receives messages and writes to Postgres
**Depends on**: Worktree 1 (DB schema + writer)

**Deliverables**:
- `qc_trace/server/__init__.py`
- `qc_trace/server/app.py` — HTTP server (stdlib `http.server` or lightweight)
- `qc_trace/server/handlers.py` — `/ingest`, `/sessions`, `/health` endpoints
- `qc_trace/server/batch.py` — accumulate + flush logic (100 rows or 5s timeout)
- `Dockerfile` for the ingest server
- `tests/test_ingest_server.py`

### Worktree 3: `worktrees/daemon-core`
**Branch**: `feat/traced-daemon-v2`
**Agent**: daemon-core
**Purpose**: File watcher, collectors, state tracking, HTTP pusher
**Depends on**: Worktree 2 (ingest API contract)

**Deliverables**:
- `qc_trace/daemon/__init__.py`
- `qc_trace/daemon/config.py` — daemon configuration
- `qc_trace/daemon/watcher.py` — file discovery for all 4 sources
- `qc_trace/daemon/collector.py` — source-specific collectors using existing transforms
- `qc_trace/daemon/state.py` — file processing state (JSON on disk, atomic writes)
- `qc_trace/daemon/pusher.py` — HTTP client with retry queue + exponential backoff
- `qc_trace/daemon/main.py` — main daemon loop (poll → collect → push)
- `qc_trace/daemon/__main__.py` — entry point
- `tests/test_daemon_*.py`

### Worktree 4: `worktrees/ops-cli`
**Branch**: `feat/traced-ops`
**Agent**: ops-cli
**Purpose**: CLI, logging, monitoring, service files
**Independent** (can run in parallel, needs API contract only)

**Deliverables**:
- `qc_trace/cli/traced.py` — CLI: `start`, `stop`, `status`, `logs`, `db init`
- `qc_trace/daemon/progress.py` — progress JSON writer
- `scripts/qc-traced.service` (systemd)
- `scripts/com.quickcall.traced.plist` (launchd)
- `scripts/install.sh`, `scripts/uninstall.sh`
- Structured logging to `~/.qc-trace/qc-traced.log`
- `tests/test_cli.py`

---

## Postgres Schema

```sql
-- Sessions table
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,
    model TEXT,
    first_seen TIMESTAMPTZ DEFAULT NOW(),
    last_updated TIMESTAMPTZ DEFAULT NOW(),
    raw_file_path TEXT
);

-- Messages (core table, receives COPY inserts)
CREATE TABLE messages (
    id TEXT PRIMARY KEY,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    source TEXT NOT NULL,
    source_schema_version INT NOT NULL,
    msg_type TEXT NOT NULL,
    timestamp TIMESTAMPTZ,
    content TEXT,
    thinking TEXT,
    model TEXT,
    raw_data JSONB,
    raw_file_path TEXT,
    raw_line_number INT,
    ingested_at TIMESTAMPTZ DEFAULT NOW()
);

-- Token usage (1:1 with messages that have tokens)
CREATE TABLE token_usage (
    message_id TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
    input_tokens INT DEFAULT 0,
    output_tokens INT DEFAULT 0,
    cached_tokens INT DEFAULT 0,
    thinking_tokens INT DEFAULT 0
);

-- Tool calls (1:1 with tool_call messages)
CREATE TABLE tool_calls (
    message_id TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
    tool_id TEXT,
    tool_name TEXT NOT NULL,
    tool_input JSONB
);

-- Tool results (1:1 with tool_result messages)
CREATE TABLE tool_results (
    message_id TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
    call_id TEXT,
    output TEXT,
    status TEXT CHECK (status IN ('success', 'failure'))
);

-- Indexes for common queries
CREATE INDEX idx_messages_session ON messages(session_id);
CREATE INDEX idx_messages_type ON messages(msg_type);
CREATE INDEX idx_messages_source ON messages(source);
CREATE INDEX idx_messages_timestamp ON messages(timestamp);
CREATE INDEX idx_sessions_source ON sessions(source);
```

---

## Retry Mechanism (Daemon → Server)

```
On HTTP POST failure:
  1. Add batch to RetryQueue (bounded, drop oldest if full)
  2. Retry with exponential backoff: 1s, 2s, 4s, 8s, 16s, max 60s
  3. Log each retry attempt
  4. On success, drain queued batches
  5. On persistent failure (>5 min), log error, continue collecting
     (new data queued, old data eventually pushed when server recovers)
```

---

## Progress JSON Schema (Agent Coordination)

Each agent writes to `project_logs/{agent-name}_{work_name}_progress.json`:

```json
{
  "agent_name": "infra-db",
  "worktree": "worktrees/infra-db",
  "branch": "feat/traced-infra",
  "status": "in_progress | completed | blocked",
  "started_at": "2026-02-06T10:00:00Z",
  "updated_at": "2026-02-06T12:30:00Z",
  "completed_at": null,
  "current_task": "Implementing batch COPY writer",
  "completed_tasks": ["Created schema.sql", "Implemented connection pool"],
  "blocked_by": [],
  "provides_context": {
    "files_created": ["qc_trace/db/schema.sql", "qc_trace/db/writer.py"],
    "api_contract": "POST /ingest accepts JSON array of NormalizedMessage dicts",
    "config_keys": ["pg_host", "pg_port", "pg_dbname", "pg_user", "pg_password"]
  },
  "issues": [31],
  "errors": []
}
```

Agents check each other's progress files when blocked. The `provides_context` field shares API contracts and file paths between worktrees.

---

## Dependencies

```toml
# pyproject.toml
[project.optional-dependencies]
daemon = [
    "psycopg[binary]>=3.1.0",
    "psycopg_pool>=3.1.0",
]
```

Core `qc_trace` library stays zero-dependency. The daemon and server use `psycopg3` for Postgres. The ingest server uses stdlib `http.server` (no Flask/FastAPI — keeping it lightweight).

---

## GitHub Issues to Create

| Title | Labels | Worktree | Assignee |
|-------|--------|----------|----------|
| [Epic] qc-traced v2: Postgres backend with ingest server | epic, enhancement | — | sagarsrc |
| [Infra] Docker Postgres + DB schema + writer module | enhancement | infra-db | sagarsrc |
| [Server] HTTP ingest server with batch writes | enhancement | ingest-server | sagarsrc |
| [Daemon] File watcher, collectors, state, HTTP pusher | enhancement | daemon-core | sagarsrc |
| [Ops] CLI, logging, monitoring, service files | enhancement | ops-cli | sagarsrc |

---

## Execution Order

```
Phase 1 (parallel):
  ├── Worktree 1: infra-db        (Docker + schema + DB writer)
  └── Worktree 4: ops-cli         (CLI + logging + service files)

Phase 2 (after Worktree 1):
  └── Worktree 2: ingest-server   (HTTP API using DB writer from WT1)

Phase 3 (after Worktree 2):
  └── Worktree 3: daemon-core     (watcher + collectors + HTTP pusher)

Phase 4: Integration
  └── Merge all branches: infra → ingest-server → daemon-core → ops-cli → main
  └── Integration tests with full stack running
```

---

## Merge Strategy

1. Each worktree works on its own feature branch off main
2. Merge order follows dependency chain: `infra` → `ingest-server` → `daemon-core` → `ops-cli`
3. Each merge uses `git merge --no-ff` to preserve history
4. Integration worktree resolves any conflicts
5. Final PR from integration branch → main

---

## Verification

1. **Docker**: `docker compose up -d` starts Postgres + ingest server
2. **Schema**: `qc-traced db init` creates all tables
3. **Smoke test**: Place a test JSONL in `~/.claude/projects/test/`, start daemon, verify data in Postgres within 10s
4. **Retry**: Stop ingest server, verify daemon queues messages, restart server, verify queued data arrives
5. **Status**: `qc-traced status` shows daemon PID, server health, files watched, messages ingested
6. **Tests**: `pytest tests/` — all pass
7. **Logs**: `~/.qc-trace/qc-traced.log` shows structured entries
8. **Progress**: `project_logs/*.json` files track each agent's work
