fix(mobile): unblock targeted SSH session tab refresh (#17486)

* Fix targeted mobile SSH session tab refresh

* Preserve fail-open explicit workspace resolution

* Strengthen SSH session refresh oracle
This commit is contained in:
Jinwoo Hong
2026-08-31 15:18:21 -04:00
committed by GitHub
parent bbbb59e18c
commit 63d60e0ef0
3 changed files with 170 additions and 13 deletions
+86
View File
@@ -2220,6 +2220,92 @@ describe('OrcaRuntimeService', () => {
expect(getRepos).not.toHaveBeenCalled()
})
it('does not block a targeted mobile session tab list on an unrelated worktree scan', async () => {
const remoteWorktreeId = 'repo-ssh::/remote/worktree'
const remotePtyId = 'ssh:ssh-target@@remote-pty'
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
activeRepoId: 'repo-ssh',
activeWorktreeId: remoteWorktreeId,
activeTabIdByWorktree: { [remoteWorktreeId]: 'remote-tab' },
tabsByWorktree: {
[remoteWorktreeId]: [
{
id: 'remote-tab',
ptyId: remotePtyId,
worktreeId: remoteWorktreeId,
title: 'Remote terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'remote-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: remotePtyId })
}
}),
'ssh:ssh-target'
)
const remoteRepo = {
...store.getRepos()[0],
id: 'repo-ssh',
connectionId: 'ssh-target'
}
runtimeStore.getRepos = () => [remoteRepo]
runtimeStore.getRepo = (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
const runtime = new OrcaRuntimeService(runtimeStore as never)
const listProcesses = vi.fn(async () => [
{
id: remotePtyId,
incarnationId: 'remote-incarnation',
terminalHandle: 'term_remote',
title: 'Remote terminal',
cwd: '/remote/worktree',
worktreeId: remoteWorktreeId
}
])
runtime.setPtyController({
listProcesses,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const listWorktrees = vi.fn(() => new Promise<never>(() => {}))
registerSshGitProvider('ssh-target', { listWorktrees } as never)
vi.useFakeTimers()
try {
let timeoutId: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<null>((resolve) => {
timeoutId = setTimeout(() => resolve(null), 1_000)
})
const resultPromise = runtime.listMobileSessionTabs(`id:${remoteWorktreeId}`)
await Promise.resolve()
await vi.advanceTimersByTimeAsync(1_000)
const result = await Promise.race([resultPromise, timeout])
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
}
expect(result).not.toBeNull()
expect(listWorktrees).not.toHaveBeenCalled()
expect(listProcesses).toHaveBeenCalledOnce()
expect(listProcesses).toHaveBeenCalledWith(
'ssh-target',
expect.objectContaining({ deadlineMs: expect.any(Number) })
)
expect(result).toMatchObject({
worktree: remoteWorktreeId,
tabs: [expect.objectContaining({ type: 'terminal', parentTabId: 'remote-tab' })]
})
} finally {
vi.useRealTimers()
unregisterSshGitProvider('ssh-target')
}
})
it('hydrates persisted tabs when the store cannot report repos', async () => {
// Why: #9343 read the repo gate as `getRepos?.() ?? []`, so a store that cannot
// report its inventory looked like "every repo is gone" and hydrated nothing —
+76 -8
View File
@@ -9634,10 +9634,13 @@ export class OrcaRuntimeService {
private async refreshMobileSessionPtyInventory(
targetWorktreeId: string | null = null
): Promise<PtyControllerInventory | null> {
// Targeted mobile polls must not queue behind an aggregate census that may
// be waiting on an unrelated SSH provider.
if (targetWorktreeId !== null && targetWorktreeId !== FLOATING_TERMINAL_WORKTREE_ID) {
return this.performMobileSessionPtyRecordsRefresh(targetWorktreeId)
}
if (targetWorktreeId !== FLOATING_TERMINAL_WORKTREE_ID) {
// Non-floating refreshes all query the aggregate controller inventory;
// coalesce targeted and all-worktree callers so they cannot invalidate
// one another through the shared aggregate generation fence.
// Fleet-wide refreshes share one aggregate controller inventory.
const pending = this.pendingMobileSessionPtyAggregateInventoryRefresh
if (pending) {
return pending
@@ -9663,13 +9666,63 @@ export class OrcaRuntimeService {
}
// Why: floating PTY identity is explicit, so polling must not resolve every Git/SSH worktree.
const isFloatingWorkspace = targetWorktreeId === FLOATING_TERMINAL_WORKTREE_ID
const resolvedWorktrees = isFloatingWorkspace ? [] : await this.listResolvedWorktrees()
const resolvedWorktrees = isFloatingWorkspace
? []
: targetWorktreeId
? this.listResolvedWorktreesForExplicitTarget(targetWorktreeId)
: await this.listResolvedWorktrees()
// An explicit mobile worktree belongs to one execution host. Query only
// that provider; aggregate inventory would wait on unrelated SSH hosts.
const targetExecutionHost = targetWorktreeId
? (resolvedWorktrees.find((worktree) => worktree.id === targetWorktreeId)?.hostId ??
this.tryGetWorkspaceSessionHostIdForWorktree(targetWorktreeId))
: null
const parsedTargetHost = targetExecutionHost ? parseExecutionHostId(targetExecutionHost) : null
// Paired/runtime-owned workspaces have a separate controller; this runtime
// cannot inspect them and must not silently query its local PTY provider.
if (parsedTargetHost?.kind === 'runtime') {
return null
}
const targetConnectionId =
parsedTargetHost?.kind === 'ssh'
? parsedTargetHost.targetId
: targetWorktreeId
? null
: undefined
return await this.refreshPtyWorktreeRecordsWithControllerInventory(
resolvedWorktrees,
isFloatingWorkspace ? targetWorktreeId : null
targetWorktreeId,
undefined,
targetConnectionId
)
}
/** Targeted mobile opens must not wait for an unrelated SSH/Git worktree scan. */
private listResolvedWorktreesForExplicitTarget(targetWorktreeId: string): ResolvedWorktree[] {
const cached =
this.resolvedWorktreeCache && this.resolvedWorktreeCache.expiresAt > Date.now()
? this.resolvedWorktreeCache.worktrees
: null
const targetWorktree =
cached?.find((worktree) => worktree.id === targetWorktreeId) ??
(() => {
const scope = parseWorkspaceKey(targetWorktreeId)
if (scope?.type === 'folder') {
const folder = this.store
?.getFolderWorkspaces?.()
.find((workspace) => workspace.id === scope.folderWorkspaceId)
return folder ? this.folderWorkspaceToResolvedWorktree(folder) : null
}
return this.buildResolvedWorktreeFromId(targetWorktreeId)
})()
if (!targetWorktree) {
return []
}
return cached
? includeTargetResolvedWorktree(cached, targetWorktree)
: this.listKnownResolvedWorktreesForExplicitTarget(targetWorktreeId, targetWorktree)
}
async activateMobileSessionTab(
worktreeSelector: string,
tabId: string,
@@ -34893,7 +34946,7 @@ export class OrcaRuntimeService {
if (!parsed?.repoId || !parsed.worktreePath) {
return null
}
const repo = this.store?.getRepos().find((entry) => entry.id === parsed.repoId)
const repo = this.store?.getRepos?.()?.find((entry) => entry.id === parsed.repoId)
const git = {
path: parsed.worktreePath,
head: '',
@@ -34927,7 +34980,9 @@ export class OrcaRuntimeService {
}
const target = splitWorktreeIdForFilesystem(targetWorktreeId)
if (!target?.repoId || !target.worktreePath) {
return []
// Folder workspace keys have no repo/path tuple, but the converted row
// is already authoritative for this explicit target.
return [targetWorktree]
}
const worktreeIds = new Set(
Object.keys(this.store.getAllWorktreeMeta()).filter((worktreeId) => {
@@ -35496,7 +35551,8 @@ export class OrcaRuntimeService {
resolvedWorktrees: ResolvedWorktree[],
targetWorktreeId: string | null = null,
deadline?: number,
connectionId?: string | null
connectionId?: string | null,
retryStale = false
): Promise<PtyControllerInventory | null> {
if (targetWorktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
const targetedLiveness = this.refreshFloatingWorkspacePtyLiveness()
@@ -35570,6 +35626,18 @@ export class OrcaRuntimeService {
inventoryGeneration &&
this.ptyControllerAggregateInventoryGeneration <= inventoryGeneration
if (!isCurrentInventory) {
// A fleet census that began after this targeted poll must not turn a
// user-driven open into an empty result. Re-query the owning provider;
// the second generation is then fenced against both operations.
if (targetWorktreeId !== null && !retryStale) {
return this.refreshPtyWorktreeRecordsWithControllerInventory(
resolvedWorktrees,
targetWorktreeId,
deadline,
connectionId,
true
)
}
return null
}
const sessions = sessionsResult.value.processes
@@ -133,7 +133,7 @@ describe('authoritative session tab inventory publication', () => {
expect(collections).toBe(4)
})
it('coalesces targeted and all-host PTY refreshes behind one aggregate census', async () => {
it('keeps targeted PTY refreshes independent from an aggregate census', async () => {
const runtime = createInventoryRuntime()
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [] })
@@ -143,7 +143,7 @@ describe('authoritative session tab inventory publication', () => {
terminalIdentityByPtyId: new Map(),
queriedHostIds: new Set(['local'])
}
let resolveRefresh: ((inventory: typeof emptyInventory) => void) | undefined
const pendingResolves: ((inventory: typeof emptyInventory) => void)[] = []
const internals = runtime as unknown as {
refreshMobileSessionPtyInventory: (targetWorktreeId?: string | null) => Promise<unknown>
performMobileSessionPtyRecordsRefresh: (targetWorktreeId: string | null) => Promise<unknown>
@@ -151,7 +151,7 @@ describe('authoritative session tab inventory publication', () => {
const perform = vi.spyOn(internals, 'performMobileSessionPtyRecordsRefresh').mockImplementation(
() =>
new Promise((resolve) => {
resolveRefresh = resolve
pendingResolves.push(resolve)
})
)
@@ -160,10 +160,13 @@ describe('authoritative session tab inventory publication', () => {
const targeted = internals.refreshMobileSessionPtyInventory('repo::/target')
await Promise.resolve()
expect(perform).toHaveBeenCalledOnce()
resolveRefresh?.(emptyInventory)
expect(perform).toHaveBeenCalledTimes(2)
expect(perform).toHaveBeenNthCalledWith(1, null)
expect(perform).toHaveBeenNthCalledWith(2, 'repo::/target')
pendingResolves[1]?.(emptyInventory)
await targeted
pendingResolves[0]?.(emptyInventory)
await expect(allHosts).resolves.toEqual({ snapshots: [], authoritative: true })
})