Skip to content
Early preview — you found Guuey before launch · official launch soon

Deployments & rollback

read as .md

Every deploy of a Guuey agent — from Studio, guuey deploy, or guuey agent apply — produces a build: an immutable record that holds everything the agent runs from. This page explains what that record contains, why the hash an apply hands back really is the hash of what serves, and what the platform guarantees (and refuses to fake) when you roll forward or back. For the commands and CI wiring, see Agents as code; for what the pod itself looks like, see Concepts: hosting.

A build’s payload is its snapshot: your guuey.json, re-serialized into one canonical string with the system prompt inlined byte-exact (a { "file": "prompts/system.md" } reference is resolved on your side and the file’s bytes are embedded, so the snapshot is self-contained — no lookup back into your repo). If your app uses Guuey’s generative-UI binding, that entry is injected before the snapshot freezes, so the pod receives it the same way it receives everything else.

Canonical form is what makes hashes meaningful. The platform doesn’t store your bytes as-sent; it parses the document against the schema and re-serializes it, which emits keys in a fixed order. The same logical document therefore always produces the same string — and the same sha256 — regardless of how your editor formatted it. Two consequences you can rely on:

  • Idempotency on the reconcile path. An apply (guuey agent apply) or rollback whose assembled snapshot byte-matches the newest active build queues nothing and does not roll the pod — a no-op CI run is genuinely a no-op. guuey deploy and Studio are simple triggers: they always queue a new build, even for identical bytes.
  • A stable drift gate. deployedContentHash.snapshot in the reconcile response hashes the exact persisted string; the agentDef and systemPrompt hashes cover your submitted bytes verbatim, so your pipeline can compare them against sha256sum of the reviewed files and fail red on any mismatch.

Builds are numbered per app, monotonically: each new deploy takes the highest existing build number plus one. A build moves through queued → (building for code-mode) → deployinglive; when a newer build goes live, its predecessor is stamped superseded. failed and superseded are terminal — no workload behind them, and they never block the next deploy. The newest active build (queued, building, deploying, or live) is the reference point: it is what “unchanged” and rollback compare against, and during a rollout the old live row and the new in-flight row briefly coexist until the handover completes.

GET /v1/apps/:id/deployments keeps the full history — every build’s status, plus the snapshot hash and provenance for builds that came through agent apply or a rollback (Studio and guuey deploy builds carry null for both) — and each build has its own …/deployments/:n/status endpoint your pipeline can poll to liveness.

Row-driven serving: the hash IS what serves

Section titled “Row-driven serving: the hash IS what serves”

The deployment record is not a log of what happened — it is the input to what runs. The pod boots by reading its snapshot from its build’s record; there is no side channel through which configuration reaches a serving agent. The runtime image is pinned on the record by immutable image digest, never a mutable tag. For no-code apps the platform advances that digest when it ships a runtime update — auto-update is the default, and --runtime-auto-update off pins the app to the runtime captured at its last deploy — always by rewriting the record first, so the record still names what runs (runtime updates). Code-mode images are per-build and change only with a new build.

That is why the snapshot hash in a reconcile or rollback response is a guarantee rather than a receipt: nothing can change what serves without writing a new build record with a new hash. If the hash you got back matches the bytes you reviewed, the agent that answers your users is running those bytes — today and until the next build goes live.

Agent pods are long-lived, so a redeploy is a rolling update, not a restart: the new build’s pod starts alongside the old one, and the old pod is only taken out of service after the new one is ready. In-flight conversations on the old pod are drained, not cut off.

“Ready” is a real check, not a liveness ping. Each pod exposes a readiness check that reports not-ready when the pod cannot do its job — a required colocated tool degraded, or the pod draining for shutdown. A pod that receives its shutdown signal flips to not-ready immediately and stays that way; within one probe window traffic stops routing to it, and any request that still lands on it during the drain gets a structured 503 with a Retry-After and succeeds on a live pod on retry (see Concepts: hosting).

Combined with snapshot idempotency, this gives the operational property CI depends on: an apply only ever rolls the pod when the bytes changed, and when it does, users never see a gap.

guuey agent rollback --to <n> re-serves a previous build’s exact bytes as a new build — a new number, a fresh rollout, but the pinned build’s persisted snapshot as its payload. The platform does not “rebuild from that commit”; it re-serves the string that build’s pod booted from.

The guarantee is strict: before queuing, the stored snapshot is run back through the same canonical assembly a fresh deploy uses, and the result must be byte-identical to what was stored. If it no longer round-trips — the canonical form has moved since that build (a schema default was added, a generative-UI binding the build pre-dated) — or the target is a code-mode build or a record without a snapshot, the rollback answers 409 ROLLBACK_NOT_EXACT and names the remedy (guuey agent apply from that commit). Guuey refuses to serve an approximation and call it a rollback.

One boundary to know today: a build whose snapshot pre-dates the app’s generative-UI binding — deployed in the window before the binding was provisioned, typically a deploy fired right after app creation — is refused with ROLLBACK_NOT_EXACT; re-apply from the repo instead. Studio and guuey deploy builds otherwise round-trip, because every deploy path persists the same canonical, binding-injected form.

Three more properties worth knowing:

  • Every gate re-runs. A rollback passes the same checks as a fresh deploy — schema, lints, tier and size ceilings, build caps. Old bytes don’t grandfather past current limits.
  • Build material only. Rollback never touches the app’s access policy (auth mode, allowed origins, guest access). If the old manifest declared different app.access, apply from that commit instead.
  • Provenance travels with the bytes. The new record carries the pinned build’s repo/path/sha verbatim — the bytes are that commit’s — plus a separate rolledBackFrom marker naming the build it re-served.

Provenance (repo, path, sha) is an audit label, not a verified fact: it is asserted by the caller at apply time and read back on the live build and every historical one. Guuey does not check out your repo to confirm it. The hashes are the truth — the drift gate exists so your pipeline proves, cryptographically, that the persisted bytes are the reviewed bytes, and provenance then tells a human which commit to look at. Builds deployed outside CI (Studio, guuey deploy) simply carry no provenance.

guuey.json carries a root schema version ("1" today), bumped only when the document shape changes in a way an older reader cannot interpret. The CLI and the platform compare against the same constant, published in @guuey/config, and both sides refuse rather than best-effort parse a document from the future:

  • The CLI stops before any network call with SCHEMA_TOO_NEW and tells you to upgrade @guuey/cli.
  • The reconcile and deploy routes answer 400 SCHEMA_UNSUPPORTED when the document is newer than the platform serves.

An older schema is accepted only through a real migration to the current one; none exists yet (there has only been one version), so today the rule is simply “equal, or refused”. The point of the stance is that a document is never silently reinterpreted: what a build means is fixed by its schema version, which is exactly what makes byte-exact rollback a meaningful promise.