octoturgeandClaude Opus 5 ae986f371a Make the frame consumer correct, and stop it inventing failures
Six bugs, found while writing down the `stream:camera_frames` contract
this worker is one end of.

**It never read the task.** It pulled one field, `printer_id`, from a
producer writing `printerId`, and substituted the literal string
"unknown" when that missed - which was always. Every other field,
including `r2_object_key`, the only thing saying *which picture*, was
never looked at. Nothing errored: a stream entry is a flat string map,
and asking it for an absent field is not a failure. `src/task.rs` is now
the Rust mirror of
`continuum-schemas/schemas/continuum/v1/ai/frame_task.schema.json` and a
missing field is an error, not a default. Its tests include the
regression directly: a camelCase entry must fail to parse rather than
yield defaults.

**It invented print failures.** `inference::detect` returned a fixed
`spaghetti` at 0.91 confidence for every input, including the empty
slice it was always handed, so a running worker declared a failure for
every frame of every print on evidence it had never looked at. Harmless
only because nothing subscribes to the alert channel - the moment
anything does, it is a false-alarm generator pointed at an operator. It
now abstains. An unimplemented model returning "nothing wrong" is a safe
default; one returning "failure" is not.

**It discarded the backlog.** The consumer group was created at `$`,
meaning "only entries added from now on", so a worker that was down
while a farm printed came back up and silently skipped every frame it
had missed. For a failure detector that is the opposite of the job. Now
`0`.

**It stranded entries forever.** An entry delivered to a consumer that
died before acking stays pending: it has been handed out, so no other
consumer is offered it, and nothing retried it. Now reclaimed via
`XPENDING`/`XCLAIM` once idle past `CONTINUUM_CLAIM_IDLE_MS`, and
dead-lettered onto `stream:camera_frames:dead` - with the reason and the
original fields, so it can be inspected and replayed - once it has been
delivered `CONTINUUM_MAX_DELIVERIES` times, so a poison entry can
neither be retried forever nor block the queue.

**One bad moment killed it.** `xread_options(...).await?` and
`reporter.report(...).await?` both propagated out of the loop, so a
single transient Valkey error ended the process. Reads now retry with a
growing delay and give up only after ten consecutive failures; a report
failure leaves the entry unacknowledged for redelivery rather than
taking the worker down with it. The `XACK` result is no longer discarded
with `let _: RedisResult<i64>`.

**`report` would panic** on `detections[0]` for an empty slice. The one
caller checks, which makes it a trap for the next one rather than a
live bug; it now picks the highest-confidence detection and returns
early on none.

Also corrects two comments that were simply false: `reporter` claimed
`channel:print_alerts` is "what continuum-backend's WebSocket fan-out
subscribes to" - nothing anywhere subscribes to it - and the README
advertised the fabricated detection as a deliberate aid to
understanding the pipeline. The README now states plainly what is real
(the loop, tested) and what is not (no frame fetch, no model, no
subscriber, no persistence), because "the plumbing works and detects
nothing" is the useful thing to know before enabling this.

Verified: 10 unit tests, zero clippy warnings, and
`continuum-backend/scripts/e2e-ai-frames.sh` - the real producer against
this real binary over real Valkey - asserting a well-formed task is
consumed and acked, a legacy camelCase entry is dead-lettered naming the
field it lacks, an entry stranded on a dead consumer is reclaimed, and
nothing is published to the alert channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-11 16:59:18 +00:00

continuum-ai-worker

