Skip to content
← All Posts
Build notes

SolScribe's background work now survives a crash

Durable job queues replace fragile background tasks in SolScribe, with support for PostgreSQL and SQLite.

A transcription in SolScribe can no longer get stranded in pending. Every asynchronous step in the pipeline now runs on a real job queue, and a job is written in the same transaction as the row that justifies it.

Before, creating a transcription and starting its work were two separate acts. If the process died between them, the row existed and nothing was ever going to pick it up. You would come back to a recording that looked like it was thinking, forever.

What replaced the goroutines

Transcribe, post-process and session merge each used to be a bare goroutine kicked off after the write. Recurring maintenance ran on hand-rolled tickers, one per service. Both patterns lose their work when the process stops, and neither leaves a record that it was supposed to happen.

All of it now goes through internal/jobs. On PostgreSQL the backend is River. On SQLite it is an equivalent local outbox table, because River is Postgres-only and SQLite is the default for self-hosted installs. Both honour the same contract, so the durability guarantee does not depend on which database you chose.

The recurring passes moved too: auto-cleanup, the session sweep, webhook redelivery, webhook log and outbox pruning, the brain catch-up sweep, the daily digest check and the two slow cloud maintenance passes. Intervals and behaviour are unchanged. Auto-cleanup keeps its live enable and disable toggle and its audit-gated, fail-closed deletion, which the queue schedules and never bypasses.

One ticker stayed: the RunPod poll runs every few seconds and does not belong in a durable queue.

Four settings tune it: JOBS_MAX_WORKERS, JOBS_POLL_INTERVAL, JOBS_MAX_ATTEMPTS and JOBS_RIVER_MIGRATE.

Also in this release

Models download from Settings. Each tier card now shows whether the model it resolves to is already on disk, and offers a one-click download when it is not. Progress is coarse, a handful of discrete stages rather than a percentage, and the cache state comes from the WhisperX worker’s own check. An interrupted download reads as not downloaded rather than half-there.

The model picker is three tiers, not seven checkpoint names. Fast, Balanced and Accurate replace a bare dropdown of model IDs with no descriptions. Balanced resolves to an English-only or multilingual model depending on the transcript’s language setting. Stored config now holds the tier, and every concrete model ID remains valid wherever it was already in use.

The speaker panel is one row per speaker. It used to render up to seven independent notification strips, and a single speaker could appear in three of them at once. It is now one row per speaker with a status column and an action column. Enrolled speakers stay visible instead of vanishing on save, undo lives on the row it affects, declining a merge suggestion persists, and errors attach to the row that failed.

Auto-Action Rules is gone. It never worked from the UI. The frontend and backend disagreed on field names and value shapes on every create, and score thresholds ran a 0 to 1 scale against an underlying 1 to 10 score. Webhooks and one-click tagging already cover what it was for. The action_rules table stays; only the broken feature reading it is removed.