diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts index 0d8d58a8804..84913c69a32 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts @@ -96,12 +96,13 @@ async function resumeOne( }) } catch (error) { const reason = error instanceof Error ? error.message : String(error) - const live = (error as { owner?: string }).owner + const owner = + typeof error === 'object' && error !== null ? Reflect.get(error, 'owner') : undefined return { sessionId, outcome: 'refused', reason, - ...(live === undefined ? {} : { owner: live }) + ...(typeof owner === 'string' ? { owner } : {}) } } } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts index 174368ccb15..de8c8a36071 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts @@ -43,6 +43,7 @@ function turnItem( } function record(overrides: { chain?: AgentSessionRecord['providerHandleChain'] } = {}) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a literal fixture standing in for a durable record; the code under test reads only lease, provider, location and providerHandleChain. return { schemaVersion: 2, sessionId: SESSION, @@ -110,6 +111,7 @@ function claudeRecord( leafUuid: string | null, providerSessionId = CLAUDE_PROVIDER_SESSION ): AgentSessionRecord { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; this only swaps the provider and its Claude handle chain. return { ...record(), provider: 'claude', @@ -127,6 +129,7 @@ function claudeRecord( } function journal(items: AgentJournalRenderItem[], isReadOnly = false) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the code under test calls only isReadOnly and snapshot(); a real AgentSessionJournal needs an on-disk SQLite store. return { isReadOnly, snapshot: () => ({ items, submissions: [] }) } as never } @@ -416,7 +419,9 @@ describe('the restart-resume surface', () => { ]) return { restartResume: createStructuredAgentSessionRestartResume( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the collaborator reads only getRecord and resumeMarkers from the store, and supportsCreate from the adapter. { store, adapter: { supportsCreate: () => true } } as never, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the live-session map is read for journal, hasProviderChild and fence only. sessions as never, { revealSession: async () => ({ readable: true }), @@ -429,6 +434,7 @@ describe('the restart-resume surface', () => { send: async ({ envelope, body }) => { sent.push({ sessionId: envelope.sessionId, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: restartContinuationBody builds exactly one text block, which is what this assertion reads. text: (body.blocks[0] as { text: string }).text }) return { ok: true } @@ -513,6 +519,7 @@ describe('the restart-resume surface', () => { // predicate drops the session. Reporting "nothing happened" would leave the user pressing a dead // button for a session that IS running. it('reports a session the chat pane already re-acquired as resumed, not as nothing', async () => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; only claimStatus is overridden, to model a lease the pane re-took. const liveRecord = { ...record(), lease: { ...record().lease, claimStatus: 'live' } @@ -537,6 +544,7 @@ describe('the restart-resume surface', () => { // Relaxing the lease clause must not relax the whole predicate. "Resume all" targets every // marker, so a held-but-ineligible session would otherwise be consumed and counted as resumed. it('refuses to settle an already-live session the predicate rejects', async () => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; only claimStatus is overridden, to model a lease the pane re-took. const liveRecord = { ...record(), lease: { ...record().lease, claimStatus: 'live' } diff --git a/src/main/runtime/agent-session-record-store-file.ts b/src/main/runtime/agent-session-record-store-file.ts index 9d39871e109..19d218d1d2b 100644 --- a/src/main/runtime/agent-session-record-store-file.ts +++ b/src/main/runtime/agent-session-record-store-file.ts @@ -102,6 +102,7 @@ function parseState( if (typeof parsed !== 'object' || parsed === null) { return null } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every field is read back as `unknown` and validated below before use; adding `resumeMarkers` brought this long-standing assertion into the changed-code gate. const file = parsed as { schemaVersion?: unknown hostId?: unknown @@ -110,6 +111,7 @@ function parseState( retiredClaimKeys?: unknown unusableRecords?: unknown visibleSessionIds?: unknown + resumeMarkers?: unknown } if ( !Number.isSafeInteger(file.schemaVersion) || @@ -227,9 +229,7 @@ function parseState( } state.visibleSessionIdsIndexPresent = visibleSessionIds.present visibleSessionIds.ids.forEach((sessionId) => state.visibleSessionIds.add(sessionId)) - state.resumeMarkers = parseAgentSessionResumeMarkers( - (parsed as { resumeMarkers?: unknown }).resumeMarkers - ) + state.resumeMarkers = parseAgentSessionResumeMarkers(file.resumeMarkers) return { state, needsRewrite } } diff --git a/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx b/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx index df25223c051..f33c1a730f5 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx @@ -82,13 +82,7 @@ function ResumeCandidateRow({ {formatShortTimeAgo(candidate.recordedAt, listedAt)} - diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx b/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx index 9eb21b3eb9d..0c57b48cfd8 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx @@ -94,20 +94,20 @@ function ContinuationExplainer(): React.JSX.Element { - -

+ +

{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}
@@ -248,12 +248,15 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { point, so the list scrolls inside the dialog while the header and primary action stay. */} - - - {translate( - 'auto.components.NativeChatResumeOnRestartModal.title', - 'Reconnect interrupted chats?' - )} + + {/* Plain wrapper owns the icon spacing; DialogTitle owns its own. */} + + + {translate( + 'auto.components.NativeChatResumeOnRestartModal.title', + 'Reconnect interrupted chats?' + )} + {interruptedByUpdate @@ -266,6 +269,15 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null { '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.' )} + {/* 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.' + )} +

+ const recordedAt = Reflect.get(value, 'recordedAt') + const trigger = Reflect.get(value, 'trigger') return ( - isMarkerField(marker.sessionId) && - isMarkerField(marker.turnId) && - isMarkerField(marker.providerHandleRoot) && - Number.isSafeInteger(marker.recordedAt) && - (marker.recordedAt as number) >= 0 && - (marker.trigger === 'quit' || marker.trigger === 'update') + isMarkerField(Reflect.get(value, 'sessionId')) && + isMarkerField(Reflect.get(value, 'turnId')) && + isMarkerField(Reflect.get(value, 'providerHandleRoot')) && + typeof recordedAt === 'number' && + Number.isSafeInteger(recordedAt) && + recordedAt >= 0 && + (trigger === 'quit' || trigger === 'update') ) }