fix(mobile): scope session matching by execution host

This commit is contained in:
Brennan Benson
2026-08-23 14:42:46 -07:00
parent d47fa88956
commit a2dc078836
3 changed files with 61 additions and 5 deletions
@@ -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' }),
@@ -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)
@@ -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<AiVaultSession, 'cwd' | 'executionHostId'>
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'