diff --git a/src/main/persistence-orcad-migration-shell-history.test.ts b/src/main/persistence-orcad-migration-shell-history.test.ts index 1bfa3846a93..e2546275d95 100644 --- a/src/main/persistence-orcad-migration-shell-history.test.ts +++ b/src/main/persistence-orcad-migration-shell-history.test.ts @@ -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) }) }) diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts index 56cd41d1426..055909ba4bd 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts @@ -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, diff --git a/src/main/runtime/orca-runtime-migration-catalog.ts b/src/main/runtime/orca-runtime-migration-catalog.ts index 08c67b1d42d..d9d9c9bf0c6 100644 --- a/src/main/runtime/orca-runtime-migration-catalog.ts +++ b/src/main/runtime/orca-runtime-migration-catalog.ts @@ -108,13 +108,21 @@ export abstract class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithRes */ async installPendingOrcadMigrationShellHistory(): Promise { 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 diff --git a/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts b/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts index a73d4907bda..ae0cf8440ca 100644 --- a/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts +++ b/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts @@ -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 + buildResolvedWorktreeFromId: (worktreeId: string) => unknown + getResolvedWorktreeMap: () => Promise> +} + +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 + ) + }) }) diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index 46e2cb22ba2..2d6babcc502 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -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']