mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
chore(wsl-mover): split the journal step out of the driver, and format
The review fixes took wsl-move-driver.ts past the line limit; the export-and-journal step moves to its own module unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* The driver's first durable step: export the slept scope and journal it as `prepared`. Nothing has
|
||||
* reached the host yet, so every failure here is a refusal with its reason, never a put-back.
|
||||
*/
|
||||
import type { WslMoveRefusal } from '../../../shared/wsl-move/wsl-move-driver-types'
|
||||
import type { WslMoveCutover } from '../../../shared/wsl-move/wsl-move-cutover'
|
||||
import { journalWslMove } from './wsl-move-delta-plan'
|
||||
import type { WslMoveDriverDeps } from './wsl-move-driver-ports'
|
||||
import {
|
||||
exportRequestOf,
|
||||
isWslMoveScopeChanged,
|
||||
manifestTooLargeRefusal,
|
||||
refusal,
|
||||
type WslMovePrepared
|
||||
} from './wsl-move-driver-plan'
|
||||
import { resetWslMoveToZero } from './wsl-move-reset'
|
||||
import { sleptScrollbackReader, type WslMoveSleptScrollback } from './wsl-move-slept-scrollback'
|
||||
|
||||
export async function journalSleptWslMove(
|
||||
deps: Pick<WslMoveDriverDeps, 'exportManifest' | 'userDataPath'>,
|
||||
prepared: WslMovePrepared,
|
||||
migrationId: string,
|
||||
startedAt: Date,
|
||||
slept: WslMoveSleptScrollback
|
||||
): Promise<WslMoveCutover | WslMoveRefusal> {
|
||||
if (slept.problems.length > 0) {
|
||||
return refusal('wsl_move_export_incomplete', { reasons: [...slept.problems] })
|
||||
}
|
||||
try {
|
||||
// The slept state, which the session freeze then holds for the rest of the move.
|
||||
const exported = deps.exportManifest(
|
||||
exportRequestOf(prepared, migrationId, startedAt, sleptScrollbackReader(slept))
|
||||
)
|
||||
if (exported.blockers.length > 0) {
|
||||
return refusal('wsl_move_export_incomplete', { reasons: [...exported.blockers] })
|
||||
}
|
||||
return journalWslMove({
|
||||
userDataPath: deps.userDataPath,
|
||||
plan: prepared.plan,
|
||||
manifest: exported.manifest,
|
||||
destinationName: prepared.host.name,
|
||||
automationsToReenable: exported.automationsToReenable,
|
||||
now: () => startedAt
|
||||
})
|
||||
} catch (error) {
|
||||
// A write that landed before it threw would leave a record no host ever saw: drop it.
|
||||
await resetWslMoveToZero(
|
||||
{ userDataPath: deps.userDataPath, destination: null },
|
||||
migrationId
|
||||
).catch(() => {})
|
||||
const tooLarge = manifestTooLargeRefusal(error)
|
||||
if (tooLarge) {
|
||||
return tooLarge
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (message === 'wsl_move_already_in_flight') {
|
||||
return refusal('wsl_move_already_in_flight')
|
||||
}
|
||||
if (isWslMoveScopeChanged(error)) {
|
||||
return refusal('wsl_move_scope_changed')
|
||||
}
|
||||
console.warn('[wsl-move] Could not prepare the move:', error)
|
||||
return refusal('wsl_move_export_incomplete', {
|
||||
reasons: [`Orca could not prepare these projects' saved state (${message}).`]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
removeWslMoveCutover,
|
||||
writeWslMoveCutover
|
||||
} from './wsl-move-cutover-journal'
|
||||
import { journalWslMove } from './wsl-move-delta-plan'
|
||||
import { journalSleptWslMove } from './wsl-move-driver-journal'
|
||||
import type { WslMoveDriverDeps, WslMoveHost } from './wsl-move-driver-ports'
|
||||
import {
|
||||
readSleptScrollback,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
import {
|
||||
exportRequestOf,
|
||||
isWslMoveScopeChanged,
|
||||
manifestTooLargeRefusal,
|
||||
prepareWslMove,
|
||||
refusal,
|
||||
type WslMovePrepared
|
||||
@@ -128,47 +127,9 @@ async function runMove(
|
||||
const { panes } = paused
|
||||
const slept = await readSleptScrollback(deps, panes, distro, migrationId)
|
||||
try {
|
||||
if (slept.problems.length > 0) {
|
||||
return refusal('wsl_move_export_incomplete', { reasons: [...slept.problems] })
|
||||
}
|
||||
let journaled: WslMoveCutover
|
||||
try {
|
||||
// The slept state, which the session freeze then holds for the rest of the move.
|
||||
const exported = deps.exportManifest(
|
||||
exportRequestOf(prepared, migrationId, startedAt, sleptScrollbackReader(slept))
|
||||
)
|
||||
if (exported.blockers.length > 0) {
|
||||
return refusal('wsl_move_export_incomplete', { reasons: [...exported.blockers] })
|
||||
}
|
||||
journaled = journalWslMove({
|
||||
userDataPath: deps.userDataPath,
|
||||
plan: prepared.plan,
|
||||
manifest: exported.manifest,
|
||||
destinationName: host.name,
|
||||
automationsToReenable: exported.automationsToReenable,
|
||||
now: () => startedAt
|
||||
})
|
||||
} catch (error) {
|
||||
// A write that landed before it threw would leave a record no host ever saw: drop it.
|
||||
await resetWslMoveToZero(
|
||||
{ userDataPath: deps.userDataPath, destination: null },
|
||||
migrationId
|
||||
).catch(() => {})
|
||||
const tooLarge = manifestTooLargeRefusal(error)
|
||||
if (tooLarge) {
|
||||
return tooLarge
|
||||
}
|
||||
if (describe(error) === 'wsl_move_already_in_flight') {
|
||||
return refusal('wsl_move_already_in_flight')
|
||||
}
|
||||
if (isWslMoveScopeChanged(error)) {
|
||||
return refusal('wsl_move_scope_changed')
|
||||
}
|
||||
// Nothing is journaled yet, so nothing needs putting back; say why instead of a bare reset.
|
||||
console.warn('[wsl-move] Could not prepare the move:', error)
|
||||
return refusal('wsl_move_export_incomplete', {
|
||||
reasons: [`Orca could not prepare these projects' saved state (${describe(error)}).`]
|
||||
})
|
||||
const journaled = await journalSleptWslMove(deps, prepared, migrationId, startedAt, slept)
|
||||
if ('outcome' in journaled) {
|
||||
return journaled
|
||||
}
|
||||
const quiesced = await quiesceWslMove(
|
||||
journaled,
|
||||
@@ -192,7 +153,9 @@ async function runMove(
|
||||
}
|
||||
return quiesced.reason === 'terminals-blocked'
|
||||
? refusal('wsl_move_terminals_blocked', { blockers: quiesced.blockers ?? [] })
|
||||
: refusal('wsl_move_pause_failed', { detail: pauseDetail(quiesced.reason, quiesced.detail) })
|
||||
: refusal('wsl_move_pause_failed', {
|
||||
detail: pauseDetail(quiesced.reason, quiesced.detail)
|
||||
})
|
||||
}
|
||||
try {
|
||||
return await moveQuiesced(deps, host, quiesced.cutover, prepared, running, slept)
|
||||
|
||||
@@ -16,7 +16,14 @@ const scope = { workspaceIds: [WT], workspaceKeys: new Set([WT]) }
|
||||
|
||||
const RELAY = 10
|
||||
// The WSL relay (`init`) starts the pane's login shell as a session leader.
|
||||
const relay: WslGuestProcessRow = { pid: RELAY, ppid: 1, sid: 1, argc: 1, comm: 'init', paneKey: null }
|
||||
const relay: WslGuestProcessRow = {
|
||||
pid: RELAY,
|
||||
ppid: 1,
|
||||
sid: 1,
|
||||
argc: 1,
|
||||
comm: 'init',
|
||||
paneKey: null
|
||||
}
|
||||
const shellA: WslGuestProcessRow = {
|
||||
pid: 100,
|
||||
ppid: RELAY,
|
||||
@@ -49,9 +56,7 @@ describe('classifyWslGuestPaneForeground', () => {
|
||||
['a script waiting at `read`', [child(150, 'bash', { argc: 2 })]],
|
||||
['`sh -c` running a command', [child(150, 'sh', { argc: 3 })]]
|
||||
])('blocks %s', (_label, extra) => {
|
||||
expect(classifyWslGuestPaneForeground([relay, shellA, ...extra], KEY_A)).toBe(
|
||||
'process-running'
|
||||
)
|
||||
expect(classifyWslGuestPaneForeground([relay, shellA, ...extra], KEY_A)).toBe('process-running')
|
||||
})
|
||||
|
||||
it("ignores another pane's work and the relay above the shell", () => {
|
||||
@@ -97,7 +102,13 @@ describe('collectWslMoveGateInputs with a guest foreground reader', () => {
|
||||
})
|
||||
const base: WslMoveGateFactSources = {
|
||||
listTerminals: () => [
|
||||
{ ptyId: 'pty-1', workspaceId: WT, paneKey: KEY_A, wslDistro: 'Ubuntu', foregroundAgent: null }
|
||||
{
|
||||
ptyId: 'pty-1',
|
||||
workspaceId: WT,
|
||||
paneKey: KEY_A,
|
||||
wslDistro: 'Ubuntu',
|
||||
foregroundAgent: null
|
||||
}
|
||||
],
|
||||
inspect: windowsSaysLive,
|
||||
agentRows: () => [],
|
||||
|
||||
@@ -265,7 +265,7 @@ describe('exportWslMoveManifest', () => {
|
||||
expect(exportWslMoveManifest(storeFor(state), REQUEST).blockers).toEqual([])
|
||||
})
|
||||
|
||||
it("is not refused by a project an earlier move retained, whose agent stays here", () => {
|
||||
it('is not refused by a project an earlier move retained, whose agent stays here', () => {
|
||||
const state = sourceState()
|
||||
const retainedWorktree = 'old::\\\\wsl.localhost\\Ubuntu\\home\\me\\old'
|
||||
state.repos.push(repo('old', '\\\\wsl.localhost\\Ubuntu\\home\\me\\old'))
|
||||
@@ -292,7 +292,9 @@ describe('exportWslMoveManifest', () => {
|
||||
// a project reset with a digest mismatch until a restart (D1b-10 run on awin).
|
||||
const state = sourceState()
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the in-memory shape the load normalizes.
|
||||
;(state.repos[0] as unknown as Record<string, unknown>).lastSeenAt = new Date('2026-10-20T00:00:00.000Z')
|
||||
;(state.repos[0] as unknown as Record<string, unknown>).lastSeenAt = new Date(
|
||||
'2026-10-20T00:00:00.000Z'
|
||||
)
|
||||
const { manifest } = exportWslMoveManifest(storeFor(state), REQUEST)
|
||||
expect(() =>
|
||||
assertOrcadMigrationManifestDigest(JSON.parse(JSON.stringify(manifest)))
|
||||
@@ -314,7 +316,11 @@ describe('exportWslMoveManifest', () => {
|
||||
})
|
||||
|
||||
describe('only the selected projects can refuse it', () => {
|
||||
const agentIn = (worktreeId: string, tabId: string, leafId: string): SleepingAgentSessionRecord => ({
|
||||
const agentIn = (
|
||||
worktreeId: string,
|
||||
tabId: string,
|
||||
leafId: string
|
||||
): SleepingAgentSessionRecord => ({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
tabId,
|
||||
worktreeId,
|
||||
@@ -395,7 +401,9 @@ describe('exportWslMoveManifest', () => {
|
||||
// The awin run: a move right after sleeping terminals reset with a digest mismatch, because the
|
||||
// signature covered session fields the manifest parser does not keep.
|
||||
const state = sourceState()
|
||||
const sleptTab: Record<string, unknown> = { ...state.workspaceSession.tabsByWorktree[WORKTREE]![0] }
|
||||
const sleptTab: Record<string, unknown> = {
|
||||
...state.workspaceSession.tabsByWorktree[WORKTREE]![0]
|
||||
}
|
||||
sleptTab.fieldNoParserKeeps = 1
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an extra field on a real tab.
|
||||
state.workspaceSession.tabsByWorktree[WORKTREE] = [sleptTab as unknown as TerminalTab]
|
||||
|
||||
@@ -31,7 +31,9 @@ function spoolRoot(): string {
|
||||
return join(root, 'move-1')
|
||||
}
|
||||
|
||||
function sleptSession(extra: Partial<WorkspaceSessionState['terminalLayoutsByTabId'][string]> = {}) {
|
||||
function sleptSession(
|
||||
extra: Partial<WorkspaceSessionState['terminalLayoutsByTabId'][string]> = {}
|
||||
) {
|
||||
// What Sleep leaves: the layout names the pane, with at most an older renderer buffer.
|
||||
return {
|
||||
...getDefaultWorkspaceSession(),
|
||||
@@ -69,8 +71,11 @@ describe('a slept pane on the snapshot channel', () => {
|
||||
const history = Buffer.from('$ make\r\nok\r\n$ ', 'utf8')
|
||||
// Before the fix the moved terminal opened empty: nothing was carried.
|
||||
expect(projectOrcadMigrationSessionScrollback(sleptSession()).snapshots).toEqual([])
|
||||
const projected = projectOrcadMigrationSessionScrollback(sleptSession(), undefined, false, () =>
|
||||
history
|
||||
const projected = projectOrcadMigrationSessionScrollback(
|
||||
sleptSession(),
|
||||
undefined,
|
||||
false,
|
||||
() => history
|
||||
)
|
||||
const ref = makeTerminalScrollbackSnapshotRef(TAB, LEAF)
|
||||
expect(projected.blockedItems).toEqual([])
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
* own ref. Each buffer is spooled to a file, so only hashes and sizes stay in memory.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { closeSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
closeSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES } from '../../../shared/orcad-migration/orcad-migration-scrollback'
|
||||
import { parsePaneKey } from '../../../shared/stable-pane-id'
|
||||
@@ -154,7 +162,10 @@ export function withSleptSnapshots(
|
||||
if (!descriptor || !entry || entry.sha256 !== descriptor.sha256) {
|
||||
return source.readOrcadMigrationSourceSnapshotChunk(manifest, ref, offset)
|
||||
}
|
||||
const length = Math.max(0, Math.min(ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES, entry.byteLength - offset))
|
||||
const length = Math.max(
|
||||
0,
|
||||
Math.min(ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES, entry.byteLength - offset)
|
||||
)
|
||||
const chunk = Buffer.alloc(length)
|
||||
const fd = openSync(entry.path, 'r')
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user