Skip to Content
Operate EnergonUpgrade and recover

Follow this procedure to merge a published upstream release, deploy a schema update, or recover interrupted retention cleanup without putting production data at risk. It is written for the human operator who controls the D1 database and Worker deployment. The order matters because runtime bootstrap and stale-claim recovery cover only specific failure cases: validate, migrate D1, deploy the Worker, and verify the live contract. Catching up uses a release tag, not unreleased main, because a merge to main can be the live deploy. For loss of catalog or object data, use Back up and restore D1 and R2 instead.

Upgrade and recover an Energon

Catch up to a published release

Merge the GitHub Release tag named on Admin or by gh release view --repo tmchow/energon, not unreleased main. Do not use GitHub Sync fork. If this checkout has update-from-upstream , follow that skill. The hand procedure stays in INSTALL.md  when the skill is absent.

Resolve the deployment and upstream repositories from their Git remotes, including independent copies with no GitHub fork parent. Fetch a published upstream release into a new update branch and verify common ancestry before merging. Review conflicts and preserve private customizations; do not reset to upstream or mirror-push. Follow Review an upstream release  for the exact procedure. An update prepares a reviewable branch; accepting it onto main is a separate action.

Keep this Energon’s wrangler.toml, instance-skill.json, generated plugin, and marketplace catalogs. Compare new keys only against wrangler.example.toml. Do not create, delete, empty, or rebind the live D1 or R2.

Inspect the actual deployment workflows and list Actions variables in the verified deployment repository. An explicit repository-level ENABLE_PRODUCTION_DEPLOY=false disables the stock deploy job. A missing entry can inherit an organization variable; establish that value before treating deployment as off. A failed read leaves deployment behavior unknown. With the stock workflow, true means landing on main migrates D1 and deploys. Accept the update only within the operator’s authorization, including any possible live deployment. A code update request does not automatically accept the branch onto main. Turning automation on later does not replay a previous push.

Before any remote migrate, record a D1 Time Travel bookmark (npx wrangler d1 time-travel info with this deployment’s database name). Do not restore it. An ordinary upgrade does not need a durable R2 copy; that capture is backup-this-energon  and Back up and restore D1 and R2.

Migrate before the Worker changes

Energon represents schema in three places: append-only migrations record deployment history, src/db.ts creates and upgrades runtime tables, and src/schema.sql documents the current shape. Runtime bootstrap helps old Energons start, but SQLite CREATE and ADD COLUMN migrations are not generally idempotent. Deploying code first can therefore create a column before Wrangler records its migration.

1. Validate the deployment repository locally

After merging the release tag, take new configuration keys from wrangler.example.toml, keep Energon customizations in supported variables and instance-skill.json, and run the repository gates:

npm install npx wrangler d1 migrations apply energon --local npm run skill:render -- --check npx wrangler types npm run typecheck npm run lint npm test

--check fails when generated catalogs lag the templates or version.txt. After merging a release, run npm run skill:render so the generated plugin and marketplace manifests take the release version from version.txt and /setup can advertise npx skills add, then commit those generated files and rerun --check. Plugin versions follow Energon releases even when skill content is unchanged. The schema tests require runtime table declarations and src/schema.sql to expose the same columns. The database unit test also verifies that a 0005-shaped database gains needed columns before indexes are created.

2. Migrate before deploying

The first live Worker is laptop Wrangler. Automatic updates are optional and come after that deploy is verified. See After the first deploy .

When ENABLE_PRODUCTION_DEPLOY is true and the stock CI deploy job is intact, landing the release on main is the deploy: tests, remote D1 migrations, then wrangler deploy. Do not also run a local deploy. When the variable is explicitly false, merging updates the deployment repository (and usually the plugin marketplace) without changing the live Worker; deploy this release later with laptop Wrangler if asked.

If a human operator uses Wrangler from an authenticated local laptop, the order is still:

npx wrangler d1 info energon npx wrangler d1 time-travel info energon npx wrangler d1 migrations apply energon --remote npx wrangler deploy

Use this deployment’s D1 database_name when it differs. Do not reverse migrate and deploy. Do not run them from an untrusted cloud agent environment. Stop on a migration failure. Never stamp d1_migrations or execute ad hoc schema SQL.

3. Recover from a duplicate-column migration failure

