Pause, resume and stop, as one variant carrying a verb rather than three
near-identical ones - matching the gateway's `PrintControl`, which exists so
there is one method across four vendor implementations instead of three.
Confirmed live on a P1P (2026-10-11), which is also where the two warnings
in the description come from. A stop is a cancel and is not undoable: the
printer reports the cancelled print as FAILED. And `result: "success"` from
the printer means the command was accepted, not that anything changed - all
three verbs answered success while it sat IDLE with nothing to pause - so a
caller that needs to know the state changed has to read telemetry back.
The enum is `pause|resume|stop` and deliberately not `cancel`, even though
`cancel` is Moonraker's word and the Klipper implementation behind this does
send a cancel. A verb only one vendor recognises must not validate on the
wire.
The "no start a print variant" note stays, with its reason corrected:
starting an uploaded file has now been confirmed by hand, so what is missing
is the path around it - nothing uploads a sliced file to a gateway and
nothing decides which printer runs which job.
`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`.
Two cleanups, one cause each.
The root tsconfig.json carried the compilerOptions directly with no `include`,
which made `tsc -b` treat the whole repository as one project: it ignored each
package's `outDir` and emitted .js/.d.ts next to every source file, which then
got committed. So every typecheck dirtied the tree and the build output was in
git. The root is now a solution file that only references the real projects,
shared options moved to tsconfig.base.json, and the six emitted files are
removed from the index. Output goes to packages/ts-types/dist, which was
already gitignored.
@sinclair/typebox is now pinned exactly rather than by caret range. It is
load-bearing in a way a range should not be allowed near: continuum-backend's
Elysia requires it at runtime without declaring it as a dependency at all, and
@continuum/ts-types resolves its own copy from here, so two different versions
mean two structurally incompatible TSchema types and nothing from this package
validates anything there.
Worth recording what that looked like, because the error pointed elsewhere.
Elysia accepts `TSchema | StandardSchemaV1Like`; when the TSchema branch failed
on the version skew, TypeScript reported the *other* branch's failure,
`'~standard' is missing`. That reads as "Elysia wants a Standard Schema
validator, TypeBox is wrong here", which is not true - Elysia is TypeBox-native
and Standard Schema is just the alternative for Zod/Valibot users. The fix was
only ever to make the versions match.
0.34.52 rather than 0.33.x because Elysia 1.4 calls `t.Module`, which 0.34
introduced. continuum-backend pins the identical version and
tests/dependency_identity.test.ts there fails if the two ever drift again.
continuum-proxy and continuum-backend each had their own idea of what the
edge WebSocket carried, and the two did not overlap anywhere: the backend
expected an `auth` handshake the gateway never sends, telemetry shaped one
printer per message where the gateway sends a batch, and UUID printer ids a
gateway has no way to know. Neither end had a command path that matched the
other. Nothing failed, because nothing was written down for both sides to
mirror. This is that document.
schemas/continuum/v1/edge/ is canonical: client_message, server_message,
printer_telemetry and printer_command. packages/ts-types gets its first
real TypeBox mirror - the placeholder there said to add one "once something
actually consumes it", and continuum-backend's edge route now does. The
Rust mirror stays in continuum-proxy/src/uplink/protocol.rs rather than
packages/rust-types, since the gateway is its only Rust consumer.
Two decisions worth recording:
Field names are snake_case, unlike the backend's HTTP routes. The other end
is serde-derived Rust, and snake_case is what those structs produce without
a rename attribute on every field. It is also what the rest of this
repository already uses.
Optional fields accept an explicit null as well as being absent. A gateway
omits what it has no value for, which is what these schemas describe - but
serde emits null by default unless every field is annotated, and a single
such field would otherwise fail validation and take a whole farm's
telemetry batch down with it. Strict in what we send, liberal in what we
accept; the contract tests on both sides pin down each half.
Also bumps this package's @sinclair/typebox to ^0.33.12 to match what
continuum-backend already pins. It was on ^0.32.0, which resolved to a
separate 0.32.37 install - two TypeBox versions means two incompatible
TSchema identities, and Elysia rejected every schema from here on those
grounds alone.
Continuum is intended to be sold, so every dependency has to permit
distribution in closed-source commercial software. Nothing recorded that
before: our own crates and npm packages carried no licence field at all,
which made `cargo deny` report them as `unlicensed` errors and left the
actual policy nowhere.
Crates now declare `LicenseRef-Proprietary` with `publish = false`.
That string is deliberately not a real SPDX identifier, so it must never
appear in a `deny.toml` allow list - a third-party crate claiming it
would be a genuine finding. npm packages use `"license": "UNLICENSED"`,
the equivalent convention there.
`deny.toml` holds the permissive-only allow list. The policy itself, the
audit, and the three categories cargo-deny cannot see (external
binaries, ML model weights, self-hosted servers) are documented in
continuum-common's docs/LICENSES.md.
They described the old flat continuum/v1 shape, replaced by the
material/printer/vendor restructure. Nothing outside this repo imported
them (checked continuum-proxy, continuum-ai-worker, continuum-backend,
continuum-app). Left both packages in place as empty shells - add a
hand-written mirror back per new schema file once something actually
consumes it.
Nothing imported the proto-generated code (zero references to
continuum-rust-types or @continuum/ts-types's proto export in any
consumer repo), and protobuf's real value - one schema compiled to a
compact binary wire format for both languages - never applied: BE<->proxy
and FE<->BE are both JSON. Also fixes 8 broken $ref paths in the bambulab
schemas and a root tsconfig.json that was sweeping the gitignored
examples/ directory into typecheck.
- proto/continuum/v1/*.proto -> schemas/continuum/v1/*.schema.json
- packages/rust-types: hand-written serde structs instead of
prost-build codegen
- packages/ts-types: drop the generated/ proto barrel export, keep the
hand-written TypeBox schemas as the TS source of truth
- root package.json: drop build:ts, protoc, ts-proto
protoc warned that print_job.proto imported template.proto without using it
- removed. ts-types/package.json was missing protobufjs and long, which the
ts-proto-generated code imports at runtime - tsc failed with 'cannot find
module'. src/index.ts also re-exports './generated' as a namespace, but
ts-proto wasn't configured to emit an index barrel file for it - added
outputIndex=true to the protoc invocation.
Verified with a clean 'bun run build' (both TS and Rust codegen) and
'tsc -b'.
Protobuf data model (Template/PrintJob/MaterialProfile/PrintParameters)
with codegen targets for TypeBox+TS types (packages/ts-types) and
prost-derived Rust structs (packages/rust-types).