Slop Docs Home

Data Models

SQLite via Prisma. All data scoped to watched repos. 13 models, one repository file per concern.

SQLite Prisma Multi-repo scoped Pure passthroughs

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.

EnvironmentFileNotes
Productionslop.dbDefault path
Developmentslop_dev.dbDev environment
Testslop_test.dbReset by vitest.global-setup.ts
Bulk-delete middleware: A 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.

FieldTypeConstraintsDescription
idString@idowner/name slug — primary key and repo identifier
pathStringrequiredAbsolute filesystem path to the local checkout
baseBranchString?optionalOverride for the default branch (e.g. main)
parallelismCapInt?optionalMax concurrent workers for this repo
removedAtDateTime?optionalSoft-delete timestamp; non-null means the repo is inactive
createdAtDateTime@default(now())Row creation time
  • Soft delete via removedAtlistRepos() filters removedAt = null; listAllRepos() returns everything.
  • The id field 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.

FieldTypeConstraintsDescription
idString@id @default(cuid())Unique worker identifier
repoIdStringFK → RepoOwning repo slug
issueNumberIntrequiredGitHub or internal issue number
issueTitleStringrequiredIssue title at claim time
issueSourceString@default("github")"github" or "internal"
statusStringrequiredCurrent lifecycle status
worktreePathStringrequiredAbsolute path to the git worktree
prNumberInt?optionalGitHub PR number once created
conflictAttemptsInt@default(0)Number of conflict-resolution attempts
ciAttemptsInt@default(0)Number of CI-fix attempts
verificationAttemptsInt@default(0)Number of verification attempts
reviewAttemptsInt@default(0)Number of review attempts
agentPidInt?optionalOS PID of the running agent process
sessionIdString?optionalClaude session ID for resume
statusBeforeReportString?optionalStatus saved before entering the reporting phase
statusBeforePauseString?optionalStatus saved before pausing
archivedAtDateTime?optionalSoft-archive timestamp for terminal workers
agentModelOverrideString?optionalPer-worker model override
agentEffortOverrideString?optionalPer-worker effort level override
autoMergeModeOverrideString?optionalOverride for auto-merge behavior
autoReviewModeOverrideString?optionalOverride for auto-review behavior
createdAtDateTime@default(now())Row creation time
updatedAtDateTime@updatedAtAuto-updated on every write
  • CAS updates: updateWorkerStatusFrom and reclaimTerminalWorker use updateMany with a where clause that includes the expected current status, returning count > 0 to detect races.
  • Soft archive: terminal workers are not deleted — archiveFinishedWorkers sets archivedAt; listWorkers filters archivedAt = 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.

FieldTypeConstraintsDescription
workerIdString@id FK → WorkerOne-to-one with Worker (workerId is the PK)
summaryString?optionalHuman-readable summary text
filesChangedStringrequiredJSON-encoded FileChange[] array
durationMsInt?optionalTotal wall-clock duration of the run
costUsdFloat?optionalTotal USD cost of Claude API calls
inputTokensInt?optionalTotal input tokens consumed
outputTokensInt?optionalTotal output tokens produced
cacheReadTokensInt?optionalPrompt-cache read tokens
cacheCreationTokensInt?optionalPrompt-cache creation tokens
numTurnsInt?optionalNumber of agent turns
implementGateShaString?optionalGit SHA when implementation gate passed
  • upsertWorkerReport allows the report to be written incrementally (summary may arrive separately from stats).
  • filesChanged is stored as a JSON string and decoded at the application layer into FileChange[].

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.

FieldTypeConstraintsDescription
idString@id @default(cuid())Unique run identifier
kindStringrequiredRun type — one of RunKind values
targetString?optionalHuman-readable description of what is being run
statusStringrequired"running", "completed", or "failed"
workerIdString?optionalFK → Worker (null for repo-level runs)
repoIdString?optionalFK → Repo scope
sessionIdString?optionalClaude session ID
modelString?optionalModel used for this run
effortString?optionalEffort level setting
costUsdFloat?optionalUSD cost of this run
inputTokensInt?optionalInput tokens consumed
outputTokensInt?optionalOutput tokens produced
durationMsInt?optionalWall-clock duration (ms)
numTurnsInt?optionalNumber of agent turns
lastErrorString?optionalError message if status is "failed"
reportString?optionalFree-form report text from the run
  • failInterruptedRuns bulk-updates all status = "running" rows to "failed" on daemon startup, cleaning up runs interrupted by a crash.
  • The worker relation uses onDelete: SetNull so 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.

