Data Models
SQLite via Prisma. All data scoped to watched repos. 13 models, one repository file per concern.
Overview
Slop uses SQLite as its persistence layer, accessed via Prisma ORM with the @prisma/adapter-better-sqlite3 driver-adapters pattern. All application data lives in a single SQLite file whose path is resolved at runtime by resolveDatabaseUrl().
The database uses a multi-repo scoping pattern: most domain tables carry a repoId foreign key referencing Repo.id (the owner/name slug). Worker, ReadyIssue, ReadyOrder, IssueConfig, InternalIssue, Label, and Batch rows are all scoped this way.
Busy timeout: 5000 ms, set on the SQLite connection to reduce "database is locked" errors under concurrent writes.
The client singleton in src/db/client.ts lazily dereferences a globalThis.prismaClient instance, allowing disconnectPrisma() to replace it between tests without leaking connections.
| Environment | File | Notes |
|---|---|---|
| Production | slop.db | Default path |
| Development | slop_dev.db | Dev environment |
| Test | slop_test.db | Reset by vitest.global-setup.ts |
deleteMany query middleware blocks unscoped bulk deletes on protected models (Repo, Worker, WorkerReport, Run, Event, IssueConfig, InternalIssue, InternalIssueComment) in non-test, non-ALLOW_DESTRUCTIVE environments.
Repo
Repo
Represents a watched GitHub repository that Slop manages. The id is the owner/name slug, making it both primary key and human-readable identifier. One Slop instance manages many repos; all domain data scopes to a Repo row.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | String | @id | owner/name slug — primary key and repo identifier |
path | String | required | Absolute filesystem path to the local checkout |
baseBranch | String? | optional | Override for the default branch (e.g. main) |
parallelismCap | Int? | optional | Max concurrent workers for this repo |
removedAt | DateTime? | optional | Soft-delete timestamp; non-null means the repo is inactive |
createdAt | DateTime | @default(now()) | Row creation time |
- Soft delete via
removedAt—listRepos()filtersremovedAt = null;listAllRepos()returns everything. - The
idfield IS the slug (owner/name), so there is no separate slug column. - Relations:
workers,readyIssues,readyOrders,issueConfigs,internalIssues,labels,batches,snapshot.
Worker
Worker
One unit of work: Slop's agent running against a single GitHub or internal issue. Tracks the full lifecycle from claim through merge or failure.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | String | @id @default(cuid()) | Unique worker identifier |
repoId | String | FK → Repo | Owning repo slug |
issueNumber | Int | required | GitHub or internal issue number |
issueTitle | String | required | Issue title at claim time |
issueSource | String | @default("github") | "github" or "internal" |
status | String | required | Current lifecycle status |
worktreePath | String | required | Absolute path to the git worktree |
prNumber | Int? | optional | GitHub PR number once created |
conflictAttempts | Int | @default(0) | Number of conflict-resolution attempts |
ciAttempts | Int | @default(0) | Number of CI-fix attempts |
verificationAttempts | Int | @default(0) | Number of verification attempts |
reviewAttempts | Int | @default(0) | Number of review attempts |
agentPid | Int? | optional | OS PID of the running agent process |
sessionId | String? | optional | Claude session ID for resume |
statusBeforeReport | String? | optional | Status saved before entering the reporting phase |
statusBeforePause | String? | optional | Status saved before pausing |
archivedAt | DateTime? | optional | Soft-archive timestamp for terminal workers |
agentModelOverride | String? | optional | Per-worker model override |
agentEffortOverride | String? | optional | Per-worker effort level override |
autoMergeModeOverride | String? | optional | Override for auto-merge behavior |
autoReviewModeOverride | String? | optional | Override for auto-review behavior |
createdAt | DateTime | @default(now()) | Row creation time |
updatedAt | DateTime | @updatedAt | Auto-updated on every write |
- CAS updates:
updateWorkerStatusFromandreclaimTerminalWorkeruseupdateManywith awhereclause that includes the expected current status, returningcount > 0to detect races. - Soft archive: terminal workers are not deleted —
archiveFinishedWorkerssetsarchivedAt;listWorkersfiltersarchivedAt = null. - Unique constraint:
@@unique([repoId, issueSource, issueNumber])prevents duplicate workers per issue. - Override fields shadow global config for a single worker; they are copied from IssueConfig at claim time.
WorkerReport
WorkerReport
Post-completion summary of a worker's run: files changed, cost, token counts, and the git SHA at which the implementation gate ran.
| Field | Type | Constraints | Description |
|---|---|---|---|
workerId | String | @id FK → Worker | One-to-one with Worker (workerId is the PK) |
summary | String? | optional | Human-readable summary text |
filesChanged | String | required | JSON-encoded FileChange[] array |
durationMs | Int? | optional | Total wall-clock duration of the run |
costUsd | Float? | optional | Total USD cost of Claude API calls |
inputTokens | Int? | optional | Total input tokens consumed |
outputTokens | Int? | optional | Total output tokens produced |
cacheReadTokens | Int? | optional | Prompt-cache read tokens |
cacheCreationTokens | Int? | optional | Prompt-cache creation tokens |
numTurns | Int? | optional | Number of agent turns |
implementGateSha | String? | optional | Git SHA when implementation gate passed |
upsertWorkerReportallows the report to be written incrementally (summary may arrive separately from stats).filesChangedis stored as a JSON string and decoded at the application layer intoFileChange[].
Run
Run
Represents a single agent invocation (one harness execution -- claude, codex, or copilot). A Worker may have multiple Runs: initial implement, then a CI-fix run, then a verification run, and so on.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | String | @id @default(cuid()) | Unique run identifier |
kind | String | required | Run type — one of RunKind values |
target | String? | optional | Human-readable description of what is being run |
status | String | required | "running", "completed", or "failed" |
workerId | String? | optional | FK → Worker (null for repo-level runs) |
repoId | String? | optional | FK → Repo scope |
sessionId | String? | optional | Claude session ID |
model | String? | optional | Model used for this run |
effort | String? | optional | Effort level setting |
costUsd | Float? | optional | USD cost of this run |
inputTokens | Int? | optional | Input tokens consumed |
outputTokens | Int? | optional | Output tokens produced |
durationMs | Int? | optional | Wall-clock duration (ms) |
numTurns | Int? | optional | Number of agent turns |
lastError | String? | optional | Error message if status is "failed" |
report | String? | optional | Free-form report text from the run |
failInterruptedRunsbulk-updates allstatus = "running"rows to"failed"on daemon startup, cleaning up runs interrupted by a crash.- The
workerrelation usesonDelete: SetNullso Run rows survive worker deletion. - Indexes:
@@index([status]),@@index([workerId]),@@index([repoId, kind, status]).
Event
Event
Append-only log of structured events. Every meaningful state change, log line, or error from a Worker or Run is recorded here. Used for the live console stream and the event history view.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | Int | @id @default(autoincrement()) | Monotonically increasing row ID |
workerId | String? | optional | FK → Worker (mutually exclusive with runId) |
runId | String? | optional | FK → Run (mutually exclusive with workerId) |
type | String | required | Event type string (e.g. "worker.state_changed") |
message | String | required | Human-readable event message |
level | String | @default("info") | Log level: "error", "warn", or "info" |
createdAt | DateTime | required | Event timestamp (set by caller, not @default) |
rawContent | String? | optional | Raw agent output content (large; may be null) |
- Auto-increment
idprovides a secondary sort key so events with identicalcreatedAttimestamps have a deterministic order. EventOwner = { workerId: string } | { runId: string }— every event must belong to exactly one owner.- Queries are ordered by
[createdAt, id]for stable chronological listing.
Config
Config
Key-value store for global application settings. All configuration (GitHub token, model, merge mode, etc.) lives here. Keys are defined in CONFIG_KEYS (49 string keys), exported from config.repo.ts.
| Field | Type | Constraints | Description |
|---|---|---|---|
key | String | @id | Configuration key (one of CONFIG_KEYS) |
value | String | required | Configuration value (always stored as string) |
updatedAt | DateTime | @updatedAt | Auto-updated on every write |
setConfigandsetConfigBatchboth use upsert so callers never need to know whether a key already exists.deleteAllConfighas nowhereclause and is protected by the bulk-delete middleware in production.- 49 CONFIG_KEYS cover: GitHub token, agent model/effort, merge/review/address modes, poll intervals, concurrency limits, timeouts, and more.
IssueConfig
IssueConfig
Per-issue overrides for a watched repo. Allows individual issues to use a different model, effort level, merge mode, etc. than the global config. Also stores story-point estimates.
| Field | Type | Constraints | Description |
|---|---|---|---|
repoId | String | PK component | Owning repo slug |
issueNumber | Int | PK component | GitHub or internal issue number |
agentModelOverride | String? | optional | Override agent model for this issue |
agentEffortOverride | String? | optional | Override effort level |
implementModeOverride | String? | optional | Override implementation mode |
autoMergeModeOverride | String? | optional | Override auto-merge behavior |
autoReviewModeOverride | String? | optional | Override auto-review behavior |
autoAddressModeOverride | String? | optional | Override auto-address behavior |
acceptedReviewLevelOverride | String? | optional | Override accepted review level |
pointsAutoScored | Boolean | @default(false) | Whether story points were auto-estimated |
storyPoints | Int? | optional | Story point estimate |
modelEffortFromStoryPoints | Boolean | @default(true) | Whether to derive model/effort from story points |
- Composite primary key
@@id([repoId, issueNumber])— exactly oneIssueConfigrow per issue per repo. - Override fields are copied onto the Worker row at claim time so the worker's overrides are immutable after creation.
RepoSnapshot
RepoSnapshot
Cached snapshot of a repo's GitHub state (open/closed issues, pull requests, releases, base CI status). Avoids redundant GitHub API calls; refreshed by the daemon poll loop.
| Field | Type | Constraints | Description |
|---|---|---|---|
repoId | String | @id FK → Repo | One-to-one with Repo |
data | String | required | JSON-serialized RepoSnapshot object (large blob) |
fetchedAt | DateTime | required | When this snapshot was fetched from GitHub |
- One row per repo;
saveRepoSnapshotupserts on every refresh. datais decoded at the application layer into theRepoSnapshotdomain type.
Other Models
Supporting models for the ready queue, internal issues, labels, and batch imports.
ReadyOrder
Defines the priority ordering for issues in the ready queue. The daemon consumes issues in ascending position order. replaceReadyOrder atomically swaps all positions for a repo in a single transaction.
Composite PK: (repoId, issueSource, issueNumber)
ReadyIssue
Set of issues approved for the daemon to pick up. Separate from ReadyOrder so presence (ready or not) is independent of ordering. addReadyIssue uses upsert with update: {} — idempotent on conflict.
Composite PK: (repoId, issueSource, issueNumber)
InternalIssue
Issues created and managed inside Slop, not sourced from GitHub. Supports the same worker lifecycle. Labels stored as JSON array string; sequential number auto-assigned per repo inside a transaction.
Fields: id, repoId, number, title, body, labels (JSON), state, createdAt, updatedAt
Label
Label definitions for a repo. Mirrors GitHub label concepts (name, color, description) but stored locally. LIFECYCLE_LABELS in src/types/label.ts defines standard labels: refined, wip, reviewing, refining, reviewed, bug.
Unique: (repoId, name)
Batch
A named group of issues that can be imported together in one action — e.g. from a PRD. Status is "open" or "archived". Relations: issues BatchIssue[].
Fields: id, name, repoId, status, createdAt
BatchIssue
Join table linking a Batch to a specific issue by number and source. Does not foreign-key into Worker or InternalIssue directly. Unique on (batchId, source, issueNumber).
Fields: id, batchId, source, issueNumber, repoId (denormalized)
Repository Pattern
All database access goes through files in src/db/*.repo.ts. Four inviolable rules:
Pure Prisma passthroughs
Repository functions call the Prisma client and return its result. No business logic, no conditional branching on values, no transformations beyond what is stated in the function signature.
No try/catch
Errors propagate to callers. The daemon and server actions decide what to do with database errors; repositories never catch or swallow them.
One concern per file
Each file is named after the primary model it owns (e.g. worker.repo.ts owns Worker). Cross-model transactions are allowed within one file when they are atomically related.
Domain types where needed
Some repos return typed domain objects instead of raw Prisma rows. internal-issue.repo.ts and label.repo.ts parse stored JSON and return types from src/types/.
Worker Status Lifecycle
17 statuses. A Worker moves through them as the daemon drives it forward. The lifecycle is linear in the common path but has branches for CI failures, review cycles, and conflicts.
lastError contains the reasonstatusBeforePause records where to resumeTERMINAL_STATUSES): merged, failed, cancelled. Workers in these states are eligible for archiving via archiveFinishedWorkers and will not be picked up by the daemon.Reconciliation:
reconcileStatus({ status, issueState, prState }) computes the display badge. GitHub state is authoritative for display when the issue is closed or the PR is closed without merging, except that "reporting" always wins.