From 1397ba5d06a207615d9be1389783b1f7d2e02731 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 5 Sep 2026 02:12:27 -0700 Subject: [PATCH 1/3] docs(worktrees): plan no-op reconciliation optimization Refs #806 --- ...-05-worktree-reconciliation-performance.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md diff --git a/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md new file mode 100644 index 00000000..0f3e727b --- /dev/null +++ b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md @@ -0,0 +1,42 @@ +# Worktree reconciliation performance + +Status: implementation in progress. Issue: #806. Base: origin/main 5d641845. + +## Scope and invariants + +Preserve canonical projection identity on no-op and avoid replaying retained +evidence when its inputs are unchanged. Do not alter lifecycle/readiness, +provider evidence precedence, or stale-hydration contribution reversal. +Keep catalog-ready notification even for unchanged catalogs: initial history +can replace a projection between refreshes, and that external replacement must +still trigger correction. Cache content identity, not freshness timestamps. + +## Implementation sequence + +1. Add reference/no-replay regression tests and a sanitized-fixture benchmark; + run against the unchanged implementation to record failures/baseline. +2. Make canonicalization reference-stable and cache replay by cwd, baseline, + retained-evidence generation and catalog content identity. Keep the existing + bounded replay algorithm for invalidations rather than change attribution. +3. Cover irrelevant/empty batches, identical successful refresh, changed + catalogs, hydration replacement, evidence eviction and session teardown. +4. Run focused tests, typing/lint checks and before/after benchmark. Review the + diff, update issue evidence, open a complete PR and obtain current CI. + Do not merge without explicit user confirmation. + +## Later independent increments + +- #763: per-session React subscriptions and stable action/context boundaries. +- #762: renderer screen interest with fresh view-switch snapshots; backend + parsing and lifecycle/readiness remain ungated. +- #767: only remaining verified diagnostic costs. Disabled memory sampling + already exits early. Main worktree-index no-op persistence is functional + metadata, not something to disable as a diagnostic. +- A6 owns the separate #802–805 subagent/remote findings; do not overlap. + +## Verification evidence + +Prior isolated audit: 1000/1000 value-identical canonical projections changed +identity; retained 500-record replay cost about 2 ms per unrelated batch. These +are not app-wide typing latency claims. Record reproducible measurements below +before proposing completion. From cccae6e1a6543b1d26cb59947e938c1d678a793b Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 5 Sep 2026 02:16:05 -0700 Subject: [PATCH 2/3] perf(worktrees): reuse unchanged projections without replaying evidence Keep catalog freshness separate from content identity and invalidate replay on baseline, cwd and bounded evidence generation changes. Preserve catalog-ready callbacks so late history hydration still receives attribution correction. Fixes #806 --- ...-05-worktree-reconciliation-performance.md | 24 +++- scripts/benchmark-worktree-reconciliation.mts | 39 +++++ ...LiveWorktreeReconciler.performance.test.ts | 134 ++++++++++++++++++ .../work-context/LiveWorktreeReconciler.ts | 77 ++++++++-- src/shared/work-context/tracker.ts | 21 ++- 5 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 scripts/benchmark-worktree-reconciliation.mts create mode 100644 src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts diff --git a/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md index 0f3e727b..9e91b572 100644 --- a/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md +++ b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md @@ -1,6 +1,6 @@ # Worktree reconciliation performance -Status: implementation in progress. Issue: #806. Base: origin/main 5d641845. +Status: implemented; PR/CI review pending. Issue: #806. Base: origin/main 5d641845. ## Scope and invariants @@ -40,3 +40,25 @@ Prior isolated audit: 1000/1000 value-identical canonical projections changed identity; retained 500-record replay cost about 2 ms per unrelated batch. These are not app-wide typing latency claims. Record reproducible measurements below before proposing completion. + +The standalone `scripts/benchmark-worktree-reconciliation.mts` measured 200 +irrelevant batches per window size on this Mac (same process setup per run): + +| Retained records | Before median / p95 ms | After median / p95 ms | Identity changes before → after | +| --- | --- | --- | --- | +| 0 | 0.001959 / 0.008416 | 0.000667 / 0.002292 | 200 → 0 | +| 100 | 0.312375 / 0.442167 | 0.000458 / 0.001375 | 200 → 0 | +| 500 | 2.118166 / 2.476792 | 0.000333 / 0.000375 | 200 → 0 | + +The first regression run on unchanged production code failed four of five +new tests at identity assertions; the stale-hydration control passed. With the +fix, the focused shared/renderer fixture suite passed 20 tests before adding +explicit cwd-change and empty-catalog invalidation controls. No wall-clock +threshold is enforced in tests; a provider-record getter proves no replay. + +Final local verification: full typecheck, test contract and five-fixture privacy +verification passed; 24 focused unit tests and two worktree-bar renderer tests +passed. Full unit suite: 2138 passed, one failed because the existing image +corpus check references a removed private session. The identical failure was +reproduced on unchanged main (tracked by #684/#669/#641); no test was skipped or +weakened. All seven new regression tests pass. Public CI remains the gate. diff --git a/scripts/benchmark-worktree-reconciliation.mts b/scripts/benchmark-worktree-reconciliation.mts new file mode 100644 index 00000000..b70ae34d --- /dev/null +++ b/scripts/benchmark-worktree-reconciliation.mts @@ -0,0 +1,39 @@ +// Run from the repository root: +// TSX_TSCONFIG_PATH=tsconfig.web.json node --import tsx scripts/benchmark-worktree-reconciliation.mts +// WHY standalone, not a timing assertion in Vitest: CI host load changes the +// timings; the regression suite asserts identities and actual retained-record +// reads deterministically. This reports isolated costs, not typing latency. +import { readFileSync } from 'node:fs' +import { LiveWorktreeReconciler } from '../src/renderer/src/workspace/work-context/LiveWorktreeReconciler.js' + +const fixtureRoot = 'testing/fixtures/worktree-live-attribution/' +const fixture = JSON.parse(readFileSync(`${fixtureRoot}codex-0151-worktree-window.json`, 'utf8')) +const catalog = JSON.parse(readFileSync(`${fixtureRoot}git-worktree-identities.json`, 'utf8')).worktrees +const original = fixture.records.find((record: { payload?: { item?: { type?: string } } }) => + record.payload?.item?.type === 'CommandExecution') + +for (const retained of [0, 100, 500]) { + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees: catalog }), + onCatalogReady: () => undefined, + }) + await reconciler.refresh(fixture.git.main.path) + let projection = reconciler.observe('bench', fixture.git.main.path, + Array.from({ length: retained }, (_, index) => ({ entry: { + ...original, timestamp: new Date(1700000000000 + index * 1000).toISOString(), + } })), { workActivity: null, workContext: null }) + const samples: number[] = [] + let identityChanges = 0 + for (let index = 0; index < 200; index += 1) { + const started = performance.now() + const next = reconciler.observe('bench', fixture.git.main.path, + [{ entry: { type: 'irrelevant' } }], projection) + samples.push(performance.now() - started) + if (next.workActivity !== projection.workActivity || next.workContext !== projection.workContext) identityChanges += 1 + projection = next + } + samples.sort((a, b) => a - b) + console.log(JSON.stringify({ retained, batches: samples.length, identityChanges, + medianMs: samples[100], p95Ms: samples[190] })) + reconciler.dispose() +} diff --git a/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts b/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts new file mode 100644 index 00000000..f0aff47c --- /dev/null +++ b/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from 'vitest' +import { canonicalizeWorktreeActivity, ingestWorktreeRawEvent } from '@shared/work-context/tracker' +import { LiveWorktreeReconciler, type WorktreeRuntimeProjection } from './LiveWorktreeReconciler' + +const main = { path: '/repo', branch: 'main', head: null, detached: false } +const linked = { path: '/repo/linked', branch: 'feature', head: null, detached: false } +const write = { + type: 'assistant', timestamp: '2026-09-01T00:00:00Z', cwd: '/repo', + message: { content: [{ type: 'tool_use', name: 'Write', input: { file_path: '/repo/linked/file.ts' } }] }, +} +const empty = (): WorktreeRuntimeProjection => ({ workActivity: null, workContext: null }) + +describe('worktree reconciliation invalidation boundaries', () => { + it('keeps canonical state and context identities unless Git changes their values', () => { + const state = ingestWorktreeRawEvent({ state: null, raw: write, sessionCwd: '/repo', worktrees: [main, linked] }) + const canonical = canonicalizeWorktreeActivity(state, [main, linked]) + expect(canonicalizeWorktreeActivity(canonical, [{ ...main }, { ...linked }])).toBe(canonical) + const detached = canonicalizeWorktreeActivity(canonical, [main, { ...linked, branch: null, detached: true }]) + expect(detached).not.toBe(canonical) + expect(detached.primary?.branch).toBeNull() + expect(detached.timeline).toBe(canonical.timeline) + }) + + it('keeps quiet-session projection identity across identical successful refreshes', async () => { + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees: [{ ...main }] }), + onCatalogReady: () => undefined, cacheTtlMs: 0, + }) + await reconciler.refresh('/repo') + const projection = reconciler.project({ sessionId: 'quiet', cwd: '/repo', projection: empty() }) + await reconciler.refresh('/repo') + expect(reconciler.project({ sessionId: 'quiet', cwd: '/repo', projection })).toBe(projection) + }) + + it('does not re-read retained records for empty/irrelevant batches or identical catalogs', async () => { + // A getter counts actual provider-record reads without spying on private + // cache fields. A value-equality assertion alone would miss wasteful replay + // that eventually compares equal; wall-clock assertions would be flaky. + const readMessage = vi.fn(() => write.message) + const raw = { ...write, get message() { return readMessage() } } + const onCatalogReady = vi.fn() + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees: [{ ...main }, { ...linked }] }), + onCatalogReady, cacheTtlMs: 0, + }) + await reconciler.refresh('/repo') + const projection = reconciler.observe('busy', '/repo', [{ entry: raw }], empty()) + expect(readMessage).toHaveBeenCalled() + readMessage.mockClear() + expect(reconciler.observe('busy', '/repo', [], projection)).toBe(projection) + expect(reconciler.observe('busy', '/repo', [{ entry: { type: 'irrelevant' } }], projection)).toBe(projection) + await reconciler.refresh('/repo') + expect(reconciler.project({ sessionId: 'busy', cwd: '/repo', projection })).toBe(projection) + expect(readMessage).not.toHaveBeenCalled() + // Must still notify: history may have replaced the caller's projection. + expect(onCatalogReady).toHaveBeenCalledTimes(2) + }) + + it('corrects external stale hydration even when the catalog did not change', async () => { + let projection = empty() + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees: [{ ...main }, { ...linked }] }), + cacheTtlMs: 0, + onCatalogReady: cwd => { projection = reconciler.project({ sessionId: 'race', cwd, projection }) }, + }) + await reconciler.refresh('/repo') + projection = reconciler.observe('race', '/repo', [{ entry: write }], projection) + const expectedTouches = projection.workActivity?.touched + const stale = ingestWorktreeRawEvent({ state: null, raw: write, sessionCwd: '/repo', worktrees: [main] }) + projection = { workActivity: stale, workContext: stale.primary } + await reconciler.refresh('/repo') + expect(projection.workContext?.worktreePath).toBe('/repo/linked') + expect(projection.workActivity?.touched['/repo/linked'].score).toBe(expectedTouches?.['/repo/linked'].score) + expect(projection.workActivity?.touched['/repo'].writeCount ?? 0).toBe(0) + }) + + it('invalidates on cwd change even when both directories share the same catalog', async () => { + const worktrees = [main, linked] + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees }), + onCatalogReady: () => undefined, + }) + await reconciler.refresh('/repo') + await reconciler.refresh('/repo/linked') + const first = reconciler.observe('moved', '/repo', [], empty()) + expect(first.workContext?.worktreePath).toBe('/repo') + const moved = reconciler.observe('moved', '/repo/linked', [], first) + expect(moved.workContext?.worktreePath).toBe('/repo/linked') + expect(reconciler.observe('moved', '/repo/linked', [], moved)).toBe(moved) + }) + + it('distinguishes a successfully loaded empty catalog from a pending catalog', async () => { + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees: [] }), + onCatalogReady: () => undefined, + }) + let projection = reconciler.observe('empty-catalog', '/repo', [], empty()) + expect(projection.workActivity).toBeNull() + await reconciler.refresh('/repo') + projection = reconciler.project({ sessionId: 'empty-catalog', cwd: '/repo', projection }) + expect(projection.workActivity).not.toBeNull() + expect(reconciler.observe('empty-catalog', '/repo', [], projection)).toBe(projection) + }) + + it('invalidates replay on catalog expansion/removal, branch changes and evidence eviction', async () => { + let worktrees = [main] + const reconciler = new LiveWorktreeReconciler({ + loadWorktrees: async () => ({ ok: true, worktrees }), + onCatalogReady: () => undefined, cacheTtlMs: 0, recentRawLimit: 1, + }) + await reconciler.refresh('/repo') + let projection = reconciler.observe('change', '/repo', [{ entry: write }], empty()) + expect(projection.workContext?.worktreePath).toBe('/repo') + worktrees = [main, linked] + await reconciler.refresh('/repo') + projection = reconciler.project({ sessionId: 'change', cwd: '/repo', projection }) + expect(projection.workContext?.worktreePath).toBe('/repo/linked') + worktrees = [main, { ...linked, branch: 'renamed' }] + await reconciler.refresh('/repo') + projection = reconciler.project({ sessionId: 'change', cwd: '/repo', projection }) + expect(projection.workContext?.branch).toBe('renamed') + worktrees = [main] + await reconciler.refresh('/repo') + projection = reconciler.project({ sessionId: 'change', cwd: '/repo', projection }) + expect(projection.workContext?.worktreePath).toBe('/repo') + const another = { ...write, timestamp: '2026-09-01T00:01:00Z' } + projection = reconciler.observe('change', '/repo', [{ entry: another }], projection) + expect(projection.workActivity?.touched['/repo'].writeCount).toBe(2) + expect(reconciler.observe('change', '/repo', [], projection)).toBe(projection) + reconciler.forgetSession('change') + const replacement = reconciler.observe('change', '/repo', [], empty()) + expect(replacement.workActivity?.touched['/repo']?.writeCount ?? 0).toBe(0) + }) +}) diff --git a/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.ts b/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.ts index a6b543e3..4665c1aa 100644 --- a/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.ts +++ b/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.ts @@ -37,6 +37,20 @@ type CacheEntry = { type RefreshOutcome = 'cached' | 'ready' | 'failed' | 'disposed' +type SessionEvidence = { + baseline: WorktreeRuntimeProjection + recentRaw: unknown[] + revision: number + lastEmitted: WorktreeRuntimeProjection + replay?: { + cwd: string + baseline: WorktreeRuntimeProjection + revision: number + catalog: WorktreeIdentity[] | undefined + projection: WorktreeRuntimeProjection + } +} + type Options = { loadWorktrees(cwd: string): Promise onCatalogReady(cwd: string): void @@ -59,11 +73,7 @@ const DEFAULT_RECENT_RAW_LIMIT = 500 */ export class LiveWorktreeReconciler { private readonly cache = new Map() - private readonly evidenceBySession = new Map() + private readonly evidenceBySession = new Map() private readonly loadWorktrees: Options['loadWorktrees'] private readonly onCatalogReady: Options['onCatalogReady'] private readonly now: () => number @@ -102,6 +112,7 @@ export class LiveWorktreeReconciler { evidence.recentRaw, ), recentRaw: evidence.recentRaw, + revision: evidence.revision, lastEmitted: projection, } } @@ -115,6 +126,7 @@ export class LiveWorktreeReconciler { // already-lossy projection. baseline: projection, recentRaw: [], + revision: 0, lastEmitted: projection, } @@ -127,6 +139,10 @@ export class LiveWorktreeReconciler { .map(({ entry }) => entry) .filter(entry => extractWorktreeActivityEvents(entry, this.now()).length > 0) evidence.recentRaw.push(...relevantRaw) + // Length is not a generation: after eviction this window stays at 500 + // while its contents keep changing. Irrelevant transport batches do not + // advance it and must not re-extract/re-fold the retained provider records. + if (relevantRaw.length > 0) evidence.revision += 1 if (evidence.recentRaw.length > this.recentRawLimit) { const evicted = evidence.recentRaw.splice( 0, @@ -160,15 +176,24 @@ export class LiveWorktreeReconciler { .then(result => { if (this.disposed) return 'disposed' as const if (!result.ok) return 'failed' as const + const previous = this.cache.get(cwd) this.cache.set(cwd, { - worktrees: result.worktrees, + // Freshness and content are separate. Git IPC returns new arrays + // even on a cache hit. Keep content identity when every field and + // ordering agree (the first checkout is the authoritative repo root). + worktrees: previous && sameCatalog(previous.worktrees, result.worktrees) + ? previous.worktrees + : result.worktrees, refreshedAt: this.now(), inflight: null, }) // WHY notify only after the catalog is committed: the consumer can now // replay every record that arrived while IPC was pending against one // stable Git snapshot. Calling before set would recreate the original - // race with an empty catalog under a more testable class name. + // race with an empty catalog under a more testable class name. Notify + // even for unchanged contents: an independent history load may have + // replaced a caller's projection and still needs retained-evidence + // correction. project() itself skips replay when its inputs agree. this.onCatalogReady(cwd) return 'ready' as const }) @@ -218,6 +243,7 @@ export class LiveWorktreeReconciler { evidence.recentRaw, ), recentRaw: evidence.recentRaw, + revision: evidence.revision, lastEmitted: params.projection, } this.evidenceBySession.set(params.sessionId, evidence) @@ -229,15 +255,24 @@ export class LiveWorktreeReconciler { private rebuild( cwd: string, - evidence: { - baseline: WorktreeRuntimeProjection - recentRaw: unknown[] - }, + evidence: SessionEvidence, ): WorktreeRuntimeProjection { - const projection = this.foldRaw(cwd, evidence.baseline, evidence.recentRaw) const cached = this.cache.get(cwd) - if (!cached || cached.refreshedAt <= 0) return projection - return this.canonicalProjection(cwd, projection, cached.worktrees) + const catalog = cached && cached.refreshedAt > 0 ? cached.worktrees : undefined + const replay = evidence.replay + // This is an input cache, not an output deep comparison. Replaying 500 + // records to discover an identical result still blocks input on the renderer + // thread, and ingestion timestamps can make the output look different. + // External hydration resets this cache when adopting its baseline above; + // catalog changes still take the original full correction/reversal path. + if (replay && replay.cwd === cwd && replay.baseline === evidence.baseline && + replay.revision === evidence.revision && replay.catalog === catalog) { + return replay.projection + } + const folded = this.foldRaw(cwd, evidence.baseline, evidence.recentRaw) + const projection = catalog ? this.canonicalProjection(cwd, folded, catalog) : folded + evidence.replay = { cwd, baseline: evidence.baseline, revision: evidence.revision, catalog, projection } + return projection } private releaseRetainedEvidenceKeys( @@ -370,9 +405,13 @@ export class LiveWorktreeReconciler { }), worktrees, ) + const workContext = deriveAgentWorkContext(workActivity) + if (workActivity === projection.workActivity && workContext === projection.workContext) { + return projection + } return { workActivity, - workContext: deriveAgentWorkContext(workActivity), + workContext, } } @@ -414,6 +453,14 @@ export class LiveWorktreeReconciler { } } +function sameCatalog(left: WorktreeIdentity[], right: WorktreeIdentity[]): boolean { + return left.length === right.length && left.every((worktree, index) => { + const other = right[index]! + return worktree.path === other.path && worktree.branch === other.branch && + worktree.head === other.head && worktree.detached === other.detached + }) +} + function sameProjection( left: WorktreeRuntimeProjection, right: WorktreeRuntimeProjection, diff --git a/src/shared/work-context/tracker.ts b/src/shared/work-context/tracker.ts index c44fa0d4..47aa8ab6 100644 --- a/src/shared/work-context/tracker.ts +++ b/src/shared/work-context/tracker.ts @@ -215,11 +215,14 @@ export function canonicalizeWorktreeActivity( state: WorktreeActivityState, worktrees: WorktreeIdentity[], ): WorktreeActivityState { - return { - ...state, - active: canonicalizeContext(state.active, worktrees), - primary: canonicalizeContext(state.primary, worktrees), - } + const active = canonicalizeContext(state.active, worktrees) + const primary = canonicalizeContext(state.primary, worktrees) + // Catalog refresh is not activity. The live renderer uses these references + // to decide whether a session needs a state update; cloning on every Git + // cache hit made even quiet sibling sessions rerender and append diagnostics. + return active === state.active && primary === state.primary + ? state + : { ...state, active, primary } } export function deriveAgentWorkContext( @@ -248,6 +251,12 @@ function canonicalizeContext( if (!context?.worktreePath) return context const matched = matchWorktree(context.worktreePath, worktrees) if (!matched) return context + const repoRoot = worktrees[0]?.path ?? context.repoRoot + if ( + context.worktreePath === matched.path && + context.branch === matched.branch && + context.repoRoot === repoRoot + ) return context return { ...context, worktreePath: matched.path, @@ -255,6 +264,6 @@ function canonicalizeContext( // provider's earlier branch string here would undo contextFromPath's Git // authority whenever an async cache refresh canonicalizes existing state. branch: matched.branch, - repoRoot: worktrees[0]?.path ?? context.repoRoot, + repoRoot, } } From 29e2dc8109dae6bb96d53f1b1f3ca4edfcfeb819 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 5 Sep 2026 17:38:48 -0700 Subject: [PATCH 3/3] test(worktrees): retitle replay-cache coverage as invalidation tests LiveWorktreeReconciler.performance.test.ts asserts identity and invalidation contracts, never timing; the .performance suffix implied a benchmark tier the repo does not define (unit/renderer/system key off their own suffixes). An .invalidation.test.ts name documents what the file actually protects. No config or CI references the previous name. Also records the independent-review boundary notes in the plan: sameCatalog compares head, so any checkout change intentionally invalidates catalog identity and replays the bounded window; and the pre-existing pre-catalog evidence-eviction loss is tracked as a separate issue rather than fixed in this PR. Refs #806 --- ...-09-05-worktree-reconciliation-performance.md | 16 ++++++++++++++++ ... LiveWorktreeReconciler.invalidation.test.ts} | 0 2 files changed, 16 insertions(+) rename src/renderer/src/workspace/work-context/{LiveWorktreeReconciler.performance.test.ts => LiveWorktreeReconciler.invalidation.test.ts} (100%) diff --git a/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md index 9e91b572..a1fa7035 100644 --- a/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md +++ b/docs/superpowers/plans/2026-09-05-worktree-reconciliation-performance.md @@ -62,3 +62,19 @@ passed. Full unit suite: 2138 passed, one failed because the existing image corpus check references a removed private session. The identical failure was reproduced on unchanged main (tracked by #684/#669/#641); no test was skipped or weakened. All seven new regression tests pass. Public CI remains the gate. + +## Independent review resolution (cc cccae6e1) + +Both orchestrated reviewers approving, no correctness defects in the invalidation +keys, identity retention, or accounting-reversal path. Adopted: renamed +`LiveWorktreeReconciler.performance.test.ts` to +`LiveWorktreeReconciler.invalidation.test.ts` — it asserts identity/invalidation +contracts, never timing, and "performance" implied a benchmark tier this repo +does not have. Recorded as boundaries rather than defects: `sameCatalog` +compares `head`, so any commit in any checkout invalidates catalog identity and +replays the bounded evidence window for that cwd at the next refresh (correct, +but worth knowing); the replay cache freezes fold-time timestamps for quiet +sessions, which no renderer reads. A pre-existing, unrelated evidence-loss edge — +>500 relevant records buffered before the first successful Git IPC reply get +folded against an empty catalog and dropped — was filed separately, not fixed +in this PR. diff --git a/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts b/src/renderer/src/workspace/work-context/LiveWorktreeReconciler.invalidation.test.ts similarity index 100% rename from src/renderer/src/workspace/work-context/LiveWorktreeReconciler.performance.test.ts rename to src/renderer/src/workspace/work-context/LiveWorktreeReconciler.invalidation.test.ts