From a2dc0788360db7b9131c2a70e4b40fdaf2525aec Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:42:46 -0700 Subject: [PATCH] fix(mobile): scope session matching by execution host --- .../agent-history-resume-target.test.ts | 28 ++++++++++++++ .../agent-history-resume-target.ts | 1 + .../agent-history-session-worktree.ts | 37 ++++++++++++++++--- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/mobile/src/agent-history/agent-history-resume-target.test.ts b/mobile/src/agent-history/agent-history-resume-target.test.ts index d8e788981fd..f4cf1454cea 100644 --- a/mobile/src/agent-history/agent-history-resume-target.test.ts +++ b/mobile/src/agent-history/agent-history-resume-target.test.ts @@ -209,6 +209,34 @@ describe('mobile AI Vault resume target guards', () => { }) }) + it('uses repo ownership when legacy worktree rows omit hostId', () => { + const target = resolveMobileAiVaultSessionResumeTarget({ + session: session({ cwd: '/mnt/c/Users/ada/orca/feature/src' }), + activeWorktreeId: 'route-wt', + worktrees: [ + worktree({ + worktreeId: 'ssh-lookalike', + repoId: 'ssh-repo', + path: '/mnt/c/Users/ada/orca/feature' + }), + worktree({ + worktreeId: 'local-wsl', + path: String.raw`C:\Users\ada\orca\feature` + }), + worktree({ worktreeId: 'route-wt', path: String.raw`C:\Users\ada\orca\main` }) + ], + repos + }) + + expect(target).toEqual({ + status: 'ready', + worktreeId: 'local-wsl', + targetStatus: 'local', + workspacePath: String.raw`C:\Users\ada\orca\feature`, + terminalPlatform: null + }) + }) + it('falls back to the active route worktree when the session worktree is archived', () => { const target = resolveMobileAiVaultSessionResumeTarget({ session: session({ cwd: '/Users/ada/repo/archive/src' }), diff --git a/mobile/src/agent-history/agent-history-resume-target.ts b/mobile/src/agent-history/agent-history-resume-target.ts index a63a7ca92c7..1c55a64a626 100644 --- a/mobile/src/agent-history/agent-history-resume-target.ts +++ b/mobile/src/agent-history/agent-history-resume-target.ts @@ -125,6 +125,7 @@ export function resolveMobileAiVaultSessionResumeTarget(args: { const sessionWorktree = resolveMobileAgentHistorySessionWorktree({ session: args.session, worktrees: args.worktrees, + repos: args.repos, activeWorktreeId: args.activeWorktreeId }) const sessionWorktreeId = canResumeInMobileSessionWorktree(sessionWorktree) diff --git a/mobile/src/agent-history/agent-history-session-worktree.ts b/mobile/src/agent-history/agent-history-session-worktree.ts index 2e1577a2aaa..f8df18e632e 100644 --- a/mobile/src/agent-history/agent-history-session-worktree.ts +++ b/mobile/src/agent-history/agent-history-session-worktree.ts @@ -4,7 +4,11 @@ import { normalizedWslPathCandidateAliases, wslAliasedPathDepth } from '../../../src/shared/wsl-path-aliases' -import { normalizeExecutionHostId } from '../../../src/shared/execution-host' +import { + getWorktreeExecutionHostId, + normalizeExecutionHostId, + type ExecutionHostId +} from '../../../src/shared/execution-host' import { splitWorktreeIdForFilesystem } from '../../../src/shared/worktree/id' import type { AiVaultSession } from '../../../src/shared/ai-vault-types' import type { Worktree } from '../worktree/workspace-list-types' @@ -20,11 +24,18 @@ export type MobileAgentHistorySessionWorktreeInfo = { type WorktreeCandidate = { worktree: WorktreeWithPriorIds path: string + hostId: ExecutionHostId pathDepth: number ownsNormalizedCwd: (normalizedCwd: string) => boolean source: 'current-path' | 'prior-path' } +type WorktreeRepoHost = { + id: string + connectionId?: string | null + executionHostId?: ExecutionHostId | null +} + type WorktreeWithPriorIds = Worktree & { priorWorktreeIds?: readonly string[] } @@ -32,6 +43,7 @@ type WorktreeWithPriorIds = Worktree & { export function resolveMobileAgentHistorySessionWorktree(args: { session: Pick worktrees: readonly Worktree[] + repos?: readonly WorktreeRepoHost[] activeWorktreeId: string | null }): MobileAgentHistorySessionWorktreeInfo | null { if (!args.session.cwd) { @@ -40,7 +52,7 @@ export function resolveMobileAgentHistorySessionWorktree(args: { const sessionHostId = normalizeExecutionHostId(args.session.executionHostId) const cwdAliases = normalizedWslPathCandidateAliases(args.session.cwd) const best = findBestMobileWorktreeCandidate( - buildMobileWorktreeCandidates(args.worktrees), + buildMobileWorktreeCandidates(args.worktrees, args.repos ?? []), sessionHostId, cwdAliases ) @@ -67,8 +79,7 @@ function findBestMobileWorktreeCandidate( ): WorktreeCandidate | null { let best: WorktreeCandidate | null = null for (const candidate of candidates) { - const worktreeHostId = normalizeExecutionHostId(candidate.worktree.hostId) - if (sessionHostId && worktreeHostId && worktreeHostId !== sessionHostId) { + if (sessionHostId && candidate.hostId !== sessionHostId) { continue } if (!normalizedCwdAliases.some(candidate.ownsNormalizedCwd)) { @@ -87,13 +98,28 @@ export function canResumeInMobileSessionWorktree( return Boolean(worktreeInfo && worktreeInfo.status !== 'archived') } -function buildMobileWorktreeCandidates(worktrees: readonly Worktree[]): WorktreeCandidate[] { +function buildMobileWorktreeCandidates( + worktrees: readonly Worktree[], + repos: readonly WorktreeRepoHost[] +): WorktreeCandidate[] { const candidates: WorktreeCandidate[] = [] + const repoById = new Map(repos.map((repo) => [repo.id, repo])) for (const worktree of worktrees as readonly WorktreeWithPriorIds[]) { + const repo = repoById.get(worktree.repoId) + const hostId = getWorktreeExecutionHostId( + worktree, + repo + ? { + connectionId: repo.connectionId ?? null, + executionHostId: repo.executionHostId ?? null + } + : undefined + ) if (hasUsablePath(worktree.path)) { candidates.push({ worktree, path: worktree.path, + hostId, pathDepth: wslAliasedPathDepth(worktree.path), ownsNormalizedCwd: createWslAliasedPathInsideOrEqualMatcher(worktree.path), source: 'current-path' @@ -107,6 +133,7 @@ function buildMobileWorktreeCandidates(worktrees: readonly Worktree[]): Worktree candidates.push({ worktree, path: parsed.worktreePath, + hostId, pathDepth: wslAliasedPathDepth(parsed.worktreePath), ownsNormalizedCwd: createWslAliasedPathInsideOrEqualMatcher(parsed.worktreePath), source: 'prior-path'