A duplicate-column error after a Worker-first deployment indicates that runtime bootstrap may already have added the column while Wrangler still considers its migration pending.

  1. Stop the deployment; do not retry the same migration loop blindly.
  2. Have a human operator inspect the affected table with PRAGMA table_info in the Cloudflare D1 console.
  3. Compare the actual column and type with the pending migration and the current runtime schema.
  4. Only after confirmation, follow the human production procedure to record that migration as applied.
  5. Rerun the normal migrate-before-deploy workflow and all verification checks.

Agents must not hand-edit d1_migrations, run production d1 execute, or improvise repair SQL. If no human can verify the actual production schema, leave the deployment stopped and escalate with the migration filename and the observed error.

Migration 0005_handles.sql needs special care: it rebuilds the site tables to use (handle, slug) as the primary key. Runtime ensureSchema can add a handle column but cannot reshape an old primary key. Follow the migration’s warning rather than applying the rebuild to an already runtime-modified database.

4. Respect the token-expiry rollback floor

Once any token has a non-null expires_at, an older Worker that predates expiry enforcement would authenticate finite-lifetime tokens again, including expired ones. Before considering a rollback below that build, a human operator must identify all active finite-lifetime tokens:

SELECT id, user_email, label, expires_at FROM tokens WHERE expires_at IS NOT NULL AND revoked_at IS NULL;

Stay on or above the first expiry-enforcing build whenever possible. If a deeper rollback is unavoidable, the owners must revoke those tokens before the old Worker becomes active. To audit Never tokens after disabling future Never mints, use the same query with expires_at IS NULL.

ALLOW_UNLIMITED_TOKENS=false is not a recovery control for existing credentials. It removes Never only from future mint choices. Revoke existing tokens explicitly before a rollback or policy tightening that requires them gone.

5. Understand expiry-cleanup recovery

Expiry has two enforcement paths. Cron selects bounded batches of expired sites and loose files on each run. Reads and writes independently return 410 for expired objects, so a missed cron does not revive content. From v1.10.0, an expired site is first marked as removed, and its versions and storage are reclaimed only after in-flight reads and cleanup guards allow it. The purge-claim sequence below still describes loose files and older releases.

Purge claims are written before R2 deletion so a concurrent TTL patch or file replacement cannot preserve a catalog row whose bytes are disappearing. A failed R2 delete restores the prior writer. A fresh claim blocks a second purger; a claim at least 60 seconds old may be reclaimed automatically.

claim exact expired row | v delete R2 bytes -- failure --> restore prior writer, surface error | v delete child rows --> delete owner row --> purge cache prefix --> release quota

If cleanup appears stuck, first confirm whether the object still returns 410 and whether the next cron run reclaims the stale claim. Do not delete .wrangler/state during local investigation; it contains the human’s local database. Do not kill unrelated Wrangler or workerd processes by pattern.

6. Verify after upgrade or recovery

curl -fsS https://energon.company.example/v1/health curl -fsS https://energon.company.example/v1/help npm run skill:render -- --check

Confirm health, resolved retention and token policy, this Energon’s origins, policy, and SOP, capacity limits, and rendered-plugin consistency.

Landing the release or deploying the Worker does not update plugins already installed on agent machines. Claude Code and Codex keep a versioned local copy and skip the update while the reported version is unchanged. Once the marketplace has the new plugin, users refresh it with their client, using the marketplace and plugin names from /v1/help: npx skills update yourco-energon -g for the Skills CLI, claude plugin marketplace update then claude plugin update for Claude Code, or codex plugin marketplace upgrade then codex plugin add for Codex. Refresh an installed plugin  has the exact commands. The well-known skill on the content origin changes as soon as the new Worker serves it (allow five minutes for edge caches), but copies installed from it with npx skills add do not; reinstall them the same way. For a schema incident, preserve the failing migration filename, the exact error, the human’s schema evidence, and the successful rerun result.

Stop conditions

Never rewrite an old migration to match the current schema. Add new history under migrations/, keep runtime bootstrap aligned, and update src/schema.sql as the current documented shape.

Never deploy the Worker before migrations. If it already happened, stop and use the human-only verification procedure above; do not let an agent stamp migration state.

Never roll back below token-expiry enforcement while active finite-lifetime tokens remain. Stay above the floor or revoke them first.

