Skip to content

fix(git): a timed-out worktree list is never read as 'no family' - #1450

Merged
Juliusolsson05 merged 9 commits into
integration/batch-2026-09-27-qfrom
fix/worktree-timeout-consumers
Sep 27, 2026
Merged

Juliusolsson05 merged 9 commits into
integration/batch-2026-09-27-qfrom
fix/worktree-timeout-consumers

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1430 (found by #1429 review b, findings 2 and 3). Refs #1250.

All four consumers are delivered. B6 manager-verified a and b as MERGE-READY at 3279b5b, which also covers the history bullet's fix (6492db7).

What was wrong

#1429 made the Worktrees panel, GitBar and worktrees.read say a git timeout. The exported listWorktreesForCwd still answered [] on a timeout, and four other consumers read that as "this checkout has no siblings":

  • Conversations picker: resolveFamily fell back to the cwd alone, so from a linked checkout the main checkout's conversations dropped out. The service cached that guess for 3 s, and nothing said so.
  • Worktree activity: answered { ok: false }, the same as "not a repository". The dump showed "Agent activity: unavailable", as if the index were missing.
  • Agent-activity repo root: the recorder filed a worktree's activity under the worktree folder instead of its repository.
  • Renderer history: history chunks were attributed against an empty worktree family.

Fix

  • listWorktreesForCwdDetailed is exported, and each consumer acts on timedOut.
  • Conversations:
    • RepositoryFamily.gitTimedOut, and ListWorktrees accepts the detailed answer (a plain list still means git answered);
    • a timed-out discovery answers the request but is not cached;
    • ConversationListResponse.family.gitTimedOut, and the picker shows a muted status line outside cwd scope: "Git didn't answer in time. Conversations from this repository's other worktrees may be missing."
  • Worktree activity:
    • the handler answers { ok: false, timedOut: true };
    • loadWorktreeDump.activityTimedOut and worktrees.read's activityTimedOut carry it;
    • the dump line reads "Agent activity: unavailable (Git timed out)".
  • Repo root: resolveRepoRootAfterGit retries a timeout once. After two timeouts it throws RepoRootUnknown. The recorder then records that interval's repository as Unknown ('', the store's existing "no repository" value) and warns; it never records the cwd. The worktree row keeps its cwd, and the next interval asks git again. It never loops. A real non-repository (git answered, no worktrees) keeps the cwd, as before.
    • Steering q126: the first version of this PR fell back to the cwd. The store persisted it and the summary grouped by it, so a worktree folder became a repository of its own that no later interval could fold back, contrary to what this body claimed. Fixed in 74a5c0b.
  • History:
    • worktreesForAttribution returns null for a timeout, and both history paths then skip attribution for that chunk;
    • they hand the chunk to the live worktree reconciler instead (WorkspaceRefs.worktreeReconcilerRef, handHistoryToReconciler), so a recovered catalog replays it (round 1, a+b);
    • when the reconciler already holds a fresh catalog, refresh answers cached and never notifies, so the hand-off replays at once (replayCachedCatalog; verification a, 9596a99);
    • the older-history loader hands a page over only while the pane's context is unknown, the same recency rule as its answered-git backfill (verification b, 9596a99);
    • an older page enters the reconciler's window as the oldest evidence (observe(..., 'older')). Before this, a scroll-up during a timeout was appended as the newest evidence, and a recovered catalog moved the pane to the older worktree. Overflow is dropped, never folded over newer evidence (B6 verification, 6492db7);
    • a non-repository or missing git keeps [].
  • Conversations picker paging: a page from a different family than the pages before it (git recovered between pages) restarts from page 1 instead of being appended (round 1, a). The note shows only in Repository scope (round 1, b).
  • listWorktreesForCwd removed: it had no callers left (round 1, c).

