|
| 1 | +# Live Activity Cost Tracking Design |
| 2 | + |
| 3 | +**Date:** 2026-03-31 |
| 4 | + |
| 5 | +**Project:** Helix-iOS |
| 6 | + |
| 7 | +**Status:** Approved for planning |
| 8 | + |
| 9 | +## Goal |
| 10 | + |
| 11 | +Add a lock-screen Live Activity for active conversation sessions that: |
| 12 | + |
| 13 | +- shows the active detected question while the answer is being generated and streamed |
| 14 | +- shows cumulative session cost across transcription, question detection, and answer generation |
| 15 | +- supports OpenAI plus Chinese providers with provider-specific pricing and parsing |
| 16 | +- supports an interrupting `Ask Now` button that runs from the Live Activity without opening the app UI |
| 17 | +- supports a separate `Open App` button that explicitly foregrounds Helix |
| 18 | + |
| 19 | +## Non-Goals |
| 20 | + |
| 21 | +- Rebuilding the assistant stack in Swift |
| 22 | +- Approximating cost when usage or pricing metadata is unavailable |
| 23 | +- Opening the app automatically for `Ask Now` |
| 24 | +- Supporting multiple simultaneous response generations |
| 25 | + |
| 26 | +## Current State |
| 27 | + |
| 28 | +- Native Live Activity scaffolding already exists in the iOS app and widget extension. |
| 29 | +- Flutter does not currently call the native `startLiveActivity`, `updateLiveActivity`, or `stopLiveActivity` methods. |
| 30 | +- The conversation pipeline already exists in Dart and is authoritative for question detection, answer generation, provider selection, and manual ask behavior. |
| 31 | +- The LLM abstraction currently streams text only. It does not expose usage metadata or cost. |
| 32 | +- The OpenAI realtime transcription path handles transcript events but does not currently parse usage metadata. |
| 33 | +- Chinese providers are routed through the existing provider stack, but pricing and usage normalization do not exist yet. |
| 34 | + |
| 35 | +## Product Decisions |
| 36 | + |
| 37 | +- Session cost includes all supported AI work in the active session: |
| 38 | + - transcription |
| 39 | + - question detection |
| 40 | + - answer generation |
| 41 | +- Cost display must be provider-specific. |
| 42 | +- If usage or pricing is incomplete for a provider or model, Helix shows usage-only or incomplete-cost state instead of invented numbers. |
| 43 | +- `Ask Now` must run from the Live Activity without opening the app UI. |
| 44 | +- `Ask Now` is interrupting: |
| 45 | + - if an old response is still streaming, Helix cancels it and starts a new analysis immediately |
| 46 | + - the newest ask wins |
| 47 | +- A separate `Open App` control must be available. |
| 48 | + |
| 49 | +## Recommended Architecture |
| 50 | + |
| 51 | +### 1. Keep One Assistant System |
| 52 | + |
| 53 | +Helix should keep a single authoritative conversation system in Dart. |
| 54 | + |
| 55 | +- `ConversationEngine` remains the source of truth for session state, question detection, and answer generation. |
| 56 | +- Provider routing remains in the existing provider stack. |
| 57 | +- The new feature adds orchestration and accounting layers around the existing pipeline rather than duplicating assistant behavior in Swift. |
| 58 | + |
| 59 | +### 2. Add a Dart Live Activity Service |
| 60 | + |
| 61 | +Create a Dart `LiveActivityService` responsible for: |
| 62 | + |
| 63 | +- subscribing to engine lifecycle, status, question detection, answer streaming, and provider/model state |
| 64 | +- maintaining a `LiveActivitySessionSnapshot` |
| 65 | +- bridging that snapshot to the native iOS methods that start, update, and stop the Live Activity |
| 66 | +- pinning the active question while the answer area updates independently |
| 67 | + |
| 68 | +This service should own the display state contract. The widget extension should render a prepared state object, not derive product logic itself. |
| 69 | + |
| 70 | +### 3. Add a Background Ask Path |
| 71 | + |
| 72 | +Add a native iOS `AppIntent` for `Ask Now` that runs from the Live Activity surface. |
| 73 | + |
| 74 | +- The intent must not open the UI. |
| 75 | +- The intent updates the Live Activity into `Analyzing`. |
| 76 | +- The intent hands off work to a headless Helix worker path that reuses the existing Dart pipeline. |
| 77 | +- The result flows back into the same Live Activity state update path. |
| 78 | + |
| 79 | +Add a separate `Open App` path using explicit deep linking. |
| 80 | + |
| 81 | +### 4. Keep Native Live Activity Code Thin |
| 82 | + |
| 83 | +The native iOS side should only: |
| 84 | + |
| 85 | +- start, update, and stop the activity |
| 86 | +- render SwiftUI lock-screen and Dynamic Island views |
| 87 | +- host the interaction intents |
| 88 | +- bridge background intent execution into the headless Helix worker |
| 89 | + |
| 90 | +It should not own provider pricing logic or independent question-answer logic. |
| 91 | + |
| 92 | +## Cost Accounting Model |
| 93 | + |
| 94 | +### Ledger Model |
| 95 | + |
| 96 | +Session cost should be tracked as a ledger, not a single number guessed from the last answer. |
| 97 | + |
| 98 | +Each ledger item stores: |
| 99 | + |
| 100 | +- `operationType` |
| 101 | +- `providerId` |
| 102 | +- `modelId` |
| 103 | +- `usage` |
| 104 | +- `pricingVersion` |
| 105 | +- `calculatedCost` |
| 106 | +- `currency` |
| 107 | +- `startedAt` |
| 108 | +- `completedAt` |
| 109 | +- `status` |
| 110 | + |
| 111 | +`operationType` has three values: |
| 112 | + |
| 113 | +- `transcription` |
| 114 | +- `questionDetection` |
| 115 | +- `answerGeneration` |
| 116 | + |
| 117 | +### Usage Rules |
| 118 | + |
| 119 | +- Usage must come from provider-native metadata when available. |
| 120 | +- Chat-style providers must emit final usage information after the request finishes. |
| 121 | +- Realtime transcription and realtime answer flows must parse usage from their completion events. |
| 122 | +- If a request is canceled, the ledger should keep the partial attempt with its actual known usage and cost status. |
| 123 | + |
| 124 | +### Pricing Rules |
| 125 | + |
| 126 | +- Pricing is provider-specific and model-specific. |
| 127 | +- Pricing should live in an app-level registry, not in UI code. |
| 128 | +- The registry must support different rate dimensions when relevant: |
| 129 | + - input |
| 130 | + - output |
| 131 | + - cached input |
| 132 | + - transcription |
| 133 | +- If usage exists but pricing is unknown, Helix shows incomplete cost state. |
| 134 | +- If pricing exists but usage is missing, Helix does not estimate from text length. |
| 135 | + |
| 136 | +### Provider Scope |
| 137 | + |
| 138 | +The design must support: |
| 139 | + |
| 140 | +- OpenAI |
| 141 | +- DeepSeek |
| 142 | +- Qwen |
| 143 | +- Zhipu |
| 144 | + |
| 145 | +## Live Activity UX |
| 146 | + |
| 147 | +### Lock-Screen Content |
| 148 | + |
| 149 | +While a session is active, the Live Activity shows: |
| 150 | + |
| 151 | +- session mode |
| 152 | +- provider and active model |
| 153 | +- elapsed session duration |
| 154 | +- cumulative session cost |
| 155 | +- active question |
| 156 | +- answer state and answer text |
| 157 | + |
| 158 | +### Question and Answer Behavior |
| 159 | + |
| 160 | +- Once a question is detected, it becomes the pinned active question. |
| 161 | +- The pinned question stays visible while the answer is generated and streamed. |
| 162 | +- The answer section updates independently underneath the question. |
| 163 | +- A newer detected question replaces the previous pinned question. |
| 164 | + |
| 165 | +### Ask Now Behavior |
| 166 | + |
| 167 | +- `Ask Now` immediately interrupts any in-flight response. |
| 168 | +- The current response generation is canceled. |
| 169 | +- The Live Activity switches to `Analyzing`. |
| 170 | +- Helix reuses the latest available transcript context and starts a fresh analysis cycle. |
| 171 | +- The previous answer stops streaming and is replaced by the new active turn when ready. |
| 172 | +- Cost ledger entries for interrupted attempts remain recorded, but the UI only presents the newest active turn. |
| 173 | + |
| 174 | +### Open App Behavior |
| 175 | + |
| 176 | +- `Open App` always foregrounds Helix explicitly. |
| 177 | +- `Open App` does not imply a new ask. |
| 178 | +- It is a navigation control, not an analysis control. |
| 179 | + |
| 180 | +### Error and Edge States |
| 181 | + |
| 182 | +- If there is not enough context for `Ask Now`, show a short `Need more context` state and return to listening. |
| 183 | +- If provider usage is unavailable, show a clear incomplete-cost state. |
| 184 | +- If background execution fails, show a compact error state and keep the session alive. |
| 185 | +- Only one response generation may be active at a time. |
| 186 | + |
| 187 | +## State Machine |
| 188 | + |
| 189 | +The Live Activity state machine should be semantic and minimal: |
| 190 | + |
| 191 | +- `Listening` |
| 192 | +- `Analyzing` |
| 193 | +- `Answering` |
| 194 | +- `ShowingResult` |
| 195 | +- `Error` |
| 196 | +- `Ended` |
| 197 | + |
| 198 | +State transitions: |
| 199 | + |
| 200 | +- session start -> `Listening` |
| 201 | +- detected question -> `Analyzing` |
| 202 | +- answer stream begins -> `Answering` |
| 203 | +- answer completes -> `ShowingResult` |
| 204 | +- `Ask Now` during `Answering` -> cancel old response, then `Analyzing` |
| 205 | +- session stop -> `Ended` |
| 206 | + |
| 207 | +## Testing Requirements |
| 208 | + |
| 209 | +- verify question pinning while answer text changes |
| 210 | +- verify ledger accumulation across transcription, question detection, and answer generation |
| 211 | +- verify provider-specific usage parsing and pricing lookup |
| 212 | +- verify interrupted `Ask Now` behavior cancels the old response and starts a new one |
| 213 | +- verify incomplete-cost display rules |
| 214 | +- verify `Open App` does not trigger analysis |
| 215 | +- verify session end behavior cleans up the activity correctly |
| 216 | + |
| 217 | +## Risks |
| 218 | + |
| 219 | +- Background intent handoff into a headless Flutter worker is the most complex integration point. |
| 220 | +- Provider usage shapes may differ enough that normalization needs careful tests. |
| 221 | +- Realtime flows may expose usage only at completion, which means cost display must tolerate short pending windows. |
| 222 | + |
| 223 | +## Success Criteria |
| 224 | + |
| 225 | +- A user in an active session can read the current question and streamed answer on the lock screen. |
| 226 | +- The displayed cost reflects provider-specific accumulated session cost across approved operation types. |
| 227 | +- `Ask Now` works without opening the app UI and interrupts old responses correctly. |
| 228 | +- `Open App` is available as a distinct control. |
| 229 | +- The feature works with OpenAI and the approved Chinese providers through a shared accounting abstraction. |
0 commit comments