FieldTypeConstraintsDescription
idInt@id @default(autoincrement())Monotonically increasing row ID
workerIdString?optionalFK → Worker (mutually exclusive with runId)
runIdString?optionalFK → Run (mutually exclusive with workerId)
typeStringrequiredEvent type string (e.g. "worker.state_changed")
messageStringrequiredHuman-readable event message
levelString@default("info")Log level: "error", "warn", or "info"
createdAtDateTimerequiredEvent timestamp (set by caller, not @default)
rawContentString?optionalRaw agent output content (large; may be null)
  • Auto-increment id provides a secondary sort key so events with identical createdAt timestamps 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.

FieldTypeConstraintsDescription
keyString@idConfiguration key (one of CONFIG_KEYS)
valueStringrequiredConfiguration value (always stored as string)
updatedAtDateTime@updatedAtAuto-updated on every write
  • setConfig and setConfigBatch both use upsert so callers never need to know whether a key already exists.
  • deleteAllConfig has no where clause 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.

FieldTypeConstraintsDescription
repoIdStringPK componentOwning repo slug
issueNumberIntPK componentGitHub or internal issue number
agentModelOverrideString?optionalOverride agent model for this issue
agentEffortOverrideString?optionalOverride effort level
implementModeOverrideString?optionalOverride implementation mode
autoMergeModeOverrideString?optionalOverride auto-merge behavior
autoReviewModeOverrideString?optionalOverride auto-review behavior
autoAddressModeOverrideString?optionalOverride auto-address behavior
acceptedReviewLevelOverrideString?optionalOverride accepted review level
pointsAutoScoredBoolean@default(false)Whether story points were auto-estimated
storyPointsInt?optionalStory point estimate
modelEffortFromStoryPointsBoolean@default(true)Whether to derive model/effort from story points
  • Composite primary key @@id([repoId, issueNumber]) — exactly one IssueConfig row 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.

FieldTypeConstraintsDescription
repoIdString@id FK → RepoOne-to-one with Repo
dataStringrequiredJSON-serialized RepoSnapshot object (large blob)
fetchedAtDateTimerequiredWhen this snapshot was fetched from GitHub
  • One row per repo; saveRepoSnapshot upserts on every refresh.
  • data is decoded at the application layer into the RepoSnapshot domain 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:

1

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.

2

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.

3

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.

4

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/.

Shared Types

TypeSource FileDescription
WorkerStatussrc/types/worker.tsUnion of all 17 valid status strings, derived from WORKER_STATUSES const tuple
TERMINAL_STATUSESsrc/types/worker.tsConst: ["merged", "failed", "cancelled"]
reconcileStatussrc/types/worker.tsComputes display badge variant from worker status + GitHub states
RunKindsrc/types/run.ts"implement" | "verify" | "conflict" | "ci-fix" | "pr_review" | "skill" and more
RunStatussrc/types/run.ts"running" | "completed" | "failed"
FileChangesrc/types/worker-report.ts{ path: string; op: "create" | "edit" | "delete" }
WorkerReportViewsrc/types/worker-report.tsAssembled view: { summary, filesChanged, stats, steps, phases }
InternalIssuesrc/types/internal-issue.tsDomain type with labels: string[] (decoded from DB JSON string)
RepoSnapshotsrc/types/repo-snapshot.tsFull cached snapshot: { repo, fetchedAt, openIssues, closedIssues, pullRequests, releases, baseCi }
CiStatussrc/types/repo-snapshot.ts"passing" | "failing" | "pending" | "none"
Labelsrc/types/label.ts{ id, repoId, name, color, description, createdAt, updatedAt }
LIFECYCLE_LABELSsrc/types/label.tsStandard label definitions: refined, wip, reviewing, refining, reviewed, bug
AgentRowsrc/types/agent.tsFlattened Run row for the Agents list UI
BatchOptionsrc/types/batch.ts{ id, name, issueCount } for dropdowns
EventOwnersrc/db/event.repo.ts{ workerId: string } | { runId: string }

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.

claimed
Worker row created; agent not yet started
implementing
Agent is running, writing code
verifying
Verification run in progress (post-implementation check)
waiting_ci
PR created; waiting for CI checks to pass
fixing_ci
CI failed; CI-fix agent is running
waiting_review
PR ready; waiting for automated review to start
in_review
PR review agent is running
waiting_address
Review comments filed; waiting for address run to start
in_address
Address-comments agent is running
waiting_merge
PR approved and CI passing; waiting for merge slot
resolving_conflict
Merge conflict detected; conflict-resolution agent running
merging
Merge API call in flight
reporting
Post-merge report generation in progress
merged
Terminal: PR successfully merged
failed
Terminal: unrecoverable error; lastError contains the reason
cancelled
Terminal: manually cancelled by the user
paused
Suspended by user; statusBeforePause records where to resume
Terminal statuses (TERMINAL_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.