Tests (fail-first)

  • service.system.test.ts, on the recorded conversation corpus, from a linked checkout with the list timing out:
    • family.gitTimedOut;
    • a second request re-asks git, so nothing was cached;
    • when git answers, the main checkout's rows are back, and more of them.
  • worktreeActivity.test.ts, the real handler: timeout vs non-repository vs repository.
  • resolveRepoRoot.test.ts: answered, retry-then-answered, two timeouts throw, and a non-repository keeps the cwd.
  • AgentActivityRecorder.test.ts (q126): the real recorder and store with the real retry policy. Two timeouts then an answer give the first interval under Unknown and the second under the repository, and no worktree-folder repository ever appears. It failed before 74a5c0b, showing a /dev/agent-code/.worktrees/fix repository.
  • control.renderer.test.ts: activityTimedOut through the real worktrees.read and loadWorktreeDump, plus the dump line.
  • worktreesForAttribution.test.ts: the three git:worktrees answers.
  • ConversationsPicker.renderer.test.tsx: the note when git timed out, and none when it answered.
  • historyWorktreeTimeout.renderer.test.ts runs the real reconciler with the recorded codex-0151 window and the recorded three-worktree catalog:
    • recovery after the timeout reaches worktree-2;
    • a catalog that was already cached repaints at once;
    • B6's sequence lands on worktree-1: the newest chunk is the same recorded records moved to worktree-1, and the recorded window is the older page. It was red before 6492db7 (worktree-2).
  • history.renderer.test.tsx:
    • the older loader hands its page over as 'older';
    • it hands nothing over when the context is already known.
  • The Codex continuity harness mounts useIpcSubscriptions, and now carries worktreeReconcilerRef (f5fc358, the CI failure at 9596a99).
  • Mutations caught, one per consumer (the q126 cwd fallback is the red state of the recorder test above):
    • caching a timed-out family (1 red);
    • activity answering as not-a-repo (1 red);
    • no retry (2 red);
    • a timeout attributed as [] (2 red);
    • the dump hiding the timeout (1 red).
  • Verification-pass mutations, each 1 red:
    • no replay on a cached catalog;
    • no known-context guard;
    • an older page appended instead of prepended;
    • the loader passing newest.
  • npx tsc -b clean at 3279b5b (origin/main merged in). Main's side of the conflict in service.system.test.ts was empty; this branch's timeout test was kept. 1654/1654 across 216 files: conversations, renderer workspace, the conversations feature, agent activity and the worktree-activity IPC.

Residuals (accepted by B6)

  • LiveWorktreeReconciler.retainOlder's two overflow branches have no test (surviving mutations, and B6 found no failure sequence):
    • with a cached catalog, the overflow is dropped rather than folded;
    • without one, the overflow moves to the front of deferredRaw.
  • The production publish of worktreeReconcilerRef in useIpcSubscriptions is unasserted. The Codex continuity harness mounts it and proves it runs without throwing.
  • Out of scope, filed separately by B6: a git failure that is not a timeout is still read as "not a repository".

