Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0c8a7f1
fix(native-chat): an older Orca keeps a chat with newer content read-…
brennanb2025 Oct 2, 2026
4a636db
feat(native-chat): a chat an older Orca keeps read-only says so befor…
brennanb2025 Oct 2, 2026
e40377c
fix(native-chat): only a newer build's value goes read-only; damage r…
brennanb2025 Oct 2, 2026
b9ed555
fix(native-chat): a read-only chat locks its composer and says why there
brennanb2025 Oct 2, 2026
fed6129
fix(native-chat): a read-only chat pages back through its history
brennanb2025 Oct 2, 2026
9745a4e
test(native-chat): an unknown key in a question entry is read and kept
brennanb2025 Oct 2, 2026
918d962
test(mobile): the read-only overlay test typechecks
brennanb2025 Oct 2, 2026
a6f5d8c
chore(native-chat): satisfy the changed-code gate in admission and th…
brennanb2025 Oct 2, 2026
f123cc9
Merge remote-tracking branch 'origin/main' into brennanb2025/journal-…
brennanb2025 Oct 2, 2026
acfb832
fix(native-chat): a new value inside a known block or goal arm reads …
brennanb2025 Oct 2, 2026
922bf0c
fix(native-chat): a read-only chat reconnects for a whole page, so an…
brennanb2025 Oct 2, 2026
80e8327
fix(native-chat): every desktop write control follows the one read-on…
brennanb2025 Oct 2, 2026
fff016e
fix(mobile): every phone write control follows the one read-only fact
brennanb2025 Oct 2, 2026
d7e0077
test(native-chat): the newest release hydrates a read-only chat and s…
brennanb2025 Oct 2, 2026
42ea1d3
test(native-chat): a read-only chat reads idle even with a send its p…
brennanb2025 Oct 2, 2026
d543a0a
test(native-chat): composer lock test without type assertions
brennanb2025 Oct 2, 2026
b9c9efa
fix(native-chat): round-3 read-only fixes
brennanb2025 Oct 2, 2026
6e96ad1
test(native-chat): count picker mounts without an effect
brennanb2025 Oct 2, 2026
eeb87aa
fix(native-chat): read-only keeps other failures' words and shuts ope…
brennanb2025 Oct 2, 2026
76013aa
test(native-chat): lock-lands picker test uses a typed surface
brennanb2025 Oct 2, 2026
7963cc9
fix(native-chat): a locked composer draws its reason
brennanb2025 Oct 2, 2026
7c17fc7
fix(native-chat): sync the locked-placeholder flag in an effect, not …
brennanb2025 Oct 2, 2026
3c2f7b2
fix(native-chat): a read-only chat is not offered for resume or count…
brennanb2025 Oct 2, 2026
112286f
fix(native-chat): a refused resume continuation keeps the refusal's r…
brennanb2025 Oct 2, 2026
6516578
fix(native-chat): a newer Orca's chat keeps its resume offer however …
brennanb2025 Oct 2, 2026
42a49ea
chore(native-chat): one import of the session wire types in the conti…
brennanb2025 Oct 2, 2026
1150b05
docs(native-chat): resume and read-only comments match the current rule
brennanb2025 Oct 2, 2026
3cca472
test(native-chat): a closed set of a journal row cannot change withou…
brennanb2025 Oct 2, 2026
c4857f8
feat(native-chat): a body or plan subject of a newer kind is kept and…
brennanb2025 Oct 2, 2026
dbf8a23
fix(native-chat): rewind narrows a retained body by its kind before n…
brennanb2025 Oct 2, 2026
c25d7cd
fix(native-chat): one chat that cannot open no longer stops the start…
brennanb2025 Oct 2, 2026
fe3a82a
fix(native-chat): a damaged chat fails to load and nothing is deleted
brennanb2025 Oct 2, 2026
6612b07
chore(native-chat): no reset or adapter left on the open's fixtures
brennanb2025 Oct 2, 2026
74f7afb
feat(native-chat): an approval of a newer Orca's subject kind can onl…
brennanb2025 Oct 2, 2026
55b9cfd
test(native-chat): the closed-set guard says a missed version bump fa…
brennanb2025 Oct 2, 2026
96d97c6
test(native-chat): the closed-set guard names what a missed version b…
brennanb2025 Oct 2, 2026
d821d07
fix(native-chat): a row the reader rejects is never written, and a ch…
brennanb2025 Oct 2, 2026
1c0f103
fix(native-chat): a newer Orca's approval subject is carried as it wa…
brennanb2025 Oct 2, 2026
0670e4b
fix(native-chat): a card's cancel sends nothing without a turn again,…
brennanb2025 Oct 2, 2026
6bfb9e8
fix(native-chat): a card this build cannot answer leaves the composer…
brennanb2025 Oct 3, 2026
fa08271
Merge remote-tracking branch 'origin/main' into brennanb2025/journal-…
brennanb2025 Oct 3, 2026
f369bef
feat(native-chat): a chat a newer Orca saved fails to load and says t…
brennanb2025 Oct 3, 2026
096953f
test(native-chat): a newer Orca's records open no chat, and a status …
brennanb2025 Oct 3, 2026
6ab0c2e
test(native-chat): the status fake's cast says what the feed reads
brennanb2025 Oct 3, 2026
26429a5
fix(native-chat): a chat's read that failed for good takes the whole …
brennanb2025 Oct 3, 2026
2440311
test(native-chat): the final-read-failure test names its reasons
brennanb2025 Oct 3, 2026
fcf216c
fix(native-chat): a newer Orca's chat keeps its restart offer, and it…
brennanb2025 Oct 3, 2026
732e1db
refactor(native-chat): the newer-Orca chats a restart listing skips l…
brennanb2025 Oct 3, 2026
fa2c9ad
fix(native-chat): a refused chat load keeps where it failed, so the l…
brennanb2025 Oct 3, 2026
cfccc5f
chore(native-chat): the reducer's window merge back where main has it…
brennanb2025 Oct 3, 2026
2cbfe20
test(agent-hooks): the rename test's journal fakes carry submissions,…
brennanb2025 Oct 3, 2026
72330f9
Merge remote-tracking branch 'origin/main' into brennanb2025/journal-…
brennanb2025 Oct 3, 2026
d7281e3
test(agent-hooks): the rename test's journal fakes say what the statu…
brennanb2025 Oct 3, 2026
5504adc
fix(native-chat): Dismiss all ends the restart offers this Orca lists…
brennanb2025 Oct 3, 2026
7dc43f5
Merge remote-tracking branch 'origin/main' into brennanb2025/journal-…
brennanb2025 Oct 5, 2026
5d9f109
test(native-chat): a resend against a newer Orca's store answers unkn…
brennanb2025 Oct 5, 2026
47fa092
Merge remote-tracking branch 'origin/main' into brennanb2025/journal-…
brennanb2025 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions config/reliability-gates.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@
"protection": "partial",
"owner": "agent-session-runtime",
"layer": "runtime-unit",
"surfaces": ["structured chat journal replay", "structured chat recovery"],
"surfaces": ["structured chat journal replay", "structured chat damaged history"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "remote-runtime"],
"coveredPlatforms": ["macos"],
Expand All @@ -648,25 +648,36 @@
"motivatingLinks": [
"https://github.com/stablyai/orca/blob/main/src/main/native-chat/agent-session-journal/journal-open.ts"
],
"invariant": "Replay preserves latest revisions, original item order, fences, aliases, submissions, repair precedence, read-only schema latching and cursor cleanup while retaining live items rather than all historical bodies.",
"oracle": "Replay 2,048 16 KiB revisions into one latest item with less than 8 MiB sampled live heap growth; preserve prefix and future-schema latching after a gap, malformed suffix repair precedence, and hold no SQLite read snapshot across reduction (a mid-replay checkpoint is not busy). Existing journal and subscriber tests cover replayed content and recovery.",
"invariant": "Replay preserves latest revisions, original item order, fences, aliases, submissions, a newer schema winning over damage and cursor cleanup while retaining live items rather than all historical bodies; damage fails the load and deletes nothing.",
"oracle": "Replay 2,048 16 KiB revisions into one latest item with less than 8 MiB sampled live heap growth; name a future-schema row read past a gap, name the first damage, fail a damaged load with every row kept, and hold no SQLite read snapshot across reduction (a mid-replay checkpoint is not busy). Existing journal and subscriber tests cover replayed content.",
"commands": [
// As the 2026-09-11 evidence run ran it; the recovery test it names went with the read-time repair.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd -t f 'agent-session-journal-recovery' src
fd -t f 'journal-damage.test.ts|journal-streaming-replay.test.ts' src

Repository: stablyai/orca

Length of output: 293


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- current gate ---'
sed -n '630,675p' config/reliability-gates.jsonc
printf '%s\n' '--- merge-base diff ---'
git diff --unified=8 62451920ed161a342dc870f93a570d4cd126428b 5504adc2452aa69c76949047e283804827d7719e -- config/reliability-gates.jsonc | sed -n '1,220p'
printf '%s\n' '--- vitest command and relevant notes ---'
rg -n -C 3 'vitest|agent-session-journal-recovery\.test\.ts|journal-damage\.test\.ts|missing named|filter' package.json config src/main/native-chat/agent-session-journal --glob '!**/*.snap' | head -240
printf '%s\n' '--- recovery test files tracked at head ---'
git ls-tree -r --name-only 5504adc2452aa69c76949047e283804827d7719e -- src | rg 'agent-session-journal-recovery|journal-(damage|streaming-replay)\.test\.ts' || true
printf '%s\n' '--- base gate ---'
git show 62451920ed161a342dc870f93a570d4cd126428b:config/reliability-gates.jsonc | sed -n '630,675p'

Repository: stablyai/orca

Length of output: 38244


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- gate command consumers ---'
rg -n -C 4 'commands|evidenceRuns|reliability-gates' config/scripts/check-reliability-gates.mjs config --glob '*.md' --glob '*.mjs' --glob '*.jsonc' | head -240
printf '%s\n' '--- gate checker ---'
sed -n '1,240p' config/scripts/check-reliability-gates.mjs
printf '%s\n' '--- project notes about missing test filters ---'
rg -n -i -C 3 'missing (named )?(test|file)|test path|file filter|vitest.*filter|filter.*vitest|silently.*(skip|run)|no test files' config/reliability-gates.jsonc config --glob '*.md' --glob '*.jsonc' --glob '*.mjs' | head -200

Repository: stablyai/orca

Length of output: 41686


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- exact stale-path guidance in reliability gates ---'
rg -n -i -C 4 'silently (skip|run)|missing (named )?(file|path)|stale (command|path)|deleted.*(command|path)|command.*(deleted|stale)' config/reliability-gates.jsonc
printf '%s\n' '--- checker command and evidence rules ---'
sed -n '110,210p' config/scripts/check-reliability-gates.mjs

Repository: stablyai/orca

Length of output: 41333


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- gate commands and evidence runs ---'
nl -ba config/reliability-gates.jsonc | sed -n '650,730p'
printf '%s\n' '--- repository precedent ---'
nl -ba config/reliability-gates.jsonc | sed -n '5261,5264p'
printf '%s\n' '--- checker requirements ---'
nl -ba config/scripts/check-reliability-gates.mjs | sed -n '122,130p;196,202p'

Repository: stablyai/orca

Length of output: 17232


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- recovery-test path at merge base ---'
git ls-tree -r --name-only 62451920ed161a342dc870f93a570d4cd126428b -- src | rg 'agent-session-journal-recovery\.test\.ts' || true
printf '%s\n' '--- recovery-test path at reviewed head ---'
git ls-tree -r --name-only 5504adc2452aa69c76949047e283804827d7719e -- src | rg 'agent-session-journal-recovery\.test\.ts' || true
printf '%s\n' '--- PR status for the path and journal tests ---'
git diff --name-status 62451920ed161a342dc870f93a570d4cd126428b 5504adc2452aa69c76949047e283804827d7719e -- src/main/native-chat/agent-session-wire src/main/native-chat/agent-session-journal | rg 'recovery|corruption|damage|streaming' || true

Repository: stablyai/orca

Length of output: 746


Retire the command that names the removed recovery test.

agent-session-journal-recovery.test.ts is deleted in this PR. The 2026-09-11 command can report success without running that path. Remove that command and its matching evidence run; keep the 2026-10-02 run as current evidence.

Suggested cleanup
       "commands": [
-        // As the 2026-09-11 evidence run ran it; the recovery test it names went with the read-time repair.
-        "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
         "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts",
...
       "evidenceRuns": [
-        {
-          "date": "2026-09-11",
-          "runner": "local",
-          "platform": "macos",
-          "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
-          "result": "passed",
-          "durationSeconds": 7.71,
-          "summary": "245 tests passed across 22 files. Retained-heap oracle fails on baseline at 68.6 MB and passes under 8 MiB with streaming; gap/schema and cursor-cleanup assertions passed."
-        },
         {
           "date": "2026-10-02",

"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts"
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts",
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts src/main/native-chat/agent-session-journal/journal-damage.test.ts"
],
"testFiles": [
"src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts",
"src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts"
"src/main/native-chat/agent-session-journal/journal-damage.test.ts"
],
"assertionRefs": [
{
"file": "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts",
"assertions": [
"releases superseded revision bodies while reducing a long journal",
"holds no read snapshot while reducing, so a checkpoint can pass mid-replay",
"keeps the prefix but latches read-only for a future row beyond a gap",
"keeps gap repair precedence when a later row is malformed",
"rejects an unanchored prefix before a later gap"
"reads past a gap to a future row, which names the newer row and no damage",
"names the first damage when a later row is malformed too",
"names a missing epoch row before a later gap"
]
},
{
"file": "src/main/native-chat/agent-session-journal/journal-damage.test.ts",
"assertions": [
"fails to load and keeps every row: %s",
"reads past damage to a newer build's row, which fails the load as a newer Orca's instead",
"is refused when it is written, and the chat still loads with every other row",
"is founded afresh on open, with nothing deleted, and takes writes"
]
}
],
Expand All @@ -679,6 +690,15 @@
"result": "passed",
"durationSeconds": 7.71,
"summary": "245 tests passed across 22 files. Retained-heap oracle fails on baseline at 68.6 MB and passes under 8 MiB with streaming; gap/schema and cursor-cleanup assertions passed."
},
{
"date": "2026-10-02",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts",
"result": "passed",
"durationSeconds": 5.56,
"summary": "450 tests passed across 42 files, run as the vitest invocation the test script makes. Damage fails the load with every row kept; the retained-heap, newer-row-past-a-gap and cursor-cleanup assertions passed."
}
],
"runtimeBudget": {
Expand Down
1 change: 1 addition & 0 deletions mobile/src/session/MobileNativeChatOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function MobileNativeChatOverlay({
folded={folded}
status={session.status}
error={session.error}
readFailedFinally={session.readFailedFinally === true}
agent={controller.nativeChatAgent}
agentWorking={controller.nativeChatAgentWorking}
canStop={controller.nativeChatCanStop}
Expand Down
52 changes: 52 additions & 0 deletions mobile/src/session/MobileNativeChatPermission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,56 @@ describe('MobileNativeChatPermission', () => {
expect(content.findAllByProps({ children: 'raw json that must not be shown' })).toHaveLength(0)
expect(actions.findAllByProps({ children: 'Approve plan' })).toHaveLength(1)
})

describe("a subject of a kind this build cannot draw: a newer Orca's", () => {
const NEEDS_NEWER_ORCA = 'This request needs a newer version of Orca.'

async function renderNewer(detail: string | undefined) {
const onRespond = vi.fn(async () => true)
const onCancel = vi.fn(async () => true)
await act(async () => {
renderer = create(
createElement(MobileNativeChatPermission, {
permission: {
title: 'Review proposed change',
...(detail ? { detail } : {}),
// A subject kind a newer build wrote; this build draws only plans.
subject: JSON.parse('{"kind":"diff","path":"a.ts"}'),
prompt: { itemId: 'approval-1', expectedRevision: 2 },
options: [
{ label: 'Approve', send: 'allow' },
{ label: 'Deny', send: 'deny' }
]
},
onRespond,
onCancel
})
)
})
const actions = renderer!.root.findByProps({ testID: 'native-chat-approval-actions' })
return { onRespond, onCancel, options: actions.findAllByType('Pressable') }
}

it('with no detail: says so, answers nothing, and only its cancel reaches the host', async () => {
const { onRespond, onCancel, options } = await renderNewer(undefined)
expect(
renderer!.root.findByProps({ testID: 'native-chat-approval-needs-newer-orca' }).props
.children
).toBe(NEEDS_NEWER_ORCA)
expect(options).toHaveLength(2)
expect(options.every((option) => option.props.disabled === true)).toBe(true)
const cancel = renderer!.root.findByProps({ accessibilityLabel: 'Cancel' })
expect(cancel.props.disabled).toBe(false)
await act(async () => cancel.props.onPress())
expect(onCancel).toHaveBeenCalledWith({ itemId: 'approval-1', expectedRevision: 2 })
expect(onRespond).not.toHaveBeenCalled()
})

it('with a detail: shows it, and still approves nothing', async () => {
const { options } = await renderNewer('# Release')
const content = renderer!.root.findByProps({ testID: 'native-chat-approval-content' })
expect(content.findAllByProps({ children: '# Release' })).toHaveLength(1)
expect(options.every((option) => option.props.disabled === true)).toBe(true)
})
})
})
21 changes: 18 additions & 3 deletions mobile/src/session/MobileNativeChatPermission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { ShieldQuestion, X } from 'lucide-react-native'
import { MobileMarkdown } from '../components/MobileMarkdown'
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
import {
isNewerApprovalSubject,
isPlanApprovalSubject
} from '../../../src/shared/agent-session-approval-subject'
import type { MobileChatPermission } from './mobile-native-chat-permission'

// Renders a detected agent permission ask as a card with tappable options.
Expand All @@ -19,6 +23,8 @@ function MobileNativeChatPermissionImpl({
}): React.JSX.Element {
const [submitting, setSubmitting] = useState(false)
const submittingRef = useRef(false)
// A newer Orca's subject: its detail is shown, and only the card's cancel answers.
const newerSubject = isNewerApprovalSubject(permission.subject)
const hasContext = Boolean(
permission.description ||
permission.decisionReason ||
Expand Down Expand Up @@ -93,7 +99,7 @@ function MobileNativeChatPermissionImpl({
{permission.matchedAskRule.source}
</Text>
) : null}
{permission.subject?.kind === 'plan' ? (
{isPlanApprovalSubject(permission.subject) ? (
<View>
<MobileMarkdown content={permission.subject.text} />
{permission.subject.filePath ? (
Expand All @@ -103,6 +109,11 @@ function MobileNativeChatPermissionImpl({
) : permission.detail ? (
<Text style={styles.detail}>{permission.detail}</Text>
) : null}
{newerSubject ? (
<Text testID="native-chat-approval-needs-newer-orca" style={styles.detail}>
This request needs a newer version of Orca.
</Text>
) : null}
</ScrollView>
) : null}
<View testID="native-chat-approval-actions" style={styles.options}>
Expand All @@ -114,11 +125,12 @@ function MobileNativeChatPermissionImpl({
style={({ pressed }) => [
styles.option,
isPrimary ? styles.optionPrimary : styles.optionSecondary,
pressed && !submitting && styles.optionPressed
pressed && !submitting && styles.optionPressed,
newerSubject && styles.disabled
]}
hitSlop={6}
onPress={() => respond(option.send)}
disabled={submitting}
disabled={submitting || newerSubject}
>
<Text style={[styles.optionText, isPrimary && styles.optionTextPrimary]}>
{option.label}
Expand Down Expand Up @@ -212,6 +224,9 @@ const styles = StyleSheet.create({
optionPressed: {
opacity: 0.7
},
disabled: {
opacity: 0.5
},
optionText: {
color: colors.textPrimary,
fontSize: typography.bodySize,
Expand Down
36 changes: 36 additions & 0 deletions mobile/src/session/MobileNativeChatView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ type Overrides = {
keyboardInset?: number
hasMore?: boolean
onLoadEarlier?: () => void
status?: Parameters<typeof MobileNativeChatView>[0]['status']
error?: string
readFailedFinally?: boolean
}

function assistantTurn(id: string, text: string): NativeChatMessage {
Expand Down Expand Up @@ -214,6 +217,39 @@ describe('MobileNativeChatView', () => {
})
}

// A read no retry gets past takes the whole pane, even over a transcript already on screen: its
// words alone, and nothing that could only be refused again.
it('leaves only the words of a read that failed for good, over a loaded transcript', async () => {
const loaded = [assistantTurn('m1', 'earlier reply')]
const words = 'This chat was saved by a newer Orca. Update Orca to open it.'
const shown = (text: string) =>
renderer!.root.findAll((node) => node.type === 'Text' && node.props.children === text)
const composers = () => renderer!.root.findAll((node) => node.type === 'Composer')
const lists = () => renderer!.root.findAll((node) => node.type === 'FlatList')
const failure = {
messages: loaded,
folded: loaded,
status: 'error' as const,
error: words,
canStop: true,
permission: { title: 'Approve?', options: [{ label: 'Allow', send: '1' }] },
// A resend answered unknown says nothing beside the read's words.
sendErrorMessage: 'Message unconfirmed — check chat before retrying'
}
await render({ ...failure, readFailedFinally: true })
expect(shown(words)).toHaveLength(1)
expect(composers()).toHaveLength(0)
expect(lists()).toHaveLength(0)
expect(banners()).toHaveLength(0)
expect(renderer!.root.findAllByProps({ accessibilityLabel: 'Stop the agent' })).toHaveLength(0)

// A failure that can clear keeps the transcript and the composer.
await update({ ...failure, error: "Orca couldn't open this chat's history right now." })
expect(listIds()).toEqual(['m1'])
expect(composers()).toHaveLength(1)
expect(banners()).toHaveLength(1)
})

it('renders the route-reported failure verbatim', async () => {
await render({ sendErrorMessage: 'Permission reply failed' })

Expand Down
24 changes: 16 additions & 8 deletions mobile/src/session/MobileNativeChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type Props = MobileQueuedSlotProps & {
folded: NativeChatMessage[]
status: MobileNativeChatStatus
error?: string
/** The read failed for good (damage, a newer Orca's chat): its error takes the whole pane. */
readFailedFinally?: boolean
/** Resolved agent for this chat; names the empty-state copy (desktop parity). */
agent?: string | null
agentWorking?: boolean
Expand Down Expand Up @@ -145,6 +147,7 @@ export function MobileNativeChatView({
folded,
status,
error,
readFailedFinally = false,
agent,
agentWorking,
canStop = agentWorking,
Expand Down Expand Up @@ -307,6 +310,18 @@ export function MobileNativeChatView({
const showLoading = status === 'loading' && messages.length === 0

const lockReason = useSettledMobileNativeChatInputLock(inputLockReason)
const emptyStateView = emptyState ? (
<View style={styles.center}>
<Text style={styles.emptyTitle}>{emptyState.title}</Text>
<Text style={styles.emptySubtitle}>{emptyState.subtitle}</Text>
</View>
) : null

// Whatever was already on screen: nothing here can act on a chat that cannot load, and its words
// say why once, as a fresh open's do.
if (readFailedFinally && emptyStateView) {
return <View style={[styles.root, { paddingBottom: bottomPad }]}>{emptyStateView}</View>
}

return (
<View style={[styles.root, { paddingBottom: bottomPad }]}>
Expand Down Expand Up @@ -354,14 +369,7 @@ export function MobileNativeChatView({
turns.waitingRows,
renderItem
)}
ListEmptyComponent={
emptyState ? (
<View style={styles.center}>
<Text style={styles.emptyTitle}>{emptyState.title}</Text>
<Text style={styles.emptySubtitle}>{emptyState.subtitle}</Text>
</View>
) : null
}
ListEmptyComponent={emptyStateView}
/>
</GestureDetector>
{/* Jump-to-latest control. */}
Expand Down
11 changes: 8 additions & 3 deletions mobile/src/session/mobile-structured-agent-session-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,18 @@ class AgentSessionRpcResponseError extends Error {
}
}

/** The refusal a failed read met, from a thrown error or a stream's error frame. */
export function agentSessionReadFailureRefusal(failure: unknown) {
return readAgentSessionErrorRefusal(
typeof failure === 'object' && failure !== null && 'error' in failure ? failure.error : failure
)
}

/** A failed read of a chat's history as the pane shows it, from a thrown error or a stream's error
* frame (`{ message, error }`): a thrown refusal's message is its bare code, so its words come
* from the refusal in the error's data. */
export function agentSessionReadFailureText(failure: unknown): string {
const refusal = readAgentSessionErrorRefusal(
typeof failure === 'object' && failure !== null && 'error' in failure ? failure.error : failure
)
const refusal = agentSessionReadFailureRefusal(failure)
if (refusal) {
return agentSessionWriteNoticeEnglish(
agentSessionWriteNoticeParts(agentSessionRefusalFailure(refusal), 'read-history')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import {
agentSessionReadFailureRefusal,
agentSessionReadFailureText,
callAgentSession,
requestStructuredAgentSessionMutation
Expand Down Expand Up @@ -43,6 +44,18 @@ describe('a refusal the host threw', () => {
expect(agentSessionReadFailureText({ type: 'error', message: 'Connection interrupted' })).toBe(
'Connection interrupted'
)
// The refusal itself rides beside the words, so the view can tell a failure no retry gets past.
expect(
agentSessionReadFailureRefusal({
type: 'error',
message: THROWN_JOURNAL_REFUSAL.message,
error: THROWN_JOURNAL_REFUSAL
})
).toEqual(THROWN_JOURNAL_REFUSAL.data.refusal)
expect(agentSessionReadFailureRefusal(thrown)).toEqual(THROWN_JOURNAL_REFUSAL.data.refusal)
expect(
agentSessionReadFailureRefusal({ type: 'error', message: 'Connection interrupted' })
).toBeUndefined()
})

it("fails a write with the refusal's words, not as unconfirmed", async () => {
Expand Down
3 changes: 3 additions & 0 deletions mobile/src/session/use-mobile-native-chat-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export type MobileNativeChatSession = {
* wait for this to clear. */
transcriptLoading: boolean
error?: string
/** The read failed for good (damage, or a newer Orca's chat): the error says why, once, and
* there is nothing to send into. */
readFailedFinally?: boolean
/** True when an older page may exist (the last read filled the window). */
hasMore: boolean
/** Whether an older-history page is currently loading. */
Expand Down
Loading
Loading