Skip to content

Commit e24f56b

Browse files
fix: share one project-list build across callers; log slow builds (#36)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwhRmzUt3nLnhqU1NJyzpW
1 parent 5b8869a commit e24f56b

2 files changed

Lines changed: 49 additions & 7 deletions

File tree

‎src/main/api/deckApi.ts‎

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export interface DeckApiBundle {
6363
}
6464

6565
const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
66+
/** A project list slower than this is logged with where its time went (see projects:list). */
67+
const SLOW_PROJECT_LIST_MS = 20_000;
6668
const CLAUDE_TASKS = join(homedir(), '.claude', 'tasks');
6769
const CODEX_SESSIONS = join(homedir(), '.codex', 'sessions');
6870
const ANTIGRAVITY_DIR = join(homedir(), '.gemini', 'antigravity');
@@ -203,17 +205,40 @@ export function createDeckApi(cfg: DeckApiConfig): DeckApiBundle {
203205
lastUserMessage: (p, session) => getProvider(session.agentId).lastUserMessage(p, session.id),
204206
entry: (p) => cfg.store.get(p),
205207
});
206-
invoke('projects:list', allow('observe'), async () => {
208+
// ONE build at a time, shared by every caller. The deck gives up on an answer after 150s and asks
209+
// again on its next refresh, but giving up does not stop the walk here — so on a machine that had
210+
// just booted (cold disk, every repository's git and the whole Codex store still to read) each retry
211+
// started a second and third walk on top of the first, and none finished: SIHYEONG-4080 logged five
212+
// 150s timeouts in a row on 2026-10-12. A late caller now joins the walk already under way.
213+
let projectListInFlight: ReturnType<typeof buildProjectList> | null = null;
214+
invoke('projects:list', allow('observe'), () => {
215+
if (projectListInFlight) return projectListInFlight;
207216
const scan = makeDeckScan();
208-
return buildProjectList({
209-
nowMs: Date.now(),
217+
// Where a slow build spent its time, summed per stage across the worker pool — the only way to
218+
// tell git from the transcript stores after the fact on a machine nobody was watching.
219+
const spent = { scan: 0, git: 0, sessions: 0, cue: 0 };
220+
const timed = async <T>(stage: keyof typeof spent, work: () => Promise<T> | T): Promise<T> => {
221+
const t = Date.now();
222+
try { return await work(); } finally { spent[stage] += Date.now() - t; }
223+
};
224+
const started = Date.now();
225+
let count = 0;
226+
projectListInFlight = buildProjectList({
227+
nowMs: started,
210228
thresholds: effThresholds(),
211-
scan: memoScan,
212-
git: (dir) => getGitInfo(dir),
213-
sessions: (p) => scan.sessions(p),
214-
resumeCue: (p, session) => getProvider(session.agentId).lastUserMessage(p, session.id),
229+
scan: () => timed('scan', memoScan),
230+
git: (dir) => timed('git', () => getGitInfo(dir)),
231+
sessions: (p) => timed('sessions', () => scan.sessions(p)),
232+
resumeCue: (p, session) => timed('cue', () => getProvider(session.agentId).lastUserMessage(p, session.id)),
215233
getEntry: (p) => cfg.store.get(p),
234+
}).then((list) => { count = list.length; return list; }).finally(() => {
235+
projectListInFlight = null;
236+
const ms = Date.now() - started;
237+
if (ms >= SLOW_PROJECT_LIST_MS) {
238+
cfg.diagnostics?.write('warn', 'deck', `projects:list took ${ms}ms for ${count} projects (summed: scan=${spent.scan} git=${spent.git} sessions=${spent.sessions} cue=${spent.cue}ms)`);
239+
}
216240
});
241+
return projectListInFlight;
217242
});
218243
invoke('project:memory', allow('observe'), (projectPath: string, fresh?: boolean) => {
219244
const projectPathString = String(projectPath);

‎src/main/ipc.cockpit.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ beforeAll(() => {
7070
store: {
7171
getFolders: () => [{ path: ALLOWED_ROOT, kind: 'root' }], getAgent: () => storedAgent,
7272
getSessionSummary: () => true, getAiSessionSummary: () => false,
73+
getThresholds: () => null,
7374
},
7475
sendError: vi.fn(),
7576
defaultLanguage: 'en',
@@ -374,3 +375,19 @@ describe('cockpit:conversationsLastUsed', () => {
374375
expect(await lastUsed(null, null)).toEqual([]);
375376
});
376377
});
378+
379+
// The deck gives up on a slow list after 150s and asks again, but giving up does not stop the walk in
380+
// the main process. Each retry used to start ANOTHER full walk on top of the first — on a machine that
381+
// had just booted, none of them ever finished.
382+
describe('projects:list', () => {
383+
it('shares the build already under way instead of starting another', async () => {
384+
const list = handlers.get('projects:list')!;
385+
const first = list(null);
386+
const second = list(null);
387+
expect(second).toBe(first);
388+
await first;
389+
const third = list(null);
390+
expect(third).not.toBe(first); // once it has landed, the next ask builds fresh
391+
await third;
392+
});
393+
});

0 commit comments

Comments
 (0)