From 98134f6f7b8921f0964d99395a2fa91b91ea806e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:28:48 -0700 Subject: [PATCH] perf: add versioned git repository snapshots --- .../git/git-repository-snapshot-owner.test.ts | 388 ++++++++++++++++++ src/main/git/git-repository-snapshot-owner.ts | 319 ++++++++++++++ .../git/git-repository-snapshot-projection.ts | 210 ++++++++++ .../git-upstream-status-read-owner.test.ts | 120 ------ .../git/git-upstream-status-read-owner.ts | 39 -- src/main/git/status.test.ts | 33 ++ src/main/git/status.ts | 57 ++- src/main/git/upstream.test.ts | 20 +- src/main/git/upstream.ts | 6 +- src/main/providers/ssh-git-provider.test.ts | 69 ++++ src/main/providers/ssh-git-provider.ts | 62 ++- ...sitory-snapshot-owner-publication-bench.ts | 250 +++++++++++ ...it-repository-snapshot-owner-2026-07-30.md | 235 +++++++++++ 13 files changed, 1607 insertions(+), 201 deletions(-) create mode 100644 src/main/git/git-repository-snapshot-owner.test.ts create mode 100644 src/main/git/git-repository-snapshot-owner.ts create mode 100644 src/main/git/git-repository-snapshot-projection.ts delete mode 100644 src/main/git/git-upstream-status-read-owner.test.ts delete mode 100644 src/main/git/git-upstream-status-read-owner.ts create mode 100644 tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts create mode 100644 tools/benchmarks/results/phase2-git-repository-snapshot-owner-2026-07-30.md diff --git a/src/main/git/git-repository-snapshot-owner.test.ts b/src/main/git/git-repository-snapshot-owner.test.ts new file mode 100644 index 00000000000..88ebbeec826 --- /dev/null +++ b/src/main/git/git-repository-snapshot-owner.test.ts @@ -0,0 +1,388 @@ +import { describe, expect, it, vi } from 'vitest' +import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../shared/types' +import { + GitRepositorySnapshotOwner, + type GitRepositoryExecutionIdentity, + type GitRepositoryStatusIdentity +} from './git-repository-snapshot-owner' +import { MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES } from './git-repository-snapshot-projection' + +const native = { kind: 'native' } as const +const defaultStatusIdentity: GitRepositoryStatusIdentity = { + includeIgnored: false, + reuseLineStats: false, + bypassEffectiveUpstreamNegativeCache: false, + limit: 1_000, + sharedLinkPaths: [] +} +const statusResult: GitStatusResult = { + entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged', added: 2, removed: 1 }], + conflictOperation: 'merge', + head: 'abc123', + branch: 'refs/heads/main', + upstreamStatus: { + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 1, + behind: 2 + } +} + +function deferred(): { + promise: Promise + resolve: (value: T) => void + reject: (error: unknown) => void +} { + let resolve!: (value: T) => void + let reject!: (error: unknown) => void + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve + reject = innerReject + }) + return { promise, resolve, reject } +} + +function query( + executionIdentity: GitRepositoryExecutionIdentity = native, + worktreePath = '/repo', + identity: GitRepositoryStatusIdentity = defaultStatusIdentity, + pushTarget?: GitPushTarget +) { + return { executionIdentity, worktreePath, statusIdentity: identity, pushTarget } +} + +describe('GitRepositorySnapshotOwner', () => { + it('publishes immutable combined projections with monotonic revisions', async () => { + const now = vi.fn().mockReturnValueOnce(100).mockReturnValueOnce(200) + const owner = new GitRepositorySnapshotOwner(128, 8, now) + + await owner.readStatus( + native, + '/repo', + defaultStatusIdentity, + undefined, + async () => statusResult + ) + const first = owner.getSnapshot(query()) + + expect(first).toMatchObject({ + revision: 1, + generatedAt: 100, + repositoryIdentity: { head: 'abc123', branch: 'refs/heads/main' }, + status: { + entries: statusResult.entries, + didHitLimit: false, + statusLength: null, + lineStatsState: 'complete' + }, + upstream: statusResult.upstreamStatus, + conflicts: 'merge', + worktreeGraphVersion: 0, + freshness: { + status: { state: 'fresh', generation: 0, revision: 1 }, + upstream: { state: 'fresh', generation: 0, revision: 1 }, + worktreeGraph: { state: 'placeholder', generation: 0 } + } + }) + expect(Object.isFrozen(first)).toBe(true) + expect(Object.isFrozen(first?.status)).toBe(true) + expect(Object.isFrozen(first?.status.entries)).toBe(true) + expect(Object.isFrozen(first?.status.entries[0])).toBe(true) + + const refreshedUpstream = { + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 3, + behind: 0, + behindCommitsArePatchEquivalent: true + } + await owner.readUpstream(native, '/repo', undefined, async () => refreshedUpstream) + const second = owner.getSnapshot(query()) + + expect(second?.revision).toBe(2) + expect(second?.generatedAt).toBe(200) + expect(second?.status.entries).toEqual(first?.status.entries) + expect(second?.upstream).toEqual(refreshedUpstream) + expect(second?.freshness.status.revision).toBe(1) + expect(second?.freshness.upstream.revision).toBe(2) + }) + + it('does not publish rejected reads and always retries settled work', async () => { + const owner = new GitRepositorySnapshotOwner() + const failure = new Error('read failed') + const statusLoad = vi + .fn<(_signal: AbortSignal) => Promise>() + .mockRejectedValueOnce(failure) + .mockResolvedValueOnce(statusResult) + .mockResolvedValueOnce(statusResult) + .mockRejectedValueOnce(failure) + const upstreamLoad = vi + .fn<() => Promise>() + .mockRejectedValueOnce(failure) + .mockResolvedValueOnce({ hasUpstream: false, ahead: 0, behind: 0 }) + .mockResolvedValueOnce({ hasUpstream: false, ahead: 0, behind: 0 }) + .mockRejectedValueOnce(failure) + + await expect( + owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad) + ).rejects.toBe(failure) + await expect(owner.readUpstream(native, '/repo', undefined, upstreamLoad)).rejects.toBe(failure) + expect(owner.getSnapshot(query())).toBeNull() + expect(owner.getRetentionState().repositories).toBe(0) + + await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad) + await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad) + await owner.readUpstream(native, '/repo', undefined, upstreamLoad) + await owner.readUpstream(native, '/repo', undefined, upstreamLoad) + + expect(statusLoad).toHaveBeenCalledTimes(3) + expect(upstreamLoad).toHaveBeenCalledTimes(3) + expect(owner.getSnapshot(query())?.revision).toBe(4) + + await expect( + owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad) + ).rejects.toBe(failure) + await expect(owner.readUpstream(native, '/repo', undefined, upstreamLoad)).rejects.toBe(failure) + expect(owner.getSnapshot(query())).toMatchObject({ + revision: 4, + freshness: { + status: { state: 'failed' }, + upstream: { state: 'failed' } + } + }) + }) + + it('keeps status cancellation scoped to each lease', async () => { + const owner = new GitRepositorySnapshotOwner() + const pending = deferred() + const load = vi.fn((_signal: AbortSignal) => pending.promise) + const firstController = new AbortController() + const secondController = new AbortController() + const firstError = new Error('first cancelled') + + const first = owner.readStatus( + native, + '/repo', + defaultStatusIdentity, + firstController.signal, + load + ) + const second = owner.readStatus( + native, + '/repo', + defaultStatusIdentity, + secondController.signal, + load + ) + firstController.abort(firstError) + + await expect(first).rejects.toBe(firstError) + expect(load).toHaveBeenCalledOnce() + expect(load.mock.calls[0][0].aborted).toBe(false) + pending.resolve(statusResult) + await expect(second).resolves.toBe(statusResult) + expect(owner.getSnapshot(query())?.revision).toBe(1) + }) + + it('prefers a newer embedded upstream projection after invalidation', async () => { + const owner = new GitRepositorySnapshotOwner() + await owner.readUpstream(native, '/repo', undefined, async () => ({ + hasUpstream: true, + upstreamName: 'old/main', + ahead: 0, + behind: 0 + })) + owner.invalidate() + await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({ + ...statusResult, + upstreamStatus: { + hasUpstream: true, + upstreamName: 'fresh/main', + ahead: 1, + behind: 0 + } + })) + + expect(owner.getSnapshot(query())).toMatchObject({ + upstream: { upstreamName: 'fresh/main' }, + freshness: { upstream: { state: 'fresh', generation: 1 } } + }) + }) + + it('suppresses stale status and upstream completion across both mutation fences', async () => { + const owner = new GitRepositorySnapshotOwner() + const statuses = Array.from({ length: 3 }, () => deferred()) + const upstreams = Array.from({ length: 3 }, () => deferred()) + let statusIndex = 0 + let upstreamIndex = 0 + const readStatus = () => + owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, () => { + const pending = statuses[statusIndex] + statusIndex += 1 + return pending.promise + }) + const readUpstream = () => + owner.readUpstream(native, '/repo', undefined, () => { + const pending = upstreams[upstreamIndex] + upstreamIndex += 1 + return pending.promise + }) + + const before = [readStatus(), readUpstream()] + await Promise.resolve() + owner.invalidate() + const during = [readStatus(), readUpstream()] + await Promise.resolve() + owner.invalidate() + const after = [readStatus(), readUpstream()] + await Promise.resolve() + + statuses[0].resolve({ ...statusResult, head: 'before' }) + statuses[1].resolve({ ...statusResult, head: 'during' }) + statuses[2].resolve({ ...statusResult, head: 'after' }) + upstreams[0].resolve({ hasUpstream: true, upstreamName: 'before/main', ahead: 0, behind: 0 }) + upstreams[1].resolve({ hasUpstream: true, upstreamName: 'during/main', ahead: 0, behind: 0 }) + upstreams[2].resolve({ hasUpstream: true, upstreamName: 'after/main', ahead: 0, behind: 0 }) + + await Promise.all([...before, ...during, ...after]) + expect(owner.getSnapshot(query())).toMatchObject({ + revision: 2, + repositoryIdentity: { head: 'after' }, + upstream: { upstreamName: 'after/main' }, + freshness: { + status: { state: 'fresh', generation: 2 }, + upstream: { state: 'fresh', generation: 2 } + } + }) + }) + + it('isolates every host, path, status option, and upstream target dimension', async () => { + const owner = new GitRepositorySnapshotOwner() + const statusLoad = vi.fn(async () => statusResult) + const upstreamLoad = vi.fn(async () => ({ hasUpstream: false, ahead: 0, behind: 0 })) + const statusCases: [GitRepositoryExecutionIdentity, string, GitRepositoryStatusIdentity][] = [ + [native, '/repo-a', defaultStatusIdentity], + [native, '/repo-b', defaultStatusIdentity], + [{ kind: 'wsl', distro: 'Ubuntu' }, '/repo-a', defaultStatusIdentity], + [{ kind: 'wsl', distro: 'Debian' }, '/repo-a', defaultStatusIdentity], + [native, '/repo-a', { ...defaultStatusIdentity, includeIgnored: true }], + [native, '/repo-a', { ...defaultStatusIdentity, reuseLineStats: true }], + [native, '/repo-a', { ...defaultStatusIdentity, bypassEffectiveUpstreamNegativeCache: true }], + [native, '/repo-a', { ...defaultStatusIdentity, limit: 20 }], + [native, '/repo-a', { ...defaultStatusIdentity, sharedLinkPaths: ['node_modules'] }] + ] + const target = { remoteName: 'fork', branchName: 'feature' } + const targets: (GitPushTarget | undefined)[] = [ + undefined, + target, + { ...target, remoteName: 'origin' }, + { ...target, branchName: 'other' }, + { ...target, remoteUrl: 'https://github.com/example/fork.git' }, + { ...target, remoteCreated: false }, + { ...target, remoteCreated: true } + ] + + await Promise.all( + statusCases.map(([host, path, identity]) => + owner.readStatus(host, path, identity, undefined, statusLoad) + ) + ) + await Promise.all( + targets.map((candidate) => owner.readUpstream(native, '/repo-a', candidate, upstreamLoad)) + ) + + expect(statusLoad).toHaveBeenCalledTimes(statusCases.length) + expect(upstreamLoad).toHaveBeenCalledTimes(targets.length) + }) + + it('bounds repositories and projection variants without merging evicted identities', async () => { + const owner = new GitRepositorySnapshotOwner(2, 2) + const evictedPending = deferred() + const evicted = owner.readStatus( + native, + '/repo-a', + defaultStatusIdentity, + undefined, + () => evictedPending.promise + ) + await owner.readStatus(native, '/repo-b', defaultStatusIdentity, undefined, async () => ({ + ...statusResult, + head: 'repo-b' + })) + await owner.readStatus(native, '/repo-c', defaultStatusIdentity, undefined, async () => ({ + ...statusResult, + head: 'repo-c' + })) + evictedPending.resolve({ ...statusResult, head: 'repo-a-late' }) + await evicted + + expect(owner.getRetentionState().repositories).toBe(2) + expect(owner.getSnapshot(query(native, '/repo-a'))).toBeNull() + expect(owner.getSnapshot(query(native, '/repo-c'))?.repositoryIdentity.head).toBe('repo-c') + + await owner.readStatus( + native, + '/repo-c', + { ...defaultStatusIdentity, includeIgnored: true }, + undefined, + async () => statusResult + ) + await owner.readStatus( + native, + '/repo-c', + { ...defaultStatusIdentity, reuseLineStats: true }, + undefined, + async () => statusResult + ) + const target = { remoteName: 'fork', branchName: 'feature' } + await owner.readUpstream(native, '/repo-c', undefined, async () => ({ + hasUpstream: false, + ahead: 0, + behind: 0 + })) + await owner.readUpstream(native, '/repo-c', target, async () => ({ + hasUpstream: false, + ahead: 0, + behind: 0 + })) + await owner.readUpstream(native, '/repo-c', { ...target, branchName: 'other' }, async () => ({ + hasUpstream: false, + ahead: 0, + behind: 0 + })) + + expect(owner.getRetentionState()).toEqual({ + repositories: 2, + statusProjections: 3, + upstreamProjections: 2 + }) + }) + + it('bounds retained status payloads without changing the caller result', async () => { + const owner = new GitRepositorySnapshotOwner() + const entries = Array.from( + { length: MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES + 1 }, + (_, index) => ({ + path: `generated/file-${index}.ts`, + status: 'untracked' as const, + area: 'untracked' as const + }) + ) + const result = { entries, conflictOperation: 'unknown' as const } + + const returned = await owner.readStatus( + native, + '/large-repo', + { ...defaultStatusIdentity, limit: 0 }, + undefined, + async () => result + ) + const snapshot = owner.getSnapshot( + query(native, '/large-repo', { ...defaultStatusIdentity, limit: 0 }) + ) + + expect(returned.entries).toHaveLength(MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES + 1) + expect(snapshot?.status.entries).toHaveLength(MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES) + expect(snapshot?.status.retentionTruncated).toBe(true) + }) +}) diff --git a/src/main/git/git-repository-snapshot-owner.ts b/src/main/git/git-repository-snapshot-owner.ts new file mode 100644 index 00000000000..bf792bfa316 --- /dev/null +++ b/src/main/git/git-repository-snapshot-owner.ts @@ -0,0 +1,319 @@ +import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../shared/types' +import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' +import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner' +import { + createGitRepositoryProjectionFreshness, + EMPTY_GIT_IGNORED_PATHS, + EMPTY_GIT_STATUS_ENTRIES, + freezeGitRepositoryStatus, + freezeGitRepositoryUpstream, + gitRepositoryScopeKey, + gitRepositoryStatusKey, + gitRepositoryUpstreamKey, + type GitRepositoryExecutionIdentity, + type GitRepositorySnapshot, + type GitRepositorySnapshotQuery, + type GitRepositoryStatusIdentity, + type GitRepositoryStatusProjection +} from './git-repository-snapshot-projection' + +export type { + GitRepositoryExecutionIdentity, + GitRepositoryProjectionFreshness, + GitRepositorySnapshot, + GitRepositorySnapshotQuery, + GitRepositoryStatusIdentity +} from './git-repository-snapshot-projection' + +export const MAX_GIT_REPOSITORY_SNAPSHOTS = 64 +export const MAX_GIT_REPOSITORY_PROJECTION_VARIANTS = 4 + +type ProjectionRecord = { + generation: number + generatedAt: number + revision: number + value: T + failedGeneration?: number +} + +type RepositoryRecord = { + status: Map> + upstream: Map>> +} + +function clampBound(value: number, maximum: number): number { + return Number.isFinite(value) ? Math.min(maximum, Math.max(1, Math.floor(value))) : maximum +} + +export class GitRepositorySnapshotOwner { + private readonly statusReads = new GitStatusReadLeaseOwner() + private readonly upstreamReads = new InFlightPromiseDedupe() + private readonly repositories = new Map() + private readonly maxRepositories: number + private readonly maxProjectionVariants: number + private generation = 0 + private revision = 0 + + constructor( + maxRepositories = MAX_GIT_REPOSITORY_SNAPSHOTS, + maxProjectionVariants = MAX_GIT_REPOSITORY_PROJECTION_VARIANTS, + private readonly now: () => number = Date.now + ) { + this.maxRepositories = clampBound(maxRepositories, MAX_GIT_REPOSITORY_SNAPSHOTS) + this.maxProjectionVariants = clampBound( + maxProjectionVariants, + MAX_GIT_REPOSITORY_PROJECTION_VARIANTS + ) + } + + readStatus( + executionIdentity: GitRepositoryExecutionIdentity, + worktreePath: string, + identity: GitRepositoryStatusIdentity, + signal: AbortSignal | undefined, + load: (sharedSignal: AbortSignal) => Promise + ): Promise { + const repositoryKey = gitRepositoryScopeKey(executionIdentity, worktreePath) + const projectionKey = gitRepositoryStatusKey(identity) + const generation = this.generation + return this.statusReads.lease( + stableInFlightKey([repositoryKey, projectionKey]), + signal, + async (sharedSignal) => { + const repository = this.getOrCreateRepository(repositoryKey) + try { + const result = await load(sharedSignal) + this.publishStatus(repositoryKey, repository, projectionKey, generation, result) + return result + } catch (error) { + const retained = repository.status.get(projectionKey) + if ( + !sharedSignal.aborted && + generation === this.generation && + this.repositories.get(repositoryKey) === repository && + retained + ) { + retained.failedGeneration = generation + } + this.removeEmptyRepository(repositoryKey, repository) + throw error + } + } + ) + } + + readUpstream( + executionIdentity: GitRepositoryExecutionIdentity, + worktreePath: string, + pushTarget: GitPushTarget | undefined, + load: () => Promise + ): Promise { + const repositoryKey = gitRepositoryScopeKey(executionIdentity, worktreePath) + const projectionKey = gitRepositoryUpstreamKey(pushTarget) + const generation = this.generation + return this.upstreamReads.run(stableInFlightKey([repositoryKey, projectionKey]), async () => { + const repository = this.getOrCreateRepository(repositoryKey) + try { + const result = await load() + this.publishUpstream(repositoryKey, repository, projectionKey, generation, result) + return result + } catch (error) { + const retained = repository.upstream.get(projectionKey) + if ( + generation === this.generation && + this.repositories.get(repositoryKey) === repository && + retained + ) { + retained.failedGeneration = generation + } + this.removeEmptyRepository(repositoryKey, repository) + throw error + } + }) + } + + getSnapshot(query: GitRepositorySnapshotQuery): GitRepositorySnapshot | null { + const repositoryKey = gitRepositoryScopeKey(query.executionIdentity, query.worktreePath) + const repository = this.repositories.get(repositoryKey) + if (!repository) { + return null + } + this.touchRepository(repositoryKey, repository) + const statusIdentity = gitRepositoryStatusKey(query.statusIdentity) + const upstreamIdentity = gitRepositoryUpstreamKey(query.pushTarget) + const statusRecord = repository.status.get(statusIdentity) + const upstreamRecord = repository.upstream.get(upstreamIdentity) + if (!statusRecord && !upstreamRecord) { + return null + } + + const statusFreshness = createGitRepositoryProjectionFreshness( + statusRecord, + this.generation, + statusIdentity + ) + const embeddedUpstreamRecord = + query.pushTarget === undefined && statusRecord?.value.upstream ? statusRecord : undefined + const useEmbeddedUpstream = + embeddedUpstreamRecord && + (!upstreamRecord || embeddedUpstreamRecord.revision > upstreamRecord.revision) + const selectedUpstreamRecord = useEmbeddedUpstream ? embeddedUpstreamRecord : upstreamRecord + const upstreamFreshness = selectedUpstreamRecord + ? createGitRepositoryProjectionFreshness( + selectedUpstreamRecord, + this.generation, + useEmbeddedUpstream ? `status:${statusIdentity}` : upstreamIdentity + ) + : createGitRepositoryProjectionFreshness(undefined, this.generation, upstreamIdentity) + const revision = Math.max(statusRecord?.revision ?? 0, upstreamRecord?.revision ?? 0) + const generatedAt = Math.max(statusRecord?.generatedAt ?? 0, upstreamRecord?.generatedAt ?? 0) + const status = statusRecord?.value + return Object.freeze({ + revision, + generatedAt, + repositoryIdentity: status?.repositoryIdentity ?? Object.freeze({ head: null, branch: null }), + status: + status?.status ?? + Object.freeze({ + entries: EMPTY_GIT_STATUS_ENTRIES, + didHitLimit: false, + statusLength: null, + ignoredPaths: EMPTY_GIT_IGNORED_PATHS, + lineStatsState: 'missing' as const, + retentionTruncated: false + }), + upstream: useEmbeddedUpstream + ? embeddedUpstreamRecord.value.upstream + : (upstreamRecord?.value ?? null), + conflicts: status?.conflicts ?? null, + worktreeGraphVersion: 0, + freshness: Object.freeze({ + repositoryIdentity: statusFreshness, + status: statusFreshness, + upstream: upstreamFreshness, + conflicts: statusFreshness, + worktreeGraph: Object.freeze({ + state: 'placeholder', + generation: this.generation, + currentGeneration: this.generation, + revision: null, + identity: null + }) + }) + }) + } + + invalidate(): void { + this.generation += 1 + this.statusReads.invalidate() + this.upstreamReads.clear() + } + + getRetentionState(): Readonly<{ + repositories: number + statusProjections: number + upstreamProjections: number + }> { + let statusProjections = 0 + let upstreamProjections = 0 + for (const repository of this.repositories.values()) { + statusProjections += repository.status.size + upstreamProjections += repository.upstream.size + } + return Object.freeze({ + repositories: this.repositories.size, + statusProjections, + upstreamProjections + }) + } + + private getOrCreateRepository(repositoryKey: string): RepositoryRecord { + const existing = this.repositories.get(repositoryKey) + if (existing) { + this.touchRepository(repositoryKey, existing) + return existing + } + const repository = { status: new Map(), upstream: new Map() } + this.repositories.set(repositoryKey, repository) + while (this.repositories.size > this.maxRepositories) { + const oldestKey = this.repositories.keys().next().value + if (oldestKey === undefined) { + break + } + this.repositories.delete(oldestKey) + } + return repository + } + + private touchRepository(repositoryKey: string, repository: RepositoryRecord): void { + this.repositories.delete(repositoryKey) + this.repositories.set(repositoryKey, repository) + } + + private removeEmptyRepository(repositoryKey: string, repository: RepositoryRecord): void { + if ( + this.repositories.get(repositoryKey) === repository && + repository.status.size === 0 && + repository.upstream.size === 0 + ) { + this.repositories.delete(repositoryKey) + } + } + + private publishStatus( + repositoryKey: string, + repository: RepositoryRecord, + projectionKey: string, + generation: number, + result: GitStatusResult + ): void { + if (generation !== this.generation || this.repositories.get(repositoryKey) !== repository) { + return + } + const revision = this.nextRevision() + const generatedAt = this.now() + this.remember(repository.status, projectionKey, { + generation, + generatedAt, + revision, + value: freezeGitRepositoryStatus(result) + }) + } + + private publishUpstream( + repositoryKey: string, + repository: RepositoryRecord, + projectionKey: string, + generation: number, + result: GitUpstreamStatus + ): void { + if (generation !== this.generation || this.repositories.get(repositoryKey) !== repository) { + return + } + this.remember(repository.upstream, projectionKey, { + generation, + generatedAt: this.now(), + revision: this.nextRevision(), + value: freezeGitRepositoryUpstream(result) + }) + } + + private remember(projections: Map, key: string, value: T): void { + projections.delete(key) + projections.set(key, value) + while (projections.size > this.maxProjectionVariants) { + const oldestKey = projections.keys().next().value + if (oldestKey === undefined) { + break + } + projections.delete(oldestKey) + } + } + + private nextRevision(): number { + this.revision += 1 + return this.revision + } +} + +export const nativeAndWslGitRepositorySnapshotOwner = new GitRepositorySnapshotOwner() diff --git a/src/main/git/git-repository-snapshot-projection.ts b/src/main/git/git-repository-snapshot-projection.ts new file mode 100644 index 00000000000..ddf444f80b1 --- /dev/null +++ b/src/main/git/git-repository-snapshot-projection.ts @@ -0,0 +1,210 @@ +import type { + GitConflictOperation, + GitPushTarget, + GitStatusEntry, + GitStatusResult, + GitUpstreamStatus +} from '../../shared/types' +import { stableInFlightKey } from '../../shared/in-flight-promise-dedupe' + +export const MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES = 1_000 +export const MAX_GIT_REPOSITORY_SNAPSHOT_IGNORED_PATHS = 1_000 +export const MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS = 64 * 1_024 + +export type GitRepositoryExecutionIdentity = + | { kind: 'native' } + | { kind: 'wsl'; distro: string } + | { kind: 'ssh-provider'; connectionId: string } + +export type GitRepositoryStatusIdentity = { + includeIgnored: boolean + reuseLineStats: boolean + bypassEffectiveUpstreamNegativeCache: boolean + limit: number | null + sharedLinkPaths: readonly string[] +} + +export type GitRepositoryProjectionFreshness = Readonly<{ + state: 'missing' | 'fresh' | 'stale' | 'failed' | 'placeholder' + generation: number + currentGeneration: number + revision: number | null + identity: string | null +}> + +export type GitRepositorySnapshot = Readonly<{ + revision: number + generatedAt: number + repositoryIdentity: Readonly<{ head: string | null; branch: string | null }> + status: Readonly<{ + entries: readonly Readonly[] + didHitLimit: boolean + statusLength: number | null + ignoredPaths: readonly string[] + lineStatsState: 'missing' | 'complete' | 'skipped-at-limit' + retentionTruncated: boolean + }> + upstream: Readonly | null + conflicts: GitConflictOperation | null + worktreeGraphVersion: number + freshness: Readonly<{ + repositoryIdentity: GitRepositoryProjectionFreshness + status: GitRepositoryProjectionFreshness + upstream: GitRepositoryProjectionFreshness + conflicts: GitRepositoryProjectionFreshness + worktreeGraph: GitRepositoryProjectionFreshness + }> +}> + +export type GitRepositoryStatusProjection = { + repositoryIdentity: Readonly<{ head: string | null; branch: string | null }> + status: GitRepositorySnapshot['status'] + upstream: Readonly | null + conflicts: GitConflictOperation +} + +export type GitRepositorySnapshotQuery = { + executionIdentity: GitRepositoryExecutionIdentity + worktreePath: string + statusIdentity: GitRepositoryStatusIdentity + pushTarget?: GitPushTarget +} + +export const EMPTY_GIT_STATUS_ENTRIES = Object.freeze([]) as readonly Readonly[] +export const EMPTY_GIT_IGNORED_PATHS = Object.freeze([]) as readonly string[] + +export function gitRepositoryScopeKey( + executionIdentity: GitRepositoryExecutionIdentity, + worktreePath: string +): string { + return stableInFlightKey([executionIdentity, worktreePath]) +} + +export function gitRepositoryStatusKey(identity: GitRepositoryStatusIdentity): string { + return stableInFlightKey([ + identity.includeIgnored, + identity.reuseLineStats, + identity.bypassEffectiveUpstreamNegativeCache, + identity.limit, + identity.sharedLinkPaths + ]) +} + +export function gitRepositoryUpstreamKey(pushTarget: GitPushTarget | undefined): string { + return stableInFlightKey([ + pushTarget + ? [ + 'explicit-target', + pushTarget.remoteName, + pushTarget.branchName, + pushTarget.remoteUrl ?? null, + pushTarget.remoteCreated ?? null + ] + : ['configured-upstream'] + ]) +} + +export function freezeGitRepositoryUpstream( + status: GitUpstreamStatus +): Readonly { + return Object.freeze({ ...status }) +} + +function copyBoundedGitStatusEntries(entries: GitStatusEntry[]): { + entries: readonly Readonly[] + textCodeUnits: number +} { + const retained: Readonly[] = [] + let textCodeUnits = 0 + for (const entry of entries) { + const entryCodeUnits = entry.path.length + (entry.oldPath?.length ?? 0) + if ( + retained.length >= MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES || + textCodeUnits + entryCodeUnits > MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS + ) { + break + } + retained.push( + Object.freeze({ + ...entry, + ...(entry.submodule ? { submodule: Object.freeze({ ...entry.submodule }) } : {}) + }) + ) + textCodeUnits += entryCodeUnits + } + return { entries: Object.freeze(retained), textCodeUnits } +} + +function copyBoundedGitIgnoredPaths( + ignoredPaths: string[], + textCodeUnits: number +): readonly string[] { + const retained: string[] = [] + let retainedCodeUnits = textCodeUnits + for (const ignoredPath of ignoredPaths) { + if ( + retained.length >= MAX_GIT_REPOSITORY_SNAPSHOT_IGNORED_PATHS || + retainedCodeUnits + ignoredPath.length > MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS + ) { + break + } + retained.push(ignoredPath) + retainedCodeUnits += ignoredPath.length + } + return Object.freeze(retained) +} + +export function freezeGitRepositoryStatus(result: GitStatusResult): GitRepositoryStatusProjection { + const boundedEntries = copyBoundedGitStatusEntries(result.entries) + const ignoredPaths = copyBoundedGitIgnoredPaths( + result.ignoredPaths ?? [], + boundedEntries.textCodeUnits + ) + const retentionTruncated = + boundedEntries.entries.length !== result.entries.length || + ignoredPaths.length !== (result.ignoredPaths?.length ?? 0) + return Object.freeze({ + repositoryIdentity: Object.freeze({ + head: result.head ?? null, + branch: result.branch ?? null + }), + status: Object.freeze({ + entries: boundedEntries.entries, + didHitLimit: result.didHitLimit === true, + statusLength: result.statusLength ?? null, + ignoredPaths, + lineStatsState: result.didHitLimit === true ? 'skipped-at-limit' : 'complete', + retentionTruncated + }), + upstream: result.upstreamStatus ? freezeGitRepositoryUpstream(result.upstreamStatus) : null, + conflicts: result.conflictOperation + }) +} + +export function createGitRepositoryProjectionFreshness( + record: + | { + generation: number + revision: number + failedGeneration?: number + } + | undefined, + generation: number, + identity: string +): GitRepositoryProjectionFreshness { + const state = + record?.failedGeneration === generation + ? 'failed' + : record + ? record.generation === generation + ? 'fresh' + : 'stale' + : 'missing' + return Object.freeze({ + state, + generation: record?.generation ?? generation, + currentGeneration: generation, + revision: record?.revision ?? null, + identity + }) +} diff --git a/src/main/git/git-upstream-status-read-owner.test.ts b/src/main/git/git-upstream-status-read-owner.test.ts deleted file mode 100644 index 4eecfd035fe..00000000000 --- a/src/main/git/git-upstream-status-read-owner.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types' -import { - GitUpstreamStatusReadOwner, - type GitUpstreamStatusExecutionIdentity -} from './git-upstream-status-read-owner' - -const upstreamStatus: GitUpstreamStatus = { - hasUpstream: true, - upstreamName: 'origin/main', - ahead: 1, - behind: 0 -} - -function deferredPromise(): { - promise: Promise - resolve: (value: T) => void - reject: (error: unknown) => void -} { - let resolve!: (value: T) => void - let reject!: (error: unknown) => void - const promise = new Promise((innerResolve, innerReject) => { - resolve = innerResolve - reject = innerReject - }) - return { promise, resolve, reject } -} - -describe('GitUpstreamStatusReadOwner', () => { - it('shares one live read across ten identical callers', async () => { - const owner = new GitUpstreamStatusReadOwner() - const pending = deferredPromise() - const load = vi.fn(() => pending.promise) - - const reads = Array.from({ length: 10 }, () => - owner.read({ kind: 'native' }, '/repo', undefined, load) - ) - await Promise.resolve() - - expect(load).toHaveBeenCalledTimes(1) - pending.resolve(upstreamStatus) - await expect(Promise.all(reads)).resolves.toEqual( - Array.from({ length: 10 }, () => upstreamStatus) - ) - }) - - it('isolates paths, execution hosts, and every explicit target field', async () => { - const owner = new GitUpstreamStatusReadOwner() - const load = vi.fn().mockResolvedValue(upstreamStatus) - const baseTarget: GitPushTarget = { - remoteName: 'fork', - branchName: 'feature' - } - const cases: [GitUpstreamStatusExecutionIdentity, string, GitPushTarget | undefined][] = [ - [{ kind: 'native' }, '/repo-a', undefined], - [{ kind: 'native' }, '/repo-b', undefined], - [{ kind: 'wsl', distro: 'Ubuntu' }, '/repo-a', undefined], - [{ kind: 'wsl', distro: 'Debian' }, '/repo-a', undefined], - [{ kind: 'native' }, '/repo-a', baseTarget], - [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteName: 'origin' }], - [{ kind: 'native' }, '/repo-a', { ...baseTarget, branchName: 'other' }], - [ - { kind: 'native' }, - '/repo-a', - { ...baseTarget, remoteUrl: 'https://github.com/example/fork.git' } - ], - [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: false }], - [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: true }], - [{ kind: 'ssh-provider' }, '/repo-a', baseTarget] - ] - - await Promise.all( - cases.map(([identity, worktreePath, target]) => - owner.read(identity, worktreePath, target, load) - ) - ) - - expect(load).toHaveBeenCalledTimes(cases.length) - }) - - it('runs fresh work after success and rejection', async () => { - const owner = new GitUpstreamStatusReadOwner() - const failure = new Error('upstream failed') - const load = vi - .fn<() => Promise>() - .mockResolvedValueOnce(upstreamStatus) - .mockRejectedValueOnce(failure) - .mockResolvedValueOnce(upstreamStatus) - - await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe( - upstreamStatus - ) - await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).rejects.toBe(failure) - await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe( - upstreamStatus - ) - expect(load).toHaveBeenCalledTimes(3) - }) - - it('fences before, during, and after mutation generations', async () => { - const owner = new GitUpstreamStatusReadOwner() - const pendingReads = Array.from({ length: 3 }, () => deferredPromise()) - const load = vi.fn(() => pendingReads[load.mock.calls.length - 1].promise) - - const before = owner.read({ kind: 'native' }, '/repo', undefined, load) - await Promise.resolve() - owner.invalidate() - const during = owner.read({ kind: 'native' }, '/repo', undefined, load) - await Promise.resolve() - owner.invalidate() - const after = owner.read({ kind: 'native' }, '/repo', undefined, load) - await Promise.resolve() - - pendingReads.forEach((pending) => pending.resolve(upstreamStatus)) - await expect(Promise.all([before, during, after])).resolves.toEqual( - Array.from({ length: 3 }, () => upstreamStatus) - ) - expect(load).toHaveBeenCalledTimes(3) - }) -}) diff --git a/src/main/git/git-upstream-status-read-owner.ts b/src/main/git/git-upstream-status-read-owner.ts deleted file mode 100644 index be297f90e8f..00000000000 --- a/src/main/git/git-upstream-status-read-owner.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types' -import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' - -export type GitUpstreamStatusExecutionIdentity = - | { kind: 'native' } - | { kind: 'wsl'; distro: string } - | { kind: 'ssh-provider' } - -export class GitUpstreamStatusReadOwner { - private readonly inFlightReads = new InFlightPromiseDedupe() - - read( - executionIdentity: GitUpstreamStatusExecutionIdentity, - worktreePath: string, - pushTarget: GitPushTarget | undefined, - load: () => Promise - ): Promise { - const key = stableInFlightKey([ - executionIdentity, - worktreePath, - pushTarget - ? [ - 'explicit-target', - pushTarget.remoteName, - pushTarget.branchName, - pushTarget.remoteUrl ?? null, - pushTarget.remoteCreated ?? null - ] - : ['configured-upstream'] - ]) - return this.inFlightReads.run(key, load) - } - - invalidate(): void { - this.inFlightReads.clear() - } -} - -export const nativeAndWslGitUpstreamStatusReadOwner = new GitUpstreamStatusReadOwner() diff --git a/src/main/git/status.test.ts b/src/main/git/status.test.ts index 4bd645c7141..75a024e7ef5 100644 --- a/src/main/git/status.test.ts +++ b/src/main/git/status.test.ts @@ -108,6 +108,7 @@ import { getBranchCompare, getCommitCompare, getDiff, + getGitRepositorySnapshot, getStagedCommitContext, getStatus, getSubmoduleStatus, @@ -1176,6 +1177,38 @@ describe('getStatus', () => { ) }) + it('publishes the live native status result into the repository snapshot owner', async () => { + gitExecFileAsyncMock.mockResolvedValueOnce({ + stdout: + '# branch.oid abcdef1234567890\n' + + '# branch.head feature/snapshot\n' + + '# branch.upstream origin/feature/snapshot\n' + + '# branch.ab +2 -1\n' + + '1 .M N... 100644 100644 100644 aaaa aaaa src/app.ts\n' + }) + + const result = await getStatus('/snapshot-repo') + const snapshot = getGitRepositorySnapshot('/snapshot-repo') + + expect(snapshot).toMatchObject({ + repositoryIdentity: { + head: 'abcdef1234567890', + branch: 'refs/heads/feature/snapshot' + }, + status: { + entries: result.entries, + didHitLimit: false, + lineStatsState: 'complete' + }, + upstream: result.upstreamStatus, + conflicts: 'unknown', + freshness: { + status: { state: 'fresh' }, + upstream: { state: 'fresh' } + } + }) + }) + it('coalesces identical in-flight status reads without caching after settle', async () => { readFileMock.mockResolvedValue('gitdir: /repo/.git/worktrees/feature\n') existsSyncMock.mockReturnValue(false) diff --git a/src/main/git/status.ts b/src/main/git/status.ts index 3bd825c8622..0ffdb345ece 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -12,6 +12,7 @@ import type { GitConflictOperation, GitDiffResult, GitFileStatus, + GitPushTarget, GitStatusEntry, GitStatusResult, GitUpstreamStatus @@ -52,9 +53,12 @@ import { getLargeDiffRenderLimit } from '../../shared/large-diff-render-limit' import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' import type { GitRuntimeOptions } from './git-runtime-options' import { gitOptionsForWorktree } from './git-runtime-options' -import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner' import { parseGitRevListFirstParentOid } from '../../shared/git-rev-list-output' -import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner' +import { + nativeAndWslGitRepositorySnapshotOwner, + type GitRepositoryExecutionIdentity, + type GitRepositorySnapshot +} from './git-repository-snapshot-owner' import { beginGitStatusLineStatsCacheWrite, clearGitStatusLineStatsCache, @@ -94,13 +98,11 @@ const effectiveUpstreamStatusInFlight = new Map>() const gitDiffReadDedupe = new InFlightPromiseDedupe() const effectiveUpstreamStatusWriteGeneration = new Map() -const statusReadLeaseOwner = new GitStatusReadLeaseOwner() // Why: clear both diff and status in-flight caches; clearing only diff would let getStatus() join a pre-mutation read. export function invalidateGitReadCaches(): void { gitDiffReadDedupe.clear() - statusReadLeaseOwner.invalidate() - nativeAndWslGitUpstreamStatusReadOwner.invalidate() + nativeAndWslGitRepositorySnapshotOwner.invalidate() clearGitStatusLineStatsCache() clearSubmodulePathsCache() resolvedUpstreamNameCache.clear() @@ -219,26 +221,41 @@ export async function getStatus( options: GetStatusOptions = {} ): Promise { gitDiffReadDedupe.clear() - // Why: dedupe only concurrent identical reads; after settle, callers must run a fresh read. - const cacheKey = getStatusReadKey(worktreePath, options) - return statusReadLeaseOwner.lease(cacheKey, options.signal, (sharedSignal) => - runGetStatus(worktreePath, { ...options, signal: sharedSignal }) + return nativeAndWslGitRepositorySnapshotOwner.readStatus( + getStatusExecutionIdentity(options), + worktreePath, + getStatusSnapshotIdentity(options), + options.signal, + (sharedSignal) => runGetStatus(worktreePath, { ...options, signal: sharedSignal }) ) } -function getStatusReadKey(worktreePath: string, options: GetStatusOptions): string { - // Why: each key part can change the output shape or runtime routing. - const limit = resolveGitStatusLimit(options.limit) - return stableInFlightKey([ +export function getGitRepositorySnapshot( + worktreePath: string, + options: GetStatusOptions = {}, + pushTarget?: GitPushTarget +): GitRepositorySnapshot | null { + return nativeAndWslGitRepositorySnapshotOwner.getSnapshot({ + executionIdentity: getStatusExecutionIdentity(options), worktreePath, - options.wslDistro ?? '', - options.includeIgnored === true, - options.reuseLineStats === true, - options.bypassEffectiveUpstreamNegativeCache === true, - limit, + statusIdentity: getStatusSnapshotIdentity(options), + pushTarget + }) +} + +function getStatusExecutionIdentity(options: GetStatusOptions): GitRepositoryExecutionIdentity { + return options.wslDistro ? { kind: 'wsl', distro: options.wslDistro } : { kind: 'native' } +} + +function getStatusSnapshotIdentity(options: GetStatusOptions) { + return { + includeIgnored: options.includeIgnored === true, + reuseLineStats: options.reuseLineStats === true, + bypassEffectiveUpstreamNegativeCache: options.bypassEffectiveUpstreamNegativeCache === true, + limit: resolveGitStatusLimit(options.limit), // Why: this changes which entries survive, so it must not share a cache slot. - options.sharedLinkPaths ?? [] - ]) + sharedLinkPaths: options.sharedLinkPaths ?? [] + } } /** Remove untracked entries that are shared symlinks Orca created. diff --git a/src/main/git/upstream.test.ts b/src/main/git/upstream.test.ts index d0288e4fd12..77e67e1b00a 100644 --- a/src/main/git/upstream.test.ts +++ b/src/main/git/upstream.test.ts @@ -11,7 +11,7 @@ vi.mock('./runner', () => ({ })) import { getUpstreamStatus, invalidateGitUpstreamStatusReads } from './upstream' -import { runWithGitReadCacheInvalidation } from './status' +import { getGitRepositorySnapshot, runWithGitReadCacheInvalidation } from './status' const missingTrackingRefError = new Error( "fatal: ambiguous argument 'HEAD@{u}': unknown revision or path not in the working tree.\n" + @@ -71,6 +71,24 @@ describe('getUpstreamStatus', () => { ) }) + it('publishes the live native upstream result into the repository snapshot owner', async () => { + gitExecFileAsyncMock + .mockResolvedValueOnce({ stdout: 'main\n' }) + .mockResolvedValueOnce({ stdout: 'origin/main\n' }) + .mockResolvedValueOnce({ stdout: '2\t0\n' }) + + const result = await getUpstreamStatus('/snapshot-upstream-repo') + const snapshot = getGitRepositorySnapshot('/snapshot-upstream-repo') + + expect(snapshot).toMatchObject({ + upstream: result, + freshness: { + status: { state: 'missing' }, + upstream: { state: 'fresh' } + } + }) + }) + it('isolates physical reads by worktree, native or WSL host, and every target field', async () => { gitExecFileAsyncMock.mockImplementation((args: string[]) => { if (args[0] === 'symbolic-ref') { diff --git a/src/main/git/upstream.ts b/src/main/git/upstream.ts index 9c4100bf507..680d31d0317 100644 --- a/src/main/git/upstream.ts +++ b/src/main/git/upstream.ts @@ -5,14 +5,14 @@ import { getEffectiveGitUpstreamStatus } from '../../shared/git-effective-upstre import { getPublishTargetStatus } from '../../shared/git-publish-target-status' import { gitExecFileAsync } from './runner' import { validateGitPushTarget } from './push-target-validation' -import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner' +import { nativeAndWslGitRepositorySnapshotOwner } from './git-repository-snapshot-owner' type GitExecOptions = { wslDistro?: string } export function invalidateGitUpstreamStatusReads(): void { - nativeAndWslGitUpstreamStatusReadOwner.invalidate() + nativeAndWslGitRepositorySnapshotOwner.invalidate() } function gitExecOptions( @@ -85,7 +85,7 @@ export function getUpstreamStatus( const executionIdentity = options.wslDistro ? ({ kind: 'wsl', distro: options.wslDistro } as const) : ({ kind: 'native' } as const) - return nativeAndWslGitUpstreamStatusReadOwner.read( + return nativeAndWslGitRepositorySnapshotOwner.readUpstream( executionIdentity, worktreePath, pushTarget, diff --git a/src/main/providers/ssh-git-provider.test.ts b/src/main/providers/ssh-git-provider.test.ts index dde91d98843..71910c6b322 100644 --- a/src/main/providers/ssh-git-provider.test.ts +++ b/src/main/providers/ssh-git-provider.test.ts @@ -1121,6 +1121,75 @@ describe('SshGitProvider', () => { expect(mux.request).toHaveBeenCalledTimes(3) }) + it('publishes combined immutable snapshots per provider incarnation', async () => { + mux.request.mockImplementation((method) => { + if (method === 'git.status') { + return Promise.resolve({ + entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged' }], + conflictOperation: 'rebase', + head: 'abc123', + branch: 'refs/heads/feature' + }) + } + if (method === 'git.upstreamStatus') { + return Promise.resolve({ + hasUpstream: true, + upstreamName: 'origin/feature', + ahead: 1, + behind: 0 + }) + } + return Promise.resolve(undefined) + }) + + await provider.getStatus('/home/user/repo') + await provider.getUpstreamStatus('/home/user/repo') + const snapshot = provider.getRepositorySnapshot('/home/user/repo') + const replacement = new SshGitProvider('conn-1', mux as never) + + expect(snapshot).toMatchObject({ + repositoryIdentity: { head: 'abc123', branch: 'refs/heads/feature' }, + status: { entries: [{ path: 'src/app.ts' }] }, + upstream: { upstreamName: 'origin/feature' }, + conflicts: 'rebase', + freshness: { + status: { state: 'fresh' }, + upstream: { state: 'fresh' } + } + }) + expect(Object.isFrozen(snapshot)).toBe(true) + expect(replacement.getRepositorySnapshot('/home/user/repo')).toBeNull() + }) + + it('marks retained snapshots stale across the existing SSH mutation fence', async () => { + mux.request.mockImplementation((method) => { + if (method === 'git.status') { + return Promise.resolve({ + entries: [], + conflictOperation: 'unknown', + head: 'abc123', + branch: 'refs/heads/main' + }) + } + return Promise.resolve(undefined) + }) + + await provider.getStatus('/home/user/repo') + const before = provider.getRepositorySnapshot('/home/user/repo') + await provider.stageFile('/home/user/repo', 'src/app.ts') + const after = provider.getRepositorySnapshot('/home/user/repo') + + expect(before?.freshness.status.state).toBe('fresh') + expect(after?.revision).toBe(before?.revision) + expect(after?.freshness.status).toMatchObject({ + state: 'stale', + generation: before!.freshness.status.generation + }) + expect(after!.freshness.status.currentGeneration).toBeGreaterThan( + after!.freshness.status.generation + ) + }) + it('getUpstreamStatus forwards an explicit push target', async () => { const upstreamResult = { hasUpstream: true, upstreamName: 'fork/feature', ahead: 0, behind: 1 } mux.request.mockResolvedValue(upstreamResult) diff --git a/src/main/providers/ssh-git-provider.ts b/src/main/providers/ssh-git-provider.ts index 51499f8e8d0..c2ca854cc5a 100644 --- a/src/main/providers/ssh-git-provider.ts +++ b/src/main/providers/ssh-git-provider.ts @@ -32,8 +32,10 @@ import { } from '../git/max-buffer-overflow' import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' import { gitExecMutatesRepository } from '../../shared/git-exec-mutation' -import { GitStatusReadLeaseOwner } from '../git/git-status-read-lease-owner' -import { GitUpstreamStatusReadOwner } from '../git/git-upstream-status-read-owner' +import { + GitRepositorySnapshotOwner, + type GitRepositorySnapshot +} from '../git/git-repository-snapshot-owner' type NonInteractiveExecQueueEntry = { started: boolean @@ -85,8 +87,7 @@ function filterUntrackedPorcelainStatus(stdout: string | undefined): string | un export class SshGitProvider implements IGitProvider { private readonly gitDiffReadDedupe = new InFlightPromiseDedupe() - private readonly statusReadLeaseOwner = new GitStatusReadLeaseOwner() - private readonly upstreamStatusReadOwner = new GitUpstreamStatusReadOwner() + private readonly repositorySnapshotOwner = new GitRepositorySnapshotOwner() private connectionId: string private mux: SshChannelMultiplexer @@ -103,8 +104,7 @@ export class SshGitProvider implements IGitProvider { private invalidateGitReads(): void { this.gitDiffReadDedupe.clear() - this.statusReadLeaseOwner.invalidate() - this.upstreamStatusReadOwner.invalidate() + this.repositorySnapshotOwner.invalidate() } private loggedWorktreeIsCleanFallback = false @@ -142,17 +142,23 @@ export class SshGitProvider implements IGitProvider { ...upstreamCacheBypassArgs, ...lineStatsReuseArgs } - const key = stableInFlightKey([ + return this.repositorySnapshotOwner.readStatus( + { kind: 'ssh-provider', connectionId: this.connectionId }, worktreePath, - options?.includeIgnored === true, - options?.bypassEffectiveUpstreamNegativeCache === true, - options?.reuseLineStats === true - ]) - return this.statusReadLeaseOwner.lease(key, options?.signal, async (sharedSignal) => { - return (await this.mux.request('git.status', request, { - signal: sharedSignal - })) as GitStatusResult - }) + { + includeIgnored: options?.includeIgnored === true, + reuseLineStats: options?.reuseLineStats === true, + bypassEffectiveUpstreamNegativeCache: + options?.bypassEffectiveUpstreamNegativeCache === true, + limit: null, + sharedLinkPaths: [] + }, + options?.signal, + async (sharedSignal) => + (await this.mux.request('git.status', request, { + signal: sharedSignal + })) as GitStatusResult + ) } async getSubmoduleStatus( @@ -510,8 +516,8 @@ export class SshGitProvider implements IGitProvider { worktreePath: string, pushTarget?: GitPushTarget ): Promise { - return this.upstreamStatusReadOwner.read( - { kind: 'ssh-provider' }, + return this.repositorySnapshotOwner.readUpstream( + { kind: 'ssh-provider', connectionId: this.connectionId }, worktreePath, pushTarget, async () => @@ -522,6 +528,26 @@ export class SshGitProvider implements IGitProvider { ) } + getRepositorySnapshot( + worktreePath: string, + options?: GitProviderStatusOptions, + pushTarget?: GitPushTarget + ): GitRepositorySnapshot | null { + return this.repositorySnapshotOwner.getSnapshot({ + executionIdentity: { kind: 'ssh-provider', connectionId: this.connectionId }, + worktreePath, + statusIdentity: { + includeIgnored: options?.includeIgnored === true, + reuseLineStats: options?.reuseLineStats === true, + bypassEffectiveUpstreamNegativeCache: + options?.bypassEffectiveUpstreamNegativeCache === true, + limit: null, + sharedLinkPaths: [] + }, + pushTarget + }) + } + async pushBranch( worktreePath: string, publish = false, diff --git a/tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts b/tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts new file mode 100644 index 00000000000..397963ab763 --- /dev/null +++ b/tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts @@ -0,0 +1,250 @@ +import { Session } from 'node:inspector' +import { performance } from 'node:perf_hooks' +import type { GitStatusEntry, GitStatusResult } from '../../src/shared/types' +import { GitStatusReadLeaseOwner } from '../../src/main/git/git-status-read-lease-owner' +import { + GitRepositorySnapshotOwner, + type GitRepositoryStatusIdentity +} from '../../src/main/git/git-repository-snapshot-owner' + +type Arm = 'lease-only' | 'snapshot-owner' +type Measurement = { cpuMicrosecondsPerRead: number; wallMicrosecondsPerRead: number } +type HeapProfileNode = { selfSize: number; children?: HeapProfileNode[] } + +const native = { kind: 'native' } as const +const statusIdentity: GitRepositoryStatusIdentity = { + includeIgnored: false, + reuseLineStats: false, + bypassEffectiveUpstreamNegativeCache: false, + limit: 1_000, + sharedLinkPaths: [] +} +const configurations = [ + { entries: 0, timingReads: 10_000, warmupReads: 2_000, allocationReads: 5_000 }, + { entries: 100, timingReads: 2_000, warmupReads: 400, allocationReads: 500 }, + { entries: 1_000, timingReads: 250, warmupReads: 50, allocationReads: 100 } +] +const timingRounds = 11 +const allocationRounds = 7 + +function createStatusResult(entryCount: number): GitStatusResult { + const entries: GitStatusEntry[] = Array.from({ length: entryCount }, (_, index) => ({ + path: `src/features/feature-${index}/index.ts`, + status: 'modified', + area: 'unstaged', + added: index % 7, + removed: index % 5 + })) + return { + entries, + conflictOperation: 'unknown', + head: '0123456789abcdef', + branch: 'main', + upstreamStatus: { + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 2, + behind: 3, + behindCommitsArePatchEquivalent: false + } + } +} + +function createReader(arm: Arm, result: GitStatusResult): () => Promise { + if (arm === 'lease-only') { + const owner = new GitStatusReadLeaseOwner() + return () => owner.lease('native:/repo', undefined, async () => result) + } + const owner = new GitRepositorySnapshotOwner() + return () => owner.readStatus(native, '/repo', statusIdentity, undefined, async () => result) +} + +async function runReads(read: () => Promise, repetitions: number): Promise { + for (let index = 0; index < repetitions; index += 1) { + await read() + } +} + +function median(values: number[]): number { + const sorted = [...values].sort((left, right) => left - right) + return sorted[Math.floor(sorted.length / 2)] +} + +async function measureTiming( + read: () => Promise, + repetitions: number +): Promise { + const startedCpu = process.cpuUsage() + const startedWall = performance.now() + await runReads(read, repetitions) + const wallMilliseconds = performance.now() - startedWall + const cpu = process.cpuUsage(startedCpu) + return { + cpuMicrosecondsPerRead: (cpu.user + cpu.system) / repetitions, + wallMicrosecondsPerRead: (wallMilliseconds * 1_000) / repetitions + } +} + +function post(session: Session, method: string, parameters?: object): Promise { + return new Promise((resolve, reject) => { + session.post(method, parameters, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result as T) + } + }) + }) +} + +function sumSampledBytes(node: HeapProfileNode): number { + return ( + node.selfSize + + (node.children?.reduce((total, child) => total + sumSampledBytes(child), 0) ?? 0) + ) +} + +async function measureSampledAllocation( + read: () => Promise, + repetitions: number +): Promise { + globalThis.gc?.() + const session = new Session() + session.connect() + try { + await post(session, 'HeapProfiler.startSampling', { + samplingInterval: 128, + includeObjectsCollectedByMajorGC: true, + includeObjectsCollectedByMinorGC: true + }) + await runReads(read, repetitions) + const result = await post<{ profile: { head: HeapProfileNode } }>( + session, + 'HeapProfiler.stopSampling' + ) + return sumSampledBytes(result.profile.head) / repetitions + } finally { + session.disconnect() + } +} + +async function verifyCallerResult(result: GitStatusResult): Promise { + const owner = new GitRepositorySnapshotOwner() + const before = JSON.stringify(result) + const returned = await owner.readStatus( + native, + '/repo', + statusIdentity, + undefined, + async () => result + ) + const snapshot = owner.getSnapshot({ + executionIdentity: native, + worktreePath: '/repo', + statusIdentity + }) + return { + returnedOriginalReference: returned === result, + callerResultUnchanged: JSON.stringify(result) === before, + callerResultFrozen: Object.isFrozen(result), + callerEntriesFrozen: Object.isFrozen(result.entries), + snapshotFrozen: Object.isFrozen(snapshot), + snapshotEntriesFrozen: Object.isFrozen(snapshot?.status.entries), + snapshotEntryCopies: result.entries.every( + (entry, index) => snapshot?.status.entries[index] !== entry + ) + } +} + +async function main(): Promise { + if (!globalThis.gc) { + throw new Error('Run with node --expose-gc --import tsx') + } + + const results = [] + for (const configuration of configurations) { + const statusResult = createStatusResult(configuration.entries) + const readers = { + 'lease-only': createReader('lease-only', statusResult), + 'snapshot-owner': createReader('snapshot-owner', statusResult) + } + await runReads(readers['lease-only'], configuration.warmupReads) + await runReads(readers['snapshot-owner'], configuration.warmupReads) + + const timing: Record = { + 'lease-only': [], + 'snapshot-owner': [] + } + for (let round = 0; round < timingRounds; round += 1) { + const order: Arm[] = + round % 2 === 0 ? ['lease-only', 'snapshot-owner'] : ['snapshot-owner', 'lease-only'] + for (const arm of order) { + timing[arm].push(await measureTiming(readers[arm], configuration.timingReads)) + } + } + + const sampledAllocation: Record = { + 'lease-only': [], + 'snapshot-owner': [] + } + for (let round = 0; round < allocationRounds; round += 1) { + for (const arm of ['lease-only', 'snapshot-owner'] as const) { + sampledAllocation[arm].push( + await measureSampledAllocation(readers[arm], configuration.allocationReads) + ) + } + } + + const medianCpu = { + 'lease-only': median(timing['lease-only'].map((value) => value.cpuMicrosecondsPerRead)), + 'snapshot-owner': median( + timing['snapshot-owner'].map((value) => value.cpuMicrosecondsPerRead) + ) + } + const medianWall = { + 'lease-only': median(timing['lease-only'].map((value) => value.wallMicrosecondsPerRead)), + 'snapshot-owner': median( + timing['snapshot-owner'].map((value) => value.wallMicrosecondsPerRead) + ) + } + const medianAllocatedBytes = { + 'lease-only': median(sampledAllocation['lease-only']), + 'snapshot-owner': median(sampledAllocation['snapshot-owner']) + } + results.push({ + entries: configuration.entries, + timingReadsPerRound: configuration.timingReads, + timingRounds, + allocationReadsPerRound: configuration.allocationReads, + allocationRounds, + medianCpuMicrosecondsPerRead: medianCpu, + incrementalCpuMicrosecondsPerRead: medianCpu['snapshot-owner'] - medianCpu['lease-only'], + medianWallMicrosecondsPerRead: medianWall, + incrementalWallMicrosecondsPerRead: medianWall['snapshot-owner'] - medianWall['lease-only'], + medianSampledAllocatedBytesPerRead: medianAllocatedBytes, + incrementalSampledAllocatedBytesPerRead: + medianAllocatedBytes['snapshot-owner'] - medianAllocatedBytes['lease-only'], + immutablePublicationOperations: { + entryCopies: configuration.entries, + objectFreezeCalls: configuration.entries + 6 + }, + callerVerification: await verifyCallerResult(statusResult) + }) + } + console.log( + JSON.stringify( + { + node: process.version, + platform: `${process.platform}-${process.arch}`, + results + }, + null, + 2 + ) + ) +} + +void main().catch((error: unknown) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/tools/benchmarks/results/phase2-git-repository-snapshot-owner-2026-07-30.md b/tools/benchmarks/results/phase2-git-repository-snapshot-owner-2026-07-30.md new file mode 100644 index 00000000000..f796da39129 --- /dev/null +++ b/tools/benchmarks/results/phase2-git-repository-snapshot-owner-2026-07-30.md @@ -0,0 +1,235 @@ +# Phase 2 Git repository snapshot owner — 2026-07-30 + +## Decision + +Retain the first live host-scoped, versioned Git repository snapshot owner. Native, exact-distro +WSL, and each `SshGitProvider` incarnation now route both status and upstream reads through this +owner while current APIs continue returning fresh post-settlement work. No TTL result reuse, Git +command, SSH RPC method, payload, renderer subscription, or new RPC was added. + +Checkpoint: `b680c59da6d28767af9ae3cd5592ca8b6038db0d`. + +## Live ownership + +`GitRepositorySnapshotOwner` now owns the existing cancellation-safe status leases, upstream +in-flight sharing, mutation generations, and bounded snapshot publication. The native/WSL process +uses one host-keyed owner; each SSH provider constructs its own owner, and the key also includes +its connection id. A replacement provider therefore begins with no retained snapshot even when it +reuses the same connection id. + +Status identities include: + +- native versus the exact WSL distro or the per-instance SSH provider; +- worktree path; +- `includeIgnored`; +- `reuseLineStats`; +- effective-upstream negative-cache bypass; +- resolved local status limit; and +- ordered shared-link paths. + +Upstream identities include the same execution host and worktree scope, configured-upstream versus +explicit-target mode, and every `GitPushTarget` field: `remoteName`, `branchName`, `remoteUrl`, and +absent versus explicit `remoteCreated: false`. + +## Snapshot projection + +Each immutable projection contains: + +- monotonic process-owner revision and generation timestamp; +- repository head and branch identity; +- immutable status entries, ignored paths, limit state, status length, line-stat state, and + retention-truncation state; +- configured or explicit-target upstream status, including patch-equivalence fields; +- conflict operation; +- worktree graph version placeholder `0`; and +- per-projection `missing`, `fresh`, `stale`, `failed`, or `placeholder` state with publication + generation, current generation, revision, and exact projection identity. + +A status result's embedded configured upstream can populate the combined snapshot without another +command. A later explicit upstream read updates only its exact target projection. For configured +upstream, the newest direct or status-embedded projection wins, so a fresh post-invalidation status +cannot be shadowed by a stale direct read. + +The existing pre/post mutation wrappers increment the owner generation and clear only joinability. +Already-issued caller promises may settle, but their captured record identity and generation must +still match before they can publish. Reads begun before or during a mutation therefore cannot +repopulate the current snapshot after either fence. + +## Retention bounds + +The owner retains at most: + +- 64 repository/worktree scopes; +- four status variants and four upstream variants per scope; +- 1,000 status entries and 1,000 ignored paths per status projection; and +- 64 Ki code units across retained status and ignored paths. + +Current status callers still receive their complete original result. If snapshot retention trims a +limit-disabled or unusually large result, `retentionTruncated` is true. Repository and projection +maps use deterministic insertion eviction; publication also requires the exact retained record +object, so a late completion from an evicted identity cannot publish into a newly created record +with the same path. + +## A/B physical counts + +Opt-in deterministic benchmarks issue ten concurrent identical calls against the mocked physical +Git and mux boundaries. + +| Boundary | A — checkpoint | B — snapshot owner | +| ---------------------------------------- | -------------: | -----------------: | +| Local status, unsignalled callers | 1 | 1 | +| Local status, distinct-signalled callers | 1 | 1 | +| Local configured-upstream Git calls | 4 | 4 | +| SSH status, unsignalled callers | 1 | 1 | +| SSH status, distinct-signalled callers | 1 | 1 | +| SSH upstream RPCs | 1 | 1 | + +The exact local status command remains: + +```text +git -c core.quotePath=false status --porcelain=v2 --branch --untracked-files=all +``` + +The configured-upstream chain remains: + +```text +git symbolic-ref --quiet --short HEAD +git rev-parse --abbrev-ref HEAD@{u} +git rev-list --left-right --count HEAD...origin/main +git log --oneline --cherry-mark --right-only HEAD...origin/main -- +``` + +SSH methods and default payloads remain exactly: + +```json +[ + { + "method": "git.status", + "payload": { "worktreePath": "/home/user/repo" } + }, + { + "method": "git.upstreamStatus", + "payload": { "worktreePath": "/home/user/repo" } + } +] +``` + +True status options and explicit targets retain their existing fields; false or absent status +options and absent `pushTarget` remain omitted. Baseline artifacts are +`/tmp/git-snapshot-status-a.json`, `/tmp/git-snapshot-upstream-a.json`, +`/tmp/git-snapshot-ssh-status-a.json`, and `/tmp/git-snapshot-ssh-upstream-a.json`; retained +artifacts use the corresponding `-b.json` names. + +## Publication hot-path retention audit + +The deterministic publication benchmark compares sequential, already-settled +`GitStatusReadLeaseOwner` reads with `GitRepositorySnapshotOwner.readStatus` reads returning the +same representative result. It therefore isolates owner admission, immutable bounded copying, +freezing, and publication without measuring Git subprocess or parsing time. The retained harness +is `tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts` and runs with: + +```text +node --expose-gc --import tsx tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts +``` + +On Node 24.18.0, Darwin arm64, Apple M4 Max, each arm received 2,000 / 400 / 50 warmup reads for +empty / 100-entry / 1,000-entry payloads. Timing then used 11 alternating rounds of 10,000 / 2,000 +/ 250 reads per arm; allocation used seven rounds of 5,000 / 500 / 100 reads per arm with V8 heap +sampling at 128-byte intervals and an explicit GC before each profile. The table is the median of +the per-process medians from four independent process runs: + +| Status entries | Lease CPU µs/read | Snapshot CPU µs/read | Incremental CPU µs/read | Incremental wall µs/read | Incremental sampled allocation/read | +| -------------: | ----------------: | -------------------: | ----------------------: | -----------------------: | ----------------------------------: | +| 0 | 0.455 | 1.198 | +0.743 | +0.683 | +1,612 bytes | +| 100 | 0.473 | 4.027 | +3.554 | +3.501 | +15,971 bytes | +| 1,000 | 0.510 | 29.464 | +28.954 | +28.485 | +151,086 bytes | + +Incremental CPU ranges across the four processes were 0.710–0.804, 3.505–3.586, and +28.116–29.652 µs/read respectively. V8 sampling measures transient allocation rather than retained +heap; the exact immutable work is zero / 100 / 1,000 entry copies and six / 106 / 1,006 +`Object.freeze` calls. The owner replaces the prior retained projection after each settled read, +so these temporary copies do not escape the existing one-projection-per-identity bound. + +Every size returned the exact original `GitStatusResult` reference. JSON content and caller-owned +freeze state remained unchanged, while the snapshot, its status collection, and every retained +entry copy were separately frozen. This directly verifies that publication does not mutate or +substitute the current caller result. + +No implementation optimization was retained. The 0.7 µs empty-result delta is too small to +justify conditional empty-array paths, and copying plus deep freezing are the mechanisms that keep +caller mutation out of retained snapshots. Deferring those operations until `getSnapshot` would +weaken the owner's uniformly immutable publication contract, while lazily loading query/freshness +code would make the synchronous exported live seam unavailable. The 10,250 raw / 2,043 gzip main +cost is fully main-owned; the former specialized upstream owner is removed, and preload plus +renderer remain byte-identical. At 29 µs for the 1,000-entry bound, publication is proportionate to +the process/RPC Git work and parsing it follows, without introducing a TTL cache or another +physical read. + +## Production A/B + +Fresh production builds were archived outside the worktree: + +- A: `/tmp/orca-phase2-git-repository-snapshot-a.yuLlk5/out` +- B: `/tmp/orca-phase2-git-repository-snapshot-b-final.75XkBW/out` + +| Artifact | A raw / gzip | B raw / gzip | Change | A SHA-256 | B SHA-256 | +| ---------------------- | ----------------- | ----------------- | ---------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| Electron main entry | 779,267 / 174,769 | 789,517 / 176,812 | +10,250 / +2,043 | `64526204d377f8ca634de5940d77b3f2eff1421bd7a8e7346b0ffcc189be21da` | `6413904f6420882168b8993c1240614bacde1de92dfb26a55c4db67c84007b8e` | +| Electron preload entry | 130,798 / 20,642 | 130,798 / 20,642 | 0 / 0 | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` | + +The unchanged Electron-main raw budget is 825,109 bytes; B retains 35,592 bytes of headroom. File +counts remain 184 main, one preload, and 787 renderer artifacts. + +| Tree | A SHA-256 | B SHA-256 | +| -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| Main | `7f6b86e1b736f43d8d259b4ff90f53dd78b0de7ab516dd0f54edf931034bfe2e` | `619e5977034ec79f679489666a826205500de88f51af39c3a3ccf99fbb68874c` | +| Preload | `06584e5cc8835554c7e0897efe5c578a9f66c3049033b2a3b79b1ca9a88462be` | `06584e5cc8835554c7e0897efe5c578a9f66c3049033b2a3b79b1ca9a88462be` | +| Renderer | `5f6156236d93a03256beb23df69ef705a78dd54a1290c58cc4330fbe8cc34e74` | `5f6156236d93a03256beb23df69ef705a78dd54a1290c58cc4330fbe8cc34e74` | + +Preload and every renderer artifact are byte-identical. The renderer manifest is byte-identical +with SHA-256 `92ad617f8c8aee6d4c4a23622f524266cbe9c93b3fe65544d1acb833dc3f0d67`. +Complete A and B validation found 778 manifest records, three HTML entries, 6,247 static edges, 213 +dynamic edges, and 860 emitted references, with zero missing or escaping targets, zero cross-entry +imports, and zero cycles. + +Renderer entry measurements remain 8,416,540 raw / 1,877,823 gzip for index, 4,507,253 / +984,615 for popout, and 4,360,652 / 928,355 for web. JS/CSS counts remain 292/2, 77/2, and 33/1. + +## Validation + +- Current-behavior baseline suite: 211 passed. +- Final focused owner/status/upstream/provider suite: 228 passed. +- Physical-count candidate suite: 215 passed. +- Publication benchmark: four independent process runs; caller identity/immutability checks passed + for empty, 100-entry, and 1,000-entry results. +- Post-audit focused lease/snapshot-owner suite: 14 passed. +- Broad owner, status/cache, native/WSL, upstream, remote, SSH provider/dispatch/session, + filesystem mutation, and runtime-routing suite: 536 passed, 1 skipped. +- `pnpm run typecheck:node`: passed. +- Targeted `oxlint --deny-warnings`: passed. +- Targeted `oxfmt --check`: passed. +- `pnpm run check:max-lines-ratchet`: passed with 354 grandfathered suppressions and no new bypass. +- Fresh A and B `pnpm run build:electron-vite`: passed. +- Fresh post-audit production build reproduced B exactly: main 789,517 raw / 176,812 gzip with + SHA-256 `6413904f6420882168b8993c1240614bacde1de92dfb26a55c4db67c84007b8e`, preload + 130,798 / 20,642 with SHA-256 + `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f`, and renderer + manifest SHA-256 `92ad617f8c8aee6d4c4a23622f524266cbe9c93b3fe65544d1acb833dc3f0d67`. +- `pnpm run check:electron-bundle-budgets`: passed against fresh B. +- Complete renderer manifest/path and entry-cycle validation: passed for A and B. +- `git diff --check`: passed. + +## Next seam and limitations + +The next slice can migrate Checks and active polling to the exported snapshot seam, then add +revision subscriptions and runtime/mobile projection transport. This tranche intentionally leaves +those consumers and all RPC surfaces unchanged. + +- Revisions and generations are process-local and reset with the host owner. +- Local mutation invalidation remains intentionally coarse across the native/WSL owner; it may + mark unrelated retained projections stale, but it cannot merge identities or return cached data + through current APIs. +- Snapshot status retention may be a bounded prefix when the current caller explicitly disables + the Git status limit; current callers still receive the complete fresh result. +- Counts use mocked physical boundaries and do not measure live subprocess, WSL, or SSH latency. +- No packaged cross-platform or live SSH smoke was run.