diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts
index b2919fc88e4..50bb3216978 100644
--- a/src/main/persistence.test.ts
+++ b/src/main/persistence.test.ts
@@ -6608,7 +6608,7 @@ describe('Store', () => {
// ── Workspace Session ──────────────────────────────────────────────
- it('get/set workspace session round-trips', async () => {
+ it('get/set workspace session round-trips (and stamps the capture version)', async () => {
const store = await createStore()
const session = {
activeRepoId: 'r1',
@@ -6618,7 +6618,9 @@ describe('Store', () => {
terminalLayoutsByTabId: {}
}
store.setWorkspaceSession(session)
- expect(store.getWorkspaceSession()).toEqual(session)
+ // Why: main stamps every full write so the next launch knows this build
+ // could preserve agent sessions across a restart (#5356).
+ expect(store.getWorkspaceSession()).toEqual({ ...session, agentSessionCaptureVersion: 1 })
})
it('patches workspace session without replacing unchanged slices', async () => {
diff --git a/src/main/persistence.ts b/src/main/persistence.ts
index a72a9238fdd..e893849d044 100644
--- a/src/main/persistence.ts
+++ b/src/main/persistence.ts
@@ -104,6 +104,7 @@ import {
ONBOARDING_FINAL_STEP
} from '../shared/constants'
import { parseWorkspaceSession } from '../shared/workspace-session-schema'
+import { AGENT_SESSION_CAPTURE_VERSION } from '../shared/agent-session-upgrade-notice'
import { normalizeUsagePercentageDisplay } from '../shared/usage-percentage-display'
import { isExistingPersistedProfile } from '../shared/project-order-manual-default-notice'
import { resolveUsagePercentageDisplayChangeNoticeDismissed } from '../shared/usage-percentage-display-change-notice'
@@ -5653,12 +5654,21 @@ export class Store {
}
setWorkspaceSession(session: PersistedState['workspaceSession'], hostId?: string | null): void {
+ // Why: main is the authority for what lands on disk, so stamp the
+ // capture-format version on every full write here. The renderer quit
+ // payload (buildWorkspaceSessionPayload) does not carry it, and a full
+ // write replaces the session — without this, the quit flush would drop the
+ // stamp and re-arm the pre-#5232 upgrade notice (#5356).
+ const stamped: WorkspaceSessionState = {
+ ...session,
+ agentSessionCaptureVersion: AGENT_SESSION_CAPTURE_VERSION
+ }
const resolved = this.resolveHostId(hostId)
if (resolved === LOCAL_EXECUTION_HOST_ID) {
- this.setLocalWorkspaceSession(session)
+ this.setLocalWorkspaceSession(stamped)
return
}
- this.setHostWorkspaceSession(resolved, session)
+ this.setHostWorkspaceSession(resolved, stamped)
}
/** Persist a non-'local' host partition. The PTY-binding race protections in
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 014e3950e42..67617802d5f 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -119,6 +119,7 @@ import {
persistWorkspaceSessionByHostSync
} from './lib/workspace-session-host-persistence'
import { collectFolderWorkspaceKeysFromSession } from './lib/workspace-session-hydration-keys'
+import { notifyPreUpgradeAgentSessionLossIfNeeded } from './lib/pre-upgrade-agent-session-loss-notice'
import {
getStartupErrorFallbackUI,
hydratePersistedUIAfterStartupRead
@@ -975,6 +976,11 @@ function App(): React.JSX.Element {
actions.hydrateEditorSession(sessionRead.session, sessionHydrationOptions)
actions.hydrateBrowserSession(sessionRead.session, sessionHydrationOptions)
})
+ // Why: a session written by a build predating quit-time agent capture
+ // (#5232) cannot restore its live agent terminals — they cold-restore
+ // as blank shells. Warn once, non-destructively, off the raw loaded
+ // session rather than letting them vanish silently (#5356).
+ notifyPreUpgradeAgentSessionLossIfNeeded(sessionRead.session)
// Why: prune lastVisitedAtByWorktreeId entries whose worktrees
// no longer exist. Must run AFTER hydration — before this point,
// async repo loads may not have populated worktreesByRepo yet and
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 257200dcc77..0defc1f3819 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -126,7 +126,9 @@
"fe21e8f6f5": "Go back ({{value0}})",
"064bd07810": "Go back",
"ce37cf5279": "Toggle sidebar ({{value0}})",
- "e4b9e7dff7": "Toggle sidebar"
+ "e4b9e7dff7": "Toggle sidebar",
+ "preUpgradeAgentSessionLossTitle": "Agent sessions could not be restored",
+ "preUpgradeAgentSessionLossBody": "After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again."
},
"web": {
"WebConnect": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 5c0fc46acc2..7f1adfa956f 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -126,7 +126,9 @@
"fe21e8f6f5": "Volver ({{value0}})",
"064bd07810": "Volver",
"ce37cf5279": "Alternar barra lateral ({{value0}})",
- "e4b9e7dff7": "Alternar barra lateral"
+ "e4b9e7dff7": "Alternar barra lateral",
+ "preUpgradeAgentSessionLossTitle": "Agent sessions could not be restored",
+ "preUpgradeAgentSessionLossBody": "After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again."
},
"web": {
"WebConnect": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 75efd9ce994..14f611b7dd8 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -126,7 +126,9 @@
"fe21e8f6f5": "戻る ({{value0}})",
"064bd07810": "戻る",
"ce37cf5279": "サイドバーの切り替え ({{value0}})",
- "e4b9e7dff7": "サイドバーの切り替え"
+ "e4b9e7dff7": "サイドバーの切り替え",
+ "preUpgradeAgentSessionLossTitle": "Agent sessions could not be restored",
+ "preUpgradeAgentSessionLossBody": "After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again."
},
"web": {
"WebConnect": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 2ddefe47f79..0fed63bff33 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -126,7 +126,9 @@
"fe21e8f6f5": "돌아가기({{value0}})",
"064bd07810": "돌아가기",
"ce37cf5279": "사이드바 표시/숨기기({{value0}})",
- "e4b9e7dff7": "사이드바 표시/숨기기"
+ "e4b9e7dff7": "사이드바 표시/숨기기",
+ "preUpgradeAgentSessionLossTitle": "Agent sessions could not be restored",
+ "preUpgradeAgentSessionLossBody": "After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again."
},
"web": {
"WebConnect": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 9e74716b26a..79d897b9f7d 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -126,7 +126,9 @@
"fe21e8f6f5": "返回 ({{value0}})",
"064bd07810": "返回",
"ce37cf5279": "切换侧边栏 ({{value0}})",
- "e4b9e7dff7": "切换侧边栏"
+ "e4b9e7dff7": "切换侧边栏",
+ "preUpgradeAgentSessionLossTitle": "Agent sessions could not be restored",
+ "preUpgradeAgentSessionLossBody": "After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again."
},
"web": {
"WebConnect": {
diff --git a/src/renderer/src/lib/pre-upgrade-agent-session-loss-notice.tsx b/src/renderer/src/lib/pre-upgrade-agent-session-loss-notice.tsx
new file mode 100644
index 00000000000..fd07d200b50
--- /dev/null
+++ b/src/renderer/src/lib/pre-upgrade-agent-session-loss-notice.tsx
@@ -0,0 +1,62 @@
+import { toast } from 'sonner'
+import { translate } from '@/i18n/i18n'
+import { useAppStore } from '@/store'
+import type { WorkspaceSessionState } from '../../../shared/types'
+import {
+ AGENT_SESSION_CAPTURE_VERSION,
+ shouldNotifyPreUpgradeAgentSessionLoss
+} from '../../../shared/agent-session-upgrade-notice'
+import { patchWorkspaceSessionByHost } from './workspace-session-host-persistence'
+
+/**
+ * On the first launch after upgrading from a build predating quit-time agent
+ * capture (#5232), a live agent terminal cold-restores as a blank shell because
+ * its session id was never persisted — and, before this notice, silently
+ * (#5356). The lost session cannot be conjured, so tell the user once,
+ * non-destructively, then stamp the session so it never re-fires.
+ */
+export function notifyPreUpgradeAgentSessionLossIfNeeded(session: WorkspaceSessionState): void {
+ // Fast path: a capture-capable build already stamped this session — no notice
+ // to show and nothing to reconcile, so skip the extra disk write too.
+ if ((session.agentSessionCaptureVersion ?? 0) >= AGENT_SESSION_CAPTURE_VERSION) {
+ return
+ }
+
+ // Why: this runs inline in the startup hydration path, so a fault here must
+ // never block the app from mounting — degrade to "no notice" instead.
+ try {
+ if (shouldNotifyPreUpgradeAgentSessionLoss(session)) {
+ toast(
+
+ {translate(
+ 'auto.App.preUpgradeAgentSessionLossTitle',
+ 'Agent sessions could not be restored'
+ )}
+ ,
+ {
+ description: translate(
+ 'auto.App.preUpgradeAgentSessionLossBody',
+ 'After updating, agent terminals (Claude, Codex, and others) from your previous session started as fresh shells. Older versions could not save agent sessions across an update, so they could not be resumed. This will not happen again.'
+ ),
+ // Sticky + dismissible: important enough to be seen, but purely
+ // informational — it blocks nothing and deletes nothing.
+ duration: Infinity,
+ dismissible: true
+ }
+ )
+ }
+
+ // Consume the pre-fix signal immediately (not only at the next graceful
+ // quit) so a crash before quit can't replay the notice.
+ void patchWorkspaceSessionByHost(
+ window.api.session,
+ { agentSessionCaptureVersion: AGENT_SESSION_CAPTURE_VERSION },
+ useAppStore.getState()
+ ).catch(() => {
+ // Best-effort: the quit-time full write re-stamps, so a failed patch just
+ // means the notice may re-evaluate next launch, never a crash.
+ })
+ } catch {
+ // Non-fatal: never let the upgrade notice interfere with startup.
+ }
+}
diff --git a/src/shared/agent-session-upgrade-notice.test.ts b/src/shared/agent-session-upgrade-notice.test.ts
new file mode 100644
index 00000000000..591f79e37dc
--- /dev/null
+++ b/src/shared/agent-session-upgrade-notice.test.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect } from 'vitest'
+import type { SleepingAgentSessionRecord } from './agent-session-resume'
+import type { TerminalTab, WorkspaceSessionState } from './types'
+import {
+ AGENT_SESSION_CAPTURE_VERSION,
+ shouldNotifyPreUpgradeAgentSessionLoss
+} from './agent-session-upgrade-notice'
+
+function agentTab(launchAgent: string | undefined): TerminalTab {
+ return { launchAgent } as unknown as TerminalTab
+}
+
+function record(origin: SleepingAgentSessionRecord['origin']): SleepingAgentSessionRecord {
+ return { origin } as unknown as SleepingAgentSessionRecord
+}
+
+function session(
+ overrides: Partial<
+ Pick<
+ WorkspaceSessionState,
+ 'agentSessionCaptureVersion' | 'tabsByWorktree' | 'sleepingAgentSessionsByPaneKey'
+ >
+ >
+): Pick<
+ WorkspaceSessionState,
+ 'agentSessionCaptureVersion' | 'tabsByWorktree' | 'sleepingAgentSessionsByPaneKey'
+> {
+ return { tabsByWorktree: {}, ...overrides }
+}
+
+describe('shouldNotifyPreUpgradeAgentSessionLoss', () => {
+ it('warns for a pre-fix session: agent tab, no records, no stamp (#5356)', () => {
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({ tabsByWorktree: { wt: [agentTab('codex')] } })
+ )
+ ).toBe(true)
+ })
+
+ it('does not warn once the capture stamp is present', () => {
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({
+ agentSessionCaptureVersion: AGENT_SESSION_CAPTURE_VERSION,
+ tabsByWorktree: { wt: [agentTab('codex')] }
+ })
+ )
+ ).toBe(false)
+ })
+
+ it('does not warn when a quit/live capture record exists (working post-#5232 upgrade)', () => {
+ // The critical false-positive guard: on the first launch of the fixed build
+ // NO session has the brand-new stamp yet, so a working post-#5232 build must
+ // be distinguished by its resume records — those sessions ARE being restored.
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({
+ tabsByWorktree: { wt: [agentTab('codex')] },
+ sleepingAgentSessionsByPaneKey: { 'tab:0': record('quit') }
+ })
+ )
+ ).toBe(false)
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({
+ tabsByWorktree: { wt: [agentTab('codex')] },
+ sleepingAgentSessionsByPaneKey: { 'tab:0': record('live') }
+ })
+ )
+ ).toBe(false)
+ })
+
+ it('still warns when only a worktree-sleep record exists (does not resume cold-restored panes)', () => {
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({
+ tabsByWorktree: { wt: [agentTab('codex')] },
+ sleepingAgentSessionsByPaneKey: { 'tab:0': record('worktree-sleep') }
+ })
+ )
+ ).toBe(true)
+ })
+
+ it('does not warn for plain terminals with no agent tab', () => {
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(
+ session({ tabsByWorktree: { wt: [agentTab(undefined)] } })
+ )
+ ).toBe(false)
+ })
+
+ it('does not warn for an unknown/non-resumable launchAgent', () => {
+ expect(
+ shouldNotifyPreUpgradeAgentSessionLoss(session({ tabsByWorktree: { wt: [agentTab('pi')] } }))
+ ).toBe(false)
+ })
+
+ it('does not warn for a fresh session with no tabs', () => {
+ expect(shouldNotifyPreUpgradeAgentSessionLoss(session({ tabsByWorktree: {} }))).toBe(false)
+ expect(shouldNotifyPreUpgradeAgentSessionLoss(session({ tabsByWorktree: undefined }))).toBe(
+ false
+ )
+ })
+})
diff --git a/src/shared/agent-session-upgrade-notice.ts b/src/shared/agent-session-upgrade-notice.ts
new file mode 100644
index 00000000000..d9d16c3c36d
--- /dev/null
+++ b/src/shared/agent-session-upgrade-notice.ts
@@ -0,0 +1,73 @@
+import type { WorkspaceSessionState } from './types'
+import { isResumableTuiAgent } from './agent-session-resume'
+
+/**
+ * Bumped whenever the quit-time agent-session capture format changes. A session
+ * persisted by a build that captures live agents at quit (#5232/#5240) carries
+ * this stamp; builds predating that fix never wrote it. The *absence* of the
+ * stamp is the load-bearing signal that the previous writer could not preserve
+ * agent sessions across a restart — so it must NOT be added to
+ * getDefaultWorkspaceSession (a default would misclassify genuine pre-fix
+ * sessions as post-fix).
+ */
+export const AGENT_SESSION_CAPTURE_VERSION = 1
+
+type PreUpgradeNoticeSession = Pick<
+ WorkspaceSessionState,
+ 'agentSessionCaptureVersion' | 'tabsByWorktree' | 'sleepingAgentSessionsByPaneKey'
+>
+
+function hasRestorableAgentTab(
+ tabsByWorktree: WorkspaceSessionState['tabsByWorktree'] | undefined
+): boolean {
+ if (!tabsByWorktree) {
+ return false
+ }
+ for (const tabs of Object.values(tabsByWorktree)) {
+ for (const tab of tabs) {
+ if (tab.launchAgent && isResumableTuiAgent(tab.launchAgent)) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+function hasQuitOrLiveCaptureRecord(
+ records: WorkspaceSessionState['sleepingAgentSessionsByPaneKey'] | undefined
+): boolean {
+ if (!records) {
+ return false
+ }
+ for (const record of Object.values(records)) {
+ if (record.origin === 'quit' || record.origin === 'live') {
+ return true
+ }
+ }
+ return false
+}
+
+/**
+ * True when a restored session was written by a build predating quit-time agent
+ * capture (#5232) AND carried an agent terminal whose live session can no
+ * longer be resumed — the #5356 silent-loss case. The user gets a one-time,
+ * non-destructive notice instead of a blank shell appearing with no explanation.
+ *
+ * Deliberately conservative to avoid false alarms on the very first launch of
+ * the fixed build, when *every* prior session still lacks the brand-new stamp:
+ * - a present/current stamp means the writer captured sessions → never warn;
+ * - a quit/live capture record means the writer DID persist resumable agents
+ * (a working post-#5232 build) → those sessions are being restored, so
+ * never warn even though the stamp is not yet present;
+ * - only warn when the previous session actually had a resumable agent tab
+ * (`launchAgent`, which pre-fix builds already persisted) to lose.
+ */
+export function shouldNotifyPreUpgradeAgentSessionLoss(session: PreUpgradeNoticeSession): boolean {
+ if ((session.agentSessionCaptureVersion ?? 0) >= AGENT_SESSION_CAPTURE_VERSION) {
+ return false
+ }
+ if (hasQuitOrLiveCaptureRecord(session.sleepingAgentSessionsByPaneKey)) {
+ return false
+ }
+ return hasRestorableAgentTab(session.tabsByWorktree)
+}
diff --git a/src/shared/types.ts b/src/shared/types.ts
index 6c1ebaf8cc8..5b9964b3b23 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -1106,6 +1106,12 @@ export type WorkspaceSessionState = {
defaultTerminalTabsAppliedByWorktreeId?: Record
/** Provider-session resume records captured when workspaces sleep. */
sleepingAgentSessionsByPaneKey?: Record
+ /** Capability stamp: the agent-session capture format version this session
+ * was last written with. Builds predating quit-time agent capture (#5232)
+ * never wrote it, so its absence flags a pre-fix session whose live agent
+ * sessions could not be preserved across the upgrade (#5356). Never seeded
+ * into defaults — absence is the signal. */
+ agentSessionCaptureVersion?: number
}
export type WorkspaceSessionPatch = Partial
diff --git a/src/shared/workspace-session-schema.test.ts b/src/shared/workspace-session-schema.test.ts
index 97e3e68bda6..2c5cd2e2470 100644
--- a/src/shared/workspace-session-schema.test.ts
+++ b/src/shared/workspace-session-schema.test.ts
@@ -425,4 +425,33 @@ describe('parseWorkspaceSession', () => {
expect(result.value.unifiedTabs?.wt[0].viewMode).toBe('terminal')
}
})
+
+ it('preserves the agentSessionCaptureVersion capability stamp (#5356)', () => {
+ const result = parseWorkspaceSession({
+ activeRepoId: null,
+ activeWorktreeId: null,
+ activeTabId: null,
+ tabsByWorktree: {},
+ terminalLayoutsByTabId: {},
+ agentSessionCaptureVersion: 1
+ })
+ expect(result.ok).toBe(true)
+ if (result.ok) {
+ expect(result.value.agentSessionCaptureVersion).toBe(1)
+ }
+ })
+
+ it('leaves the stamp absent for a pre-fix session (absence is the signal)', () => {
+ const result = parseWorkspaceSession({
+ activeRepoId: null,
+ activeWorktreeId: null,
+ activeTabId: null,
+ tabsByWorktree: {},
+ terminalLayoutsByTabId: {}
+ })
+ expect(result.ok).toBe(true)
+ if (result.ok) {
+ expect(result.value.agentSessionCaptureVersion).toBeUndefined()
+ }
+ })
})
diff --git a/src/shared/workspace-session-schema.ts b/src/shared/workspace-session-schema.ts
index ab5d354e017..98344802c76 100644
--- a/src/shared/workspace-session-schema.ts
+++ b/src/shared/workspace-session-schema.ts
@@ -295,7 +295,13 @@ export const workspaceSessionStateSchema: z.ZodType = z.o
)
.optional(),
defaultTerminalTabsAppliedByWorktreeId: z.record(z.string(), z.literal(true)).optional(),
- sleepingAgentSessionsByPaneKey: sleepingAgentSessionsByPaneKeySchema
+ sleepingAgentSessionsByPaneKey: sleepingAgentSessionsByPaneKeySchema,
+ // Why: z.object strips unlisted keys, so the capability stamp must be declared
+ // here or it is dropped on every read and the pre-fix detection never
+ // terminates. `.catch(undefined)` degrades a corrupt value to "absent" (treat
+ // as pre-fix) rather than failing the whole-session parse. Never defaulted —
+ // its absence is the #5356 signal.
+ agentSessionCaptureVersion: z.number().int().nonnegative().optional().catch(undefined)
})
export type ParsedWorkspaceSession =
diff --git a/tests/e2e/agent-session-upgrade-loss-5356.spec.ts b/tests/e2e/agent-session-upgrade-loss-5356.spec.ts
new file mode 100644
index 00000000000..0391159e0f1
--- /dev/null
+++ b/tests/e2e/agent-session-upgrade-loss-5356.spec.ts
@@ -0,0 +1,92 @@
+import { writeFileSync } from 'node:fs'
+import path from 'node:path'
+import { test, expect } from './helpers/orca-app'
+import { getE2ECompletedOnboardingProfile } from './helpers/e2e-completed-onboarding-profile'
+import { waitForSessionReady } from './helpers/store'
+import { createRestartSession } from './helpers/orca-restart'
+
+// #5356: the first launch after upgrading FROM a pre-#5232 version silently
+// replaces every live floating-terminal agent session with a blank shell.
+// Pre-fix builds never wrote `sleepingAgentSessionsByPaneKey` at quit, so the
+// pane-level cold-restore finds nothing to resume and spawns a fresh shell —
+// with no warning at all.
+//
+// This seeds orca-data.json with exactly what such a pre-fix quit left on disk:
+// an agent terminal tab (`launchAgent`, which pre-fix builds DID persist), no
+// resume records, and no post-fix capability stamp. On the first launch of the
+// fixed build the session is unrecoverable (it genuinely cannot be conjured),
+// so the app must NON-DESTRUCTIVELY tell the user instead of silently swapping
+// in a blank shell.
+
+/** A workspace session shaped exactly like a pre-#5232 quit payload. */
+function preFixWorkspaceSession(): Record {
+ return {
+ activeRepoId: null,
+ activeWorktreeId: 'wt-5356',
+ activeTabId: 'tab5356',
+ tabsByWorktree: {
+ 'wt-5356': [
+ {
+ id: 'tab5356',
+ ptyId: null,
+ worktreeId: 'wt-5356',
+ title: 'Codex',
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 1,
+ // Pre-fix builds already persisted launchAgent, so a live agent tab
+ // is identifiable on disk even though its session id is gone.
+ launchAgent: 'codex'
+ }
+ ]
+ },
+ terminalLayoutsByTabId: {}
+ // No sleepingAgentSessionsByPaneKey, no agentSessionCaptureVersion — that
+ // absence is precisely the pre-#5232 state.
+ }
+}
+
+function seedPreFixProfile(userDataDir: string): void {
+ const profile = {
+ ...getE2ECompletedOnboardingProfile(),
+ workspaceSession: preFixWorkspaceSession()
+ }
+ writeFileSync(path.join(userDataDir, 'orca-data.json'), `${JSON.stringify(profile, null, 2)}\n`)
+}
+
+test('#5356 warns instead of silently dropping agent sessions when upgrading from a pre-#5232 version', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set.
+{}, testInfo) => {
+ const session = createRestartSession(testInfo)
+
+ try {
+ // The first launch after the update reads a pre-#5232 session off disk.
+ seedPreFixProfile(session.userDataDir)
+
+ const { app, page } = await session.launch()
+ await waitForSessionReady(page)
+
+ // Capture the loaded-pre-fix-session state either way: pre-fix builds show
+ // no notice here (the silent loss), the fixed build shows the warning.
+ await page.waitForTimeout(3_000)
+ await page.screenshot({
+ path: testInfo.outputPath('after-upgrade-relaunch.png'),
+ fullPage: true
+ })
+
+ // The lost agent session cannot be conjured, so the user must be told
+ // NON-DESTRUCTIVELY that agent state from the previous version could not be
+ // recovered. Pre-fix this notice is absent — that silent loss is the bug.
+ const notice = page.getByTestId('pre-upgrade-agent-session-loss-notice')
+ await expect(notice).toBeVisible({ timeout: 20_000 })
+
+ await page.screenshot({
+ path: testInfo.outputPath('upgrade-loss-notice.png'),
+ fullPage: true
+ })
+
+ await session.close(app)
+ } finally {
+ await session.dispose()
+ }
+})