Skip to content

feat(mobile): warn before continuing a stale, costly conversation (port #3122)#3169

Closed
Gilbert09 wants to merge 2 commits into
mainfrom
posthog-code/mobile-stale-costly-conversation-warning
Closed

feat(mobile): warn before continuing a stale, costly conversation (port #3122)#3169
Gilbert09 wants to merge 2 commits into
mainfrom
posthog-code/mobile-stale-costly-conversation-warning

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

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, and shouldWarnStaleCostlyConversation, with the same thresholds and null-safe semantics as desktop (100k tokens, 60min stale, now injected — no Date.now() inside the predicate). Reimplemented rather than imported, since mobile does not depend on @posthog/core. Reads used/cost from mobile's usage_update SessionEvents.
  • 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:

  • the pure predicate — large+stale warns, large+fresh no, small+stale no, null last-activity no, future/skewed timestamp reads as fresh, threshold boundaries;
  • usage / last-activity extraction from mobile events;
  • the latch — engaged stays engaged across appended reconnect events; acknowledge releases and stays released;
  • the staff gate — non-staff never engages.

Full mobile suite: 417 passing. tsc clean, Biome clean. Ran /simplify over the diff and applied its suggestions.

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
@Gilbert09 Gilbert09 requested a review from a team July 6, 2026 10:56
@trunk-io

trunk-io Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 7d4f41c.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(mobile): warn before continuing a s..." | Re-trigger Greptile

Comment on lines +73 to +80
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,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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
@charlesvien charlesvien closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants