Git delivery candidate cap is consumed by deleted paths and duplicate worktrees
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 94
- Forks
- 16
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 5
Description
🤖 Agent-filed: investigated and written by Claude Code on behalf of @nichenke. Every code
citation was verified against 1a1b35a, and every count was measured on the reporter's machine.
Local paths and repository names are redacted.
git_delivery_candidates() selects the first 50 session project paths with no existence check and
no worktree resolution. On a machine with a long session history, that budget is largely spent on
paths that no longer exist and on multiple worktrees of the same repository, displacing live
repositories from the scan.
Mechanism
-
git_delivery_candidates()(token_meter/app.py:6926) walks session sources, dedupes by
os.path.abspath, andbreaks at a hardcoded50(:6940). It performs no filesystem check —
delivery_project_label()only hashes the path. -
A deleted root is not detected until
_scan_candidate()(services/git_delivery.py:499) has
already spawnedgit -C <root> rev-parse --show-toplevel(:503), which fails and returns
repository_unavailable(:506). -
The
repo_keycoalescing at:514does not catch linked worktrees.repo_keyis
hash(rev-parse --show-toplevel)(:508), and in a linked worktree--show-toplevelreturns
that worktree's own root, not the main repository. Verified on this machine (paths redacted):worktree <repo>/.worktrees/<branch> --show-toplevel -> <repo>/.worktrees/<branch> --git-common-dir -> <repo>/.git main repo <repo> --show-toplevel -> <repo>Both report the same 12
refs/remotes, because remote-tracking refs live in the common dir. So
each worktree of a repository re-reads the same shared reflogs (:539) under a distinct
repo_key.
Impact
Measured on the reporter's machine — 152 Claude project directories and 181 Codex session files:
Session project paths (distinct cwd) |
170 |
|---|---|
| — path no longer exists | 99 |
| — live git path | 62 |
| — not a repository | 9 |
Distinct underlying repositories (--git-common-dir) |
41 |
| Redundant paths (live paths − repositories) | 21 |
Worst duplication: two repositories contribute 6 candidate paths each and a third contributes 4.
The live server is at the cap and reports both symptoms:
"coverage": {"repositories": 50, "measured": 11, "partial": 46,
"codes": ["no_push_history", "no_remote_tracking_refs", "ready",
"repository_unavailable", "scan_limited"]}
"sources": 1188
Only 41 repositories exist in this machine's entire session history, so the 50-repository cap is not
protecting against repository count — it is being filled by stale session history and worktree
duplicates, and scan_limited is the result.
Per-pass git subprocess cost over the full live population, as an upper bound before
MAX_COMMITS_PER_SCAN truncates:
| reflog spawns | |
|---|---|
| As-is | 1895 |
| Worktrees coalesced by common dir | 1448 |
| Redundant | 447 |
Plus three fixed spawns per live candidate (rev-parse, config --get user.email, for-each-ref)
and one wasted rev-parse per deleted path.
Not instrumented: the actual mix of paths in any single 50-candidate pass, which depends on source
ordering. Measuring it would have written to the live delivery database, so the numbers above are
full-population bounds rather than an observed pass.
Secondary: the repository-limit signal is dead
repositories_limited = len(candidate_rows) > MAX_REPOSITORIES (services/git_delivery.py:626)
can never be true, because git_delivery_candidates() already truncated to 50 upstream. The
repository_limit code (:642) and its partial bump (:643) are therefore unreachable.
Reproduced against 1a1b35a:
sources: 120 candidates returned: 50 MAX_REPOSITORIES: 50
repositories_limited would be: False
The hardcoded 50 in app.py:6940 also duplicates MAX_REPOSITORIES in
services/git_delivery.py:17, so the two can drift.
Suggested fix
In git_delivery_candidates(), before applying the cap:
- Drop roots that do not exist (
os.path.isdir). - Key the dedupe on the repository rather than the working tree, so a repository's worktrees
occupy one slot.git rev-parse --path-format=absolute --git-common-dirgives this, at the cost
of one subprocess per surviving candidate — cheaper than the full reflog pass each duplicate
currently triggers. - Import
MAX_REPOSITORIESinstead of the literal50, and raise the limit signal where the
truncation happens sorepository_limitbecomes reachable.
Design note on (1): the loop currently breaks as soon as it has 50 candidates. With an existence
filter it may walk the full source list — 1188 entries on this machine — so the cost becomes ~1188
isdir calls per pass. That is negligible against a single git spawn, but it is a change in
iteration shape worth stating.
Expected effect on this machine: 170 candidate paths reduce to 41, comfortably under the cap, and
scan_limited should stop appearing.
Related
- #30 — the watcher's interval floor. Frequency, not pass composition; the two are independent.
- #31 — fixes #30. This report applies equally before and after it.
Environment
- Revision
1a1b35a, macOS 26.6.2 arm64, Python 3.9.6 (system Python, via the server LaunchAgent)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with git_delivery_candidates() in token_meter/app.py and MAX_REPOSITORIES plus _scan_candidate() in services/git_delivery.py. Trace candidate truncation and repo_key handling; done means deleted roots are excluded, linked worktrees consume one repository slot, the shared limit is used, and repository_limit and scan_limited accurately reflect truncation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100