Never roll back to a Worker without atomic site deployments after any site has converted, and never enable SITE_VERSIONING_ENABLED while an old Worker can still write. See Upgrading to atomic site deployments.

Keep upgrades reviewable

The cleanest upgrades keep Energon changes in wrangler.toml variables and instance-skill.json, then rerender the plugin after a release merge. That reduces conflicts in hub source and makes drift machine-checkable. Review Configure Energon policy before accepting new defaults, Back up and restore D1 and R2 for data-loss incidents, Deploy an Energon host for the guarded workflow, and Operate an Energon for the full lifecycle model.

Upgrading connection and plugin setup

Apply 0014_agent_connections.sql with the other pending migrations before deploying connection support. Keep /connect behind the hub’s Access policy and /auth.md public alongside /v1*. Connection approval still requires a signed-in human; the returned token appears in the existing token list for revocation.

Upgrading to upload grants and XML isolation

The first release after v1.8.0 adds upload grants and sandboxes published XML. Apply 0022_upload_grants.sql with the other pending migrations before deploying; it adds the upload_grants table and its expiry index. The Worker’s runtime bootstrap can also create that table, so deploying the Worker first leads to the duplicate-object failure described above.

The same release adds the sandbox Content Security Policy to published application/xml, text/xml, and +xml responses. Public content responses can sit in the edge cache for up to a day without reaching the Worker, so XML fetched before the deploy keeps serving without the sandbox until its cache entry expires. After deploying, purge the content origin’s cache in Cloudflare so cached XML picks up the new header.

Revoking a person’s agent keys now also ends every upload grant those tokens minted. See Let a machine upload without a token for the feature itself.

If your deployment repository still uses the generic energon plugin identity, rerun skill:init with a unique name and the same real HTTPS origin. Commit the generated plugin and catalogs, align the Worker identity variables, and have users replace the old plugin installation and token environment name. Existing token values remain valid when TOKEN_PREFIX is unchanged. See Deploy an Energon host for setup.

Upgrading to atomic site deployments

Atomic site deployments arrived in Energon v1.10.0  with migration 0024_site_deployments.sql. The same release adds 0023_upload_grant_passwords.sql, which lets a new-file upload grant set passwords at mint; apply both with the other pending migrations. Follow this section when you upgrade from v1.9.0 or earlier.

This release changes how site bytes are stored. Old Workers write site files in place; the new Worker publishes immutable versions and moves a version pointer. Existing sites must be converted to the new layout, and that conversion is safe only after no old Worker can still write and no cache can still serve old responses without asking the new Worker. The rollout therefore has a bridge phase with conversion turned off.

  1. Migrate first. Apply 0024_site_deployments.sql with the other pending migrations, then deploy, as in Migrate before deploying. Keep cross_version_cache = false under [cache] in wrangler.toml when you merge; the release adds it.
  2. Deploy the bridge to all traffic. Leave SITE_VERSIONING_ENABLED unset and deploy the new Worker to 100% of traffic. Do not use a gradual or percentage rollout: old in-place writers and enabled versioned writers must never serve traffic at the same time. During the bridge, existing sites stay readable, new sites publish normally, and an update to a not-yet-converted site returns a retryable 409 site_busy.
  3. Drain old writers. Check the Worker’s deployment traffic allocation and confirm the old version receives none. Then wait out the longest upload an old Worker could already have accepted. The wrangler deploy command returning is not evidence that old requests have finished.
  4. Isolate caches. The old Worker let public site responses sit in the edge cache for up to a day without reaching the Worker. Purge the content origin’s cache in Cloudflare, or wait at least a day, and bypass any cache rules you configured separately for site paths. After this, site responses are private and no-store at the outer cache, and the Worker keeps its own cache keyed by version.
  5. Enable conversion. Set SITE_VERSIONING_ENABLED = "true" under [vars] and deploy.

The product’s rollout rehearsal  qualified this sequence on a disposable Worker. Each production operator still has to establish their own writer drain and inspect their own cache rules.

Watch conversion

Once enabled, each cron run converts up to five older sites within a shared budget of about 400 estimated D1 and R2 calls, 8 MiB copied, and (from v1.13.1) 3 minutes of wall time. The call budget stays under the Workers Free limit of 1,000 calls per invocation, and the time limit stays under the five-minute cron interval. The call and byte limits usually stop a run first. Before v1.13.1 the time limit was 10 seconds, so in production a run copied only about three files and a 60-file site took about 20 runs. From v1.11.0, a run keeps going until that budget is spent instead of taking one step per site, so a site of about 60 files converts in about four runs (roughly 20 minutes on the five-minute cron) rather than about ten hours. Admin, then Health, shows a Site conversions section while any remain: the pending count, whether conversion is enabled, and up to 25 sites with their current phase and any error. GET /v1/admin/health returns the same data as site_conversions.enabled, site_conversions.pending, and site_conversions.items. Sweep now, or POST /v1/admin/sweep, advances a bounded batch immediately, with the same call and byte budget but only 10 seconds of wall time because it runs inside an HTTP request. A write to an unconverted site also advances it, within a smaller budget of about 150 calls, 4 MiB, and 5 seconds, and then returns 409 site_busy. A small site usually finishes converting inside that request. The caller still has to retry with freshly read metadata, because conversion changes the site’s content_generation.

Conversion inventories a site’s files, reserves quota for the copy, copies the files into an immutable version, and then publishes that version. The site stays readable throughout. An error stops that site’s conversion without changing its published content and keeps the state for retry. The common blockers are repairable:

  • Insufficient quota. The copy needs room for the site twice until the old objects are cleaned up. Free space or raise MAX_PLATFORM_BYTES, then sweep again.
  • Missing or ambiguous objects. Conversion cannot find, or cannot unambiguously identify, an object the catalog names. Restore the object from a backup or have the owner republish it, then sweep again.

Do not delete allocation or conversion rows to clear an error. The product’s operator reference  lists read-only integrity queries to run after conversion batches.

Do not roll back past conversion

After any site converts, the version-aware Worker is the rollback floor. An older Worker reads the old in-place layout and would serve stale bytes for converted sites. Fix problems forward. Unsetting SITE_VERSIONING_ENABLED pauses further conversion but does not undo versions already published.

Existing site URLs, share passwords, and write passwords keep working unchanged through the upgrade.

Upgrading to conditional loose-file replacement

Energon v1.11.0  adds migration 0025_loose_file_generation.sql. It adds content_generation to loose_files, backfilled to 1 for every existing file, and expected_version to upload_grants. Apply it with the other pending migrations before deploying, as in Migrate before deploying; runtime bootstrap can add both columns, so deploying the Worker first leads to the duplicate-column failure described above.

Nothing changes for clients that do not send X-Energon-Expected-Version: replacements stay unconditional, and grants minted before the upgrade have no expected version. See Replace only if nobody else has for the feature.

The same release returns 409 file_recovery_required instead of 409 file_busy for files blocked on storage recovery, and lists them under File recoveries in Admin Health. Agents and grants stop retrying those files, so check that section after deploying and reconcile anything it lists.

Upgrading to the well-known skill and renamed operations

Energon v1.12.0  has no migration. It changes two things operators should plan for:

  • Renamed OpenAPI operations. The twelve deployment and deployment-grant operations lose their generated operationIds, such as postVSitesIdDeployments, for readable names such as createDeployment, and every operation now has a tag. Routes and payloads are unchanged, but a gateway app, generated SDK, or script that referenced the old names breaks. Reimport /v1/openapi.json and update those references; HTTP API lists every rename.
  • A public well-known skill, on by default. After the deploy, the content origin serves the rendered publish skill at /.well-known/agent-skills/, and /v1/help reports agent_skills_url. No Access change is needed. If you do not want it served, add AGENT_SKILLS_DISCOVERY = "false" to wrangler.toml before deploying; see Configure Energon policy.

The Worker renders that skill from a bundle npm run build:ui now writes into src/generated/, so keep the [build] command described below. After deploying, check it from outside:

curl -fsS https://energon.company.example/v1/help | jq .agent_skills_url curl -fsS https://share.company.example/.well-known/agent-skills/index.json

Upgrading to agent keys

Energon v1.13.0  has no migration. The hub now calls the agent credential a key: the page is Agent keys at /keys, and /tokens redirects there with a 301. Existing keys keep working, and nothing an agent stores changes. ENERGON_TOKEN (or your TOKEN_ENV), the ee_live_ prefix, token_expired, and the /v1/admin/tokens routes are unchanged.

  • Access. /keys is a signed-in hub path like /tokens. If your Access application protects the hub hostname with only the documented bypasses, as Configure Energon policy describes, nothing changes. If a policy names protected hub paths individually, add /keys.
  • Installed skill. The skill templates now send humans to /keys. Re-render the plugin with npm run skill:render after merging so agents stop quoting /tokens; older plugins still work through the redirect.
  • Bookmarks and runbooks. Update links to /tokens when convenient. tokens_url in /v1/help and in 401 bodies keeps its name and now points at /keys.

Upgrading to the gateway catalog

Energon v1.14.0  has no migration. It adds GET /v1/openapi-gateway.json, the catalog a tool gateway should import, and POST /v1/files/{id}/duplicate, a JSON-only way to copy a loose file. /v1/openapi.json keeps every operation it had, and no operation was renamed.

  • Tool gateways. A gateway that imported /v1/openapi.json keeps createFile, putFile, and the other byte-upload tools until you change its import to /v1/openapi-gateway.json. Then refresh the gateway; for Executor, redeploy the gateway app. See Use Energon through a tool gateway.
  • Installed skill. The skill’s gateway mode now names the gateway catalog and files.duplicateFile. Re-render the plugin with npm run skill:render after merging, and reinstall copies installed with npx skills add.
  • Clients of the duplicate path. PUT, PATCH, and DELETE on /v1/files/{id}/duplicate now return 405. Before v1.14.0, the router treated duplicate as a filename segment and applied those methods to the file itself. GET on that path still reads the file.
  • /v1/help. It gains a gateway_openapi field, and its SOP labels the byte-upload routes as the path for direct clients.

Energon v1.13.1 , the release before it, raised the cron conversion time limit described in Watch conversion and needs no action.

Energon v1.15.0  adds migration 0026_link_access.sql. It adds link_access to sites and loose_files, NOT NULL with default public, and a nullable file_link_access to upload_grants. The change is additive: every existing object stays public and nothing about its URL, passwords, or write policy changes. Apply it with the other pending migrations before deploying, as in Migrate before deploying; runtime bootstrap can add these columns, so deploying the Worker first leads to the duplicate-column failure described above.

  • Rollback. An older Worker does not know the column, so once anyone makes an object private, rolling the Worker back to v1.14.0 or earlier serves those objects publicly again, to anyone with the link. Roll forward instead. If you must roll back, first find private objects (SELECT id FROM sites WHERE link_access = 'private' and the same on loose_files) and have their creators set them public or delete them.
  • Installed skill. The skill templates now describe private publishing, X-Energon-Link-Access, and the link_private and link_access_conflict errors. Re-render the plugin with npm run skill:render after merging, and reinstall copies installed with npx skills add.
  • Tool gateways. /v1/openapi.json and /v1/openapi-gateway.json gain the link_access fields and the bad_link_access, link_access_conflict, and link_private codes. Refresh the gateway’s import to expose them. A gateway whose key belongs to its own account cannot see a person’s private objects; see Use Energon through a tool gateway.
  • Hub. The stage’s Link access disclosure gains Who can open, and the catalog marks private rows with a Private key. Select all matching these filters is now disabled while Changed since last open is on. See Keep work private to your account.
  • Exports and cleanup. Account export manifests gain link_access and include private objects. Personal cleanup reports another account’s private id as not_found; admin cleanup still sees every private object.

Preserve the UI build when merging upstream

Keep the [build] configuration in wrangler.toml, including command = "npm run build:ui" and its watch paths. From v1.12.0 that command also bundles templates/skill/ for the well-known skill. Preserve [assets].run_worker_first = ["/static/ui/*", "/static/pele/*", "/static/mermaid/*", "/static/md-expand.mjs"]; the Worker must check the request hostname before serving application assets and adds CORS and content-type headers to the diagram renderers. Keep tooling/svelte with the root npm workspace so UI checks remain available.

Wrangler and Vitest build the UI automatically. Do not hand-edit or carry old generated files from src/generated/ or public/static/ui/ into the new release. After the guarded deployment, verify both discovery responses and the interactive hub: compiled script loading, file staging/cancel, catalog search, and a disposable publication that opens at its returned URL. Use Develop and verify for local UI checks before deployment.

Last updated on