feat(mobile): warn before continuing a stale, costly conversation (port #3122)#3169
feat(mobile): warn before continuing a stale, costly conversation (port #3122)#3169Gilbert09 wants to merge 2 commits into
Conversation
Ports the desktop cost guardrail (#3122, #3152, #3154) to the mobile app. When PostHog staff open a conversation that is both large (context usage >= 100k tokens) and stale (idle >= 60min, so the prompt cache has very likely gone cold), the mobile composer is blocked with a warning that continuing will be costly. The user can "Continue anyway" to permanently acknowledge the warning for that task for the app run. - Pure, time-injected helpers in `utils/contextUsage.ts` (`extractContextUsage`, `extractLastActivityAt`, `shouldWarnStaleCostlyConversation`) with the same thresholds and null-safe semantics as desktop. Reimplemented rather than imported — mobile does not depend on `@posthog/core`. - Ephemeral (non-persisted) `staleConversationGateStore` reproduces the latch: once engaged, reconnect events cannot silently dismiss the warning; only acknowledging releases it. - `useStaleConversationGate` wires usage + `is_staff` + the gate store; non-staff never engage. - `StaleConversationCostNotice` replaces the prompt input while active, showing used tokens, idle time, and estimated USD cost when available. Unit tests cover the predicate, extraction, the latch, and the staff gate. Generated-By: PostHog Code Task-Id: 8467841d-d2da-4ad9-8bf8-073e29d727b5
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Reviews (1): Last reviewed commit: "feat(mobile): warn before continuing a s..." | Re-trigger Greptile |
| it("never engages for non-staff", () => { | ||
| mockUseUserQuery.mockReturnValue({ data: { is_staff: false } }); | ||
| const { gate } = render("t1", [usageEvent(150_000)]); | ||
| expect(gate.active).toBe(false); | ||
| expect(useStaleConversationGateStore.getState().engagedSessions.size).toBe( | ||
| 0, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Staff gate test doesn't cover the loading state
UserData.is_staff is declared is_staff?: boolean (optional), so when the user query hasn't resolved yet currentUser?.is_staff === true evaluates to false — correct, but untested. Per the parameterised-test preference, a single it.each case covering is_staff: false, is_staff: undefined, and data: null (query loading) would close this gap while collapsing three near-identical cases into one.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Parameterise the non-staff case to also cover is_staff undefined and the query-loading (data: null) state, so the gate is verified to stay inactive until the me query resolves as staff. Generated-By: PostHog Code Task-Id: 8467841d-d2da-4ad9-8bf8-073e29d727b5
What
Ports the desktop cost guardrail to the mobile app (
apps/mobile). Desktop shipped this across #3122 (core), #3152 (blocking prompt-input state), and #3154 (compact rendering).When a PostHog staff user opens a conversation that is both large (context usage ≥ 100k tokens) and stale (idle ≥ 60 minutes — the Anthropic prompt cache has very likely gone cold), the mobile composer is blocked and warns that continuing will be costly (a cold prefix rebuild at full input price). It shows the used-token count, the idle time, and the estimated USD cost when available. The user can "Continue anyway" to permanently acknowledge the warning for that task for the app run.
Non-staff users never see the warning.
How
Follows the repo architecture rules — pure logic in a util, view state in a store, the component only renders:
features/tasks/utils/contextUsage.ts— pure, time-injected helpers:extractContextUsage,extractLastActivityAt, andshouldWarnStaleCostlyConversation, with the same thresholds and null-safe semantics as desktop (100k tokens, 60min stale,nowinjected — noDate.now()inside the predicate). Reimplemented rather than imported, since mobile does not depend on@posthog/core. Readsused/costfrom mobile'susage_updateSessionEvents.features/tasks/stores/staleConversationGateStore.ts— ephemeral (non-persisted) zustand store reproducing the latch: reconnecting to a stale task immediately appends freshly-stamped events that would otherwise make the conversation look active again and dismiss the warning before the user chooses. Once engaged, only acknowledging releases it (per-task, for the app run).features/tasks/hooks/useStaleConversationGate.ts— wires context usage +is_staff(from the existing me query) + the gate store; returns{ active, usedTokens, lastActivityAt, costUsd, onContinue }.features/tasks/composer/StaleConversationCostNotice.tsx— blocking notice that replaces the prompt input on the task screen while the gate is active, using mobile theming.Tests
Unit tests (Vitest) cover:
Full mobile suite: 417 passing.
tscclean, Biome clean. Ran/simplifyover the diff and applied its suggestions.