WIP(wsl-mover): 7f-2 registry complete, coverage proof, startup re-apply (D1b-7f-2)

This commit is contained in:
Neil
2026-10-22 02:37:31 -07:00
parent a9aa4067b9
commit e3d305c843
16 changed files with 409 additions and 56 deletions
+4 -1
View File
@@ -41,6 +41,8 @@ import type { ReconnectFactsFor } from '../persistence/window-save-view'
type RegisterAppHandlersOptions = {
quitResumeRecordsFor?: QuitResumeRecordsFor
reconnectFactsFor?: ReconnectFactsFor
/** Runs after the unload checkpoint stages the window's state, before it flushes. */
afterUnloadStage?: () => void
onBeforeRelaunch?: () => void | Promise<void>
/** Only for app:relaunch, whose app.exit(0) is committed and skips will-quit. */
onBeforeRelaunchExit?: () => void | Promise<void>
@@ -121,7 +123,8 @@ export function registerAppHandlers(store: Store, options: RegisterAppHandlersOp
registerRendererShutdownCheckpointHandler(
store,
options.quitResumeRecordsFor,
options.reconnectFactsFor
options.reconnectFactsFor,
options.afterUnloadStage
)
registerMacKeyboardLayoutChangeNotifications()
ipcMain.handle('app:isProfileStateSaveDelayed', isProfileStateSaveDelayed)
@@ -572,6 +572,7 @@ describe('registerCoreHandlers', () => {
})
expect(registerAppHandlersMock).toHaveBeenCalledWith(store, {
quitResumeRecordsFor: expect.any(Function),
afterUnloadStage: expect.any(Function),
onBeforeRelaunch,
onBeforeRelaunchExit: expect.any(Function)
})
@@ -31,6 +31,7 @@ import {
} from '../runtime-environments/runtime-environments'
import { registerWslHostHandlers } from '../wsl-host-handlers'
import { registerWslMoveHandlers } from '../wsl-move-handlers'
import { reapplyWslMoveFocusRewrites } from '../../persistence/moving-wsl-projects/wsl-move-focus-reapply'
import { registerEphemeralVmHandlers } from '../ephemeral-vm'
import { registerAiVaultHandlers } from '../ai-vault'
import { registerAiVaultSearchHandlers } from '../ai-vault-search'
@@ -182,6 +183,10 @@ export function registerCoreHandlers(
runtime.shutdownReconnectFactsFor(session, partition)
}
: {}),
// The checkpoint writes the window's focus, which is stale for a project moved under it.
afterUnloadStage: () => {
reapplyWslMoveFocusRewrites(app.getPath('userData'), store)
},
onBeforeRelaunch: lifecycleOptions.onBeforeRelaunch,
// Why here, not onBeforeRelaunch: restart and profile switches quit through will-quit, which closes it.
onBeforeRelaunchExit: () => closeNotificationDismissalsBeforeRelaunch(runtime)
+4 -1
View File
@@ -102,7 +102,9 @@ function stagedSessionForMain(
export function registerRendererShutdownCheckpointHandler(
store: Store,
quitResumeRecordsFor: QuitResumeRecordsFor = () => [],
reconnectFactsFor?: ReconnectFactsFor
reconnectFactsFor?: ReconnectFactsFor,
/** Corrects what the window staged before the flush; must not throw. */
afterStage: () => void = () => {}
): void {
// Why: beforeunload cannot await, so the sync reply only reports staging.
// Durability is joined out-of-band by paths that are about to navigate.
@@ -122,6 +124,7 @@ export function registerRendererShutdownCheckpointHandler(
}
}
store.updateUI(args.ui)
afterStage()
} catch (error) {
console.error('[app] Failed to stage renderer state before unload:', error)
ok = false
@@ -0,0 +1,25 @@
/**
* The Automations page host filter after a WSL move. It selects one host or all of them, so it
* cannot also cover the WSL host; a filter on this desktop would hide every automation the move
* carried. Only then does it widen to All hosts, once: the ledgered run never repeats, so a
* later choice of this desktop stays.
*/
import type { PersistedAutomationHostFilter } from '../../../shared/automation-host-filter'
import { hostStableKey } from '../../../shared/automation-owner-key'
const DESKTOP_SELF_HOST_KEY = hostStableKey({
authority: { kind: 'desktop' },
selector: { kind: 'self' }
})
/** The filter to persist, or null when it stays. */
export function wslMoveAutomationHostFilterUpdate(
filter: PersistedAutomationHostFilter | undefined,
movedAutomationCount: number
): PersistedAutomationHostFilter | null {
return movedAutomationCount > 0 &&
filter?.kind === 'host' &&
filter.hostKey === DESKTOP_SELF_HOST_KEY
? { kind: 'all' }
: null
}
@@ -0,0 +1,34 @@
/**
* Every desktop rewrite store a committed WSL move runs, registered in run order. Startup calls it
* once, before the first rewrite pass; the coverage test checks it against the disposition tables.
*/
import type { WslMoveBrowserCookieCopyDependencies } from './wsl-move-browser-cookie-copy'
import { registerWslMoveBrowserCookieStore } from './wsl-move-browser-cookie-store'
import { registerWslMoveRendererRewriteStores } from './wsl-move-desktop-rewrite-stores'
import {
registerWslMoveHostAutomationsStore,
type WslMoveHostAutomations
} from './wsl-move-host-automations-rewrite'
import {
registerWslMoveMainRewriteStores,
type WslMoveMainRewriteProfile
} from './wsl-move-main-rewrite-stores'
import { registerWslMoveOrchestrationHistoryStore } from './wsl-move-orchestration-history-rewrite'
export type WslMoveDesktopRewriteRegistryDependencies = {
profile: WslMoveMainRewriteProfile
orchestrationDatabasePath: string
hostAutomations: WslMoveHostAutomations
browserCookies: WslMoveBrowserCookieCopyDependencies
}
export function registerWslMoveDesktopRewriteRegistry(
dependencies: WslMoveDesktopRewriteRegistryDependencies
): void {
registerWslMoveMainRewriteStores(dependencies.profile)
registerWslMoveOrchestrationHistoryStore(dependencies.orchestrationDatabasePath)
registerWslMoveRendererRewriteStores()
registerWslMoveHostAutomationsStore(dependencies.hostAutomations)
// Last among main-side stores: it throws until browser sessions exist, pausing any store after it.
registerWslMoveBrowserCookieStore(dependencies.browserCookies)
}
@@ -38,9 +38,8 @@ export type WslMoveDesktopRewriteStore = WslMoveMainRewriteStore | WslMoveRender
const STORE_ID_PATTERN = /^[A-Za-z0-9:._-]{1,128}$/
const MAX_STORES = 256
// 7f (the last store PR) flips this; until then no move may leave `destination-committed`.
// 7f-1 registers `browser-cookies` (wsl-move-browser-cookie-store.ts); the flip waits for 7c-7e.
export const WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE: boolean = false
// Every desktop-rewrite disposition has its store (wsl-move-rewrite-store-coverage.test.ts proves it).
export const WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE: boolean = true
let registryComplete = WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE
@@ -0,0 +1,71 @@
/**
* A window hydrated before a move's rewrite can still write the old focus (the session's active
* workspace, the UI's last-active worktree) for a moved project, and its unload checkpoint does.
* So at startup and after every unload checkpoint the corrective part of `ui-routing` and
* `session-globals` runs again for each committed move. Nothing else is re-applied: the additive
* rewrites (host copies, visible hosts, the automation filter) would undo a later user choice.
* No ledger entry is written; the ledgered pass alone owns the journal.
*/
import { LOCAL_EXECUTION_HOST_ID, toRuntimeExecutionHostId } from '../../../shared/execution-host'
import { WSL_MOVE_CUTOVER_PHASES } from '../../../shared/wsl-move/wsl-move-cutover'
import { wslHostsGateOpen } from '../../wsl/wsl-host-gate'
import type { Store } from '../loading-store/store'
import { listWslMoveCutovers } from './wsl-move-cutover-journal'
import { createUncToLinuxPathRewriter } from './wsl-move-path-rewriter'
import { wslMoveRewriteScope } from './wsl-move-rewrite-scope'
import { wslMoveSessionGlobalsPatch } from './wsl-move-session-rewrite'
import { wslMoveLastActiveWorktreeUpdate } from './wsl-move-ui-routing-rewrite'
export type WslMoveFocusProfile = Pick<
Store,
'getUI' | 'updateUI' | 'getWorkspaceSession' | 'patchWorkspaceSession'
>
const COMMITTED_PHASE_INDEX = WSL_MOVE_CUTOVER_PHASES.indexOf('destination-committed')
/** Never throws; a no-op with the gate closed or no committed move. Returns how many writes it made. */
export function reapplyWslMoveFocusRewrites(
userDataPath: string,
profile: WslMoveFocusProfile,
warn: (message: string) => void = (message) => console.warn(message)
): number {
if (!wslHostsGateOpen()) {
return 0
}
let writes = 0
try {
for (const cutover of listWslMoveCutovers(userDataPath)) {
if (WSL_MOVE_CUTOVER_PHASES.indexOf(cutover.phase) < COMMITTED_PHASE_INDEX) {
continue
}
const scope = wslMoveRewriteScope({
cutover,
rewritePath: createUncToLinuxPathRewriter(cutover.manifest.source.distro),
destinationHostId: toRuntimeExecutionHostId(cutover.destinationEnvironmentId),
recordSeededHostWorkspaceIds: () => {
throw new Error('wsl_move_focus_reapply_seeds_nothing')
}
})
const lastActive = wslMoveLastActiveWorktreeUpdate(profile.getUI(), scope)
if (lastActive) {
profile.updateUI(lastActive)
writes += 1
}
const focus = wslMoveSessionGlobalsPatch(
profile.getWorkspaceSession(LOCAL_EXECUTION_HOST_ID),
scope
)
if (focus) {
profile.patchWorkspaceSession(focus, LOCAL_EXECUTION_HOST_ID)
writes += 1
}
}
} catch (error) {
warn(`[wsl-move] Could not re-apply a moved project's focus: ${describe(error)}`)
}
return writes
}
function describe(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
@@ -5,6 +5,7 @@
*/
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
import type { Store } from '../loading-store/store'
import { wslMoveAutomationHostFilterUpdate } from './wsl-move-automation-filter-rewrite'
import { registerWslMoveDesktopRewriteStore } from './wsl-move-desktop-rewrite-stores'
import { wslMoveRewriteScope } from './wsl-move-rewrite-scope'
import { wslMoveHostSessionSeed, wslMoveSessionGlobalsPatch } from './wsl-move-session-rewrite'
@@ -27,7 +28,15 @@ export function registerWslMoveMainRewriteStores(profile: WslMoveMainRewriteProf
id: 'ui-routing',
side: 'main',
run: (context) => {
const updates = rewriteWslMoveUiRouting(profile.getUI(), wslMoveRewriteScope(context))
const ui = profile.getUI()
const automationHostFilter = wslMoveAutomationHostFilterUpdate(
ui.automationHostFilter,
context.cutover.manifest.payload.dormantState?.automations?.length ?? 0
)
const updates = {
...rewriteWslMoveUiRouting(ui, wslMoveRewriteScope(context)),
...(automationHostFilter ? { automationHostFilter } : {})
}
if (Object.keys(updates).length > 0) {
profile.updateUI(updates)
}
@@ -0,0 +1,152 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { WSL_MOVE_BROWSER_COOKIES_STORE_ID } from '../../../shared/wsl-move/wsl-move-browser-cookie-copy'
import {
PERSISTED_STATE_DISPOSITIONS,
WORKSPACE_SESSION_DISPOSITIONS,
type WslMoveFieldDisposition
} from '../../../shared/wsl-move/wsl-move-dispositions'
import { OUT_OF_STORE_DISPOSITIONS } from '../../../shared/wsl-move/wsl-move-out-of-store-dispositions'
import type { WslMoveRendererRewriteStoreId } from '../../../shared/wsl-move/wsl-move-renderer-rewrite-types'
import {
LOCAL_STORAGE_MODULE_DISPOSITIONS,
SQLITE_TABLE_DISPOSITIONS
} from '../../../shared/wsl-move/wsl-move-storage-dispositions'
import { UI_STATE_DISPOSITIONS } from '../../../shared/wsl-move/wsl-move-ui-dispositions'
import { registerWslMoveDesktopRewriteRegistry } from './wsl-move-desktop-rewrite-registry'
import {
isWslMoveDesktopRewriteRegistryComplete,
listWslMoveDesktopRewriteStores,
resetWslMoveDesktopRewriteStoresForTests,
WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE
} from './wsl-move-desktop-rewrite-stores'
import { WSL_MOVE_HOST_AUTOMATIONS_STORE_ID } from './wsl-move-host-automations-rewrite'
import { WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID } from './wsl-move-orchestration-history-rewrite'
vi.mock('../../browser/browser-partition-cookie-copy', () => ({
copyBrowserPartitionCookies: vi.fn(),
flushBrowserPartitionCookies: vi.fn()
}))
vi.mock('../../browser/browser-session-registry', () => ({ browserSessionRegistry: {} }))
vi.mock('../../browser/browser-session-startup', () => ({ browserSessionsInitialized: vi.fn() }))
vi.mock('../../browser/browser-route-partition-binding-runtime', () => ({
activeBrowserRoutePartitionOrcaProfileId: vi.fn(),
currentBrowserRoutePartitionBindingStore: vi.fn()
}))
vi.mock('../../browser/browser-route-partition-storage-dependencies', () => ({
releaseEvictedBrowserRoutePartitionStorage: vi.fn()
}))
const UI_ROUTING = 'ui-routing'
const SESSION_GLOBALS = 'session-globals'
const HOST_SESSION_SEED = 'host-session-seed'
const renderer = (id: WslMoveRendererRewriteStoreId): string => id
/**
* The store that carries out each disposition row it names. Keys are `<table>.<row>` after the
* disposition tables; every row here must exist, and every registered store must appear.
*/
const STORE_BY_ROW: Record<string, string> = {
'persisted.workspaceSessionsByHostId': HOST_SESSION_SEED,
// A move row, not a desktop rewrite: the host turns back on what the manifest carried disabled.
'persisted.automations': WSL_MOVE_HOST_AUTOMATIONS_STORE_ID,
'session.activeWorktreeId': SESSION_GLOBALS,
'session.activeWorkspaceExecutionHostId': SESSION_GLOBALS,
'session.activeWorkspaceKey': SESSION_GLOBALS,
'ui.lastActiveWorktreeId': UI_ROUTING,
'ui.manualRepoOrder': UI_ROUTING,
'ui.explorerDisplayRootByWorktree': UI_ROUTING,
'ui.showDotfilesByWorktree': UI_ROUTING,
'ui.workspaceCleanup': UI_ROUTING,
'ui.collapsedGroups': UI_ROUTING,
// The scalar stays; the visible host list it resolves to gains the WSL host.
'ui.workspaceHostScope': UI_ROUTING,
'ui.workspaceHostOrder': UI_ROUTING,
'ui.visibleWorkspaceHostIds': UI_ROUTING,
'ui.agentsVisibleHostIds': UI_ROUTING,
'ui.automationHostFilter': UI_ROUTING,
'localStorage.src/renderer/src/lib/quick-open-file-history.ts': renderer('quick-open-history'),
'localStorage.src/renderer/src/components/editor/ipynb-kernel-store.ts':
renderer('notebook-environments'),
'localStorage.src/renderer/src/components/editor/file-view-preference-storage.ts':
renderer('file-view-preferences'),
'sqlite.orchestration.worker_dispatches': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID,
'sqlite.orchestration.worker_terminal_resources': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID,
'outOfStore.orchestration.db': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID,
'outOfStore.browser partition (cookies, site storage)': WSL_MOVE_BROWSER_COOKIES_STORE_ID
}
const prefixed = (
prefix: string,
table: Record<string, WslMoveFieldDisposition>
): [string, WslMoveFieldDisposition][] =>
Object.entries(table).map(([key, row]) => [`${prefix}.${key}`, row])
const ROWS = new Map<string, WslMoveFieldDisposition>([
...prefixed('persisted', PERSISTED_STATE_DISPOSITIONS),
...prefixed('session', WORKSPACE_SESSION_DISPOSITIONS),
...prefixed('ui', UI_STATE_DISPOSITIONS),
...prefixed('localStorage', LOCAL_STORAGE_MODULE_DISPOSITIONS),
...prefixed('sqlite', SQLITE_TABLE_DISPOSITIONS),
...OUT_OF_STORE_DISPOSITIONS.map((row): [string, WslMoveFieldDisposition] => [
`outOfStore.${row.store}`,
row
])
])
/** The stores startup registers, through the same entry point. */
function registeredStoreIds(): string[] {
registerWslMoveDesktopRewriteRegistry({
profile: {
getUI: vi.fn(),
updateUI: vi.fn(),
getWorkspaceSession: vi.fn(),
patchWorkspaceSession: vi.fn(),
setWorkspaceSession: vi.fn(),
flushPendingOrThrowAsync: vi.fn()
},
orchestrationDatabasePath: '/nowhere/orchestration.db',
hostAutomations: { enable: vi.fn() },
browserCookies: {
userDataPath: '/nowhere',
sessionsReady: () => false,
orcaProfileId: () => null,
listEnvironments: () => [],
listBrowserProfiles: () => [],
resolveHostStorage: vi.fn(),
bindHostPartition: vi.fn(),
copyCookies: vi.fn(),
flushCookies: vi.fn()
}
})
return listWslMoveDesktopRewriteStores().map((store) => store.id)
}
afterEach(() => {
resetWslMoveDesktopRewriteStoresForTests()
})
describe('WSL move desktop rewrite registry coverage', () => {
it('gives every desktop-rewrite disposition row a store', () => {
const rewriteRows = [...ROWS]
.filter(([, row]) => row.disposition === 'rewrite-desktop' || row.rewriteDesktopCopy)
.map(([key]) => key)
expect(rewriteRows.length).toBeGreaterThan(0)
expect(rewriteRows.filter((key) => !Object.hasOwn(STORE_BY_ROW, key))).toEqual([])
})
it('names only rows the disposition tables have', () => {
expect(Object.keys(STORE_BY_ROW).filter((key) => !ROWS.has(key))).toEqual([])
})
it('maps every registered store to a row, and registers every mapped store', () => {
const registered = registeredStoreIds()
const mapped = new Set(Object.values(STORE_BY_ROW))
expect(registered.filter((id) => !mapped.has(id))).toEqual([])
expect([...mapped].filter((id) => !registered.includes(id))).toEqual([])
})
it('is marked complete', () => {
expect(WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE).toBe(true)
expect(isWslMoveDesktopRewriteRegistryComplete()).toBe(true)
})
})
@@ -20,26 +20,52 @@ describe('WSL move startup rewrite', () => {
it('neither runs the pass nor waits with the gate closed', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
const recover = vi.fn(async () => {})
await runWslMoveStartupRewrite({ userDataPath: '/data' }, recover)
const reapply = vi.fn()
await runWslMoveStartupRewrite({ userDataPath: '/data' }, { recover, reapply })
expect(recover).not.toHaveBeenCalled()
expect(reapply).not.toHaveBeenCalled()
expect(pass.run).not.toHaveBeenCalled()
})
it('resets an interrupted move before the pass, and still runs the pass if that fails', async () => {
it('resets an interrupted move, then re-applies finished ones, before the pass, and still runs the pass if either fails', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
const order: string[] = []
pass.run.mockImplementation(async () => {
order.push('pass')
})
await runWslMoveStartupRewrite({ userDataPath: '/data' }, async () => {
order.push('recover')
})
await runWslMoveStartupRewrite(
{ userDataPath: '/data' },
{
recover: async () => {
order.push('recover')
},
reapply: () => {
order.push('reapply')
}
}
)
vi.spyOn(console, 'warn').mockImplementation(() => {})
await runWslMoveStartupRewrite({ userDataPath: '/data' }, async () => {
order.push('recover-failed')
throw new Error('journal unreadable')
})
expect(order).toEqual(['recover', 'pass', 'recover-failed', 'pass'])
await runWslMoveStartupRewrite(
{ userDataPath: '/data' },
{
recover: async () => {
order.push('recover-failed')
throw new Error('journal unreadable')
},
reapply: () => {
order.push('reapply-failed')
throw new Error('profile unreadable')
}
}
)
expect(order).toEqual([
'recover',
'reapply',
'pass',
'recover-failed',
'reapply-failed',
'pass'
])
})
it('resolves once the pass finishes', async () => {
@@ -13,29 +13,42 @@ import {
// behind it, with the stale-hydration risk this wait otherwise closes.
export const WSL_MOVE_STARTUP_REWRITE_WAIT_MS = 5_000
export type WslMoveStartupRewriteSteps = {
/** Resets a move interrupted before commit; its failure never skips what follows. */
recover?: () => Promise<unknown>
/** Corrects what a stale window wrote over a finished rewrite; must not throw. */
reapply?: () => void
}
/**
* Resolves when the pass finishes or the wait runs out; at once with the gate closed. `before`
* (the reset of an interrupted move) runs first, inside the same wait, and its failure never
* skips the pass.
* Resolves when the pass finishes or the wait runs out; at once with the gate closed. `recover`,
* then `reapply`, run first inside the same wait, so a finished move is corrected even when the
* pass is slow.
*/
export function runWslMoveStartupRewrite(
options: WslMoveDesktopRewritePassOptions,
before: () => Promise<unknown> = () => Promise.resolve(),
waitMs: number = WSL_MOVE_STARTUP_REWRITE_WAIT_MS
steps: WslMoveStartupRewriteSteps = {}
): Promise<void> {
if (!wslHostsGateOpen()) {
return Promise.resolve()
}
let timer: ReturnType<typeof setTimeout> | undefined
const waited = new Promise<void>((resolve) => {
timer = setTimeout(resolve, waitMs)
timer = setTimeout(resolve, WSL_MOVE_STARTUP_REWRITE_WAIT_MS)
timer.unref?.()
})
const pass = Promise.resolve()
.then(before)
.then(steps.recover)
.catch((error: unknown) => {
console.warn('[wsl-move] Startup recovery failed before the desktop rewrite:', error)
})
.then(steps.reapply)
.catch((error: unknown) => {
console.warn(
'[wsl-move] Re-applying a finished move failed before the desktop rewrite:',
error
)
})
.then(() => runWslMoveDesktopRewritePass(options))
return Promise.race([pass, waited]).finally(() => clearTimeout(timer))
}
@@ -49,11 +49,7 @@ export function rewriteWslMoveUiRouting(
ui: WslMoveUiRoutingState,
scope: WslMoveRewriteScope
): Partial<WslMoveUiRoutingState> {
const updates: Partial<WslMoveUiRoutingState> = {}
const lastActive = ui.lastActiveWorktreeId
if (lastActive && scope.inScope(lastActive) && scope.worktreeId(lastActive) !== lastActive) {
updates.lastActiveWorktreeId = scope.worktreeId(lastActive)
}
const updates: Partial<WslMoveUiRoutingState> = { ...wslMoveLastActiveWorktreeUpdate(ui, scope) }
const hostWorktreeKey = (key: string): string =>
scope.inScope(key) ? scope.worktreeId(key) : key
const dotfiles = withHostKeys(
@@ -114,6 +110,20 @@ export function rewriteWslMoveUiRouting(
return updates
}
/**
* The last-active worktree on the host when it names a moved local one. Corrective only, so it is
* also safe to re-apply after a stale window wrote the old id back.
*/
export function wslMoveLastActiveWorktreeUpdate(
ui: Pick<WslMoveUiRoutingState, 'lastActiveWorktreeId'>,
scope: WslMoveRewriteScope
): Pick<WslMoveUiRoutingState, 'lastActiveWorktreeId'> | null {
const lastActive = ui.lastActiveWorktreeId
return lastActive && scope.inScope(lastActive) && scope.worktreeId(lastActive) !== lastActive
? { lastActiveWorktreeId: scope.worktreeId(lastActive) }
: null
}
/**
* The host form of each moved key beside the local one, which a downgrade still reads. A moved
* key that keeps its form (a folder workspace's) has only its value mapped.
@@ -16,17 +16,12 @@ import { loadWslMoveFence } from '../persistence/moving-wsl-projects/wsl-move-sc
import { recoverInterruptedWslMoves } from '../persistence/moving-wsl-projects/wsl-move-reset'
import { runWslMoveStartupRewrite } from '../persistence/moving-wsl-projects/wsl-move-startup-rewrite'
import { installWslMoveRetention } from '../persistence/moving-wsl-projects/wsl-move-retention'
import { registerWslMoveMainRewriteStores } from '../persistence/moving-wsl-projects/wsl-move-main-rewrite-stores'
import { registerWslMoveRendererRewriteStores } from '../persistence/moving-wsl-projects/wsl-move-desktop-rewrite-stores'
import { registerWslMoveOrchestrationHistoryStore } from '../persistence/moving-wsl-projects/wsl-move-orchestration-history-rewrite'
import { registerWslMoveHostAutomationsStore } from '../persistence/moving-wsl-projects/wsl-move-host-automations-rewrite'
import { registerWslMoveDesktopRewriteRegistry } from '../persistence/moving-wsl-projects/wsl-move-desktop-rewrite-registry'
import { reapplyWslMoveFocusRewrites } from '../persistence/moving-wsl-projects/wsl-move-focus-reapply'
import { enableWslHostAutomations } from '../ipc/wsl-host-automation-enable'
import { scheduleWslMoveRewriteRetry } from '../persistence/moving-wsl-projects/wsl-move-rewrite-retry'
import { orchestrationDbFile } from '../runtime/orchestration/db/orchestration-db-file'
import {
productionWslMoveBrowserCookieCopyDependencies,
registerWslMoveBrowserCookieStore
} from '../persistence/moving-wsl-projects/wsl-move-browser-cookie-store'
import { productionWslMoveBrowserCookieCopyDependencies } from '../persistence/moving-wsl-projects/wsl-move-browser-cookie-store'
import { createProfileStateStoreForStartup } from '../persistence/profile-state/profile-state-startup-authority'
import { initializeBrowserClientHostId } from '../browser/browser-client-host-id'
import { scheduleSecretProtectionGapReport } from '../host/deferred-secret-protection-report'
@@ -168,20 +163,24 @@ export async function initializeReadyFoundation(): Promise<void> {
state.store = store
setNodeDistMirrorSettingReader(() => store.getSettings().nodeDistMirrorUrl)
const userDataPath = getAppEnvironment().getPath('userData')
registerWslMoveMainRewriteStores(store)
registerWslMoveOrchestrationHistoryStore(
orchestrationDbFile(getAppEnvironment().getPath('userData'))
)
registerWslMoveRendererRewriteStores()
registerWslMoveHostAutomationsStore({
enable: (environmentId, ids) => enableWslHostAutomations(userDataPath, environmentId, ids)
registerWslMoveDesktopRewriteRegistry({
profile: store,
orchestrationDatabasePath: orchestrationDbFile(userDataPath),
hostAutomations: {
enable: (environmentId, ids) => enableWslHostAutomations(userDataPath, environmentId, ids)
},
browserCookies: productionWslMoveBrowserCookieCopyDependencies(userDataPath)
})
// Last among main-side stores: it throws until browser sessions exist, pausing any store after it.
registerWslMoveBrowserCookieStore(productionWslMoveBrowserCookieCopyDependencies(userDataPath))
// Resets a move interrupted before commit, then finishes one that committed; main-process-ready
// waits on both, bounded. Gated off, neither runs.
state.wslMoveStartupRewriteReady = runWslMoveStartupRewrite({ userDataPath }, () =>
recoverInterruptedWslMoves(userDataPath)
// Resets a move interrupted before commit, corrects stale focus on finished ones, then finishes
// one that committed; main-process-ready waits on all three, bounded. Gated off, none runs.
state.wslMoveStartupRewriteReady = runWslMoveStartupRewrite(
{ userDataPath },
{
recover: () => recoverInterruptedWslMoves(userDataPath),
reapply: () => {
reapplyWslMoveFocusRewrites(userDataPath, store)
}
}
)
// Re-marks retained rows a downgrade stripped; never awaited, since nothing it writes gates a read.
void state.wslMoveStartupRewriteReady.then(() => {
+5 -1
View File
@@ -78,7 +78,11 @@ export const PERSISTED_STATE_DISPOSITIONS = {
item: 22,
note: "The desktop seeds the WSL host's session from the moved 'local' rows (D1b-7c)."
},
remoteLayoutSnapshots: { disposition: 'rewrite-desktop', item: 47 },
remoteLayoutSnapshots: {
disposition: 'rederive',
item: 47,
note: "Rebuilt from the host's first tab stream; the seeded workspaceSessionsByHostId gives first paint."
},
sshTargets: notApplicableDisposition(62),
sshTargetGenerationCounter: notApplicableDisposition(62),
deletedSshConfigAliases: notApplicableDisposition(62),
@@ -25,11 +25,10 @@ export const UI_STATE_DISPOSITIONS = {
acknowledgedAgentsByPaneKey: moveDisposition(38, { note: 'Pane keys are kept.' }),
activityClearedAtByPaneKey: moveDisposition(38, { note: 'Pane keys are kept.' }),
manuallyUnreadTurnsByPaneKey: moveDisposition(38, { note: 'Pane keys are kept.' }),
trustedOrcaHooks: {
disposition: 'rewrite-desktop',
item: 40,
note: 'Kept by repoId; the trust prune must not drop it while the repo is between hosts.'
},
trustedOrcaHooks: notApplicableDisposition(
40,
'Kept by repoId, which the move keeps; wsl-moved-repo-ui-retention.ts stops the trust prune dropping it.'
),
collapsedGroups: {
disposition: 'rewrite-desktop',
item: 38,
@@ -62,7 +61,7 @@ export const UI_STATE_DISPOSITIONS = {
automationHostFilter: {
disposition: 'rewrite-desktop',
item: 38,
note: "A 'local' filter also covers the WSL host."
note: 'Single-select: a filter on this desktop widens to All hosts once, if the move carried automations.'
},
jiraQuickFilterState: notApplicableDisposition(38, 'Filters belong to the Jira host.'),
jiraSavedFilterState: notApplicableDisposition(