diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts index b1a3d8815fe..9d356578fd7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts @@ -216,9 +216,10 @@ describe('the restart-resume surface', () => { expect(snapshot).toHaveBeenCalledTimes(1) }) - // The structural guarantee behind "the checkbox can never continue": the reconnect path contains - // no send at all, so no setting, and no automatic launch, can turn it into a continuation. - it('never sends a message when reconnecting', async () => { + // Reattaching stays a send-free operation: continuing is layered on top of it, never something + // `resume` does by itself. It is no longer a guarantee about SETTINGS, though — an opted-in + // launch now calls `continueAfterRestart` instead of this. + it('never sends a message when reattaching', async () => { const { restartResume, held, sent } = surface({}) await restartResume.resume(undefined, 'modal') @@ -227,7 +228,7 @@ describe('the restart-resume surface', () => { expect(sent).toEqual([]) }) - it('reconnects and then sends exactly one continuation carrying the shared message', async () => { + it('reattaches and then sends exactly one continuation carrying the shared message', async () => { const { restartResume, held, sent } = surface({}) const result = await restartResume.continueAfterRestart(undefined, 'modal') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts index 5d77b82019d..447d59a5609 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts @@ -1,10 +1,11 @@ -// Asking an interrupted agent to carry on — always, and only, on a deliberate user action. +// Asking an interrupted agent to carry on, on the user's opt-in. // -// Reconnecting and continuing are SEPARATE operations. Reconnect reattaches and sends nothing; this -// adds one message on top of a reconnect, and only when the user pressed a control that says so. -// The automatic-reconnect setting cannot reach this module — the resume surface it calls has no -// send in it at all — so "the checkbox never continues" is structural rather than wiring -// discipline. +// Reattaching and continuing are still SEPARATE operations: `resume` reattaches and sends nothing; +// this adds one message on top of it. What changed is WHO may ask. Resuming from the restart prompt +// comes here, and so does an opted-in launch, so this module is no longer unreachable from a +// setting — do not restate that old guarantee. It is acceptable because the work is the user's own, +// the message asks the agent to verify its last action before repeating it, and the launch toast +// reports what happened. import type { AgentJournalMessageItem } from '../../../shared/agent-session-journal-types' import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire' diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts index c8034b0af17..373235b4372 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts @@ -40,10 +40,13 @@ export type StructuredAgentSessionRestartResumeSurfaces = { /** The resume-capable hold; see the runner for why a hold and not a send. */ hold: (sessionId: string, holderId: string) => Promise release: (sessionId: string, holderId: string) => void - /** The host's own send. Reached ONLY from `continueAfterRestart` — `resume` never calls it, which - * is what makes "automatic reconnect can never continue" structural. + /** The host's own send. Reached ONLY from `continueAfterRestart`; `resume` still never calls it, + * so reattaching on its own sends nothing. That is no longer a guarantee about SETTINGS, though: + * a launch the user opted into calls `continueAfterRestart` directly, which is acceptable + * because the work is the user's own, the message asks the agent to verify its last action + * before repeating it, and the launch toast reports what happened. * - * Typed against the wire result rather than a hand-written subset: an narrower local shape hid + * Typed against the wire result rather than a hand-written subset: a narrower local shape hid * `value.submission` here once, and the continuation reads it. */ send: (input: { envelope: AgentSessionMutationEnvelope @@ -70,7 +73,8 @@ export type StructuredAgentSessionRestartResume = { sessionIds: readonly string[] | undefined, owner: string ) => Promise - /** Reconnect, then ask each reconnected agent to carry on. A deliberate user action only. */ + /** Reattach, then ask each reattached agent to carry on — what the UI calls resuming, whether + * the user pressed it or opted into it happening at launch. */ continueAfterRestart: ( sessionIds: readonly string[] | undefined, owner: string @@ -163,9 +167,9 @@ export function createStructuredAgentSessionRestartResume( ) } - /** Reconnect first, then send. Continuation is a message ON TOP of a reconnect and reuses every + /** Reattach first, then send. Continuation is a message ON TOP of a reattach and reuses every * guard the resume path applies — eligibility, the admission gate, staggering, consume-once — - * rather than re-deriving any of them. A session that did not reconnect is never sent to. */ + * rather than re-deriving any of them. A session that did not reattach is never sent to. */ const continueAfterRestart = async ( sessionIds: readonly string[] | undefined, owner: string diff --git a/src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts b/src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts index ded6f7389a1..b6ae6a8f4f4 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts @@ -32,9 +32,9 @@ export const STRUCTURED_AGENT_SESSION_RESTART_RESUME_METHODS = [ } }), defineMethod({ - // Reconnect AND ask each reconnected agent to carry on. Separate from `restartResume` on - // purpose: that method sends nothing, and the automatic-reconnect setting only ever calls it, - // so no configuration can reach this one. + // Reattach AND ask each reattached agent to carry on — what the desktop prompt now calls + // resuming, and what an opted-in launch runs without asking. Still a separate method from + // `restartResume`, which sends nothing, but no longer one that only a button can reach. name: 'agentSession.restartContinue', params: RestartResumeParams, handler: async (params, ctx) => { @@ -47,6 +47,9 @@ export const STRUCTURED_AGENT_SESSION_RESTART_RESUME_METHODS = [ } }), defineMethod({ + // Reattach only, no send. The desktop prompt stopped calling this once its single action became + // resume-and-continue, but it stays: it is a published wire method, and its absence is what an + // older or non-desktop client would be met with. name: 'agentSession.restartResume', params: RestartResumeParams, handler: async (params, ctx) => { diff --git a/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx b/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx index f5f87cf593b..a6dba8394ae 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx @@ -52,7 +52,7 @@ export function ResumeCandidateRow({ className="shrink-0" aria-label={translate( 'auto.components.NativeChatResumeOnRestartModal.selectAgent', - 'Reconnect {{value0}} chat "{{value1}}" in {{value2}}', + 'Resume {{value0}} chat "{{value1}}" in {{value2}}', { value0: agentLabel, value1: title, value2: workspaceName } )} /> diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx b/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx index 52ccc901e40..0d0cc6bd114 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx @@ -85,10 +85,7 @@ afterEach(() => { consumeNativeChatResumeOnRestartDialogRequest() }) -it.each([ - ['Reconnect 1', 'agentSession.restartResume'], - ['Reconnect and continue', 'agentSession.restartContinue'] -])('keeps next-launch preference out of the current %s action', async (label, method) => { +it('keeps next-launch preference out of the current resume action', async () => { const action = Promise.withResolvers() rpc.mockImplementation(async (_target, calledMethod) => { if (calledMethod === 'agentSession.restartResumable') { @@ -99,30 +96,47 @@ it.each([ await act(async () => root.render()) await act(async () => checkbox(1).click()) await act(async () => checkbox(2).click()) - await act(async () => button(label).click()) + await act(async () => button('Resume 1').click()) expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true) expect(rpc.mock.calls.map((call) => [call[1], call[2]])).toEqual([ ['agentSession.restartResumable', undefined], - [method, { sessionIds: ['a'] }] + ['agentSession.restartContinue', { sessionIds: ['a'] }] ]) await act(async () => action.resolve({ - results: [{ sessionId: 'a', outcome: 'resumed' }], + resumed: [{ sessionId: 'a', outcome: 'resumed' }], continued: [{ sessionId: 'a', outcome: 'continued' }] }) ) expect(rpc).toHaveBeenCalledTimes(2) }) +// One primary action and one way out of it. The vacuous plain-reconnect button, the Not now button +// and the "what gets sent" popover are gone; the body copy carries the transparency now. +it('offers exactly Dismiss all and the resume action', async () => { + rpc.mockImplementation(async (_target, method) => + method === 'agentSession.restartResumable' ? { sessions: offered } : { results: [] } + ) + await act(async () => root.render()) + // Row and preference checkboxes are buttons too; the controls are what is left after them. + const controls = document.querySelectorAll('[role="dialog"] button:not([role="checkbox"])') + expect([...controls].map((entry) => entry.textContent?.trim())).toEqual([ + 'Dismiss all', + 'Resume all', + 'Close' + ]) +}) + // Snoozing saves the preference like every other way out of the dialog, and calls NOTHING: the -// offer is the host's and stays exactly where it was. -it('keeps Not now available through the status-bar offer', async () => { +// offer is the host's and stays exactly where it was. Closing the dialog is the only snooze left +// now that Not now is gone, so it has to keep doing all of that. +it('snoozes to the status-bar offer when the dialog is closed', async () => { rpc.mockImplementation(async (_target, method) => method === 'agentSession.restartResumable' ? { sessions: offered } : { results: [] } ) await act(async () => root.render()) await act(async () => checkbox(2).click()) - await act(async () => button('Not now').click()) + await act(async () => button('Close').click()) expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true) expect(rpc.mock.calls.map((call) => call[1])).toEqual(['agentSession.restartResumable']) expect(offerIds()).toEqual(['a', 'b']) @@ -169,9 +183,9 @@ it('saves Don’t ask again when the offer is dismissed outright', async () => { expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true) }) -// Reopening must ask the host again, never replay the launch answer: the chats already reconnected -// are gone from its list, and offering them back earns the user a refusal. -it('never re-offers a reconnected chat when the status entry reopens the dialog', async () => { +// Reopening must ask the host again, never replay the launch answer: the chats already resumed are +// gone from its list, and offering them back earns the user a refusal. +it('never re-offers a resumed chat when the status entry reopens the dialog', async () => { let remaining = offered rpc.mockImplementation(async (_target, method) => { if (method === 'agentSession.restartResumable') { @@ -179,7 +193,10 @@ it('never re-offers a reconnected chat when the status entry reopens the dialog' } // The host spends the claim it settled, so its next answer no longer names that chat. remaining = remaining.filter((candidate) => candidate.sessionId !== 'a') - return { results: [{ sessionId: 'a', outcome: 'resumed' }] } + return { + resumed: [{ sessionId: 'a', outcome: 'resumed' }], + continued: [{ sessionId: 'a', outcome: 'continued' }] + } }) await act(async () => root.render( @@ -190,23 +207,22 @@ it('never re-offers a reconnected chat when the status entry reopens the dialog' ) ) await act(async () => checkbox(1).click()) - await act(async () => button('Reconnect 1').click()) + await act(async () => button('Resume 1').click()) expect(offerIds()).toEqual(['b']) - - await act(async () => button('Not now').click()) + // The action closes the dialog itself; the status entry is the way back to what is left. expect(document.querySelector('[role="dialog"]')).toBeNull() - await act(async () => button('1 chat to reconnect').click()) + await act(async () => button('1 chat to resume').click()) expect(document.querySelector('[role="dialog"]')).not.toBeNull() expect(offerIds()).toEqual(['b']) - // One offered row plus the preference box — never the reconnected chat again. + // One offered row plus the preference box — never the resumed chat again. expect(document.querySelectorAll('[role="checkbox"]')).toHaveLength(2) }) -// Continuing spends the same claims reconnecting does, so the offer has to shrink with it. A count -// left standing over chats the host already handed back sends the user to a status entry that -// re-reads, finds nothing, and does nothing. -it('settles the offer for the chats a continuation reconnected', async () => { +// Resuming spends the host's claims, so the offer has to shrink with it. A count left standing over +// chats the host already handed back sends the user to a status entry that re-reads, finds nothing, +// and does nothing. +it('settles the offer for the chats a resume reattached', async () => { rpc.mockImplementation(async (_target, method) => method === 'agentSession.restartResumable' ? { sessions: offered } @@ -217,11 +233,13 @@ it('settles the offer for the chats a continuation reconnected', async () => { ) await act(async () => root.render()) await act(async () => checkbox(1).click()) - await act(async () => button('Reconnect and continue').click()) + await act(async () => button('Resume 1').click()) expect(offerIds()).toEqual(['b']) }) -it('automatically reconnects once when the launch begins opted in', async () => { +// The point of the preference. "Resume automatically" has to run the action the button runs — +// reattach AND ask each agent to carry on — or it recovers nothing that opening the chat would not. +it('resumes and continues once when the launch begins opted in', async () => { useAppStore.setState({ settings: { ...getDefaultSettings(''), @@ -230,7 +248,12 @@ it('automatically reconnects once when the launch begins opted in', async () => } }) rpc.mockImplementation(async (_target, method) => - method === 'agentSession.restartResumable' ? { sessions: offered } : { results: [] } + method === 'agentSession.restartResumable' + ? { sessions: offered } + : { + resumed: offered.map(({ sessionId }) => ({ sessionId, outcome: 'resumed' })), + continued: offered.map(({ sessionId }) => ({ sessionId, outcome: 'continued' })) + } ) await act(async () => root.render( @@ -253,11 +276,34 @@ it('automatically reconnects once when the launch begins opted in', async () => ) expect(rpc.mock.calls.map((call) => [call[1], call[2]])).toEqual([ ['agentSession.restartResumable', undefined], - ['agentSession.restartResume', {}] + ['agentSession.restartContinue', {}] ]) + // Automatic is never silent, and the offer shrinks by what the host says it reattached. + expect(toast).toHaveBeenCalledWith('Resumed 2 chats and asked them to continue') + expect(offerIds()).toEqual([]) expect(document.querySelector('[role="dialog"]')).toBeNull() }) +// An opted-in launch reports the chats the host would not take, exactly as the button does. +it('reports refused and newly ineligible chats on an opted-in launch', async () => { + useAppStore.setState({ + settings: { + ...getDefaultSettings(''), + experimentalStructuredNativeChat: true, + nativeChatResumeWorkOnRestart: true + } + }) + rpc.mockImplementation(async (_target, method) => + method === 'agentSession.restartResumable' + ? { sessions: offered } + : { resumed: [], continued: [{ sessionId: 'a', outcome: 'refused' }] } + ) + await act(async () => root.render()) + expect(toast).toHaveBeenCalledWith( + '2 chats could not be continued. Open them to continue manually.' + ) +}) + it('dispatches the selected action while a future preference save is still pending', async () => { const saved = Promise.withResolvers() useAppStore.setState({ updateSettings: () => saved.promise }) @@ -267,9 +313,9 @@ it('dispatches the selected action while a future preference save is still pendi await act(async () => root.render()) await act(async () => checkbox(1).click()) await act(async () => checkbox(2).click()) - await act(async () => button('Reconnect 1').click()) + await act(async () => button('Resume 1').click()) expect(rpc.mock.calls.at(-1)?.slice(1)).toEqual([ - 'agentSession.restartResume', + 'agentSession.restartContinue', { sessionIds: ['a'] } ]) await act(async () => saved.reject(new Error('settings write failed'))) @@ -288,7 +334,7 @@ it.each(['pending', 'unknown', 'refused', 'missing'])( } ) await act(async () => root.render()) - await act(async () => button('Reconnect and continue').click()) + await act(async () => button('Resume all').click()) const notices = vi .mocked(toast) .mock.calls.map(([text]) => text) @@ -301,34 +347,20 @@ it.each(['pending', 'unknown', 'refused', 'missing'])( } ) -it('reports refused and newly ineligible reconnects', async () => { - rpc.mockImplementation(async (_target, method) => - method === 'agentSession.restartResumable' - ? { sessions: offered } - : { results: [{ sessionId: 'a', outcome: 'refused' }] } - ) +it('reports a lost resume response without retrying the action', async () => { + rpc.mockImplementation(async (_target, method) => { + if (method === 'agentSession.restartResumable') { + return { sessions: offered } + } + throw new Error('response lost') + }) await act(async () => root.render()) - await act(async () => button('Reconnect all').click()) - expect(toast).toHaveBeenCalledWith(expect.stringContaining('2 chats could not be reconnected')) + await act(async () => button('Resume all').click()) + expect(toast).toHaveBeenCalledWith(expect.stringContaining('unconfirmed')) + expect(rpc).toHaveBeenCalledTimes(2) + expect(document.querySelector('[role="dialog"]')).toBeNull() }) -it.each(['Reconnect all', 'Reconnect and continue'])( - 'reports a lost %s response without retrying the action', - async (label) => { - rpc.mockImplementation(async (_target, method) => { - if (method === 'agentSession.restartResumable') { - return { sessions: offered } - } - throw new Error('response lost') - }) - await act(async () => root.render()) - await act(async () => button(label).click()) - expect(toast).toHaveBeenCalledWith(expect.stringContaining('unconfirmed')) - expect(rpc).toHaveBeenCalledTimes(2) - expect(document.querySelector('[role="dialog"]')).toBeNull() - } -) - it('keeps an unconfirmed delivery visible when another chat was refused', async () => { rpc.mockImplementation(async (_target, method) => method === 'agentSession.restartResumable' @@ -341,7 +373,7 @@ it('keeps an unconfirmed delivery visible when another chat was refused', async } ) await act(async () => root.render()) - await act(async () => button('Reconnect and continue').click()) + await act(async () => button('Resume all').click()) expect(toast).toHaveBeenCalledWith('1 chat could not be continued. Open it to continue manually.') expect(vi.mocked(toast).mock.calls.at(-1)?.[0]).toBe( 'Continuation delivery is unconfirmed for 1 chat. Open it to check before sending another message.' diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx b/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx index 927fcb0b8ab..ae0c80df0d3 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState, useSyncExternalStore } from 'react' -import { Info, RotateCcw } from 'lucide-react' +import { RotateCcw } from 'lucide-react' import { Button } from './ui/button' import { Checkbox } from './ui/checkbox' import { @@ -13,8 +13,6 @@ import { import { useAppStore } from '../store' import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client' import { translate } from '@/i18n/i18n' -import { Popover, PopoverContent, PopoverTrigger } from './ui/popover' -import { AGENT_SESSION_RESTART_CONTINUATION_MESSAGE } from '../../../shared/agent-session-restart-continuation' import { ResumeOnRestartGroups } from './NativeChatResumeOnRestartGroups' import { announceRestartDismissUnconfirmed, @@ -35,16 +33,17 @@ import { } from './native-chat-resume-on-restart-store' /** - * What would be reconnected, shown before anything runs. + * What would be resumed, shown before anything runs. * - * The list is the point. Reconnecting a chat that was not working starts a provider the user never + * The list is the point. Resuming a chat that was not working starts a provider the user never * asked for and puts a misleading row in front of them, so they see exactly which chats the last * teardown recorded as mid-turn and decide. The checkbox is the opt-in to skipping this prompt in * future — it removes the PROMPT, never a safety check: automatic mode calls the same RPC, which * re-derives the same predicate and staggers the same way. * - * Reconnecting restores the session at the point it stopped; it does NOT continue the interrupted - * reply — that was measured. Every user-facing string here has to keep saying so. + * Resuming reattaches each session where it stopped AND asks that agent to carry on, which is the + * only reason the prompt is worth showing: reattaching alone is what simply opening the chat does. + * The user's own prompt is never re-sent, and every string here has to keep saying so. * * Closing is a SNOOZE: the host keeps the offer and the status bar keeps a way back to it, so * looking around before deciding cannot cost the recovery. Dismiss all is the only path that spends @@ -56,47 +55,6 @@ import { // else, so there is no remote target to aim this at. const LOCAL = { kind: 'local' } as const -/** Shows the LITERAL message, read from the same constant the host sends, so the popover cannot - * drift into describing something other than what goes out. */ -function ContinuationExplainer(): React.JSX.Element { - return ( - - - - - -
-

