Skip to content

feat(images): every picture is queued and delivered in the background, so a slow render can no longer end an agent turn (Refs #799, #938) - #970

Merged
alex-mextner merged 3 commits into
mainfrom
fix/async-images-20261101
Nov 5, 2026
Merged

alex-mextner merged 3 commits into
mainfrom
fix/async-images-20261101

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What

The AI picture tools, /today, /tomorrow, /week, /month, the agenda buttons and the schedule comparison of an accepted invitation now only enqueue a picture and return at once. ImageDelivery renders through BullMQ with a 150 s bound, sends the PNG as its own message and logs one outcome per picture. Durable rows (image_deliveries, migration 070, in the first commit) let a restart resume, expire or mark unknown each picture without resending one.

Why: on 2026-11-01 a render of 73-80 s inside the agent turn used the whole 90 s budget and the user got an aborted-run notice instead of the finished text.

Refs #799, #938. Known gaps and deferred review findings: #964, #967, #969. Full-roster review debt: #961.

Evidence

…elivery (Refs #799)

Asynchronous AI pictures need a durable record of how far each one got, so
the next process can finish, expire or mark it unknown after a crash instead
of losing it or sending it twice. Migration 070 adds image_deliveries, and
ImageDeliveryRepository sits over it: a deterministic id, a forward-only
advance() that is the single claim on a send, a finish() that never ends a
claimed upload as expired or superseded, a note flag claimed once, and a
query for failed/expired rows whose user was never told.

The row also keeps the picture's slot, a hash of its render data and a
rebuild counter. A picture is rebuilt when the calendar changes while it
renders; rebuilt() records that before the new render starts, so a restart
in between resumes the rebuilt job and not the stale one that already
finished, and a delivery resumed after a restart is deduplicated or
superseded like a live one.

The migration was written as 068 on its branch and renumbered to 070 before
it was ever released: main took 069 (chat_history.turn_order) meanwhile, and
no stage or production database has applied the old name.
Docs: docs/reference/migrations/070_image_deliveries.md.

Review: single-seat Opus via omp (--pool 1), two rounds, because the Sol seat
has no quota until 2026-11-09; the full-roster review is owed in #961. The
first round's Medium (a restart during a rebuild sent the stale picture) is
fixed here by the rebuilds column and rebuilt(); the second round found only
Low documentation inaccuracies (retention wording, the slot comment, the
rollback note for rows stuck in sending), corrected in the next commit.
…, so a slow render can no longer end an agent turn (Refs #799, #938)

On 2026-11-01 the AI picture tools waited for the render inside the agent turn. On the loaded odroid a real render took 73-80 s, two waits plus the model rounds spent the whole 90 s turn budget, the answer validator timed out and the user got an aborted-run notice instead of the text the model had already written. The picture tools, /today, /tomorrow, /week, /month, the agenda buttons and the schedule comparison of an accepted invitation now only enqueue and return at once. ImageDelivery renders through BullMQ with a 150 s bound, sends the PNG as its own message and logs exactly one outcome per picture; every end the user must hear about gets one short note in their language (failed, timed out, too large, refused for good without an offer to ask again, upload cut off: may not have arrived, never sent again).

A restart reads the durable image_deliveries rows (migration 070, repository from the previous commit): it resumes pictures still within their bound, tells the user once that the bot was restarting for the ones past it without a timeout alert, and marks an interrupted upload unknown without resending it. Limits: 2 pictures per chat and 12 on the bot; schedule comparisons have their own 4 per chat and 6 on the bot. A newer picture of the same slot is recorded superseded at the moment of replacement. An end the store refuses is written again three times and once more at shutdown. Permanent Telegram refusals raise no alert.

Runbook, migration doc and CLAUDE.md describe the flow, the limits and the known gaps (#964, #967, #969). Version 0.3.6.
@alex-mextner
alex-mextner merged commit 78b6343 into main Nov 5, 2026
12 checks passed
@alex-mextner
alex-mextner deleted the fix/async-images-20261101 branch November 5, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant