feat(cloud): archive cloud workspaces instead of deleting them, and unarchive them
What & why
Archiving a cloud workspace used to delete its box immediately, and the desktop had no way back. Now archive stops the box and keeps it for 7 days. Unarchive brings the same box back with its disk and terminal sessions. After 7 days the box is deleted, and unarchive gives the same row a fresh box from its environment. The record (name, timeline, comments, tasks, sharing) is the same row either way.
Screenshots of every step, from the real app: https://app.superset.sh/page/archiving-cloud-workspaces-c8wn33
Server
cloudWorkspace.deletestops the box, marks the row archived, and queues a reap job 7 days out (/api/cloud-workspaces/reap, QStashdelay, within every plan's limit). The job deletes the box only if the row is still archived from that archive (deletedAtmust match), so archive → unarchive → archive can't reap early. If queueing fails, the box is deleted at once. The procedure keeps itsdeletename because the CLI, SDK and MCP call it.cloudWorkspace.unarchive: if the stopped box still exists, the row goes straight back toreadyand the existing wake path resumes it on open. Otherwise the row goes toprovisioningunder a new box name (nextSandboxNameFor). Vercel answers409 image_not_readyfor a deleted box's name for a few minutes, and the provision job now reads the name from the row.jobs.tsholds the QStash-or-in-process publisher that provision and reap now share.- New
unarchivedactivity event.
Desktop
- Archive happens at once, with no confirm, from the sidebar hover icon, the context menu, the details ⋯ menu and ⌘⇧⌫. Cloud targets of the delete intent are archived by
DeleteWorkspaceMount, and local workspaces keep their dialog. - Optimistic:
moveCloudWorkspaceRowmoves the row between the active and archived caches (and patchesget) before the server answers, and rolls back with a toast on error. - Archived workspace UI: an archived screen on the workspace route (name, "Archived · time", Unarchive), an archive icon after the details title, grayed names with an archive icon and an undo on hover in the list, and Unarchive in the details actions and ⋯ menu. Share and ⋯ stay visible while archived.
- The provisioning timer counts from when this session first sees the workspace provisioning (
provisioningSince), not fromcreatedAt. - Translations for all locales.
Migration
0133_cloud_workspace_unarchived_event.sql is a single ALTER TYPE "cloud_workspace_activity_event" ADD VALUE 'unarchived'. It locks only the enum type and touches no table, so it neither rewrites nor scans anything. Nothing in this release uses the value inside the migration transaction. Generated by drizzle-kit on top of main's 0132.
How I tested it
Driven with real clicks over CDP in this branch's dev app, against a local API and real Vercel boxes (dev Neon branch, a personal environment with no secrets):
- Every archive entry point archives at once, the row leaves the sidebar, and the archived screen shows immediately and stays (sampled for 4 s).
- Grace resume: I wrote a marker file into the box, archived, and unarchived 13 s later. The server went
deleted → readywith no reprovision, the marker read back, and the terminal showed "Session Contents Restored". - Reap: with a 60 s window, the box was
stoppedat +30 s and gone at +60 s. Unarchive afterwards provisioned a fresh box (ready in ≈10 s, timer started at 0:03). - Re-ran archive → details → Unarchive (resume) → sidebar archive after rebasing onto #7964.
- Timeline shows "unarchived the workspace" entries.
tscclean for desktop, api, trpc, db and ui, and mobile via turbo.biome checkandsherifare clean,check:i18npasses, and i18n tests plus 914 desktop tests in the touched areas pass.
Not verified: the QStash-delivered reap in production. Locally the job runs on an in-process timer. After deploy: archive a workspace, confirm a QStash message is scheduled for +7 days, and confirm the box is stopped, not deleted.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass (CI fails on lint warnings too) - "Allow edits from maintainers" is checked on fork PRs
Summary by cubic
Archiving a cloud workspace now stops its box and keeps it for 7 days instead of deleting it on the spot, and adds unarchive, which resumes the same box with its disk and terminal sessions. After 7 days the box is deleted, and unarchive provisions a fresh box from the environment.
Server
cloudWorkspace.deletestops the box, marks the row archived, and queues a reap job 7 days out via QStash (or in-process on a local API).cloudWorkspace.unarchiveresumes the stopped box when it still exists, otherwise provisions a fresh box under a new sandbox name.- The reap job only deletes the box if the row is still archived from that same archive, so archive → unarchive → archive can't reap early.
- Migration
0133adds theunarchivedactivity event viaALTER TYPE ... ADD VALUE.
Desktop
- Archive triggers immediately from the sidebar, context menu, details menu, and ⌘⇧⌫, with no confirmation.
- Rows move between the active and archived caches optimistically, rolling back with a toast on error.
- Archived workspaces show an archived screen with Unarchive, grayed list names with an undo on hover, and Unarchive in the details menu.
- The provisioning timer counts from when the session first sees the workspace provisioning instead of from
createdAt. - Czech and Turkish translations for the new archive strings were corrected.
Summary by CodeRabbit
- New Features
- Cloud workspaces can be archived and unarchived from the workspace list, sidebar, and workspace details.
- Archived workspaces show their status and archive time, and workspace activity records when they’re unarchived.
- Unarchiving restores an existing workspace when possible or restarts provisioning. Archived workspaces are scheduled for cleanup after seven days.
- Archive and unarchive actions are available in supported languages.