Proof audit, pass 1: [#32359](https://git.cafe/pingdotgg/t3code/pull/32359), branch `fix/issue-32347`, head [bff5e1c](https://github.com/pingdotgg/t3code/commit/bff5e1cba9b94ab1a95dde3f8a211fb941547e31).

Findings: four proof rows remain missing: **F06 failed-refresh fallback and recovery; F13 remembered-map capacity eviction; F15 WebSocket freshness markers; F17 MCP list/remote-match input when a remembered identity bypasses blocking resolution.** These are evidence gaps, not confirmed implementation defects. The current tests do not exercise warmed-then-failed refreshes or overflow the identity map. The HTTP transcript prints only canonical keys, so it cannot establish WebSocket freshness markers or MCP list behavior.

The supplied runtime evidence is acceptable for the paths it exercises. Its main transcript shows the two one-minute null regressions; the fixed transcript stays non-null across more than two one-minute TTLs; the reverse transcript remains null for subsequent reads after remote removal. The fixed recording ran [143af9f](https://github.com/pingdotgg/t3code/commit/143af9fa410f71d3b525276d52fd69b9586b942d). Comparing that revision's changed service with the reviewed head confirms the later service changes are the per-root in-flight invalidation guard, not a change to ordinary shell polling. The two added guard tests supply that later proof. No repeat of the existing happy-path or remote-removal recording is required solely because its commit is older.

Read-only scope: inspected the authoritative PR description, comments/reviews, head status, complete two-file change against the PR's recorded main base [7389423](https://github.com/pingdotgg/t3code/commit/7389423507aff8f931fdd41fd0b752589d2d810e), the service's seven tests, resolver behavior, and callers. Local `main` is a different revision; its unrelated changes were excluded. Existing test evidence is the PR's reported seven passing tests plus the reviewed assertions; tests were not rerun and no new runtime captures were made. This is a proof audit, not a CI or merge verdict.

`I1` and `I2` mean complete synthetic `RepositoryIdentity` values with different canonical keys/locators; `A`, `B`, and `C` are distinct synthetic workspace roots. `R` means `repositoryIdentityResolved`. A concrete identity is opaque to this change: optional display/provider/owner/name/origin fields and remote URL spelling do not select different cache branches. No-remote, non-repository, missing-directory, and git-probe errors that the resolver converts to null share the successful-null fixture; they are not failed enrichment lookups. A genuine failed lookup here is a defect/interruption captured by `Effect.exit`.

Every time- or ordering-dependent row is labelled **recording**. For runtime rows this means a timestamped server/API transcript, never a still. For deterministically controlled internal races/defects, the row explicitly substitutes the focused unit test. “Shared proof” means the unchanged caller maps the same already-tested service output; it is not a claim that a separate caller transcript exists.

| ID / named fixture and data | Observable expectation and callers | Diff affected? | Minimum proof / real-server seed | Existing evidence and status |
| --- | --- | --- | --- | --- |
| F01 Cold / first lookup pending: root A, no remembered value | `peek` and first `getAvailable` return null, R=false; a pending first lookup remains unresolved. HTTP shell, active hydrate, WS; deleted hydrate uses peek without enqueue. | No change to output; new map starts empty. | **recording; unit substitute** for a precisely held first lookup. Real server: create one synthetic git project through the project API in fresh isolated state and read immediately; ordinary git timing cannot guarantee catching pending work. | **COVERED — unit.** Existing “reports successful cached null…” test asserts cold/pending null and false. Main transcript also has a cold null; fixed cold is established by unit proof, not inferred from its first warm line. |
| F02 Fresh positive: root A resolves I1 | Available readers return I1, R=true. Normal shell/hydrate/list data remains complete; direct read returns I1. | Remembering is new; fresh output unchanged. | Static API response suffices. Synthetic repo with origin `https://github.com/acme/widgets.git`; create and read until the resolution event completes. Reuse F03 recording. | **COVERED — transcript + unit.** Fixed 06:25:48–06:28:09 keys are I1; “publishes repository completion…” asserts positive resolution and true. |
| F03 Expired positive: A has remembered I1, fresh cache older than 60 s | `peek`/`getAvailable` retain I1 instead of null; request schedules refresh; R remains false until a fresh success. Applies to HTTP shell, hydrate, and ProjectService shell readers. | Yes, central regression. | **recording.** Warm synthetic repo, leave remote unchanged, read HTTP shell across at least two 60 s boundaries. Identity portion shares minimum capture with F02/F04; marker proof is F15. | **COVERED — transcript + unit for identity.** Main nulls at 06:15:58/06:16:58; fixed keys stable through 06:28:09. New expiry test advances 61 s and asserts retained I1. R is separately audited in F15. |
| F04 Refresh pending with remembered I1 | `peek` returns I1 while the refresh is blocked, without waiting for it. | Yes. | **recording; unit substitute** for a precisely held refresh. No production API exposes a resolver gate. A synthetic git-delay wrapper after inner-cache expiry could approximate it; the existing Deferred-controlled resolver is the minimum reliable proof. | **COVERED — unit.** “keeps the last resolved repository identity after it expires and while it refreshes” awaits refresh-start and peeks before releasing it. HTTP ten-second samples alone do not prove the held interval. |
| F05 Failed first lookup / no remembered identity | Available readers return null, R=false. Failure notification is unresolved; direct read yields null. | No observable change when no remembered value exists. | **recording; unit-only fault seed.** Stub resolver to die. Ordinary missing remotes and failing git commands resolve successfully to null, so they do not seed this branch through the stock API. | **COVERED — unit for available output.** Existing “reports successful cached null…” test exercises a defect on `/fails` and asserts null/false. Direct-read implementation is unchanged. |
| F06 Failed refresh after I1, failure cache, then recovery to I2 | Available readers retain I1 with R=false after a failed refresh and through the 5 s failure TTL; failure does not clear memory. Retry success replaces it with I2, including after the next positive TTL. Failure notifications stay unresolved; direct read still returns null on failure. | Yes, advertised failed-refresh fallback. | **recording; unit-only fault seed.** Controlled resolver succeeds with I1, later dies, then returns I2; use TestClock and Deferred/event milestones. There is no supported stock API fault hook for this precise failure. | **MISSING.** Transcript has no failed refresh. Existing `/fails` test has never remembered I1; the new expiry test never fails. Need a focused warmed-then-failed/recovery assertion, not a fake “git command failed” capture. |
| F07 Successful null with no remembered identity: non-repo/no remote | Return null with R=true while fresh; this differs from cold/failure even though the identity field is also null. | Same output; exercises new null-delete branch harmlessly. | Static response / focused unit proof. Real seed: synthetic directory without a remote; create project and await resolution. Marker transport proof is F15. | **COVERED — unit.** Existing null test checks both notification and available null with true. No distinct recording needed for each resolver reason that yields null. |
| F08 Reverse state: remembered I1, origin removed, resolver eventually returns null | Until the resolver learns removal, cached I1 may remain; successful null clears remembered I1. Later outer-cache expiry must not resurrect it. | Yes, null must clear the new map. | **recording.** Remove synthetic origin, allow the resolver's existing 15 min positive cache to expire, then continue API reads beyond another 60 s TTL. | **COVERED — transcript.** Removal 06:29:57; null at 06:42:39, 06:43:39, 06:44:39 proves persistent clearing across subsequent reads/expiry. Time is measured from prior resolver fill, not 15 min from the removal command. |
| F09 Successful replacement: I1 then complete I2 for A | Newly resolved I2 replaces remembered I1, and expired peek later returns I2. Same remembering helper runs for worker refreshes and blocking reads. | Yes. | **recording; shared unit proof.** Real seed: change synthetic origin and wait out inner TTL; minimum controlled proof uses the existing versioned resolver tests. F17 can also capture replacement externally. | **COVERED — unit.** Same-root guard test resolves v2, releases old v1, advances 61 s, and asserts v2 fallback. Reload test also observes v2. No separate per-field/remote-provider matrix is needed because the object is stored and returned intact. |
| F10 Explicit warm invalidation: I1 remembered at A | Invalidate clears both caches and remembered I1; immediate available read is null until reload, then I2. Create, delete, root update (old and new roots), and clone completion call this path. | Yes. | **recording; existing unit is minimum proof.** Real API seed: warm synthetic project, delete it and read included-deleted metadata, or change workspace root; clone completion can force refresh. No production API exposes invalidate alone. | **COVERED — unit.** Existing “deduplicates requests…” test warms `/first`, invalidates, asserts immediate null, then observes v2. Its cache capacity 8 prevents this assertion from merely passing due to earlier eviction. |
| F11 Invalidate A during its old lookup, then newer I2 completes before old I1 | Late I1 cannot replace remembered I2. The original caller may finish its old read; the regression proof is the later expired peek. | Yes, later in-flight guard. | **recording; unit-only deterministic interleaving.** No stock API barrier coordinates resolver start/completion and invalidation. Gate the old resolver with Deferred, invalidate A, resolve I2, release I1, then advance clock. | **COVERED — unit at reviewed service revision.** “does not let a lookup invalidated mid-flight replace a newer last identity” asserts I2 after expiry. Older polling transcript is not used as proof for this guard. |
| F12 Invalidate another root B while A's lookup is pending | A still remembers its successful I1 after TTL; invalidating B must not cancel A's eligibility to remember. | Yes, per-root rather than global guard. | **recording; unit-only deterministic interleaving.** Same resolver gate, invalidate B instead of A. Public project mutations cannot guarantee this schedule. | **COVERED — unit at reviewed service revision.** “keeps a lookup's identity when another root is invalidated mid-flight” checks A after 61 s. |
| F13 Capacity eviction / update order: more remembered roots than capacity | Oldest remembered insertion is dropped; retained roots still return their last identity after cache expiry. Re-resolving an existing root moves it to newest without evicting an unrelated root merely for replacement. Null clears its own entry without consuming capacity. | Yes, new bounded Map branch. | **recording.** Stock-server capacity is 512: create 513 synthetic git roots via project API, warm them in controlled order, refresh an existing root, then inspect expired roots before queued refreshes complete. No API capacity option exists. A focused service test with capacity 2 is the smaller deterministic proof if supplied instead of a costly runtime run. | **MISSING.** Transcript has one root. All existing tests use at most three remembered roots and capacity at least 8 (or default 512); none exercises map eviction or its insertion order. |
| F14 Request dedup/bounded queue; favicon pending vs available | Repeated requests do not duplicate work; dropped work can be requested later; a hung favicon does not block repository completion. Favicon string/null does not select identity fallback. | Queue/favicon behavior unchanged; remembering runs on the resulting identity work. | **recording; shared unit proof**, no new integrated capture. Real driver could burst requests, but cannot deterministically hold favicon workers through a stock API. | **COVERED — existing units.** Dedup/bounds/reload test and the two favicon-worker tests cover these control paths. Favicon TTL/representation is outside the diff. |
| F15 WS initial/resume enrichment: fresh I1, stale I1, then fresh null | Fresh I1 includes A in `resolvedRepositoryIdentityRoots`; stale/pending/failed fallback I1 excludes A; successful null includes A so clients can clear identity. Successful change notifications drive enrichment frames; failures are filtered. Initial/resume metadata and project events share the enrichment service. | Yes, new identity-plus-unresolved output combination; transport implementation unchanged. | **recording.** Warm synthetic project over HTTP, let outer TTL expire without reads, subscribe/resume shell RPC and log full metadata frames before/after refresh; capture successful null using the F08 reverse state. A unit assertion of fallback R=false is the minimum service-level alternative, with existing stream plumbing proof reused. | **MISSING.** HTTP transcript logs canonical key only and HTTP enrichment omits R. New expiry/refresh tests assert the key but never assert fallback R=false. Existing flag test covers cold/failure-without-history and fresh null only. |
| F16 ProjectService hydrate / shell readers, active vs deleted | Active `getById`, `getByWorkspaceRoot`, snapshot and readCommitted use getAvailable; deleted hydrate uses peek and does not enqueue. getShell/listShells enrich similarly. MCP `t3_project_read`, project RPC/CLI, and project stream events inherit these values. Stored favicon overrides enrichment favicon. | Identity output affected through shared service; caller branches unchanged. | **recording by reuse of F01–F12**, not an additional caller recording. Real seed: create/update/delete a synthetic project via API and inspect project reads; these callers only map the service result. | **COVERED — shared service proof and call-graph inspection**, subject to the missing F06 failure case. No independent hydrate transcript exists or is required for unchanged mapping code. Deleted-at and stored-favicon shapes introduce no new cache branch. |
| F17 MCP list / remote delegation input: cold null versus stale I1 while refresh may produce I2 | `t3_project_list` calls listPage: null hydrate falls through to blocking readRepositoryIdentity; a stale non-null I1 now skips that read and can be used for peer matching until refresh. Local remote-delegation key uses RepositoryIdentityResolver directly; explicit target bypasses matching. Zero/one/multiple-match selection is unchanged. | Yes, listPage's existing non-null conditional now receives stale values. Local direct-resolver and explicit-target branches unchanged. | **recording.** Register synthetic peer project; call list tool with cold cache, then after warming and >60 s expiry, and after a synthetic origin change/inner-cache expiry. Record returned identities and refresh ordering. A scoped synthetic MCP request or driver invoking the real server listPage path needs no paid agent turn. One shared list recording proves the changed peer-match input; no separate full agent delegation capture is required. | **MISSING.** HTTP shell transcript never calls listPage/MCP and cannot prove its null-versus-non-null branch. No dedicated new remote/render/provider capture is otherwise required. |

Minimum completion set: retain the existing HTTP expiry and reverse recordings and existing focused unit proofs. Add **F06** at the service unit layer; add a timestamped API/driver recording covering **F13, F15, and F17**, or the focused lower-layer alternatives explicitly identified above. The API rows can share one synthetic run. Do not repeat all caller/state cross-products: only WS exposes the freshness-root list, and listPage conditionally changes whether it performs a blocking lookup. Web, desktop and mobile consume the same unchanged wire contract; there is no render path or provider-specific behavior in this diff, so screenshots, themes, viewport sizes and real provider turns add no proof.

Source anchors: [service](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/project/ProjectEnrichmentService.ts#L93), [tests](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/project/ProjectEnrichmentService.test.ts#L131), [HTTP enrichment](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/orchestration-v2/http.ts#L63), [hydrate](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/project/ProjectService.ts#L200), [listPage](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/project/ProjectService.ts#L586), [WS enrichment](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/ws.ts#L857), [WS notifications](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/ws.ts#L1259), [MCP list](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/mcp/toolkits/project/handlers.ts#L290), [remote matching](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/peer/RemoteDelegation.ts#L143), [resolver null/error handling and TTLs](https://github.com/pingdotgg/t3code/blob/bff5e1cba9b94ab1a95dde3f8a211fb941547e31/apps/server/src/project/RepositoryIdentityResolver.ts#L269).

No servers started; no worktree edits, pushes, or comments. Only this scratch artifact was written. Disk check: 106 GB available, above the 5 GB floor. Audit completed within the 45-minute budget.

FRICTION: none

VERDICT: INSUFFICIENT F06, F13, F15, F17
