fix(orcad): skip the moved shell-history pass on starts with nothing to install (#29556)

Every orcad start ran installPendingOrcadMigrationShellHistory, which paid a
process inventory with an empty worktree list. That inventory could leave a
surviving daemon session unfiled and drop its restored orchestration receipt
before any client connected. Return early unless a committed migration still
holds a received shell history, and refresh against the host's real worktrees
when one does.

Follow-up to #28902.
This commit is contained in:
Neil
2026-10-16 02:47:02 -07:00
committed by GitHub
parent da14046614
commit b95ea32797
5 changed files with 91 additions and 8 deletions
@@ -227,12 +227,20 @@ describe('orcad migration shell history through the store', () => {
const reloaded = bashHostStore()
expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull()
expect(existsSync(receivedPath(input, blobs[0].descriptor.key))).toBe(true)
expect(reloaded.hasPendingOrcadMigrationShellHistory()).toBe(true)
reloaded.installPendingOrcadMigrationReceivedBlobs({ worktreeHasLiveShells: () => true })
expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull()
expect(reloaded.hasPendingOrcadMigrationShellHistory()).toBe(true)
reloaded.installPendingOrcadMigrationReceivedBlobs({ worktreeHasLiveShells: noLiveShells })
expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH)
expect(existsSync(receivedPath(input, blobs[0].descriptor.key))).toBe(false)
// Every later orcad start skips the post-attach pass and its terminal inventory.
expect(reloaded.hasPendingOrcadMigrationShellHistory()).toBe(false)
})
it('reports nothing pending on a host that never received a migration', () => {
expect(bashHostStore().hasPendingOrcadMigrationShellHistory()).toBe(false)
})
})
@@ -1,3 +1,4 @@
import { ORCAD_MIGRATION_BLOB_KIND_CONTRACTS } from '../../../shared/orcad-migration-blob-transfer'
import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest'
import type { PersistedState } from '../../../shared/persisted-state-types'
import { parsePaneKey } from '../../../shared/stable-pane-id'
@@ -5,6 +6,7 @@ import { getHistoryRoot } from '../../terminal-history-paths'
import type { StoreRuntimeState } from '../loading-store/store-runtime-state'
import { findOrcadMigrationImportReceipt } from './orcad-catalog-receipt-ledger'
import { installOrcadMigrationReceivedBlobs } from './orcad-migration-blob-installers'
import { listOrcadMigrationReceivedBlobNames } from './orcad-migration-blob-staging'
import type { OrcadMigrationRequestIdentity } from './orcad-migration-staged-file'
type OrcadMigrationBlobInstallRuntime = Pick<
@@ -72,6 +74,21 @@ export class OrcadMigrationBlobInstallPersistence {
}
}
/** Whether any committed migration still holds a received shell history; only the runtime installs those. */
hasPendingOrcadMigrationShellHistory(): boolean {
const { runtime } = this[orcadMigrationBlobInstallContext]
return [
...(runtime.state.orcadMigrationImportReceipts ?? []),
...(runtime.state.orcadMigrationEvictedReceipts ?? [])
].some((receipt) =>
listOrcadMigrationReceivedBlobNames(
runtime.orcadMigrationBlobRoot,
receipt,
'shell-history'
).some((key) => ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['shell-history'].key.test(key))
)
}
/** Retries a history install that waited on this worktree's shells; a no-op for any other. */
installDeferredOrcadMigrationShellHistory(
worktreeId: string,
@@ -108,13 +108,21 @@ export abstract class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithRes
*/
async installPendingOrcadMigrationShellHistory(): Promise<void> {
const store = this.store
if (!store?.installPendingOrcadMigrationReceivedBlobs) {
// Why: runs on every orcad start; with nothing to install it must not pay a process inventory.
if (
!store?.installPendingOrcadMigrationReceivedBlobs ||
!store.hasPendingOrcadMigrationShellHistory?.()
) {
return
}
// Daemon shells that outlived a restart count as live only once the inventory recorded them.
// Why the real worktrees: an empty list re-files sessions it cannot resolve and drops their
// restored orchestration receipts.
if (
this.ptyController?.listProcesses &&
!(await this.refreshPtyWorktreeRecordsWithControllerInventory([]))
!(await this.refreshPtyWorktreeRecordsWithControllerInventory([
...(await this.getResolvedWorktreeMap()).values()
]))
) {
console.warn('[orcad-migration] shell history waits: the terminal inventory did not answer')
return
@@ -1,9 +1,17 @@
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime-test-mocks.spec'
import { store } from './orca-runtime-test-fixtures.spec'
import { store, TEST_WORKTREE_ID, TEST_WORKTREE_PATH } from './orca-runtime-test-fixtures.spec'
type RuntimeInternals = {
recordPtyWorktree: (ptyId: string, worktreeId: string, state?: { connected?: boolean }) => unknown
restoredOrchestrationAuthorityByPtyId: Map<string, unknown>
buildResolvedWorktreeFromId: (worktreeId: string) => unknown
getResolvedWorktreeMap: () => Promise<Map<string, unknown>>
}
function internalsOf(runtime: OrcaRuntimeService): RuntimeInternals {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these members are declared on OrcaRuntimeService's inheritance chain.
return runtime as unknown as RuntimeInternals
}
describe('a moved shell history that waited on live shells', () => {
@@ -13,8 +21,7 @@ describe('a moved shell history that waited on live shells', () => {
...store,
installDeferredOrcadMigrationShellHistory: installDeferred
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: recordPtyWorktree is declared on OrcaRuntimeService's inheritance chain.
const internals = runtime as unknown as RuntimeInternals
const internals = internalsOf(runtime)
internals.recordPtyWorktree('pty-1', 'repo-1::/srv/app', { connected: true })
internals.recordPtyWorktree('pty-2', 'repo-1::/srv/app', { connected: true })
@@ -32,21 +39,29 @@ describe('a moved shell history that waited on live shells', () => {
const installPending = vi.fn()
const runtime = new OrcaRuntimeService({
...store,
installPendingOrcadMigrationReceivedBlobs: installPending
installPendingOrcadMigrationReceivedBlobs: installPending,
hasPendingOrcadMigrationShellHistory: () => true
})
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: vi.fn(async () => [
{ id: 'daemon-pty', cwd: '/srv/app', title: 'zsh', worktreeId: 'repo-1::/srv/app' }
{ id: 'daemon-pty', cwd: '/srv/app', title: 'zsh', worktreeId: 'repo-1::/srv/app' },
// No provider owner: only the host's real worktrees can file this shell by its cwd.
{ id: 'unowned-pty', cwd: TEST_WORKTREE_PATH, title: 'zsh' }
])
})
const internals = internalsOf(runtime)
vi.spyOn(internals, 'getResolvedWorktreeMap').mockResolvedValue(
new Map([[TEST_WORKTREE_ID, internals.buildResolvedWorktreeFromId(TEST_WORKTREE_ID)]])
)
await runtime.installPendingOrcadMigrationShellHistory()
const [options] = installPending.mock.calls[0]
expect(options.worktreeHasLiveShells('repo-1::/srv/app')).toBe(true)
expect(options.worktreeHasLiveShells(TEST_WORKTREE_ID)).toBe(true)
expect(options.worktreeHasLiveShells('repo-1::/srv/other')).toBe(false)
})
@@ -54,7 +69,8 @@ describe('a moved shell history that waited on live shells', () => {
const installPending = vi.fn()
const runtime = new OrcaRuntimeService({
...store,
installPendingOrcadMigrationReceivedBlobs: installPending
installPendingOrcadMigrationReceivedBlobs: installPending,
hasPendingOrcadMigrationShellHistory: () => true
})
runtime.setPtyController({
write: () => true,
@@ -71,4 +87,37 @@ describe('a moved shell history that waited on live shells', () => {
expect(installPending).not.toHaveBeenCalled()
warn.mockRestore()
})
it('costs a start with nothing pending no inventory and keeps restored receipts', async () => {
const installPending = vi.fn()
const runtime = new OrcaRuntimeService({
...store,
installPendingOrcadMigrationReceivedBlobs: installPending,
hasPendingOrcadMigrationShellHistory: () => false
})
const listProcesses = vi.fn(async () => [{ id: 'daemon-pty', cwd: '/elsewhere', title: 'zsh' }])
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses
})
const receipt = {
ptyId: 'daemon-pty',
worktreeId: TEST_WORKTREE_ID,
terminalHandle: 'term_daemon',
paneKey: 'tab-1:1',
processIncarnation: 'daemon-pty:inc-1',
hostScope: { kind: 'local' }
}
internalsOf(runtime).restoredOrchestrationAuthorityByPtyId.set('daemon-pty', receipt)
await runtime.installPendingOrcadMigrationShellHistory()
expect(listProcesses).not.toHaveBeenCalled()
expect(installPending).not.toHaveBeenCalled()
expect(internalsOf(runtime).restoredOrchestrationAuthorityByPtyId.get('daemon-pty')).toBe(
receipt
)
})
})
@@ -65,6 +65,7 @@ export type RuntimeStore = {
installOrcadMigrationReceivedBlobs?: Store['installOrcadMigrationReceivedBlobs']
installPendingOrcadMigrationReceivedBlobs?: Store['installPendingOrcadMigrationReceivedBlobs']
installDeferredOrcadMigrationShellHistory?: Store['installDeferredOrcadMigrationShellHistory']
hasPendingOrcadMigrationShellHistory?: Store['hasPendingOrcadMigrationShellHistory']
peekOrcadMigrationTerminalRestoreHint?: Store['peekOrcadMigrationTerminalRestoreHint']
consumeOrcadMigrationTerminalRestoreHint?: Store['consumeOrcadMigrationTerminalRestoreHint']
persistPtyBinding?: Store['persistPtyBinding']