`stream:camera_frames` and `channel:print_alerts` are the only interface between `continuum-backend` and `continuum-ai-worker`, and there was nothing specifying either. They had drifted completely: the backend wrote `farmId`, `printerId`, `r2ObjectKey` and `capturedAt`; the 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, because a stream entry is a flat string map and asking it for a field that is not there is not a failure. Same shape as the /ws/edge/v1 drift, found the same way. `schemas/continuum/v1/ai/` is now canonical, with a TypeBox mirror in ts-types and a Rust mirror in the worker. Two things the schemas have to say that JSON Schema does not naturally express, both of which caused real bugs: - A Valkey stream entry is a flat field/value map of *strings*. No nesting, no numbers, no null. So FrameTask has no numeric or nested properties, and a field with no value is omitted from the XADD rather than written as `""` - an empty string is a value, and the producer was writing one for `print_job_id`, which a consumer would carry into a lookup or a uuid column. - PrintAlert, by contrast, *is* a JSON document, because PUBLISH carries one opaque string. It is also fire-and-forget with no persistence, so it can only ever be a live-console feed and never the record of a detection. The README's layout section was stale (it still listed four DTOs that were removed in 2026-09) and did not mention `edge/` or `ai/` at all. It now names both, says why the inter-service protocols live here, and notes that their Rust mirrors sit in the single repo that consumes each rather than in `packages/rust-types`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44 lines
2.7 KiB
JSON
44 lines
2.7 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/ai/frame_task.schema.json",
|
|
"title": "FrameTask",
|
|
"description": "One camera still, handed from continuum-backend to continuum-ai-worker for print-failure scoring. Produced by XADD onto the stream named below; consumed by XREADGROUP.\n\nThis is a Valkey stream entry, not a JSON document: the wire form is a flat field/value map and EVERY value is a string. That is why there are no numeric or boolean properties here and why a field with no value is omitted from the XADD rather than written as an empty string - an empty string is a value, and a consumer that treats it as one ends up looking up the object key \"\".\n\nField names are snake_case, matching every other schema in this repository. The backend's HTTP routes are camelCase; this wire is not an HTTP route, and the consumer on the other end is serde-derived Rust.",
|
|
"x-stream": "stream:camera_frames",
|
|
"x-consumer-group": "ai-worker",
|
|
"type": "object",
|
|
"properties": {
|
|
"farm_id": {
|
|
"type": "string",
|
|
"format": "uuid",
|
|
"description": "`farms.id`. Carried so a consumer can attribute load and alerts per farm without a database round trip."
|
|
},
|
|
"printer_id": {
|
|
"type": "string",
|
|
"format": "uuid",
|
|
"description": "`printers.id` - the backend's own UUID, already resolved from the gateway's printer id. NOT the gateway's `printers.toml` id, which means nothing outside its own farm."
|
|
},
|
|
"print_job_id": {
|
|
"type": "string",
|
|
"format": "uuid",
|
|
"description": "`print_jobs.id`, when the frame belongs to a tracked job. Omitted entirely when it does not - a printer may be streaming while idle, or running something queued outside Continuum. Consumers must treat an absent key and an empty value alike, and must not fabricate a job id."
|
|
},
|
|
"r2_object_key": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Where the frame actually is, in the R2 bucket. The frame bytes are deliberately NOT in the stream: a stream entry is held in memory by the server and replayed to every consumer group, and a JPEG per printer per second would make the stream the largest thing in the deployment."
|
|
},
|
|
"captured_at": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "When the camera took the frame, from the gateway's clock - not when the backend enqueued it. These can differ by a lot: a gateway buffers frames while its uplink is down, so a task may arrive minutes after the moment it describes."
|
|
}
|
|
},
|
|
"required": [
|
|
"farm_id",
|
|
"printer_id",
|
|
"r2_object_key",
|
|
"captured_at"
|
|
],
|
|
"additionalProperties": false
|
|
}
|