Repository navigation
Recover native chat messages when terminal delivery is unconfirmed #12643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
60de278
fix(native-chat): guard sends during terminal prompts
brennanb2025 484bdd4
fix(mobile): preserve prompt refusal during draft clear
brennanb2025 470215a
Merge remote-tracking branch 'origin/main' into brennanb2025/fix-stal…
brennanb2025 b71a6fd
Merge remote-tracking branch 'origin/main' into brennanb2025/fix-stal…
brennanb2025 a18365f
fix(native-chat): recover unconfirmed prompt sends
brennanb2025 66154b1
fix(native-chat): own delivery recovery timers
brennanb2025 f267f8e
fix(native-chat): synchronize delivery refs after commit
brennanb2025 7988d8e
fix(native-chat): avoid false image delivery failures
brennanb2025 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
37 changes: 37 additions & 0 deletions
37
mobile/src/session/MobileNativeChatDeliveryRecoveryNotice.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { Pressable, Text, View } from 'react-native' | ||
| import { AlertTriangle } from 'lucide-react-native' | ||
| import { colors } from '../theme/mobile-theme' | ||
| import { styles } from './mobile-native-chat-view-styles' | ||
|
|
||
| export function MobileNativeChatDeliveryRecoveryNotice({ | ||
| onShowTerminal | ||
| }: { | ||
| onShowTerminal?: () => void | ||
| }): React.JSX.Element { | ||
| return ( | ||
| <View | ||
| style={styles.deliveryRecovery} | ||
| accessibilityRole="alert" | ||
| accessibilityLiveRegion="assertive" | ||
| > | ||
| <View style={styles.deliveryRecoveryHeader}> | ||
| <View style={styles.deliveryRecoveryIcon}> | ||
| <AlertTriangle size={16} color={colors.textMuted} strokeWidth={2} /> | ||
| </View> | ||
| <View style={styles.deliveryRecoveryCopy}> | ||
| <Text style={styles.deliveryRecoveryTitle}>Message wasn’t sent</Text> | ||
| <Text style={styles.deliveryRecoveryDescription}> | ||
| The terminal may be waiting for input. Your message is still in the composer. | ||
| </Text> | ||
| </View> | ||
| </View> | ||
| <Pressable | ||
| style={({ pressed }) => [styles.deliveryRecoveryButton, pressed && styles.pressed]} | ||
| onPress={onShowTerminal} | ||
| accessibilityRole="button" | ||
| > | ||
| <Text style={styles.deliveryRecoveryButtonText}>Show terminal</Text> | ||
| </Pressable> | ||
| </View> | ||
| ) | ||
| } |
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
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
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
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
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
117 changes: 117 additions & 0 deletions
117
mobile/src/session/mobile-native-chat-prompt-delivery.test.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { createHash } from 'node:crypto' | ||
| import { describe, expect, it } from 'vitest' | ||
| import { | ||
| assignMobileNativeChatPromptSubmissions, | ||
| mobileNativeChatPromptDigest, | ||
| mobileNativeChatPromptWasAcknowledged, | ||
| type MobileNativeChatDeliveryCheck | ||
| } from './mobile-native-chat-prompt-delivery' | ||
|
|
||
| describe('mobile native chat prompt delivery', () => { | ||
| it('matches the desktop full-prompt digest contract', () => { | ||
| const prompt = ` ${'long '.repeat(60)}\nemoji 🚀 ` | ||
| expect(mobileNativeChatPromptDigest(prompt)).toBe( | ||
| `sha256:${createHash('sha256').update(prompt).digest('hex')}` | ||
| ) | ||
| }) | ||
|
|
||
| it('requires an exact newer occurrence', () => { | ||
| const check: MobileNativeChatDeliveryCheck = { | ||
| pendingId: 'pending-1', | ||
| draftKey: 'draft', | ||
| pendingKey: 'pending', | ||
| text: 'hello', | ||
| normalizedText: 'hello', | ||
| baselineTailMessageId: null, | ||
| submittedAt: 20, | ||
| expectedDigest: mobileNativeChatPromptDigest('hello'), | ||
| baseline: { streamId: 'stream', sequence: 2, digest: 'old', receivedAt: 10 }, | ||
| deadline: null | ||
| } | ||
| expect( | ||
| mobileNativeChatPromptWasAcknowledged(check, [ | ||
| { streamId: 'stream', sequence: 2, digest: check.expectedDigest, receivedAt: 21 } | ||
| ]) | ||
| ).toBe(false) | ||
| expect( | ||
| mobileNativeChatPromptWasAcknowledged(check, [ | ||
| { streamId: 'stream', sequence: 3, digest: check.expectedDigest, receivedAt: 21 } | ||
| ]) | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it('accepts only a newer host occurrence from a newly observed stream', () => { | ||
| const check: MobileNativeChatDeliveryCheck = { | ||
| pendingId: 'pending-1', | ||
| draftKey: 'draft', | ||
| pendingKey: 'pending', | ||
| text: 'hello', | ||
| normalizedText: 'hello', | ||
| baselineTailMessageId: null, | ||
| submittedAt: 9_000, | ||
| expectedDigest: mobileNativeChatPromptDigest('hello'), | ||
| baseline: { streamId: 'old-stream', sequence: 8, digest: 'old', receivedAt: 8_000 }, | ||
| deadline: null | ||
| } | ||
|
|
||
| expect( | ||
| mobileNativeChatPromptWasAcknowledged(check, [ | ||
| { | ||
| streamId: 'new-stream', | ||
| sequence: 1, | ||
| digest: check.expectedDigest, | ||
| receivedAt: 100 | ||
| } | ||
| ]) | ||
| ).toBe(false) | ||
| expect( | ||
| mobileNativeChatPromptWasAcknowledged(check, [ | ||
| { | ||
| streamId: 'new-stream', | ||
| sequence: 2, | ||
| digest: check.expectedDigest, | ||
| receivedAt: 8_001 | ||
| } | ||
| ]) | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it('assigns one occurrence to only one repeated pending send', () => { | ||
| const digest = mobileNativeChatPromptDigest('repeat') | ||
| const base = { | ||
| draftKey: 'draft', | ||
| pendingKey: 'pending', | ||
| text: 'repeat', | ||
| normalizedText: 'repeat', | ||
| baselineTailMessageId: null, | ||
| submittedAt: 20, | ||
| expectedDigest: digest, | ||
| deadline: null | ||
| } | ||
| const occurrence = { | ||
| streamId: 'stream', | ||
| sequence: 1, | ||
| digest, | ||
| receivedAt: 21 | ||
| } | ||
|
|
||
| expect([ | ||
| ...assignMobileNativeChatPromptSubmissions( | ||
| [ | ||
| { ...base, pendingId: 'first' }, | ||
| { ...base, pendingId: 'second' } | ||
| ], | ||
| [occurrence] | ||
| ).keys() | ||
| ]).toEqual(['first']) | ||
| expect([ | ||
| ...assignMobileNativeChatPromptSubmissions( | ||
| [ | ||
| { ...base, pendingId: 'first', acknowledgedBy: occurrence }, | ||
| { ...base, pendingId: 'second' } | ||
| ], | ||
| [occurrence] | ||
| ).keys() | ||
| ]).toEqual(['first']) | ||
| }) | ||
| }) | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { sha256 } from '@noble/hashes/sha256' | ||
| import type { AgentPromptSubmissionOccurrence } from '../../../src/shared/agent-status-types' | ||
|
|
||
| export const MOBILE_NATIVE_CHAT_DELIVERY_CONFIRMATION_MS = 8_000 | ||
|
|
||
| export type MobileNativeChatDeliveryCheck = { | ||
| pendingId: string | ||
| draftKey: string | ||
| pendingKey: string | null | ||
| text: string | ||
| normalizedText: string | ||
| baselineTailMessageId: string | null | ||
| expectedDigest: string | ||
| baseline?: AgentPromptSubmissionOccurrence | ||
| deadline: ReturnType<typeof setTimeout> | null | ||
| acknowledgedBy?: AgentPromptSubmissionOccurrence | ||
| } | ||
|
|
||
| function bytesToHex(bytes: Uint8Array): string { | ||
| return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') | ||
| } | ||
|
|
||
| export function mobileNativeChatPromptDigest(text: string): string { | ||
| return `sha256:${bytesToHex(sha256(new TextEncoder().encode(text)))}` | ||
| } | ||
|
|
||
| export function mobileNativeChatPromptWasAcknowledged( | ||
| check: MobileNativeChatDeliveryCheck, | ||
| occurrences: readonly AgentPromptSubmissionOccurrence[] | ||
| ): boolean { | ||
| return occurrences.some((occurrence) => { | ||
| if (occurrence.digest !== check.expectedDigest) { | ||
| return false | ||
| } | ||
| if (!check.baseline) { | ||
| return true | ||
| } | ||
| if (occurrence.streamId === check.baseline.streamId) { | ||
| return occurrence.sequence > check.baseline.sequence | ||
| } | ||
| return occurrence.receivedAt > check.baseline.receivedAt | ||
| }) | ||
| } | ||
|
|
||
| function occurrenceKey(occurrence: AgentPromptSubmissionOccurrence): string { | ||
| return `${occurrence.streamId}\0${occurrence.sequence}` | ||
| } | ||
|
|
||
| /** Assign each hook occurrence to at most one pending send. */ | ||
| export function assignMobileNativeChatPromptSubmissions( | ||
| checks: readonly MobileNativeChatDeliveryCheck[], | ||
| occurrences: readonly AgentPromptSubmissionOccurrence[] | ||
| ): Map<string, AgentPromptSubmissionOccurrence> { | ||
| const assignments = new Map<string, AgentPromptSubmissionOccurrence>() | ||
| const claimed = new Set<string>() | ||
| for (const check of checks) { | ||
| if (check.acknowledgedBy) { | ||
| assignments.set(check.pendingId, check.acknowledgedBy) | ||
| claimed.add(occurrenceKey(check.acknowledgedBy)) | ||
| } | ||
| } | ||
| for (const check of checks) { | ||
| if (check.acknowledgedBy) { | ||
| continue | ||
| } | ||
| const occurrence = occurrences.find( | ||
| (item) => | ||
| !claimed.has(occurrenceKey(item)) && mobileNativeChatPromptWasAcknowledged(check, [item]) | ||
| ) | ||
| if (occurrence) { | ||
| assignments.set(check.pendingId, occurrence) | ||
| claimed.add(occurrenceKey(occurrence)) | ||
| } | ||
| } | ||
| return assignments | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C 3 "submittedAt|export type MobileNativeChatDeliveryCheck" mobile/src/session/mobile-native-chat-prompt-delivery.ts mobile/src/session/mobile-native-chat-prompt-delivery.test.tsRepository: stablyai/orca
Length of output: 2849
🏁 Script executed:
Repository: stablyai/orca
Length of output: 5794
Remove the stale
submittedAtproperties.MobileNativeChatDeliveryCheckdoes not definesubmittedAt, and the implementation does not read it. Keep the named type literals clean at lines 26, 51, and 87.