Ablation of each mechanism added by the previous commit, running the suite
with it removed. Four pieces carried no weight and are deleted; two holes the
ablations exposed are fixed.
Deleted:
- Pre-resolving `t(...)` labels to strings before the payload memo. It only
ever guarded against an unstable `t`, which the test's own mock produced by
returning a fresh function per render; real `react-i18next` holds `t` in
`useState` and replaces it only on a language change. The mock is now
faithful and the product code is back to inline `t(...)`.
- The enabled flag in the flush signal. `activityId` and the permission key
already cover every transition that matters; the flag only shortened a
disable/re-enable round trip made inside one throttle window.
- `useMemo` around `isNativeIOSAppShell()`, a pure window read.
- Two of the three null checks re-tested in the payload memo. The activity id
and the workspace/user it is built from now travel as one `activityTarget`,
so the invariant is structural instead of re-derived for the type checker.
Fixed:
- `agentConfigs` reached the payload memo outside the throttle. It streams in
from the same batched doc-meta cache, so churning it reproduced the original
starvation exactly: zero deliveries across a two-second burst.
- The permission candidate is a fresh object per scan, and its identity was a
payload dependency. That reset the bridge debounce on every batch for as long
as a request was pending, so an alert raised mid-burst was never delivered.
The payload now depends on the candidate's key and title, not on the object.
Kept, with the reason recorded: the 250ms bridge debounce is not a second
throttle. A flush lands in the commit after the change requesting it, so
removing the debounce delivers both the stale and the fresh payload — and the
stale one marks the alert shown, dropping the fresh one at the already-alerted
early return. Four tests fail without it.
Tests: one case deleted (re-alerting on a new request covered pre-existing
dedupe untouched by this change, and failed under no ablation). The permission
test now raises the request in the middle of an ongoing burst rather than after
it, which is both the real scenario and what catches the identity regression.
Added a case for the native-shell gate, which nothing covered.
Model: claude-opus-5[1m]
Problem
useLodyLiveActivityrebuilt its entire payload on every session metadata batch.atoms/doc-metaflushes metadata in batches per macrotask, so a cold start or a reconnect catch-up republishesallActiveSessionsAtommany times per second — and each republish paid for five passes over every session plus item sorting and relative-time formatting.The bridge call had a 250ms debounce, but it did not protect any of that: the
payloadmemo produced a new object on every batch, so the debounce timer was reset before it ever fired. During a sync burst the bridge received nothing at all while the CPU burned on rebuilds nobody ever saw.A second cost was quieter: the live-status map was built by calling
findFreshSessionPresenceStateonce per session, and that helper scans the whole presence map — O(sessions × presence entries) on every batch.Approach
Throttle the input, not the bridge call. The session list and the presence-derived status map pass through one leading-edge throttle. The trailing deadline is anchored to the last emit rather than the last change, so unlike a debounce a burst of any rate cannot push it back: updates keep landing on a fixed cadence and the burst's final value always arrives.
Throttle window: 1000ms. Chosen against the two timescales that bracket it —
DOC_META_EVENT_FLUSH_BATCH_SIZE = 50republishes many times per second at the bottom, anduseStableNow(60_000)already re-renders the relative-time labels at the top. A summary of at most 8 conversation rows carries no information that goes stale inside a second, so the window buys a ~20× reduction in rebuilds during catch-up for a worst-case 1.25s (throttle + debounce) of extra latency on an ordinary status change.Permission alerts are exempt.
findLiveActivityPermissionAlertCandidateruns against the unthrottled session list — a single filtering pass with no item building, sorting, or formatting — and a new candidate key flushes the throttle window. So a pending permission request:shownPermissionAlertKeysRefde-duplication is untouched: one alert per candidate key, and while an alert is pending the effect still refuses to overwrite it with an ordinary summary.Workspace switches and re-enabling the feature flush the window for the same reason.
Compute nothing when the feature is off.
liveActivitiesEnabledand the native-iOS-shell check were only consulted in the sync effect, so a user with Live Activities turned off — and every desktop build, where the atom defaults totrue— paid for the full rebuild and threw it away. Both are now gates on the memo. The activity id is derived separately from those gates, so the disable-path and unmount-pathendConversationSummarycalls still know which activity to end after the payload has been gated off.One incidental fix: the payload memo depended on
t, whose identity drives the bridge debounce. Every label is now resolved to a string first, so an unstabletcannot restart the starvation this PR removes.Tests
packages/components/tests/live-activity-throttle.test.tsxrenders the real hook in jsdom with fake timers and asserts what the__LODY_LIVE_ACTIVITY__bridge received — no real sleeps, no wall-clock races.Both central tests were verified to fail against a deliberately broken implementation:
[[a,unread]], nob)