Skip to content

feat(server): index worktrees for active agents, outside the repo - #67

Merged
ScriptedAlchemy merged 25 commits into
mainfrom
feat/code-index-lifecycle
Nov 6, 2026
Merged

ScriptedAlchemy merged 25 commits into
mainfrom
feat/code-index-lifecycle

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Problem

A checkout indexed only when something asked it a code question, so a new project or worktree had no code graph when an agent needed one. The 10,000 file cap could also fill up on hidden folders and leave out all of a repo's own source. The index wrote .codegraph-t3* folders into the user's repo as well, and those folders showed up on disk and in the Files panel.

Change

When it indexes. T3 indexes a project when it is added and a worktree when an agent starts in it. T3 does not rebuild an idle worktree until an agent is active in it again. Builds run at low priority, with at most two at once. Listings over the cap rank first-party source first.

When it cleans up. T3 deletes a worktree's index when its thread settles, by the rules that let a worktree be deleted. A pinned or explicitly active thread that shares the worktree keeps the index. A worktree's index also goes after 7 days with no activity on its threads, unless one is pinned or marked active, and the next agent run rebuilds it. A project's own checkout keeps its index.

Maintenance. Maintenance removes a worktree's index just before it removes the folder. The index sweep follows every Maintenance run, including "Delete now" and runs with no worktree rules on, instead of keeping its own timer. Each sweep removes a settled or long-unused worktree's index, including a subagent's whose settled parent has no worktree or uses another one, and an index for a checkout that is gone or that T3 no longer knows about. The Last run report does not list index removals, because a new report type would break older clients.

Worktree cleanup. Cleanup's rule for a branch recorded by a thread in another worktree now compares threads in the same project, so a same-named branch in an unrelated repository no longer keeps a worktree.

The switch. It is at Settings > Labs > Automatic code indexing, on web and mobile. Turning it off stops running builds, and after that only a code question builds an index. T3 still removes leftovers while the switch is off.

Where it lives. Indexes are in <state>/code-index/<hash of checkout path>/, never in the repo. The SDK accepts a data directory only inside its project root, so each checkout gets a private root that holds the index and links back to the checkout's top-level entries. POSIX uses symlinks. Windows uses junctions for folders, with a copy per build when file links need a privilege. Removal deletes the whole root. It unlinks those entries and never removes recursively from the root.

Old folders in repos. A checkout's next build and every sweep remove .codegraph-t3* folders that an older version left there. This applies to all projects and worktrees T3 knows, even with the switch off. T3 deletes only files that T3 and the SDK wrote. Anything else in the control folder stays. T3 leaves alone a generation folder that holds anything else, and any folder with symlinked or multiply linked entries.

Lock safety. Removing lock files could let two servers each hold a lock on a different file. A builder now compares the file's identity before and after it takes the lock, and backs off if the identity changed. T3 deletes lock files while it holds them where the OS allows it, and on release where the OS cannot delete an open file. T3 therefore never deletes a lock that is still in use.

Configuration changes. Index metadata records a fingerprint of the contents of the checkout's codegraph.json and root .gitignore. Freshness checks detect a change or a deletion. The next refresh then rebuilds the graph, reconsiders excluded files, and removes newly excluded entries. A config path that T3 cannot read, such as a folder with that name, no longer fails builds or reads.

Badge. The Not indexed, Indexing, and Index failed states now report first builds. The badge reads the state again when an index is removed or a build stops. Code queries ignore that signal, so the signal cannot restart a build T3 just stopped. The badge shows Partial once source in a language outside the SDK's catalog is over 5% of the code. Docs, assets, and an occasional script stay Ready, and coverage still lists the unsupported count.

Original implementation verification (before this audit)

Pre-audit head on macOS

  • vp run check:changed passes against the latest main: format, lint, knip, generated GraphQL, native addon, typecheck of 36 packages, 14 test files (412 passed, 5 Windows-only skipped), and the React compiler checks.
  • Removing each new behaviour in turn made a test fail, including the 7-day boundary in both directions.
  • These checks ran against a real dev server and the web app, with the replay provider and disposable repos:
    • Adding a repo with old folders shaped like real ones removed them and kept an unrelated .codegraph-t3-g7/notes.txt. Repos stayed clean under git status --ignored.
    • A worktree thread reached Ready, the Files panel listed no index folders, and Code relationships resolved a symbol in a top-level root.ts.
    • Settling the thread deleted its whole index root, kept an uncommitted edit, and left other indexes alone.
    • With the switch off, a restart cleared old folders planted in a known repo without a rebuild. It also removed a lock-only leftover and a removed project's index, and logged no failures.
    • A worktree folder deleted outside T3 lost its index as soon as turning on a rule started a Maintenance run. Turning on "Delete unchanged worktrees" then removed another worktree and its index in one run, and kept its branch, the project's index, and a clean repo.

Windows, before the Maintenance change

  • The codeIndexLocks, CodeIntelligence, and CodeIndexReactor test files pass on a windows-2025 runner, 93 of 93. They cover lock removal, deleting index roots with their junctions, and old-folder cleanup: run.

Earlier checks, not repeated on the final head

  • On macOS, an idle edit does not rebuild and the next turn does. The switch stops a live 6,000-file build and leaves no partial index and no lost files. With another process holding the lock, a client sends one code query in 35 seconds.
  • On Linux, the follow-up scoped run passed on the head at that time, including 15 browser tests.
  • On Windows, follow-up commit 2bb2e5b fixed path-case identity, UNC rejection, and the lock journal.

Earlier verification limits (before this audit)

  • The final head has not run against a real server and client on Windows. The automated tests pass there at d75ea81.
  • Mobile and desktop have not been run on a device or in the app.
  • Build time for a repo this size has not been measured.
  • Before and after captures of the UI are still needed.

Devin wrote the Windows fixes. GPT-6 Astra Pro in ChatGPT did the follow-up review and fixes.

DeepSeek-V4.1-Flash via OpenCode did the implementation. Opus 5.5 via Claude Code in T3 Code did the design, review, and fixes.

🤖 Generated with Claude Code

Audit follow-up — 2026-11-06

Index shutdown now happens before the complete final cleanup safety sequence. Regression tests cover a recovery ref disappearing during shutdown and a tracked edit arriving in an initialized submodule worktree that requires forced removal. Both cases retain the worktree.

Cleanup also retains a worktree when Git truncates reflog or worktree-list inspection output. Windows fixtures use native paths, filesystem casing, exact allocated-byte accounting, and safe preservation of Git's hidden worktree metadata file. The shared test environment retains the Android platform flag.

Local validation during this audit: 399 selected tests passed with 6 expected platform/permissions skips; 13 selected React compiler checks passed; all 41 affected typecheck tasks passed through the supported uncached task runner. The final cleanup safety ordering passed its 32-case focused run. The three final Windows fixture corrections passed their focused tests, file format/lint, and a server typecheck with explicit exit 0.

The full Windows cleanup suite passed on 6fe77ad584d27bc3d2cc9b4084cfcf893c09919b: 209 passed, 1 existing permissions case skipped, 1 file passed. The job completed successfully in 5m 14s. This covers the two cleanup safety regressions and the three corrected Windows fixture cases.

Local browser tests remained unavailable because the Chromium installer returned a truncated archive. Live UI verification could not run because the repository's test-t3-app workflow requires the T3 Browser panel, which is unavailable in this harness.

Model: GPT-6 Astra Pro. Harness: ChatGPT.

ScriptedAlchemy and others added 2 commits November 4, 2026 21:12
A checkout only indexed when something asked it a code question, the file
cap could leave a repo's own source out of its index, and the index folders
sat inside the user's repo.

T3 now indexes a project when it is added and a worktree when an agent
starts in it. An idle worktree is not rebuilt, and its index goes when its
thread settles, by the rules that let a worktree be deleted. Settings > Labs
has one switch for all of this; while it is off, only a code question builds
an index.

Indexes now live in T3's data directory. The SDK only takes a data directory
inside its project root, so each checkout gets a private root that links back
to its top-level entries. Folders an older version left in a checkout are
removed the next time it indexes, and a sweep drops indexes for checkouts that
are gone or no longer known.