- {translate( - 'auto.components.NativeChatResumeOnRestartModal.whatIsSentTitle', - 'What Orca sends' - )} -

-

- {translate( - 'auto.components.NativeChatResumeOnRestartModal.whatIsSentBody', - 'Continuing sends one short message to each agent, telling it that Orca restarted and asking it to check its last action before carrying on. Your own prompt is never re-sent.' - )} -

-
- {AGENT_SESSION_RESTART_CONTINUATION_MESSAGE} -
-
-
-
- ) -} - export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { const structuredEnabled = useAppStore( (store) => store.settings?.experimentalStructuredNativeChat === true @@ -150,59 +108,27 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { } }, [dontAskAgain, updateSettings]) + /** + * The one action: reattach, then ask each agent to carry on. + * + * `restartContinue` is the host method behind it — reattaching without a send is a separate RPC + * this surface no longer calls, because opening the chat already does exactly that. + */ const resume = useCallback( - async (sessionIds: string[]): Promise => { - setBusy(true) - try { - void persistPreference() - const result = await callStructuredAgentSession<{ results: RestartActionOutcome[] }>( - LOCAL, - 'agentSession.restartResume', - { sessionIds } - ) - const settled = new Set(result.results.map((entry) => entry.sessionId)) - announceRestartResults(sessionIds, result.results, 'reconnect') - settleNativeChatRestartOffer([...settled]) - // An empty result means the host settled none of them — never leave the dialog sitting open - // behind a button that did nothing. - if ( - settled.size === 0 || - candidates.every((candidate) => settled.has(candidate.sessionId)) - ) { - consumeNativeChatResumeOnRestartDialogRequest() - } - } catch { - announceRestartUnconfirmed(sessionIds.length, 'reconnect') - consumeNativeChatResumeOnRestartDialogRequest() - } finally { - setBusy(false) - } - }, - [candidates, persistPreference] - ) - - /** - * Reconnect AND ask each agent to carry on. A deliberate action only. - * - * The automatic path calls `restartResume`, which has no send in it, so no setting — the - * checkbox included — can reach this. The checkbox opts into automatic RECONNECTION, never - * automatic continuation. - */ - const reconnectAndContinue = useCallback( async (sessionIds: string[]): Promise => { setBusy(true) try { void persistPreference() const result = await callStructuredAgentSession<{ - /** Which chats the host actually reconnected, and so which claims it spent. Optional + /** Which chats the host actually reattached, and so which claims it spent. Optional * because the payload is unvalidated: a shape this side did not expect must not turn a * delivered continuation into a failure report. */ resumed?: RestartActionOutcome[] continued: RestartActionOutcome[] }>(LOCAL, 'agentSession.restartContinue', { sessionIds }) announceRestartResults(sessionIds, result.continued, 'continue') - // Continuing spends the same claims reconnecting does, so the offer has to shrink the same - // way — otherwise the status bar keeps counting chats the host has already handed back. + // Resuming spends the claims, so the offer has to shrink with it — otherwise the status bar + // keeps counting chats the host has already handed back. settleNativeChatRestartOffer((result.resumed ?? []).map((entry) => entry.sessionId)) } catch { announceRestartUnconfirmed(sessionIds.length, 'continue') @@ -254,7 +180,7 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { } }} > - {/* Height is capped, never the data: seeing WHICH chats would be reconnected is the whole + {/* Height is capped, never the data: seeing WHICH chats would be resumed is the whole point, so the list scrolls inside the dialog while the header and primary action stay. */} @@ -264,37 +190,30 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { {translate( 'auto.components.NativeChatResumeOnRestartModal.title', - 'Reconnect interrupted chats?' + 'Resume interrupted chats?' )} + {/* Carries the transparency an explainer popover used to hide behind an icon: what the + agent is told, and what is NOT re-sent. */} {interruptedByUpdate ? translate( 'auto.components.NativeChatResumeOnRestartModal.updateBody', - 'These chats were mid-turn when Orca installed an update. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.' + 'These chats were mid-turn when Orca installed an update. Resuming restores each one where it stopped, with its full context, and asks the agent to check its last action before carrying on. Your own prompt is not re-sent.' ) : translate( 'auto.components.NativeChatResumeOnRestartModal.body', - 'These chats were mid-turn when Orca closed. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.' + 'These chats were mid-turn when Orca closed. Resuming restores each one where it stopped, with its full context, and asks the agent to check its last action before carrying on. Your own prompt is not re-sent.' )} - {/* The true state of things is counterintuitive — the terminal sessions survived and the - chats did not — so say so where it frames the list, not as a footnote. "kept running" - rather than "were restored": nothing reconnected them, they never stopped. */} -

- {translate( - 'auto.components.NativeChatResumeOnRestartModal.terminalSessionsUnaffected', - 'Only chats are affected — your terminal sessions kept running and need nothing from you.' - )} -

@@ -307,15 +226,6 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { />
- {/* Names both exits, because they are not the same: one keeps the offer, one spends it. - Neither loses the chats themselves. */} -

- {translate( - 'auto.components.NativeChatResumeOnRestartModal.notNowHint', - 'Not now keeps this list in the status bar. Dismiss all clears it — either way you can reopen any chat later and carry on from the same point.' - )} -

- - {/* Two groups, not three buttons: the exits stay together on the left so "Not now" is not - stranded between them and the actions. */} + {/* Two controls, and they are opposites: one spends the offer, one acts on it. Closing the + dialog is neither — it snoozes, so it needs no button of its own. */} - - {/* Quiet, not destructive: this spends an offer, and opening a chat still reconnects it. */} - - - - - - {/* Secondary, never the default: continuing sends a message, reconnecting does not. */} - - + - + : translate( + 'auto.components.NativeChatResumeOnRestartModal.resumeSelected', + 'Resume {{value0}}', + { value0: chosen.length } + )} +
diff --git a/src/renderer/src/components/native-chat-restart-action-notifications.ts b/src/renderer/src/components/native-chat-restart-action-notifications.ts index 44857e1347e..6c845622999 100644 --- a/src/renderer/src/components/native-chat-restart-action-notifications.ts +++ b/src/renderer/src/components/native-chat-restart-action-notifications.ts @@ -1,6 +1,14 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' +/** + * What Orca tells the user after acting on a restart offer. + * + * `continue` is the only action a surface takes now — resuming always reattaches AND sends. The + * `reconnect` wording belongs to the host's plain-reattach RPC, which is still published on the + * wire, so its vocabulary stays here rather than being reinvented if anything calls it again. + */ + export type RestartActionOutcome = { sessionId: string outcome: 'resumed' | 'continued' | 'pending' | 'unknown' | 'refused' @@ -12,10 +20,10 @@ function announceResumed(count: number): void { } toast( count === 1 - ? translate('auto.components.NativeChatResumeOnRestartModal.resumedOne', 'Reconnected 1 chat') + ? translate('auto.components.NativeChatResumeOnRestartModal.resumedOne', 'Resumed 1 chat') : translate( 'auto.components.NativeChatResumeOnRestartModal.resumedMany', - 'Reconnected {{value0}} chats', + 'Resumed {{value0}} chats', { value0: count } @@ -31,11 +39,11 @@ function announceContinued(count: number): void { count === 1 ? translate( 'auto.components.NativeChatResumeOnRestartModal.continuedOne', - 'Reconnected 1 chat and asked it to continue' + 'Resumed 1 chat and asked it to continue' ) : translate( 'auto.components.NativeChatResumeOnRestartModal.continuedMany', - 'Reconnected {{value0}} chats and asked them to continue', + 'Resumed {{value0}} chats and asked them to continue', { value0: count } ) ) @@ -54,7 +62,7 @@ export function announceRestartUnconfirmed(count: number, action: 'reconnect' | ) : translate( 'auto.components.NativeChatResumeOnRestartModal.reconnectUnconfirmed', - 'Reconnection is unconfirmed for {{value0}} chats. You can still open them normally.', + 'Resuming is unconfirmed for {{value0}} chats. You can still open them normally.', { value0: count, count } ) ) @@ -65,7 +73,7 @@ export function announceRestartDismissUnconfirmed(): void { toast( translate( 'auto.components.NativeChatResumeOnRestartModal.dismissUnconfirmed', - 'Dismissing the reconnect offer was not confirmed — it may still be in the status bar.' + 'Dismissing the resume offer was not confirmed — it may still be in the status bar.' ) ) } @@ -105,7 +113,7 @@ export function announceRestartResults( ) : translate( 'auto.components.NativeChatResumeOnRestartModal.reconnectRefused', - '{{value0}} chats could not be reconnected. You can still open them normally.', + '{{value0}} chats could not be resumed. You can still open them normally.', { value0: refused, count: refused } ) ) diff --git a/src/renderer/src/components/native-chat-resume-on-restart-store.ts b/src/renderer/src/components/native-chat-resume-on-restart-store.ts index 1e856c3b023..3a40e23305b 100644 --- a/src/renderer/src/components/native-chat-resume-on-restart-store.ts +++ b/src/renderer/src/components/native-chat-resume-on-restart-store.ts @@ -10,13 +10,13 @@ import { allResumeSessionIds, type ResumeCandidate } from './native-chat-resume- import { requestNativeChatResumeOnRestartDialog } from './native-chat-resume-on-restart-dialog' /** - * Which interrupted chats the host is still offering to reconnect. + * Which interrupted chats the host is still offering to resume. * * The offer is the HOST's answer, not a list whichever surface rendered first happens to be * holding. It has to be, because the host retires an offer for reasons no renderer can see — - * simply reopening a chat re-acquires its provider at the same cursor, which is the whole of what - * reconnecting would have done. So this fetches the list and both surfaces read it, and anything - * about to ACT on the offer asks the host again first. + * simply reopening a chat re-acquires its provider at the same cursor, which is the reattach half + * of a resume. So this fetches the list and both surfaces read it, and anything about to ACT on + * the offer asks the host again first. * * What stays on this side is the user's own facts: the snooze, and the preference that decides * whether the launch asks at all. @@ -100,7 +100,11 @@ export function clearNativeChatRestartOffer(): void { /** * This launch's single read of the offer, and the one decision the preference makes: ask, or - * reconnect without asking. + * resume without asking. + * + * "Resume automatically" has to mean the same thing the button means, or the preference is a lie: + * the identical call, reattaching AND asking each agent to carry on. Reattaching on its own is + * what opening the chat already does, so a silent version of that would recover nothing. * * Runs once however many surfaces mount, so the count and the dialog describe the same answer and * an opted-in launch cannot dispatch twice. @@ -117,18 +121,20 @@ async function loadLaunchOffer(): Promise { return } // Identical call to the dialog's own button; the host re-derives eligibility either way. - const result = await callStructuredAgentSession<{ results: RestartActionOutcome[] }>( - LOCAL, - 'agentSession.restartResume', - {} - ).catch(() => null) + const result = await callStructuredAgentSession<{ + /** Which chats the host reattached, and so which claims it spent. Optional because the payload + * is unvalidated, exactly as the dialog reads it. */ + resumed?: RestartActionOutcome[] + continued: RestartActionOutcome[] + }>(LOCAL, 'agentSession.restartContinue', {}).catch(() => null) if (!result) { - announceRestartUnconfirmed(offered.length, 'reconnect') + announceRestartUnconfirmed(offered.length, 'continue') return } - // Automatic must never be silent: someone who ticked the box months ago still sees this. - announceRestartResults(allResumeSessionIds(offered), result.results, 'reconnect') - settleNativeChatRestartOffer(result.results.map((entry) => entry.sessionId)) + // Automatic must never be silent: someone who ticked the box months ago still sees this, and + // this is the only place they learn a message went out on their behalf. + announceRestartResults(allResumeSessionIds(offered), result.continued, 'continue') + settleNativeChatRestartOffer((result.resumed ?? []).map((entry) => entry.sessionId)) } /** diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx index 20373467516..a0e5d37a991 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx @@ -159,13 +159,13 @@ export function NativeChatExperimentalSetting({

{translate( 'auto.components.settings.ExperimentalPane.nativeChat.resumeCopy', - 'When Orca quits or installs an update, chats that were mid-turn are offered again on the next launch. On, they are reconnected without asking and Orca tells you afterwards — the same thing as ticking "Don\'t ask again" in that prompt. Off, you choose from the list each time. Reconnecting restores a chat where it stopped; it does not continue the interrupted reply.' + 'When Orca quits or installs an update, chats that were mid-turn are offered again on the next launch. On, they are resumed without asking and Orca tells you afterwards — the same thing as ticking "Don\'t ask again" in that prompt. Off, you choose from the list each time. Resuming restores a chat where it stopped and asks the agent to check its last action before carrying on; your own prompt is not re-sent.' )}

@@ -173,7 +173,7 @@ export function NativeChatExperimentalSetting({ checked={resumeOnRestartEnabled} ariaLabel={translate( 'auto.components.settings.ExperimentalPane.nativeChat.resumeToggleLabel', - 'Toggle automatic reconnect after a restart' + 'Toggle automatic resume after a restart' )} onChange={() => updateSettings({ nativeChatResumeWorkOnRestart: !resumeOnRestartEnabled }) diff --git a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx index 48279edd9c0..b12ded6d772 100644 --- a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx +++ b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx @@ -72,8 +72,8 @@ describe('NativeChatResumeStatusSegment', () => { rpc.mockResolvedValue({ sessions: candidates }) await mount() - expect(screen.getByRole('button', { name: '2 chats available to reconnect' })).toBeTruthy() - expect(screen.getByText('2 chats to reconnect')).toBeTruthy() + expect(screen.getByRole('button', { name: '2 chats available to resume' })).toBeTruthy() + expect(screen.getByText('2 chats to resume')).toBeTruthy() expect(getNativeChatResumeOnRestartDialogRequest()).toBe(false) await act(async () => screen.getByRole('button').click()) @@ -89,8 +89,8 @@ describe('NativeChatResumeStatusSegment', () => { rpc.mockResolvedValue({ sessions: candidates.slice(0, 1) }) await mount() - expect(screen.getByRole('button', { name: '1 chat available to reconnect' })).toBeTruthy() - expect(screen.getByText('1 chat to reconnect')).toBeTruthy() + expect(screen.getByRole('button', { name: '1 chat available to resume' })).toBeTruthy() + expect(screen.getByText('1 chat to resume')).toBeTruthy() }) // The count can lag the host — another window may have dismissed the offer. The re-read decides. @@ -128,6 +128,6 @@ describe('NativeChatResumeStatusSegment', () => { await mount(true) expect(screen.getByRole('button').textContent).toContain('2') - expect(screen.queryByText('2 chats to reconnect')).toBeNull() + expect(screen.queryByText('2 chats to resume')).toBeNull() }) }) diff --git a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx index a064a6d2ebe..aef84b5da0e 100644 --- a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx @@ -8,7 +8,7 @@ import { useNativeChatRestartOffer } from '../native-chat-resume-on-restart-store' -// Why: closing the reconnect dialog is a snooze, not a decline — the host keeps the offer. This is +// Why: closing the resume dialog is a snooze, not a decline — the host keeps the offer. This is // then the only surface left carrying it, so it is always rendered rather than gated by // `statusBarItems`. @@ -39,11 +39,11 @@ export function NativeChatResumeStatusSegment({ count === 1 ? translate( 'auto.components.status.bar.NativeChatResumeStatusSegment.labelOne', - '1 chat to reconnect' + '1 chat to resume' ) : translate( 'auto.components.status.bar.NativeChatResumeStatusSegment.label', - '{{value0}} chats to reconnect', + '{{value0}} chats to resume', { value0: count } ) return ( @@ -57,11 +57,11 @@ export function NativeChatResumeStatusSegment({ count === 1 ? translate( 'auto.components.status.bar.NativeChatResumeStatusSegment.ariaLabelOne', - '1 chat available to reconnect' + '1 chat available to resume' ) : translate( 'auto.components.status.bar.NativeChatResumeStatusSegment.ariaLabel', - '{{value0}} chats available to reconnect', + '{{value0}} chats available to resume', { value0: count } ) } @@ -73,7 +73,7 @@ export function NativeChatResumeStatusSegment({ {translate( 'auto.components.status.bar.NativeChatResumeStatusSegment.tooltip', - 'Open interrupted chats available to reconnect' + 'Open interrupted chats available to resume' )} diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 8fe443f65de..1976b155371 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -80,12 +80,10 @@ "manyAgents": "{{value0}} agents", "oneAgent": "1 agent", "projects": "Folder workspaces", - "reconnectAgent": "Reconnect {{value0}} chat", - "reconnectRefused_one": "{{value0}} chat could not be reconnected. You can still open it normally.", - "reconnectRefused_other": "{{value0}} chats could not be reconnected. You can still open them normally.", - "reconnectUnconfirmed_one": "Reconnection is unconfirmed for {{value0}} chat. You can still open it normally.", - "reconnectUnconfirmed_other": "Reconnection is unconfirmed for {{value0}} chats. You can still open them normally.", - "resume": "Reconnect" + "reconnectRefused_one": "{{value0}} chat could not be resumed. You can still open it normally.", + "reconnectRefused_other": "{{value0}} chats could not be resumed. You can still open them normally.", + "reconnectUnconfirmed_one": "Resuming is unconfirmed for {{value0}} chat. You can still open it normally.", + "reconnectUnconfirmed_other": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally." }, "NewWorkspaceComposerCard": { "0e587e31fb": "yaml", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 9082bd7e6ee..66aecf6f3fe 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1946,46 +1946,38 @@ "9b40d7b018": "Copy" }, "NativeChatResumeOnRestartModal": { - "title": "Reconnect interrupted chats?", - "body": "These chats were mid-turn when Orca closed. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.", - "updateBody": "These chats were mid-turn when Orca installed an update. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.", - "terminalSessionsUnaffected": "Only chats are affected — your terminal sessions kept running and need nothing from you.", - "resume": "Reconnect", - "reconnectAgent": "Reconnect {{value0}} chat", - "resumeAll": "Reconnect all", - "resuming": "Reconnecting…", - "notNow": "Not now", + "title": "Resume interrupted chats?", + "body": "These chats were mid-turn when Orca closed. Resuming restores each one where it stopped, with its full context, and asks the agent to check its last action before carrying on. Your own prompt is not re-sent.", + "updateBody": "These chats were mid-turn when Orca installed an update. Resuming restores each one where it stopped, with its full context, and asks the agent to check its last action before carrying on. Your own prompt is not re-sent.", + "resumeAll": "Resume all", + "resuming": "Resuming…", "untitled": "Untitled chat", - "listLabel": "Chats that would be reconnected", - "notNowHint": "Not now keeps this list in the status bar. Dismiss all clears it — either way you can reopen any chat later and carry on from the same point.", - "dontAskAgain": "Don't ask again — reconnect automatically next time", - "dontAskAgainHint": "Qualifying chats will be reconnected automatically after a restart, and Orca will tell you when it happens. You can turn this off in Settings → Experimental → Chat UI.", - "resumedOne": "Reconnected 1 chat", - "resumedMany": "Reconnected {{value0}} chats", + "listLabel": "Chats that would be resumed", + "dontAskAgain": "Don't ask again (resume automatically)", + "dontAskAgainHint": "You can turn this off in Settings → Experimental → Chat UI.", + "resumedOne": "Resumed 1 chat", + "resumedMany": "Resumed {{value0}} chats", "oneAgent": "1 agent", "manyAgents": "{{value0}} agents", - "reconnectAndContinue": "Reconnect and continue", - "continuedOne": "Reconnected 1 chat and asked it to continue", - "continuedMany": "Reconnected {{value0}} chats and asked them to continue", - "whatIsSentTitle": "What Orca sends", - "whatIsSentBody": "Continuing sends one short message to each agent, telling it that Orca restarted and asking it to check its last action before carrying on. Your own prompt is never re-sent.", - "selectAgent": "Reconnect {{value0}} chat \"{{value1}}\" in {{value2}}", + "continuedOne": "Resumed 1 chat and asked it to continue", + "continuedMany": "Resumed {{value0}} chats and asked them to continue", + "selectAgent": "Resume {{value0}} chat \"{{value1}}\" in {{value2}}", "projects": "Folder workspaces", - "resumeSelected": "Reconnect {{value0}}", + "resumeSelected": "Resume {{value0}}", "continueUnconfirmed": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.", "continueUnconfirmed_one": "Continuation delivery is unconfirmed for {{value0}} chat. Open it to check before sending another message.", "continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.", - "reconnectUnconfirmed": "Reconnection is unconfirmed for {{value0}} chats. You can still open them normally.", - "reconnectUnconfirmed_one": "Reconnection is unconfirmed for {{value0}} chat. You can still open it normally.", - "reconnectUnconfirmed_other": "Reconnection is unconfirmed for {{value0}} chats. You can still open them normally.", + "reconnectUnconfirmed": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally.", + "reconnectUnconfirmed_one": "Resuming is unconfirmed for {{value0}} chat. You can still open it normally.", + "reconnectUnconfirmed_other": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally.", "continueRefused": "{{value0}} chats could not be continued. Open them to continue manually.", "continueRefused_one": "{{value0}} chat could not be continued. Open it to continue manually.", "continueRefused_other": "{{value0}} chats could not be continued. Open them to continue manually.", - "reconnectRefused": "{{value0}} chats could not be reconnected. You can still open them normally.", - "reconnectRefused_one": "{{value0}} chat could not be reconnected. You can still open it normally.", - "reconnectRefused_other": "{{value0}} chats could not be reconnected. You can still open them normally.", + "reconnectRefused": "{{value0}} chats could not be resumed. You can still open them normally.", + "reconnectRefused_one": "{{value0}} chat could not be resumed. You can still open it normally.", + "reconnectRefused_other": "{{value0}} chats could not be resumed. You can still open them normally.", "dismissAll": "Dismiss all", - "dismissUnconfirmed": "Dismissing the reconnect offer was not confirmed — it may still be in the status bar." + "dismissUnconfirmed": "Dismissing the resume offer was not confirmed — it may still be in the status bar." }, "StarNagCard": { "92b0f9d921": "is authenticated and try again.", @@ -4076,11 +4068,11 @@ "reconnect_attempt": "Attempt {{value0}}" }, "NativeChatResumeStatusSegment": { - "labelOne": "1 chat to reconnect", - "label": "{{value0}} chats to reconnect", - "ariaLabelOne": "1 chat available to reconnect", - "ariaLabel": "{{value0}} chats available to reconnect", - "tooltip": "Open interrupted chats available to reconnect" + "labelOne": "1 chat to resume", + "label": "{{value0}} chats to resume", + "ariaLabelOne": "1 chat available to resume", + "ariaLabel": "{{value0}} chats available to resume", + "tooltip": "Open interrupted chats available to resume" } } }, @@ -7136,9 +7128,9 @@ "structuredCopy": "Opt in to the host-owned structured chat runtime for Codex and Claude. Off keeps the existing terminal-backed chat path.", "structuredScope": "Local sessions only for now. WSL and remote execution hosts (including SSH) continue to use terminal chat, and Windows falls back to it unless Orca can read process start times.", "structuredToggleLabel": "Toggle updated structured native chat", - "resumeTitle": "Reconnect working chats automatically after a restart", - "resumeCopy": "When Orca quits or installs an update, chats that were mid-turn are offered again on the next launch. On, they are reconnected without asking and Orca tells you afterwards — the same thing as ticking \"Don't ask again\" in that prompt. Off, you choose from the list each time. Reconnecting restores a chat where it stopped; it does not continue the interrupted reply.", - "resumeToggleLabel": "Toggle automatic reconnect after a restart" + "resumeTitle": "Resume working chats automatically after a restart", + "resumeCopy": "When Orca quits or installs an update, chats that were mid-turn are offered again on the next launch. On, they are resumed without asking and Orca tells you afterwards — the same thing as ticking \"Don't ask again\" in that prompt. Off, you choose from the list each time. Resuming restores a chat where it stopped and asks the agent to check its last action before carrying on; your own prompt is not re-sent.", + "resumeToggleLabel": "Toggle automatic resume after a restart" }, "agentDashboard": { "title": "Agent Dashboard", diff --git a/src/shared/agent-session-restart-continuation.ts b/src/shared/agent-session-restart-continuation.ts index cd6df0397d4..00a9b687ebb 100644 --- a/src/shared/agent-session-restart-continuation.ts +++ b/src/shared/agent-session-restart-continuation.ts @@ -5,9 +5,12 @@ // would make the two lanes behave differently — and the wording is the part that tells an agent to // VERIFY its last action before repeating it. Both reasons point the same way. // -// Sending this is ALWAYS a deliberate user action. Reconnecting never sends it, and the automatic -// path never reaches this module: see `structured-agent-session-restart-resume-host`, where the -// resume surface contains no send at all. +// Sending this needs the user's OPT-IN, not their presence. Resuming from the restart prompt sends +// it, and so does the launch itself once the user ticked "resume automatically" — an earlier +// comment here promised no setting could ever reach this module, and that is no longer true. It is +// acceptable because the work being continued is the user's own, the wording above tells the agent +// to VERIFY its last action before repeating it, and the launch reports what it did. Reattaching +// without a send remains a separate operation that never comes here. export const AGENT_SESSION_RESTART_CONTINUATION_MESSAGE = "Orca restarted, so your previous reply was cut off partway through. Before continuing, check whether your most recent action completed — don't repeat it if it did. Then carry on."