Writing a resource held the request open for as long as it took: a library search, one or two long model calls, a review, then figures. Measured end to end that was six minutes on 2026-09-16 (00:40 to 00:46:16, resource 53), and Firefox abandons a request that has said nothing for five — the browser reported "NetworkError when attempting to fetch resource" while the server carried on and saved the deck anyway, so a generation that worked looked like a failure and left no status line. The request now records what was asked and answers 202; the work runs on the server as a job; the page lists what is being written, what landed and what failed, polls while anything is in flight, and reloads the library when one lands. Several can run at once, a reload loses nothing, and a boot pass marks jobs stranded by a restart as failed rather than spinning for ever. The same generation also ran with DeepSeek's thinking on, which is what made it take minutes rather than seconds: the 16,000-token write spent the whole budget reasoning and returned an empty reply (completion_tokens=16000, reasoning_chars=51573), which fired the automatic retry at four times the budget, and the 2,000-token reviews of that deck starved the same way four times over. Thinking is now off for the writing, the review of it and a revision — DeepSeek's own field, sent by the model wrapper. Other clinical routes are deliberately untouched and keep the provider default. The review inherits the writer's rule rather than hard-coding it, so a task that wants reasoning can still ask. Migration 1781500000000_resource-jobs.js adds user_resource_jobs; the container entrypoint applies it before the app starts.
30 lines
1.3 KiB
JavaScript
30 lines
1.3 KiB
JavaScript
// Generating a resource is a job, not a request. Writing a deck takes minutes
|
|
// — a library search, one or two long model calls, figures — and a browser
|
|
// holding a request open that long gives up on its own (Firefox at five
|
|
// minutes, exactly the "NetworkError when attempting to fetch resource" the
|
|
// user saw while the server carried on and saved the deck anyway). The request
|
|
// now records what was asked and returns at once; the work runs on the server,
|
|
// the page polls, and the library updates when a job lands. Several can run at
|
|
// once, and a reload loses nothing.
|
|
|
|
exports.up = pgm => pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS user_resource_jobs (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
topic TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
request JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
result JSONB,
|
|
error TEXT,
|
|
resource_id INTEGER REFERENCES user_resources(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
started_at TIMESTAMPTZ,
|
|
finished_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_resource_jobs_user_created ON user_resource_jobs (user_id, created_at DESC);
|
|
`);
|
|
|
|
exports.down = pgm => pgm.sql(`
|
|
DROP TABLE IF EXISTS user_resource_jobs;
|
|
`);
|