Task PATCH requests and server-side updateTaskForUser calls enter an Upstash
Redis sorted-set queue per task, scored by the original action timestamp.
Lua atomically stores the payload and adds it to the pending-task index.
The request drains the queue and returns the current task after processing.
Workers peek, apply in PostgreSQL, then acknowledge in Redis; they never remove
work before committing it. PostgreSQL stores operation receipts and per-field
ordering history so concurrent workers, delayed delivery, and crash retries
cannot overwrite newer fields. Equal timestamps use the operation UUID.
No durable workflow runs per change. If storing in Redis fails, nothing was
accepted and the request errors; the client retries with the same operationId.
If the request fails or dies after enqueueing, the change stays queued and is
drained by the next change to that task, or by the /api/cron/task-changes
Vercel Cron sweeper (every 5 minutes, CRON_SECRET), which drains tasks whose
pending entry is older than one minute.
Notion auto-sync pushes are debounced per project with Redis and after()
(lib/services/debounce.ts): each request marks debounce:v1:board-push:<id>,
waits 3 seconds, and pushes only if it made the latest mark, or if the burst
has lasted 30 seconds. Draining never pushes on its own; the sweeper pushes each
affected project once per run. Without Redis the push runs immediately. Redis configuration is required:
UPSTASH_REDIS_URL and
UPSTASH_REDIS_TOKEN. There is no direct-write fallback when Redis is unavailable.
Pending payloads have no expiry; database or Redis failures leave work queued.
Use a durable, non-evicting Redis deployment. Access is checked again before
applying queued work. Revoked access and deleted tasks are discarded.
{
"status": "done",
"actionAt": "2026-10-27T05:00:00.000Z",
"operationId": "ce3dc4ef-a1c1-4736-956b-fca240f34fbd"
}Capture actionAt when the user acts, not when uploading or retrying. Both
metadata fields must be supplied together. Timestamps more than five minutes in
the future are rejected; device clocks still need to be accurate. Equal timestamps
use the lexically greater normalized operation UUID. Retries are deduplicated;
reusing an ID with different fields or time returns HTTP 409.
Each field uses timestamp ordering. An older title edit can apply after a newer status change; an older status change cannot undo a newer status change. Send only changed fields to preserve independent edits. Status changes without an explicit sortIndex include an implicit position in the same ordering rules. The returned updatedAt never decreases. The database receipt journal records received timestamps, original payloads, and applied fields, and is removed when the task is deleted. Retain receipt history while clients may retry or upload delayed operations.
Legacy callers without metadata use receipt order. Existing RxCode clients do not yet send this metadata, so backend support alone does not provide device action ordering. Creation, deletion, stories, Notion imports, and run claims are outside this PATCH queue; the existing atomic first-device claim stays intact.
Deploy migration 0002_task_change_queue before deploying code that calls it. No production migration or deployment is performed by adding these files.