Listings over the cap now rank first-party source first, builds run at low
priority with at most two at once, and the badge reports first builds and
failures. The Windows link and copy paths are untested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d9eaf21

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

ScriptedAlchemy and others added 6 commits November 4, 2026 22:42
Preserve build lock inodes during cleanup and reject linked legacy lock paths. Rebuild when checkout indexing configuration changes, and follow settled ancestors when releasing subagent worktree indexes.

Add regression coverage for cleanup locking, config changes, and parent-settlement sweeps; drain background refreshes in generation assertions and document retained legacy lock files.
Maintenance kept every worktree whose agent had switched branches, because
nothing wrote HEAD back to the thread. A new WorktreeBranches service reads
HEAD and rebinds the threads on that worktree. Cleanup keeps for one pass after
a rebind, then applies the existing rules. A detached HEAD stays kept unless a
ref outside refs/t3 holds its commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A thread kept its old branch name after its agent switched branches, so the
pull request refresh skipped it and cleanup had to repair it later. Run
finalization now syncs the worktree branch before that refresh. A sync failure
only logs, so it cannot block the next turn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Launch carried its own copy of the loop that writes a branch to every thread
on a worktree. It now calls WorktreeBranches.sync after the rename, so one path
writes thread.branch for a worktree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pins the resume half of the cleanup change. Once a thread is rebound and its
worktree is removed, the next turn recreates the checkout on the branch the
agent switched to, at that commit, and not on the stale name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deleted owner's record cannot be rewritten, so only its surviving subagents
are rebound. This pins that the group comparison still lets removal proceed on
the next pass, and that the branch ref survives.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

ScriptedAlchemy and others added 7 commits November 5, 2026 07:32
Avoid defensive SQLite lock journals, share checkout identity across path casing, and reject UNC checkout junctions instead of publishing partial indexes.

