@@ -63,6 +63,8 @@ export interface DeckApiBundle {
6363}
6464
6565const 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 ;
6668const CLAUDE_TASKS = join ( homedir ( ) , '.claude' , 'tasks' ) ;
6769const CODEX_SESSIONS = join ( homedir ( ) , '.codex' , 'sessions' ) ;
6870const 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 ) ;
0 commit comments