fix(retention): exempt publicly-linked pods + point the showroom at a live room - #862
Merged
Merged
Conversation
… live room Two changes, one incident. The landing page's "Watch a live room" CTA led to "No messages yet": the pod it points at was silently emptied by the 30-day PG message-retention cron (pgRetentionService, default window, unconditional DELETE). Nothing cleared it by hand — it aged out. Any showcase built on chat messages self-destructs on a rolling window, and the replacement room (Eng Milestone, oldest message July 10) would have started thinning in 3 days. 1. REACT_APP_SHOWCASE_POD_ID -> 6a507c9b792f1ed2cbfec648 (Eng Milestone — 4 weeks of real multi-agent engineering: Fable planning and reviewing, Codex implementing, humans intervening in the open). 2. Message.deleteOlderThan honors PG_RETENTION_EXEMPT_POD_IDS (comma list), wired through the chart as backend.env.retentionExemptPodIds; dev values exempt the showroom and HQ. Env-var rather than a pod flag deliberately: the paid tier under design makes retention a per-account entitlement, and that mechanism should own per-pod retention when it lands. An env list is the smallest stopgap that cannot drift into a second tier system. The unset path stays byte-identical to the query that has run for months — pinned by test — so this is provably a no-op for deployments that never set the var. 5 new tests; the 10 existing pgRetentionService tests untouched.
samxu01
pushed a commit
that referenced
this pull request
Aug 6, 2026
The pricing section described a metered-CI-minutes model we are not building
and did not mention the two limits that now exist in code. Rewritten in both
locales to match the tier as decided:
Cloud free — unlimited BYO agents, private/invited pods, 30 DAYS of
message history (pgRetentionService, live), no card
Pro $12 — everything free has, unlimited history, Community listing
(gated on entitlements.pro, #874), 10 hosted agent seats
marked "coming soon, included when it lands", priority support
Three deliberate choices.
**"Flat. However many agents you run — we never charge per agent."** The
nearest competitor charges per seat AND meters each agent at 0.1 seat, so a
user with three agents pays more there than here. Leading with the sticker
price invites the wrong comparison; leading with FLAT states the actual
difference, and it is the same difference the headline "Humans are seats.
Agents never are" already claims.
**Hosted seats listed but marked coming soon.** They are in the plan the
buyer is choosing, and the gate lifts later — saying so is honest and keeps
the tier legible. Saying nothing would make Pro read as "history plus a
listing toggle."
**30-day history is now stated on the free card.** It has been enforced by
the retention cron for months and appeared nowhere in the product. A limit
users discover by losing data is the worst way to learn it (see #862, where
it silently emptied our own landing-page showroom).
Dropped the "metered agent-hours pool" item: it described billing that does
not exist, and pricing copy that promises a mechanism nobody built is a
phantom contract on the most consequential page.
en/zh-CN key parity asserted; landing suites green; typecheck clean.
lilyshen0722
added a commit
that referenced
this pull request
Aug 6, 2026
…874) * feat(pricing): Community listing requires the Pro entitlement (#768) Listing a pod to Community is a paid capability. Gated on `entitlements.pro` rather than on payment state directly, so the check is identical however billing is wired later — today an admin sets the flag, the same shape as the invite-code path that already gates `cloudAgents`. Reverses the permission #872 shipped an hour ago, deliberately and at Sam's call: that PR moved listing from admin-only to any-owner, which was the right fix for the inert creation choice and the wrong permanent home for a paid capability. The route, the invariants and the audit trail are unchanged; only who may promote moves. DEMOTION IS NEVER GATED, and that asymmetry is the point. A user whose subscription lapses must always be able to make their own room private again. Paywalling the exit would turn a billing event into an unwanted permanent public disclosure — the pod stays world-readable until they pay. Free users keep `private` on demand, forever. Admins bypass, matching every other entitlement check. A user with no entitlements object at all reads as free rather than throwing (pinned). Explicitly NOT gated: BYO agents. "Agents you bring connect free and unlimited" is the standing promise on the pricing page, and a per-agent cap is precisely the model this pricing exists to avoid. The paid tier adds hosted seats and unlimited history; it never subtracts what free already had. 19/19 on the route, including that a free owner is refused without any write landing, that a missing entitlements object is treated as free, and that a free owner can still always demote. * feat(landing): pricing says what the tiers actually are The pricing section described a metered-CI-minutes model we are not building and did not mention the two limits that now exist in code. Rewritten in both locales to match the tier as decided: Cloud free — unlimited BYO agents, private/invited pods, 30 DAYS of message history (pgRetentionService, live), no card Pro $12 — everything free has, unlimited history, Community listing (gated on entitlements.pro, #874), 10 hosted agent seats marked "coming soon, included when it lands", priority support Three deliberate choices. **"Flat. However many agents you run — we never charge per agent."** The nearest competitor charges per seat AND meters each agent at 0.1 seat, so a user with three agents pays more there than here. Leading with the sticker price invites the wrong comparison; leading with FLAT states the actual difference, and it is the same difference the headline "Humans are seats. Agents never are" already claims. **Hosted seats listed but marked coming soon.** They are in the plan the buyer is choosing, and the gate lifts later — saying so is honest and keeps the tier legible. Saying nothing would make Pro read as "history plus a listing toggle." **30-day history is now stated on the free card.** It has been enforced by the retention cron for months and appeared nowhere in the product. A limit users discover by losing data is the worst way to learn it (see #862, where it silently emptied our own landing-page showroom). Dropped the "metered agent-hours pool" item: it described billing that does not exist, and pricing copy that promises a mechanism nobody built is a phantom contract on the most consequential page. en/zh-CN key parity asserted; landing suites green; typecheck clean. * feat(pricing): admin can grant Pro; fix the /compare ribbon Completes the tier plumbing and fixes a live layout bug found while checking the pricing pages. **Tier data structure.** `entitlements.pro` lands beside the existing `cloudAgents` flag — the model comment already said future entitlements slot in there. `/api/auth/user` returns the whole doc, so the client learns its tier with no new endpoint. **Granting it.** `PATCH /api/admin/users/:userId/entitlements` already existed but hard-coded `cloudAgents`; it now takes either key or both. Until billing exists, this route IS the subscription. The property worth the tests: a partial patch never clears the key it did not name. Revoking a lapsed subscription must not silently switch off someone's hosted agents, and granting Pro must not wipe `cloudAgents`. Both directions pinned, plus non-boolean rejection, bots refused, and a pre-existing account with no `entitlements` object at all. **/compare rendered as a ~110px ribbon**, live. `.v2-compare__head` carried both `.v2-landing__section` — whose padding is `max(24px, calc((100% - 1120px) / 2))` — and its own `max-width: 920px`. Percentage padding resolves against the PARENT, so on a wide viewport that is 200-400px per side; capping the border box at 920px left a sliver for text. The landing page never hit it because its sections do not set max-width on the element that carries the padding. Fixed by constraining the CHILDREN instead, and guarded in v2-layout-invariants — the rule is: never put max-width on the element that carries the centring padding. Test-mock note: this route uses CJS `require('express-rate-limit')` while routes/pods.ts uses the ESM default import, so the limiter mock has to be callable and carry `.default`. Cost a failing suite; documented inline. 11 new admin-route tests, 23 layout invariants, typecheck clean both sides.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the dead-showroom incident found during YC-interview prep, and the retention interaction that caused it.
What happened
"Watch a live room" on the landing page rendered "No messages yet". Nothing and nobody cleared that pod:
pgRetentionServiceruns a daily unconditionalDELETE FROM messages WHERE created_at < NOW() - 30 days, and the showroom's content aged out. Verified: PG's lowest surviving message id is 49166 with the sequence's high-water mark intact — everything before 2026-07-06 03:07 was deleted by policy, ~49k rows, which is 30 days of normal turnover and not an incident. The interaction is the bug: any publicly-linked room built on chat messages self-destructs on a rolling window. The replacement room's oldest messages were 3 days from aging out.The change
6a507c9b…): four weeks of real multi-agent engineering, alreadypublicRead, already served by theshowcase/:podIdroute. The showcase opens at the latest messages (an ADR design consult), so the default view is strong.PG_RETENTION_EXEMPT_POD_IDShonored byMessage.deleteOlderThan, wired asbackend.env.retentionExemptPodIds; dev exempts the showroom + HQ. Env list rather than a pod flag deliberately: the paid tier being designed makes retention a per-account entitlement, and that mechanism should own per-pod retention. This is the smallest stopgap that cannot drift into a second tier system.Verification
deleteOlderThanaway, so without these the exemption had no test anywhere)helm templaterenders the env var correctly from dev values