Merge order and overlaps

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 27, 2026 08:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up of #1429. listWorktreesForCwdDetailed is exported and every other
consumer acts on timedOut: the conversations family is marked gitTimedOut,
not cached, and the picker says siblings may be missing; worktree activity
answers { ok: false, timedOut: true } (worktrees.read activityTimedOut, the
dump says 'Git timed out'); the agent-activity repo root retries once and
then throws instead of filing a worktree's activity under its folder; history
chunks skip worktree attribution on a timeout (worktreesForAttribution).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 added type:bug Something works wrong class:C3-silent-failure The app knows it failed and does not say sev:P3 Minor labels Sep 27, 2026
Juliusolsson05 and others added 3 commits September 27, 2026 08:55
… never as the worktree folder (#1430, q126)

Two git timeouts made the recorder fall back to the cwd as repoRoot; the
store persisted it and summarize grouped by it, so a worktree became a
repository of its own that later intervals could not fold back. It is now
recorded as '' (the existing Unknown), with the cwd still naming the
worktree row. Pinned through the real recorder and store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imed-out history to the reconciler

a: a page built after git recovered is not appended to one built while it
timed out (useConversationList restarts from page 1 when the family changes).
a+b: a history chunk read while git timed out is handed to the live
reconciler (WorkspaceRefs.worktreeReconcilerRef, handHistoryToReconciler)
so a recovered catalog replays it, instead of losing its worktree evidence.
b: the picker note only in Repository scope. a: assert the repository-
unknown warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… drop the dead lister (#1430 review c)

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

Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner Author

Round-1 disposition. a and b were FIX-BEFORE-MERGE, and c was MERGE-READY on the fixed head. Every valid finding is fixed fail-first.

  • a (major): pages from two families. A timed-out page 1 was followed by a recovered page 2. The rows the recovered order puts before the cursor were lost, and the warning cleared. Fixed in 619b3da: useConversationList restarts from page 1 when the family changes. It is pinned by a picker test that pages with ArrowDown across the recovery.
  • a and b (major): skipped history lost its worktree evidence. Fixed in 619b3da. A chunk read while git timed out is handed to the live worktree reconciler through WorkspaceRefs.worktreeReconcilerRef and handHistoryToReconciler, and its window replays the chunk when a later refresh gets the catalog. Pins:
    • the real reconciler with the recorded codex-0151 window reaches .../worktree-2 after git recovers;
    • the initial-history loader hands the chunk over;
    • (c) the older-history loader hands it over and attributes nothing against the unknown family (93ff416).
    • All of these mutations are red.
  • b (minor): the note showed in Everywhere. It now shows in Repository scope only; pinned.
  • a (surviving mutation): the repository-unknown warning. Now asserted.
  • c (minors):
    • the older-history hand-off and its null guard are now pinned (both mutations red);
    • the dead listWorktreesForCwd export is removed;
    • the body's counts are corrected.
  • Residual (accepted): the production publish of the reconciler ref in useIpcSubscriptions is unasserted, because mounting that hook is heavy. Every loader and reconciler test injects the ref.
  • Checks: npx tsc -b clean. The affected suites pass 606/606 across 85 files (git, worktree and activity IPC; conversations; agent activity; the worktrees, conversations and work-context features; the history actions).

Head: 93ff416.

🤖 Generated with Claude Code

…ages never override a known context (#1450 verification a/b)

a: with a fresh catalog already cached, refresh() answers 'cached' and never
notifies, so a timed-out history chunk never repainted. replayCachedCatalog
replays against a real cached catalog; the hand-off calls it on 'cached'.
Pinned with the recorded codex-0151 window (was main cwd, now worktree-2).

b: the older-history loader handed pages over even when the pane knew a
newer context; the reconciler treats observed records as newest, so a
recovered catalog moved the pane back. Hand over only while the context is
unknown, the same recency rule as the answered-git backfill. Pinned.

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

Copy link
Copy Markdown
Owner Author

Verification disposition at 9596a99. Verifiers a and b were both FIX-BEFORE-MERGE; both findings are gaps in the round-1 hand-off, and both are fixed fail-first. The capped pass is now spent.

  • a (major), a fresh cached catalog never repainted:
    • The situation: a live event had already cached the catalog, and only the history's own gitWorktrees call timed out. refresh() then answered cached and never notified, so the handed-over chunk just sat in the window.
    • Fix: LiveWorktreeReconciler.replayCachedCatalog(cwd) replays the retained evidence against a real cached catalog. It does nothing for the empty placeholder an in-flight probe writes. handHistoryToReconciler calls it when refresh answers cached.
    • Pinned with a's own repro: the recorded codex-0151 window with the catalog loaded first. Before the fix it stayed on /fixture/project-1; now it reaches .../worktree-2. Removing the replay call turns it red.
  • b (major), an older page could replace a newer context:
    • The reconciler treats what it observes as the newest evidence, so an older page handed over after the pane knew worktree-1 moved it to worktree-2.
    • Fix: the older-history loader hands a page over only while workContext is unknown. This is the same recency rule as its answered-git backfill.
    • Pinned: a pane with a known context hands nothing over and keeps it (red before the fix).
    • Initial history needs no guard: it is the transcript's tail, the newest evidence there is (recorded as a Ruling in the plan).
  • Round-1 items both verifiers confirmed fixed:
    • the paging restart;
    • the loader hand-offs;
    • the repository-unknown warning;
    • the Everywhere note.
  • Residual, unchanged: the production publish of worktreeReconcilerRef is unasserted.
  • Checks: npx tsc -b clean; the renderer hook actions and work-context suites pass 280/280.

B6: the verification cap is spent, so please manager-verify a and b at 9596a99.

🤖 Generated with Claude Code

…lerRef (#1450 CI)

useIpcSubscriptions publishes the live reconciler into WorkspaceRefs
(round 1); this harness builds refs by hand and lacked the slot, so the
publish threw. Also incidental proof the production publish runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 2 commits September 27, 2026 12:19
…nce (#1450 B6 verify)

A scroll-up during a git timeout handed the older page over as if it were
the newest, so a recovered catalog moved the pane to the older worktree.
observe() takes a position; older pages go to the old end of the window
(overflow dropped, never folded over newer baseline evidence). Pinned with
the recorded codex-0151 window + three-worktree catalog. Also fixes the
stale resolveRepoRoot header (Unknown, not the cwd).

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

# Conflicts:
#	src/main/conversations/service.system.test.ts
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

B6 manager verification (FIX) disposition, at 3279b5b (6492db7 is the fix; the rest is the origin/main merge).

  • The finding: a scroll-up during a git timeout outranked the newest chunk. It is fixed fail-first.
    • Fix: LiveWorktreeReconciler.observe(..., position). An older page enters at the OLD end of the evidence window (retainOlder), in front of deferred records when there are any, under the same 2 × limit bound. What doesn't fit is the oldest evidence there is. With a cached catalog it is dropped, never folded, because the folded baseline holds newer records.
    • Wiring: handHistoryToReconciler(..., 'older') is used by the older-history loader.
    • Pinned in historyWorktreeTimeout.renderer.test.ts:
      • the older page is the recorded codex-0151 window (worktree-2);
      • the newest chunk is the same recorded records moved to worktree-1 and 1 h later;
      • the catalog is the recorded three-worktree one.
      • Result: timeout, then timeout, then recovery lands on worktree-1. It was red before the fix (worktree-2).
    • Mutations: appending instead of prepending, and the loader passing newest, are each 1 red. history.renderer.test.tsx pins 'older'.
  • The rest of the note:
  • Out of scope: the non-timeout git failure you filed separately.
  • Checks: npx tsc -b clean; 1654/1654 across 216 files.

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 changed the base branch from main to integration/batch-2026-09-27-q September 27, 2026 21:04
@Juliusolsson05
Juliusolsson05 merged commit 74536b6 into integration/batch-2026-09-27-q Sep 27, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/worktree-timeout-consumers branch September 27, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C3-silent-failure The app knows it failed and does not say sev:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(git): other consumers still read a timed-out worktree list as an empty family

1 participant