Model: Devin built by Cognition AI`nHarness: Devin
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…data

Cleanup kept `.codegraph-t3/` in users' repos, could add new untracked lock
files there, deleted any file a user had put in that folder, and left a
lock-only folder per worktree in T3's data directory that every hourly sweep
locked again. Old folders in a checkout that no build reached stayed forever.

Removal now deletes the lock files and their folders too. So a builder cannot
trust a lock on a file that was deleted before its lock landed, it compares the
file's identity across taking the lock and backs off if it changed. Lock files
go while held where the OS allows it, and on release where an open file cannot
be deleted. Old folders lose only the files T3 and the SDK wrote, and a
generation holding anything else is left alone. Startup and hourly sweeps clear
them from every project and worktree T3 knows, even with automatic indexing off.

Also from review: a pinned or explicitly active thread keeps a shared worktree's
index, source in a language outside the SDK's catalog makes coverage partial
once it is over 5% of the code, and an unreadable codegraph.json or .gitignore
no longer fails builds and reads.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two code index tests sent SIGABRT, which Windows rejects with ENOSYS, so they
failed before reaching the behaviour under test. They now use SIGKILL on Windows
and keep SIGABRT elsewhere; production treats any unexpected exit the same.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lone

An independent check of the cleanup change found three ways to lose or strand
work. A different repository at the worktree path had its branch written to the
thread, so the thread forgot its own branch. A branch another checkout holds was
recorded even though the thread could not recreate its worktree there. And the
detached HEAD check ran before later steps that gave a holding ref time to go.

Sync now compares the worktree's repository with the project's before it writes,
and treats a branch another checkout holds like the default branch. Cleanup checks
the holding ref last, immediately before removal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cleanup counted any remote-tracking ref as holding a detached commit. A routine
`git fetch --prune` removes a remote branch that was deleted upstream, so the
commit could become unreachable after its worktree was gone.

Only local branches, tags, and the remote's default branch count now. A detached
worktree held by any other remote branch stays kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y thread

Adversarial review reproduced more ways for this change to lose work or never
finish. Removing a detached or default-branch worktree could lose a commit only
its reflog held, or strand a thread whose recorded branch another checkout held.
A remote-tracking ref counted as protecting a commit even though a routine prune
or a stale origin/HEAD could remove that protection. A run end skipped a sibling
whose write had been deferred to it. Threads with a trailing slash in their path
or no recorded branch never rebound, and mismatched records on a detached or
default-branch worktree were kept forever. A held index lock blocked rebinding,
and main counted as the default branch when the remote's default was trunk.

Cleanup now checks HEAD and the reflog against local branches and tags only, and
keeps a worktree while another checkout holds a recorded branch. Sync compares
HEAD with every thread on the worktree instead of the caller's record, reads HEAD
without depending on the index lock, finds threads by the path each one stored,
and asks the remote when no local HEAD ref names its default.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/server/src/orchestration-v2/RunFinalizationService.test.ts
@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review November 5, 2026 21:34
…vant

Code indexes outlived their use. When Maintenance removed a worktree its index
stayed until the next hourly sweep, "Delete now" never touched indexes, and an
unsettled worktree that Maintenance kept held its index forever.

Maintenance now removes a worktree's index just before the folder, while the path
still resolves and a running build can stop first. The index sweep follows every
Maintenance run instead of keeping its own timer, so both share one schedule. A
worktree's index also goes after 7 days with no activity on its threads, unless
one is pinned or marked active. The next agent run rebuilds it.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ScriptedAlchemy
ScriptedAlchemy changed the base branch from main to fix/worktree-cleanup-branch-sync November 5, 2026 21:47
ScriptedAlchemy and others added 6 commits November 5, 2026 15:39
An adversarial review of this branch reproduced commit loss that main prevents.
A rebound worktree counted as bound on its next pass, which skipped the reflog
check, so cleanup removed it and gc then pruned commits only its reflog held. A
deleted owner with no surviving threads skipped the same check. Another
repository's linked worktree at the path skipped the repository check and lost
its untracked files to `git clean`. Two detached worktrees recording one branch
were both removed, and with no local origin/HEAD a default other than main or
master could become a thread's recorded branch.

The reflog check now runs on every worktree. Rewritten commits land in the reflog
too, so an amended or rebased worktree waits until gc expires those entries.
Cleanup compares the worktree's repository with the project's before it reads the
checkout, by file identity so a differently cased path still matches. It keeps a
detached or default-branch worktree while a thread elsewhere records the same
branch. The service asks the remote for its default whatever the name, once per
checkout.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/server/src/storageCleanup.ts
Cleanup keeps a detached or default-branch worktree while a thread in another
worktree records the same branch, because only one checkout can hold it. The rule
compared branch names across all projects, so a same-named branch in an unrelated
repository kept the worktree with an untrue reason. It now compares threads in the
same project. Two projects sharing one repository are not compared, and resume
there still reports Git's error.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/server/src/orchestration-v2/RunFinalizationService.test.ts
#	apps/server/src/server.ts
#	apps/server/src/storageCleanup.test.ts
#	apps/server/src/storageCleanup.ts
@ScriptedAlchemy
ScriptedAlchemy changed the base branch from fix/worktree-cleanup-branch-sync to main November 6, 2026 07:11
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) November 6, 2026 07:12
Wait for index shutdown before the final activity, branch, local-change,
submodule, and recovery-reference checks. This preserves late tracked edits
when submodule removal requires force and retains detached history if its
last holding ref disappears during shutdown.

Keep worktrees when reflog or worktree-list inspection is truncated. Use
Effect Path for native cleanup fixture paths and preserve the Android
test-platform environment variable.
ScriptedAlchemy and others added 2 commits November 6, 2026 00:58
Assert report accounting against filesystem-provided allocated bytes instead of assuming tiny files allocate nonzero space. Move and restore the Git-created hidden file for the invalid-pointer case.
The test decided its expected result by asking whether the upper-cased path exists,
to detect the Windows runner. That is also true on a Mac, where the linux row still
compares case-sensitively, so it failed on every Mac. It now checks the real host.

Model: Opus 5.5
Harness: Claude Code in T3 Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ScriptedAlchemy
ScriptedAlchemy merged commit 112dc64 into main Nov 6, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant