Repository navigation
Description
What happened
I have four linked environments (a MacBook, a Mac mini, two Linux boxes), each running its own T3 server with the same projects: same git remotes, different home paths. I expect each project to appear once in the sidebar, grouped by repository. Instead some projects intermittently show twice: the grouped entry plus a lone copy from one machine.
On Nov 6, 2026, one Linux box restarted at 10:50 PM Central. For several minutes its project list, read over the WebSocket API, had repositoryIdentity: null for all 32 projects, then 1 or 2 nulls on back-to-back reads. Earlier the same day another Linux box returned null for almost all projects with no restart. The git remotes never changed.
Diagnosis
The null is a not-yet-resolved identity, not a failed git lookup.
Server. Shell reads (enrichProjectShells in ws.ts -> getAvailable -> peek) use Cache.getSuccess(repositoryIdentityCache, root) (apps/server/src/project/ProjectEnrichmentService.ts). getSuccess (effect 4.1.2) returns None for missing, expired or pending entries, and the success TTL is DEFAULT_SUCCESS_TTL = 1 minute, so:
- on a fresh server every project is
repositoryIdentity: nulluntil its lane resolves it; - each entry then expires every minute, and a read before the re-request finishes gets null again (the 1-2 nulls per read).
The server marks these repositoryIdentityResolved: false, so it knows null means "unknown", not "no repo".
Client. buildProjectGroups -> deriveLogicalProjectKey (packages/client-runtime/src/state/projectGrouping.ts) treats a null identity like a folder with no repo: deriveRepositoryScopedKey returns null and the key falls back to derivePhysicalProjectKey (environmentId:path). Nothing in grouping reads the resolved flag, so an unresolved copy gets its own group. retainRepositoryIdentity (shellReducer.ts) keeps an identity the client already had, but a copy with none on record yet splits.
Minor. resolveRepositoryIdentityFromCacheKey (RepositoryIdentityResolver.ts) checks code !== 0 but not timedOut on git remote -v (the rev-parse calls do). A slow call becomes a cached null (1-minute negative TTL) reported as resolved-with-no-repo.
Possible fixes: serve the last good identity while a refresh runs; don't split unresolved copies in grouping; treat timedOut as a failure.
Steps to reproduce
- Link two or more environments with a project for the same git remote at different paths; confirm they group as one sidebar entry.
- Restart one environment's server.
- Within the first minute, its project shells (WebSocket API) have
repositoryIdentity: null, and the sidebar shows a lone copy from that environment beside the group. - Keep reading every few seconds: single projects go null again as their 1-minute entries expire.
Version
0.1.8-nightly.20261106.4288 on all four environments: the desktop app and local server on the MacBook, and the servers on the Mac mini and both Linux boxes. Code checked at tag v0.1.8-nightly.20261106.4288. main is 17 commits ahead and none touch ProjectEnrichmentService.ts, RepositoryIdentityResolver.ts, projectGrouping.ts or shellReducer.ts. Triage CLI: 0.1.6.
Environment
Client: T3 Code desktop app, macOS 26 (Darwin 25.6.0) arm64. Linked environments: this MacBook, a Mac mini, two Linux servers, each running its own T3 server with the same 32 projects.
Evidence
- Linux box restarted 10:50 PM Central, Nov 6: null for 32/32 projects for several minutes, then 1-2 nulls per back-to-back read.
- Another Linux box (no restart), earlier Nov 6: null for almost all projects.
- MacBook server traces (~1.5 h): all 102
resolveFromCacheKeyspans succeeded in at most 13 ms, so these nulls are not git failures.
Related issues
- "Add to another machine" stays hidden for up to a minute after a project gets a Git remote #29929: same 1-minute
DEFAULT_SUCCESS_TTL, different symptom. - [Bug]: A project whose folder was deleted loses its repository identity and splits out of its sidebar group #18224 (closed): project splits out of its group after losing its identity.
- [Bug]: A project cloned from GitHub keeps a null repository identity after the clone finishes #19268 (closed): cloned project kept a null identity.
Fix applied or workaround
None applied. The duplicates go away on their own once identities re-resolve.
Filed by
Claude Code (Claude Opus 5.5, claude-opus-5-5), via t3 triage.
Activity
PookMook commentedon Nov 7, 2026
Another symptom of the same cause, on a single environment with no linked machines, through the HTTP shell endpoint.
What we see
We run a small CLI wrapper next to T3 Code (0.1.6, Linux). Every 5 minutes it reads
GET /api/orchestration/shelland treats each project with a non-nullrepositoryIdentityas a repository it can work in. To that wrapper, projects keep vanishing and coming back even though nothing about them changes:It's the same
getAvailable→peek→Cache.getSuccesspath described above, this time fromenrichProjectShellsinorchestration-v2/http.ts. The server traces back it up:ProjectEnrichmentService.resolveRepositoryIdentityspans start in the same second as the wrapper's reads (for example 13:06:31 and 13:11:31 UTC). So the wrapper's own read found the entry expired, gotnull, and set off the refresh. Which projects drop depends on whether anything else read them in the previous minute.What this adds to the issue
repositoryIdentity: nullfor repositories whose remotes haven't changed.repositoryIdentity, notrepositoryIdentityResolved, so a client reading it has no way to tell the two apart.nulluntil its first lookup finishes, so a client that connects right after the server starts would still see none of them as repositories. ExposingrepositoryIdentityResolved(or leavingrepositoryIdentityout until it's resolved) on the HTTP shell would let clients wait instead of guessing.Versions checked
The code involved (
ProjectEnrichmentService.ts, its 1-minuteDEFAULT_SUCCESS_TTL, and the HTTPenrichProjectShells) behaves the same in v0.1.6, v0.1.7 and0.1.8-nightly.20261107.4314. The only change in that range is to tracing (#29640).Workaround on our side
We are changing the wrapper to stop reading
repositoryIdentityat all and instead decide a project is a repository by checking for<workspaceRoot>/.gititself.matx-art commentedon Nov 9, 2026
Another symptom of the same cause: the PR review guide refuses to start and shows the wrong error.
PullRequestReviewGuideService.generatereads the project throughProjectService.getShell, so it gets the peeked identity.requireOwnRepositorytreats a null identity the same as a pull request from another repository. If nothing read the project in the previous minute, the first "Write review guide" fails with:A retry a few seconds later works, because the failed call queued the refresh. Server traces on 0.1.7 (Linux, a GitHub pull request in the project's own repository):
The MCP
write_pull_request_guideshandler (apps/server/src/mcp/toolkits/pullRequests/handlers.ts) does the same check onproject.repositoryIdentity, so agents can get the same refusal.#32359 also fixes the once-a-minute case on these paths. It does not fix the first lookup after a server restart: the identity is still null, and the guide refuses with the "another repository" message. Two changes would close that gap:
ProjectEnrichmentService.readRepositoryIdentityinstead of reading the peeked shell value.