You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make relaycast retention a property of the customer's plan rather than a deployment-wide default. Premium plans get a longer window; a top tier gets never-prune with cold storage. Free plans get a short window, which also gives abandoned workspaces a principled expiry.
That is exactly the never-prune tier, already implemented.
The 30-day figure is not a limit. It is a deployment-wide default passed at a single call site in the Workers cron:
pruneExpired(db,{defaults: {messageTtlDays: 30}})
with per-workspace overrides honoured inside pruneExpired.
So there is no engine change here. The work is a plan → retention mapping and something that writes the column.
Scope
Define the tiers — the retention window per plan, and which plan gets null (never prune). This is a pricing decision, not an engineering one; the issue should record the chosen numbers.
Write workspaces.retention when a plan changes. Provisioning and plan-change need to set it; today nothing does.
Cold storage for the never-prune tier — archive the aged slice to R2, not D1. D1 is at 1.99 GB and is the component under strain in the 2026-08-18 incident; R2 is priced for this. The Cloudflare file adapter is gaining R2 object lifecycle now, so the archive has a natural home.
Surface the boundary. Any consumer that reads back history — replay in particular — must display the retention boundary, so a truncated read announces itself rather than looking complete. A pointer to a conversation that has aged out is worse than no pointer, because it reads as coverage.
Blocked on attribution
You cannot tier what you cannot attribute. A workspace row carries no notion of which customer owns it, so a tier cannot be applied, enforced, or billed.
#339 is therefore a prerequisite, not a parallel nicety. Two of its open review threads concern a CHECK constraint that accepts a usage_classification with no classificationSource — a classified row with no provenance for the classification. That is precisely the field a billing tier keys on, so it should be corrected rather than merged past.
Two consequences worth recording
Workspace bloat becomes self-limiting. A free tier with a short TTL, combined with workspaces.expires_at from feat: add workspace lifecycle controls #338, expires abandoned workspaces by policy and continuously — replacing a one-off backfill over a hand-written "inert" predicate. Production currently holds 41,443 workspaces, of which 25,014 have no agents, messages, files, nodes or usage records.
Session replay stops needing its own retention decision. "Replay reaches back as far as your plan's retention" is a pricing sentence. See relay#1522 and the intent-trajectory-lineage workstream, where this decision supersedes a three-way choice between a null override, copying the slice at PR time, and accepting a 30-day bound.
Summary
Make relaycast retention a property of the customer's plan rather than a deployment-wide default. Premium plans get a longer window; a top tier gets never-prune with cold storage. Free plans get a short window, which also gives abandoned workspaces a principled expiry.
Decision by @khaliqgant, 2026-08-18.
The engine already does this. Nobody is using it.
This is worth stating first because it changes the size of the work:
workspaces.retentionis a per-workspace JSON column typedWorkspaceRetentionSettings, carrying independent TTLs:and from
packages/engine/src/engine/retention.ts:That is exactly the never-prune tier, already implemented.
The 30-day figure is not a limit. It is a deployment-wide default passed at a single call site in the Workers cron:
with per-workspace overrides honoured inside
pruneExpired.So there is no engine change here. The work is a plan → retention mapping and something that writes the column.
Scope
null(never prune). This is a pricing decision, not an engineering one; the issue should record the chosen numbers.workspaces.retentionwhen a plan changes. Provisioning and plan-change need to set it; today nothing does.Blocked on attribution
You cannot tier what you cannot attribute. A workspace row carries no notion of which customer owns it, so a tier cannot be applied, enforced, or billed.
#339 is therefore a prerequisite, not a parallel nicety. Two of its open review threads concern a
CHECKconstraint that accepts ausage_classificationwith noclassificationSource— a classified row with no provenance for the classification. That is precisely the field a billing tier keys on, so it should be corrected rather than merged past.Two consequences worth recording
workspaces.expires_atfrom feat: add workspace lifecycle controls #338, expires abandoned workspaces by policy and continuously — replacing a one-off backfill over a hand-written "inert" predicate. Production currently holds 41,443 workspaces, of which 25,014 have no agents, messages, files, nodes or usage records.relay#1522and theintent-trajectory-lineageworkstream, where this decision supersedes a three-way choice between a null override, copying the slice at PR time, and accepting a 30-day bound.