diff --git a/config/scripts/performance/claude-usage/claude-usage-benchmark-persistence.mjs b/config/scripts/performance/claude-usage/claude-usage-benchmark-persistence.mjs index 6235bdbe039..e0df9730978 100644 --- a/config/scripts/performance/claude-usage/claude-usage-benchmark-persistence.mjs +++ b/config/scripts/performance/claude-usage/claude-usage-benchmark-persistence.mjs @@ -255,7 +255,12 @@ export function createClaudeBenchmarkStore(arm, cacheFile, splitCacheFile, parse class BenchmarkStore extends implementation.UsageProviderStoreLifecycle { constructor() { super( - { getRepos: () => [], getAllWorktreeMeta: () => ({}) }, + { + getRepos: () => [], + getAllWorktreeMeta: () => ({}), + getRememberedWorktreeDisplayNames: () => new Map(), + whenLoaded: async () => {} + }, { logTag: '[claude-persistence-benchmark]', resolveCacheFile: () => cacheFile, diff --git a/src/main/claude-usage/persisted-usage-test-fixture.ts b/src/main/claude-usage/persisted-usage-test-fixture.ts index 32b1f4ca370..fb7ecb3bfe5 100644 --- a/src/main/claude-usage/persisted-usage-test-fixture.ts +++ b/src/main/claude-usage/persisted-usage-test-fixture.ts @@ -118,7 +118,13 @@ export async function createPersistedUsageTestFixture(getPath: Mock<() => string project, state, writeResumable, - createStore: () => new ClaudeUsageStore({ getRepos: () => [], getAllWorktreeMeta: () => ({}) }), + createStore: () => + new ClaudeUsageStore({ + getRepos: () => [], + getAllWorktreeMeta: () => ({}), + getRememberedWorktreeDisplayNames: () => new Map(), + whenLoaded: async () => {} + }), decodedSources: async () => { const read = await readClaudeUsageSourceCache(sourceRef) return normalizeClaudeUsageSourceFiles(read.sources, read.verifiedSources).processedFiles diff --git a/src/main/claude-usage/store.ts b/src/main/claude-usage/store.ts index 5272a268e19..da663d189de 100644 --- a/src/main/claude-usage/store.ts +++ b/src/main/claude-usage/store.ts @@ -12,7 +12,7 @@ import type { ClaudeUsageSummary } from '../../shared/claude-usage-types' import type { AutomationRunUsage } from '../../shared/automations/history/automation-run' -import type { Store } from '../persistence/loading-store/store' +import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata' import type { ClaudeUsagePersistedState } from './types' import { scanClaudeUsageFilesViaWorker, @@ -79,7 +79,7 @@ export class ClaudeUsageStore extends UsageProviderStoreLifecycle< ClaudeUsagePersistedState, 'hasAnyClaudeData' > { - constructor(store: Pick) { + constructor(store: UsageWorktreeSource) { super(store, { tokenUsage: { provider: 'claude', diff --git a/src/main/codex-usage/store-test-harness.ts b/src/main/codex-usage/store-test-harness.ts index 8267abf6909..2cdd3f7937a 100644 --- a/src/main/codex-usage/store-test-harness.ts +++ b/src/main/codex-usage/store-test-harness.ts @@ -68,7 +68,9 @@ export function createWorktreeUsageSession(worktreeId: string) { export function createStoreWithState(state: Partial): CodexUsageStore { const store = new CodexUsageStore({ getRepos: () => [], - getAllWorktreeMeta: () => ({}) + getAllWorktreeMeta: () => ({}), + getRememberedWorktreeDisplayNames: () => new Map(), + whenLoaded: async () => {} }) ;(store as unknown as { state: CodexUsagePersistedState }).state = { diff --git a/src/main/codex-usage/store.ts b/src/main/codex-usage/store.ts index 7e5f5515c4b..35e98288382 100644 --- a/src/main/codex-usage/store.ts +++ b/src/main/codex-usage/store.ts @@ -12,7 +12,7 @@ import type { CodexUsageSummary } from '../../shared/codex-usage-types' import type { AutomationRunUsage } from '../../shared/automations/history/automation-run' -import type { Store } from '../persistence/loading-store/store' +import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata' import type { CodexUsagePersistedState } from './types' import type { AutomationUsageLookupInput } from './codex-automation-run-attribution' import { CODEX_USAGE_SCHEMA_VERSION, codexUsageProvider } from './codex-usage-provider' @@ -79,7 +79,7 @@ export class CodexUsageStore extends UsageProviderStoreLifecycle< CodexUsagePersistedState, 'hasAnyCodexData' > { - constructor(store: Pick) { + constructor(store: UsageWorktreeSource) { super(store, { tokenUsage: { provider: 'codex', diff --git a/src/main/muse-usage/store.ts b/src/main/muse-usage/store.ts index da2ab662aff..1fba5514dcc 100644 --- a/src/main/muse-usage/store.ts +++ b/src/main/muse-usage/store.ts @@ -10,7 +10,7 @@ import type { MuseUsageSnapshot, MuseUsageSummary } from '../../shared/muse-usage-types' -import type { Store } from '../persistence/loading-store/store' +import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata' import { UsageProviderStoreLifecycle } from '../usage/provider-lifecycle/usage-provider-store-lifecycle' import { splitUsageCacheFileViaWorker } from '../usage/scan-worker/usage-scan-worker-spawn' import { @@ -43,7 +43,7 @@ export class MuseUsageStore extends UsageProviderStoreLifecycle< MuseUsagePersistedState, 'hasAnyMuseData' > { - constructor(store: Pick) { + constructor(store: UsageWorktreeSource) { super(store, { logTag: '[muse-usage]', resolveCacheFile: getMuseUsageFile, diff --git a/src/main/opencode-usage/store.ts b/src/main/opencode-usage/store.ts index 790c6d9325b..47393511a4e 100644 --- a/src/main/opencode-usage/store.ts +++ b/src/main/opencode-usage/store.ts @@ -11,7 +11,7 @@ import type { OpenCodeUsageSnapshot, OpenCodeUsageSummary } from '../../shared/opencode-usage-types' -import type { Store } from '../persistence/loading-store/store' +import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata' import type { OpenCodeUsageDailyAggregate, OpenCodeUsagePersistedState, @@ -50,7 +50,7 @@ export class OpenCodeUsageStore extends UsageProviderStoreLifecycle< OpenCodeUsagePersistedState, 'hasAnyOpenCodeData' > { - constructor(store: Pick) { + constructor(store: UsageWorktreeSource) { super(store, { tokenUsage: { provider: 'opencode', diff --git a/src/main/startup/main-process-observers.ts b/src/main/startup/main-process-observers.ts index b1e9fef30f0..e2f383b66ce 100644 --- a/src/main/startup/main-process-observers.ts +++ b/src/main/startup/main-process-observers.ts @@ -25,6 +25,7 @@ import { ClaudeUsageStore } from '../claude-usage/store' import { CodexUsageStore } from '../codex-usage/store' import { OpenCodeUsageStore } from '../opencode-usage/store' import { MuseUsageStore } from '../muse-usage/store' +import { UsageWorktreeHistory } from '../usage/worktree-attribution/usage-worktree-history' import { installRepoMaintenanceIdleGate } from '../workspaces/catalog/repo-maintenance-idle-gate' import { mainProcessState as state } from './main-process-state' import { runAfterFirstWindowShown } from './first-window-deferral' @@ -146,8 +147,13 @@ export function initializeMainProcessObservers(): void { initOnboardingCohortClassifier(store) state.stats = new StatsCollector() feedAgentSessionStats(state.stats, agentHookServer) - state.claudeUsage = new ClaudeUsageStore(store) - state.codexUsage = new CodexUsageStore(store) - state.openCodeUsage = new OpenCodeUsageStore(store) - state.museUsage = new MuseUsageStore(store) + const usageWorktrees = new UsageWorktreeHistory( + store, + join(getCanonicalUserDataPath(), 'orca-usage-worktree-history.json') + ) + state.usageWorktreeHistory = usageWorktrees + state.claudeUsage = new ClaudeUsageStore(usageWorktrees) + state.codexUsage = new CodexUsageStore(usageWorktrees) + state.openCodeUsage = new OpenCodeUsageStore(usageWorktrees) + state.museUsage = new MuseUsageStore(usageWorktrees) } diff --git a/src/main/startup/main-process-quit.ts b/src/main/startup/main-process-quit.ts index 8a9aa70f2e7..1b5a696a187 100644 --- a/src/main/startup/main-process-quit.ts +++ b/src/main/startup/main-process-quit.ts @@ -240,7 +240,8 @@ function installWillQuitHandler(): void { state.claudeUsage?.flush(), state.codexUsage?.flush(), state.openCodeUsage?.flush(), - state.museUsage?.flush() + state.museUsage?.flush(), + state.usageWorktreeHistory?.flush() ]).then(() => {}) disposeDesktopBrowserBridge() const browserClientHostShutdown = shutdownPairedRuntimeBrowserClientHosts() diff --git a/src/main/startup/main-process-state.ts b/src/main/startup/main-process-state.ts index dc665ad1b1b..e2dd75c66aa 100644 --- a/src/main/startup/main-process-state.ts +++ b/src/main/startup/main-process-state.ts @@ -5,6 +5,7 @@ import type { ClaudeUsageStore } from '../claude-usage/store' import type { CodexUsageStore } from '../codex-usage/store' import type { OpenCodeUsageStore } from '../opencode-usage/store' import type { MuseUsageStore } from '../muse-usage/store' +import type { UsageWorktreeHistory } from '../usage/worktree-attribution/usage-worktree-history' import type { CodexAccountService } from '../codex-accounts/service' import type { CodexRuntimeHomeService } from '../codex-accounts/runtime-home-service' import type { ClaudeAccountService } from '../claude-accounts/service' @@ -84,6 +85,8 @@ export const mainProcessState = { openCodeUsage: null as OpenCodeUsageStore | null, // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: widens the null slot to the store type assigned by main-process-observers. museUsage: null as MuseUsageStore | null, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: widens the null slot to the history assigned by main-process-observers. + usageWorktreeHistory: null as UsageWorktreeHistory | null, codexAccounts: null as CodexAccountService | null, codexRuntimeHome: null as CodexRuntimeHomeService | null, codexSessionMigration: null as ReturnType | null, diff --git a/src/main/usage/provider-lifecycle/usage-provider-store-lifecycle.ts b/src/main/usage/provider-lifecycle/usage-provider-store-lifecycle.ts index fe3ed77846b..23c12df6227 100644 --- a/src/main/usage/provider-lifecycle/usage-provider-store-lifecycle.ts +++ b/src/main/usage/provider-lifecycle/usage-provider-store-lifecycle.ts @@ -8,9 +8,11 @@ import { AnalyticsSessionIdStore, type AnalyticsSessionId } from '../reporting/token-usage/analytics-session-id-store' -import type { Store } from '../../persistence/loading-store/store' import { UsageCacheSnapshotWriter } from '../cache/usage-cache-snapshot-writer' -import { loadKnownUsageWorktreesByRepo } from '../worktree-attribution/usage-worktree-metadata' +import { + loadKnownUsageWorktreesByRepo, + type UsageWorktreeSource +} from '../worktree-attribution/usage-worktree-metadata' import { createWorktreeRefs, @@ -44,14 +46,14 @@ export abstract class UsageProviderStoreLifecycle< private readonly inlineCache = new UsageInlineCachePreservation() constructor( - private readonly store: Pick, + private readonly store: UsageWorktreeSource, private readonly config: UsageProviderStoreLifecycleConfig ) { this.writer = new UsageCacheSnapshotWriter(config.logTag, config.resolveCacheFile) const defaults = config.createDefaultState() this.schemaVersion = defaults.schemaVersion this.state = defaults - this.loaded = this.load() + this.loaded = Promise.all([this.load(), store.whenLoaded()]).then(() => {}) } /** Resolves once persisted state is in memory; synchronous readers must wait for it first. */ diff --git a/src/main/usage/worktree-attribution/usage-worktree-history.test.ts b/src/main/usage/worktree-attribution/usage-worktree-history.test.ts new file mode 100644 index 00000000000..e4f7c8f3d4b --- /dev/null +++ b/src/main/usage/worktree-attribution/usage-worktree-history.test.ts @@ -0,0 +1,97 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import type { Repo } from '../../../shared/repo-types' +import { UsageWorktreeHistory } from './usage-worktree-history' +import { loadKnownUsageWorktreesByRepo, type UsageWorktreeSource } from './usage-worktree-metadata' + +const repo: Repo = { + id: 'repo-1', + path: '/src/app', + displayName: 'app', + badgeColor: '#000000', + addedAt: 0 +} + +function createStore(meta: Record) { + let listener: ((write: { worktreeId: string; next: { displayName: string } }) => void) | null = + null + return { + getRepos: () => [repo], + getAllWorktreeMeta: () => meta, + onWorktreeMetaWritten: (next: NonNullable) => { + listener = next + return () => {} + }, + write(worktreeId: string, displayName: string) { + meta[worktreeId] = { displayName } + listener?.({ worktreeId, next: meta[worktreeId] }) + }, + remove(worktreeId: string) { + delete meta[worktreeId] + } + } +} + +function knownWorktrees(source: UsageWorktreeSource) { + return loadKnownUsageWorktreesByRepo(source, [repo]) + .get('repo-1') + ?.map(({ path, displayName }) => [path, displayName]) +} + +describe('UsageWorktreeHistory', () => { + let dir: string | null = null + afterEach(() => { + if (dir) { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('keeps removed worktrees attributed, across restarts', async () => { + dir = mkdtempSync(join(tmpdir(), 'usage-worktree-history-')) + const file = join(dir, 'history.json') + const store = createStore({ 'repo-1::/ws/existing': { displayName: 'existing' } }) + const history = new UsageWorktreeHistory(store, file) + store.write('repo-1::/ws/short-lived', 'short-lived') + store.remove('repo-1::/ws/existing') + store.remove('repo-1::/ws/short-lived') + + const expected = [ + ['/src/app', 'app'], + ['/ws/existing', 'existing'], + ['/ws/short-lived', 'short-lived'] + ] + expect(knownWorktrees(history)).toEqual(expected) + + await history.flush() + const restarted = new UsageWorktreeHistory(createStore({}), file) + await restarted.whenLoaded() + expect(knownWorktrees(restarted)).toEqual(expected) + }) + + it('leaves a history file from a newer build untouched', async () => { + dir = mkdtempSync(join(tmpdir(), 'usage-worktree-history-')) + const file = join(dir, 'history.json') + const newer = '{"schemaVersion":2,"worktrees":[]}' + writeFileSync(file, newer) + const store = createStore({ 'repo-1::/ws/live': { displayName: 'live' } }) + const history = new UsageWorktreeHistory(store, file) + store.write('repo-1::/ws/new', 'new') + await history.flush() + expect(readFileSync(file, 'utf8')).toBe(newer) + }) + + it('lets a live worktree in another repo own a path a removed one used', () => { + const other: Repo = { ...repo, id: 'repo-2', path: '/src/other' } + const source: UsageWorktreeSource = { + getRepos: () => [repo, other], + getAllWorktreeMeta: () => ({ 'repo-2::/ws/feat': { displayName: 'live' } }), + getRememberedWorktreeDisplayNames: () => new Map([['repo-1::/ws/feat', 'removed']]), + whenLoaded: async () => {} + } + const known = loadKnownUsageWorktreesByRepo(source, [repo, other]) + expect(known.get('repo-1')?.map(({ path }) => path)).toEqual(['/src/app']) + expect(known.get('repo-2')?.map(({ path }) => path)).toEqual(['/src/other', '/ws/feat']) + }) +}) diff --git a/src/main/usage/worktree-attribution/usage-worktree-history.ts b/src/main/usage/worktree-attribution/usage-worktree-history.ts new file mode 100644 index 00000000000..f2fe58913f6 --- /dev/null +++ b/src/main/usage/worktree-attribution/usage-worktree-history.ts @@ -0,0 +1,119 @@ +import { readFile } from 'node:fs/promises' +import { z } from 'zod' +import type { Repo } from '../../../shared/repo-types' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import { UsageCacheSnapshotWriter } from '../cache/usage-cache-snapshot-writer' +import type { UsageWorktreeSource } from './usage-worktree-metadata' + +type HistoryStore = Pick & { + onWorktreeMetaWritten: (listener: (write: { worktreeId: string }) => void) => () => void +} + +const historyFileSchema = z.object({ + schemaVersion: z.literal(1), + worktrees: z.array(z.tuple([z.string(), z.string()])) +}) + +/** Remembers every worktree Orca has had, so removing one keeps its sessions in the Orca usage scope. */ +export class UsageWorktreeHistory implements UsageWorktreeSource { + private readonly displayNames = new Map() + private readonly writer: UsageCacheSnapshotWriter + private readonly loaded: Promise + private writable = true + + constructor( + private readonly store: HistoryStore, + file: string + ) { + this.writer = new UsageCacheSnapshotWriter('[usage-worktree-history]', () => file) + for (const [worktreeId, meta] of Object.entries(store.getAllWorktreeMeta())) { + this.remember(worktreeId, meta.displayName) + } + // Record on every metadata write: worktrees are often created and deleted between usage scans. + // Read the live row, not the event: another host's write leaves the local row unchanged. + store.onWorktreeMetaWritten(({ worktreeId }) => { + const live = store.getAllWorktreeMeta()[worktreeId] + if (live && this.remember(worktreeId, live.displayName)) { + this.persist() + } + }) + this.loaded = readHistoryFile(file).then((saved) => { + if (!saved) { + this.writable = false + return + } + for (const [worktreeId, displayName] of saved) { + // Names recorded since startup are newer than the file's. + if (!this.displayNames.has(worktreeId)) { + this.displayNames.set(worktreeId, displayName) + } + } + if ([...this.displayNames].some(([worktreeId, name]) => saved.get(worktreeId) !== name)) { + this.write() + } + }) + } + + getRepos(): Repo[] { + return this.store.getRepos() + } + + getAllWorktreeMeta(): Record> { + return this.store.getAllWorktreeMeta() + } + + getRememberedWorktreeDisplayNames(): ReadonlyMap { + return this.displayNames + } + + whenLoaded(): Promise { + return this.loaded + } + + async flush(): Promise { + await this.loaded + await this.writer.flush() + } + + private remember(worktreeId: string, displayName: string | undefined): boolean { + const name = displayName ?? '' + if (this.displayNames.get(worktreeId) === name) { + return false + } + this.displayNames.set(worktreeId, name) + return true + } + + private persist(): void { + // Wait for the saved file, or this write would drop the worktrees only it remembers. + void this.loaded.then(() => this.write()) + } + + private write(): void { + if (!this.writable) { + return + } + void this.writer + .write(() => JSON.stringify({ schemaVersion: 1, worktrees: [...this.displayNames] })) + .catch(() => {}) + } +} + +/** Null when the file exists but can't be read or is from a newer build, so it is never overwritten. */ +async function readHistoryFile(file: string): Promise | null> { + let text: string + try { + text = await readFile(file, 'utf8') + } catch (error) { + return error instanceof Error && 'code' in error && error.code === 'ENOENT' ? new Map() : null + } + let json: unknown + try { + json = JSON.parse(text) + } catch { + // Unparseable: start over so the history can heal. + return new Map() + } + const parsed = historyFileSchema.safeParse(json) + return parsed.success ? new Map(parsed.data.worktrees) : null +} diff --git a/src/main/usage/worktree-attribution/usage-worktree-metadata.ts b/src/main/usage/worktree-attribution/usage-worktree-metadata.ts index 91d040da81d..62a994a194e 100644 --- a/src/main/usage/worktree-attribution/usage-worktree-metadata.ts +++ b/src/main/usage/worktree-attribution/usage-worktree-metadata.ts @@ -6,8 +6,9 @@ import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id' import { isFolderRepo } from '../../../shared/repo-kind' -import type { Store } from '../../persistence/loading-store/store' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' import { getRepoRoutingConnectionId } from '../../../shared/execution-host' +import { canonicalWorktreePath } from '../../git/worktrees/catalog/worktree-path-comparison' export type UsageWorktreeRef = { worktreeId: string @@ -19,8 +20,17 @@ function getDefaultUsageWorktreeLabel(pathValue: string): string { return basename(pathValue) } +export type UsageWorktreeSource = { + getRepos: () => Repo[] + getAllWorktreeMeta: () => Record> + /** Worktrees seen before, removed ones included, keyed by id with their display names. */ + getRememberedWorktreeDisplayNames: () => ReadonlyMap + /** Resolves once remembered worktrees are read; attribution before that would drop removed ones. */ + whenLoaded: () => Promise +} + export function loadKnownUsageWorktreesByRepo( - store: Pick, + store: Pick, repos: Repo[] ): Map { const localRepos = repos.filter((repo) => !getRepoRoutingConnectionId(repo)) @@ -46,30 +56,44 @@ export function loadKnownUsageWorktreesByRepo( seenPathsByRepo.set(repo.id, new Set([repo.path])) } - // Background analytics must not spawn git scans that re-touch macOS protected folders. - for (const [worktreeId, meta] of Object.entries(store.getAllWorktreeMeta())) { + const claimedPaths = new Set(localRepos.map((repo) => canonicalWorktreePath(repo.path))) + const addWorktree = (worktreeId: string, displayName: string, remembered: boolean): void => { const parsed = splitWorktreeId(worktreeId) if (!parsed) { - continue + return } const repo = localReposById.get(parsed.repoId) if (!repo) { - continue + return } const worktreePath = isFolderRepo(repo) ? (splitWorktreeIdForFilesystem(worktreeId)?.worktreePath ?? parsed.worktreePath) : parsed.worktreePath const seenPaths = seenPathsByRepo.get(parsed.repoId) - if (seenPaths?.has(worktreePath)) { - continue + const canonicalPath = canonicalWorktreePath(worktreePath) + // Live rows claim paths first, so a reused path is never credited to a removed worktree. + if (seenPaths?.has(worktreePath) || (remembered && claimedPaths.has(canonicalPath))) { + return } seenPaths?.add(worktreePath) + claimedPaths.add(canonicalPath) worktreesByRepo.get(parsed.repoId)?.push({ worktreeId, path: worktreePath, - displayName: meta.displayName || getDefaultUsageWorktreeLabel(worktreePath) + displayName: displayName || getDefaultUsageWorktreeLabel(worktreePath) }) } + // Background analytics must not spawn git scans that re-touch macOS protected folders. + const liveMeta = store.getAllWorktreeMeta() + for (const [worktreeId, meta] of Object.entries(liveMeta)) { + addWorktree(worktreeId, meta.displayName, false) + } + for (const [worktreeId, displayName] of store.getRememberedWorktreeDisplayNames()) { + if (!Object.hasOwn(liveMeta, worktreeId)) { + addWorktree(worktreeId, displayName, true) + } + } + return worktreesByRepo } diff --git a/src/main/usage/worktree-attribution/usage-worktree-resolver.ts b/src/main/usage/worktree-attribution/usage-worktree-resolver.ts index 7d10a04a6e9..f1cfa70c065 100644 --- a/src/main/usage/worktree-attribution/usage-worktree-resolver.ts +++ b/src/main/usage/worktree-attribution/usage-worktree-resolver.ts @@ -61,6 +61,13 @@ export async function createUsageWorktreeResolver( worktrees: readonly UsageScanWorktreeRef[] ): Promise { const canonicalized = await canonicalizeUsageWorktreePaths(worktrees, canonicalizePath) + // Most sessions run at a worktree root; skip the linear containment scan for those. + const worktreeByPath = new Map() + for (const worktree of canonicalized) { + if (!worktreeByPath.has(worktree.canonicalPath)) { + worktreeByPath.set(worktree.canonicalPath, worktree) + } + } const worktreeByCwd = new Map() return (cwd) => { const memoized = worktreeByCwd.get(cwd) @@ -68,7 +75,8 @@ export async function createUsageWorktreeResolver( if (memoized !== undefined) { return memoized } - const resolved = findContainingWorktree(cwd, canonicalized) + const resolved = + worktreeByPath.get(normalizeFsPath(cwd)) ?? findContainingWorktree(cwd, canonicalized) worktreeByCwd.set(cwd, resolved) return resolved }