Files
Codewalkers/docs/database.md
Lukas May 28521e1c20 chore: merge main into cw/small-change-flow
Integrates main branch changes (headquarters dashboard, task retry count,
agent prompt persistence, remote sync improvements) with the initiative's
errand agent feature. Both features coexist in the merged result.

Key resolutions:
- Schema: take main's errands table (nullable projectId, no conflictFiles,
  with errandsRelations); migrate to 0035_faulty_human_fly
- Router: keep both errandProcedures and headquartersProcedures
- Errand prompt: take main's simpler version (no question-asking flow)
- Manager: take main's status check (running|idle only, no waiting_for_input)
- Tests: update to match removed conflictFiles field and undefined vs null
2026-03-06 16:48:12 +01:00

265 lines
10 KiB
Markdown

# Database Module
`apps/server/db/` — SQLite database via better-sqlite3 + Drizzle ORM with hexagonal architecture.
## Architecture
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 14 repository interfaces
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 14 Drizzle adapters
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
## Tables
### initiatives
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| name | text NOT NULL | |
| status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' |
| branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') |
| createdAt, updatedAt | integer/timestamp | |
### phases
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| initiativeId | text FK → initiatives (cascade) | |
| name | text NOT NULL | |
| content | text nullable | Tiptap JSON |
| status | text enum | 'pending' \| 'approved' \| 'in_progress' \| 'completed' \| 'blocked' \| 'pending_review' |
| mergeBase | text nullable | git merge-base hash stored before phase merge, enables diff reconstruction for completed phases |
| createdAt, updatedAt | integer/timestamp | |
### phase_dependencies
`phaseId` FK → phases, `dependsOnPhaseId` FK → phases. Both cascade delete.
### tasks
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| phaseId | text nullable FK → phases (cascade) | |
| initiativeId | text nullable FK → initiatives (cascade) | |
| parentTaskId | text nullable self-ref FK (cascade) | decomposition hierarchy |
| name | text NOT NULL | |
| description | text nullable | |
| type | text enum | 'auto' |
| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' |
| priority | text enum | 'low' \| 'medium' \| 'high' |
| status | text enum | 'pending' \| 'in_progress' \| 'completed' \| 'blocked' |
| order | integer | default 0 |
| summary | text nullable | Agent result summary — propagated to dependent tasks as context |
| retryCount | integer NOT NULL | default 0, incremented on agent crash auto-retry, reset on manual retry |
| createdAt, updatedAt | integer/timestamp | |
### task_dependencies
`taskId` FK → tasks, `dependsOnTaskId` FK → tasks. Both cascade delete.
### agents
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| name | text NOT NULL UNIQUE | human-readable alias (adjective-animal) |
| taskId | text nullable FK → tasks (set null) | |
| initiativeId | text nullable FK → initiatives (set null) | |
| sessionId | text nullable | CLI session ID for resume |
| worktreeId | text NOT NULL | path to agent's git worktree |
| provider | text NOT NULL | default 'claude' |
| accountId | text nullable FK → accounts (set null) | |
| status | text enum | 'idle' \| 'running' \| 'waiting_for_input' \| 'stopped' \| 'crashed' |
| mode | text enum | 'execute' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' |
| pid | integer nullable | OS process ID |
| exitCode | integer nullable | |
| prompt | text nullable | Full assembled prompt passed to agent at spawn; persisted for durability after log cleanup |
| outputFilePath | text nullable | |
| result | text nullable | JSON |
| pendingQuestions | text nullable | JSON |
| userDismissedAt | integer/timestamp nullable | |
| createdAt, updatedAt | integer/timestamp | |
### accounts
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| email | text NOT NULL | |
| provider | text NOT NULL | default 'claude' |
| configJson | text nullable | serialized .claude.json |
| credentials | text nullable | serialized .credentials.json |
| isExhausted | integer/boolean | default false |
| exhaustedUntil | integer/timestamp nullable | |
| lastUsedAt | integer/timestamp nullable | round-robin scheduling |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### proposals
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| agentId | text FK → agents (cascade) | |
| initiativeId | text FK → initiatives (cascade) | |
| targetType | text enum | 'page' \| 'phase' \| 'task' |
| targetId | text nullable | existing entity ID, null for creates |
| title, summary, content | text | markdown body |
| metadata | text nullable | JSON |
| status | text enum | 'pending' \| 'accepted' \| 'dismissed' |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### pages
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| initiativeId | text FK → initiatives (cascade) | |
| parentPageId | text nullable self-ref FK (cascade) | root page has NULL |
| title | text NOT NULL | |
| content | text nullable | Tiptap JSON |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### projects
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| name | text NOT NULL UNIQUE | |
| url | text NOT NULL UNIQUE | git repo URL |
| defaultBranch | text NOT NULL | default 'main' |
| lastFetchedAt | integer/timestamp | nullable, updated by ProjectSyncManager |
| createdAt, updatedAt | integer/timestamp | |
### initiative_projects (junction)
`initiativeId` + `projectId` with unique index. Both FK cascade.
### messages
Self-referencing (parentMessageId) for threading. Sender/recipient types: 'agent' | 'user'.
### agent_log_chunks
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| agentId | text NOT NULL | **NO FK** — survives agent deletion |
| agentName | text NOT NULL | snapshot for display |
| sessionNumber | integer | spawn=1, resume=prev+1 |
| content | text NOT NULL | raw JSONL chunk |
| createdAt | integer/timestamp | |
Index on `agentId` for fast queries.
### conversations
Inter-agent communication records.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| fromAgentId | text NOT NULL | FK→agents ON DELETE CASCADE |
| toAgentId | text NOT NULL | FK→agents ON DELETE CASCADE |
| initiativeId | text | FK→initiatives ON DELETE SET NULL |
| phaseId | text | FK→phases ON DELETE SET NULL |
| taskId | text | FK→tasks ON DELETE SET NULL |
| question | text NOT NULL | |
| answer | text | nullable until answered |
| status | text enum | pending, answered |
| createdAt | integer/timestamp | |
| updatedAt | integer/timestamp | |
Indexes: `(toAgentId, status)` for listen polling, `(fromAgentId)`.
### chat_sessions
Persistent chat sessions for iterative refinement of phases/tasks.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| targetType | text enum | 'phase' \| 'task' |
| targetId | text NOT NULL | phase or task ID |
| initiativeId | text FK → initiatives (cascade) | |
| agentId | text FK → agents (set null) | linked agent |
| status | text enum | 'active' \| 'closed', default 'active' |
| createdAt, updatedAt | integer/timestamp | |
Indexes: `(targetType, targetId)`, `(initiativeId)`.
### chat_messages
Messages within a chat session.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| chatSessionId | text FK → chat_sessions (cascade) | |
| role | text enum | 'user' \| 'assistant' \| 'system' |
| content | text NOT NULL | |
| changeSetId | text FK → change_sets (set null) | links assistant messages to applied changes |
| createdAt | integer/timestamp | |
Index: `(chatSessionId)`.
### errands
Tracks errand work items linked to a project branch, optionally assigned to an agent.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | caller-supplied |
| description | text NOT NULL | human-readable description |
| branch | text NOT NULL | working branch name |
| baseBranch | text NOT NULL | default 'main' |
| agentId | text FK → agents (set null) | assigned agent; null if unassigned |
| projectId | text FK → projects (cascade) | owning project |
| status | text enum | active, pending_review, conflict, merged, abandoned; default 'active' |
| createdAt, updatedAt | integer/timestamp | |
### review_comments
Inline review comments on phase diffs, persisted across page reloads.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| phaseId | text FK → phases (cascade) | scopes comment to phase |
| filePath | text NOT NULL | file in diff |
| lineNumber | integer NOT NULL | line number (new-side or old-side for deletions) |
| lineType | text enum | 'added' \| 'removed' \| 'context' |
| body | text NOT NULL | comment text |
| author | text NOT NULL | default 'you' |
| resolved | integer/boolean | default false |
| createdAt, updatedAt | integer/timestamp | |
Index: `(phaseId)`.
## Repository Interfaces
14 repositories, each with standard CRUD plus domain-specific methods:
| Repository | Key Methods |
|-----------|-------------|
| InitiativeRepository | create, findById, findAll, findByStatus, update, delete |
| PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId |
| TaskRepository | + findByParentTaskId, findByPhaseId, createDependency |
| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus |
| MessageRepository | + findPendingForUser, findRequiringResponse, findReplies |
| PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId |
| ProjectRepository | + junction ops: setInitiativeProjects (diff-based), findProjectsByInitiativeId |
| AccountRepository | + findNextAvailable (round-robin), markExhausted, clearExpiredExhaustion |
| ProposalRepository | + findByAgentIdAndStatus, updateManyByAgentId, countByAgentIdAndStatus |
| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount |
| ConversationRepository | create, findById, findPendingForAgent, answer |
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
| ErrandRepository | create, findById, findAll (filter by projectId/status), update, delete |
## Migrations
Located in `drizzle/`. Applied via `ensureSchema()` on startup using Drizzle's `migrate()`.
Key rules:
- **Never use raw SQL** for schema initialization
- Run `npx drizzle-kit generate` to create migrations
- See [database-migrations.md](database-migrations.md) for full workflow
- Snapshots stale after 0008; migrations 0008+ are hand-written
Current migrations: 0000 through 0035 (36 total).