Computer-vision failure detection worker for the Continuum print farm platform. Consumes camera frame tasks from the Valkey Stream stream:camera_frames (written by continuum-backend's src/queue/) and reports failures.

What is real and what is not

The consumer-group loop is real and robust: it reads, parses, recovers entries stranded by a dead consumer, dead-letters what it cannot process, and acknowledges. The detection is not.

  • src/inference/mod.rs abstains. It has no model and it is handed no frame, so it returns no detections. It used to return a fixed spaghetti at 0.91 confidence for every call - including the empty slice it was always given - which made a running worker a false-alarm generator declaring a failure for every frame of every print, on evidence it had never looked at. A stub that returns "nothing wrong" is a safe default; a stub that returns "failure" is not.
  • Nothing fetches the frame. FrameTask::r2_object_key says where the JPEG is in R2 and no code downloads it. This is the next piece of work and the reason inference cannot do anything yet.
  • src/reporter/mod.rs publishes a real PrintAlert to channel:print_alerts, but nothing subscribes to that channel, and PUBLISH has no persistence - so an alert published today goes nowhere. The fan-out is a continuum-backend route that does not exist yet.
  • Nothing is persisted. A detection's permanent record belongs in Postgres; frame_embeddings exists for the vectors, with a pgvector ivfflat index, and has never been written to.

In short: the plumbing works and is tested; the worker does not yet detect anything. Do not enable it expecting alerts, and do not wire a console to channel:print_alerts expecting the alerts to mean something.

The contract

stream:camera_frames and channel:print_alerts are the only interface between this worker and continuum-backend. Both are specified in continuum-schemas/schemas/continuum/v1/ai/, which is canonical:

  • frame_task.schema.json - one stream entry. A flat map of strings: a stream entry has no nesting, no numbers and no null, which is why a field with no value is omitted from the XADD rather than written as "".
  • print_alert.schema.json - the JSON payload of a PUBLISH.

src/task.rs is the Rust mirror; ts-types/src/schemas/ai.ts is the TypeScript one. Change the JSON Schema first, then both mirrors.

This is written down because it drifted when it was not: the backend wrote printerId, farmId, r2ObjectKey and capturedAt, and this worker read a single field called printer_id and ignored the rest. Every task was delivered, acknowledged and attributed to a printer called "unknown", and nothing errored - asking a string map for a field that is not there is not a failure. Both ends now have a contract test, and continuum-backend/scripts/e2e-ai-frames.sh runs the real producer against this real binary over real Valkey.

Delivery semantics

situation what happens
a well-formed task scored, then XACKed
a malformed task copied to stream:camera_frames:dead with a reason, then XACKed - it will never become valid, and retrying it would block the queue
reporting fails left unacknowledged, reclaimed after CONTINUUM_CLAIM_IDLE_MS
a consumer dies mid-task its pending entries are reclaimed by XPENDING/XCLAIM on the next pass
a task fails CONTINUUM_MAX_DELIVERIES times dead-lettered, so a poison entry cannot be redelivered forever
Valkey blips logged and retried with a growing delay; the worker exits only after MAX_CONSECUTIVE_FAILURES consecutive read failures

The consumer group is created at 0, not $: $ means "only entries added from now on", so a worker that was down while a farm printed would come back up and silently skip every frame it missed. For a failure detector, dropping the backlog is the opposite of the job.

Getting started

cp .env.example .env
cargo run
cargo test                                      # the task-parsing contract
../continuum-backend/scripts/e2e-ai-frames.sh   # against real Valkey

Configuration

variable default meaning
CONTINUUM_VALKEY_URL redis://localhost:6379 RESP server (Valkey, not Redis - see continuum-common/docs/LICENSES.md)
CONTINUUM_FRAME_STREAM stream:camera_frames the work queue
CONTINUUM_DEAD_LETTER_STREAM stream:camera_frames:dead where unprocessable entries go
CONTINUUM_CONSUMER_GROUP ai-worker consumer group name
CONTINUUM_CONSUMER_NAME ai-worker-1 must be unique per process
CONTINUUM_BLOCK_MS 5000 how long XREADGROUP blocks
CONTINUUM_CLAIM_IDLE_MS 60000 how long before a pending entry may be reclaimed; must exceed the time to process one frame
CONTINUUM_MAX_DELIVERIES 5 deliveries before dead-lettering

Pipeline

continuum-proxy --(frame_batch)--> continuum-backend --(XADD)--> stream:camera_frames
       |                                                                  |
  no frame capture                                            XREADGROUP (this worker)
  path yet either                                                         v
                                                              FrameTask::from_stream_fields
                                                                          v
                                                       (missing: fetch r2_object_key from R2)
                                                                          v
                                                          inference::detect -- abstains
                                                                          v
                                       reporter::report -> channel:print_alerts (no subscriber)

Both ends of this pipeline are stubs: continuum-proxy restreams cameras through go2rtc and has no frame-capture path, so nothing sends frame_batch and the stream is empty in practice. The middle is what this repo tests.

Structure

src/
  main.rs          Consumer-group loop: recover -> read -> parse -> score -> settle
  task.rs          FrameTask: the Rust mirror of the frame_task schema
  config.rs        Settings from environment variables
  inference/       Detection - abstains; real ONNX Runtime inference goes here
  reporter/        Valkey pub/sub alert (real, but unsubscribed)
S
Description
Continuum platform: continuum-ai-worker
Readme
76 KiB
Languages
Rust 100%