@@ -268,7 +268,49 @@ async function debugJavaApplication(
268268 }
269269
270270 // Step 3: Construct and execute the debugjava command
271- const debugCommand = constructDebugCommand ( input , projectType ) ;
271+ //
272+ // For simple class names (no dot), resolve the fully-qualified class once
273+ // here so we can reuse the result for both the command construction and
274+ // the user-facing targetInfo message below. Previously these two paths
275+ // each called findFullyQualifiedClassName independently, which:
276+ // - duplicated the file system walk on the hot launch path (the FS walk
277+ // is the actual user-visible slowdown — it stats every .java file
278+ // under src/main/java up to MAX_FILE_SEARCH_DEPTH)
279+ // - made the call sites harder to reason about, since detection
280+ // ownership was split across constructDebugCommand and the targetInfo
281+ // formatting block
282+ //
283+ // After this refactor, the caller owns detection and its telemetry, and
284+ // constructDebugCommand accepts a pre-resolved name. Detection now
285+ // returns a structured result so we can emit `classNameDetection.failed`
286+ // with a precise failureReason instead of a single boolean bucket.
287+ let detectedClassName : string | null = null ;
288+ if ( ! input . target . endsWith ( '.jar' )
289+ && ! input . target . startsWith ( '-' )
290+ && ! input . target . includes ( '.' ) ) {
291+ const detection = findFullyQualifiedClassName ( input . workspacePath , input . target , projectType ) ;
292+ detectedClassName = detection . className ;
293+ if ( detection . className !== null ) {
294+ recordLaunchInternal ( {
295+ name : 'classNameDetection' ,
296+ projectType,
297+ detected : true ,
298+ } ) ;
299+ } else {
300+ // Detection failed. Emit the structured failure event so we can
301+ // distinguish "no candidate src dir" from "found file but no
302+ // package" — the previous boolean `detected: false` collapsed all
303+ // four root causes into one bucket.
304+ recordLaunchInternal ( {
305+ name : 'classNameDetection.failed' ,
306+ projectType,
307+ strategy : detection . strategy ,
308+ failureReason : detection . failureReason ,
309+ } ) ;
310+ }
311+ }
312+
313+ const debugCommand = constructDebugCommand ( input , projectType , detectedClassName ) ;
272314
273315 // Validate that we can construct a valid command
274316 if ( ! debugCommand || debugCommand === 'debugjava' ) {
@@ -297,14 +339,11 @@ async function debugJavaApplication(
297339 } else if ( input . target . includes ( '.' ) ) {
298340 targetInfo = input . target ;
299341 } else {
300- // Simple class name - check if we successfully detected the full name.
301- // `findFullyQualifiedClassName` returns a structured result: a
302- // non-null `.className` means we resolved a package, while null means
303- // we could not. Stringifying the whole object here would render as
304- // `[object Object]`, so unpack explicitly.
305- const detection = findFullyQualifiedClassName ( input . workspacePath , input . target , projectType ) ;
306- if ( detection . className !== null ) {
307- targetInfo = `${ detection . className } (detected from ${ input . target } )` ;
342+ // Simple class name - reuse the detection result resolved once in
343+ // Step 3 above (do NOT call findFullyQualifiedClassName again — it
344+ // walks the FS and the result is already in `detectedClassName`).
345+ if ( detectedClassName ) {
346+ targetInfo = `${ detectedClassName } (detected from ${ input . target } )` ;
308347 } else {
309348 targetInfo = input . target ;
310349 warningNote = ' ⚠️ Note: Could not auto-detect package name. If you see "ClassNotFoundException", please provide the fully qualified class name (e.g., "com.example.App" instead of "App").' ;
@@ -362,17 +401,21 @@ async function debugJavaApplication(
362401 resolve ( {
363402 success : false ,
364403 status : 'timeout' ,
365- message : `❌ Debug session failed to start within ${ CONSTANTS . SESSION_WAIT_TIMEOUT / 1000 } seconds for ${ targetInfo } .\n\n` +
366- `This usually indicates a problem:\n` +
367- `• Compilation errors preventing startup\n` +
368- `• ClassNotFoundException or NoClassDefFoundError\n` +
369- `• Application crashed during initialization\n` +
370- `• Incorrect main class or classpath configuration\n\n` +
371- `Action required:\n` +
372- `1. Check terminal '${ terminal . name } ' for error messages\n` +
373- `2. Verify the target class name is correct\n` +
374- `3. Ensure the project is compiled successfully\n` +
375- `4. Use get_debug_session_info() to confirm session status${ warningNote } ` ,
404+ message : `⏳ Debug session not yet detected for ${ targetInfo } after `
405+ + `${ CONSTANTS . SESSION_WAIT_TIMEOUT / 1000 } seconds.\n\n`
406+ + `This is often transient — the JVM may still be starting up (large `
407+ + `projects, cold class-loading, or remote workspaces can need additional `
408+ + `time). Telemetry shows that retrying a timed-out launch succeeds for `
409+ + `the majority of cases.\n\n`
410+ + `Recommended next actions (in order):\n`
411+ + `1. Call debug_java_application again — most timeout cases recover on retry.\n`
412+ + `2. Call get_debug_session_info() to check whether the session has since `
413+ + `become active.\n`
414+ + `3. If retrying still times out, inspect terminal '${ terminal . name } ' for `
415+ + `compilation errors, ClassNotFoundException, NoClassDefFoundError, or `
416+ + `other startup failures.\n`
417+ + `4. Verify the target class name and classpath are correct, then retry.`
418+ + `${ warningNote } ` ,
376419 terminalName : terminal . name
377420 } ) ;
378421 }
@@ -425,15 +468,18 @@ async function debugJavaApplication(
425468 return {
426469 success : true ,
427470 status : 'timeout' ,
428- message : `⚠️ Debug command sent for ${ targetInfo } , but session not detected within ${ CONSTANTS . SMART_POLLING_MAX_WAIT / 1000 } seconds.\n\n` +
429- `Possible reasons:\n` +
430- `• Application is still starting (large projects may take longer)\n` +
431- `• Compilation errors (check terminal '${ terminal . name } ' for errors)\n` +
432- `• Application may have started and already terminated\n\n` +
433- `Next steps:\n` +
434- `• Use get_debug_session_info() to check if session is now active\n` +
435- `• Check terminal '${ terminal . name } ' for error messages\n` +
436- `• If starting slowly, wait a bit longer and check again${ warningNote } ` ,
471+ message : `⏳ Debug command sent for ${ targetInfo } ; session not yet detected within `
472+ + `${ CONSTANTS . SMART_POLLING_MAX_WAIT / 1000 } seconds.\n\n`
473+ + `This is often transient — the application may still be starting in terminal `
474+ + `'${ terminal . name } '. Telemetry shows that retrying or polling for status is more `
475+ + `likely to succeed than treating this as a permanent failure.\n\n`
476+ + `Recommended next actions (in order):\n`
477+ + `1. Call get_debug_session_info() to check whether the session has since become active.\n`
478+ + `2. Call debug_java_application again — most timeout cases recover on retry. `
479+ + `In the input arguments, set "waitForSession": true (JSON object syntax) to `
480+ + `extend the wait window for slow-starting apps.\n`
481+ + `3. If retrying still times out, inspect terminal '${ terminal . name } ' for compilation `
482+ + `errors or startup failures, then retry.${ warningNote } ` ,
437483 terminalName : terminal . name
438484 } ;
439485 }
@@ -674,10 +720,17 @@ async function ensureVSCodeCompilation(workspaceUri: vscode.Uri): Promise<DebugJ
674720
675721/**
676722 * Constructs the debugjava command based on input parameters.
723+ *
724+ * @param preDetectedClassName Fully-qualified class name pre-resolved by the
725+ * caller (and already reported via the `classNameDetection` telemetry event).
726+ * When non-null and the target is a simple class name, this is used instead
727+ * of re-running findFullyQualifiedClassName here. The caller is expected to
728+ * handle telemetry emission so we do not double-count detection outcomes.
677729 */
678730function constructDebugCommand (
679731 input : DebugJavaApplicationInput ,
680- projectType : 'maven' | 'gradle' | 'vscode' | 'unknown'
732+ projectType : 'maven' | 'gradle' | 'vscode' | 'unknown' ,
733+ preDetectedClassName : string | null = null
681734) : string {
682735 let command = 'debugjava' ;
683736
@@ -693,29 +746,12 @@ function constructDebugCommand(
693746 else {
694747 let className = input . target ;
695748
696- // If target doesn't contain a dot and we can find the Java file,
697- // try to detect the fully qualified class name
698- if ( ! input . target . includes ( '.' ) ) {
699- const detection = findFullyQualifiedClassName ( input . workspacePath , input . target , projectType ) ;
700- if ( detection . className !== null ) {
701- recordLaunchInternal ( {
702- name : 'classNameDetection' ,
703- projectType,
704- detected : true ,
705- } ) ;
706- className = detection . className ;
707- } else {
708- // Detection failed. Emit the structured failure event so we can
709- // distinguish "no candidate src dir" from "found file but no
710- // package" — previous boolean `detected: false` collapsed all
711- // four root causes into one bucket.
712- recordLaunchInternal ( {
713- name : 'classNameDetection.failed' ,
714- projectType,
715- strategy : detection . strategy ,
716- failureReason : detection . failureReason ,
717- } ) ;
718- }
749+ // Use the caller-supplied detection result; we deliberately do not
750+ // call findFullyQualifiedClassName a second time here (see the
751+ // dedupe note at the call site in debugJavaApplication Step 3).
752+ // Detection telemetry is owned by the caller.
753+ if ( ! input . target . includes ( '.' ) && preDetectedClassName ) {
754+ className = preDetectedClassName ;
719755 }
720756
721757 // Use provided classpath if available, otherwise infer it
@@ -732,16 +768,6 @@ function constructDebugCommand(
732768 return command ;
733769}
734770
735- /**
736- * Result of the launch-time fully-qualified class name lookup.
737- *
738- * `className` is non-null on success; on failure we expose the structured
739- * `failureReason` + `strategy` so telemetry can pinpoint why detection
740- * gave up. The boolean `detected: true / false` event was historically
741- * the only signal — it collapsed four very different root causes
742- * (sourceDirMissing / fileNotFound / parseError / noPackageDeclaration)
743- * into one bucket and made on-call triage impossible.
744- */
745771/**
746772 * Result of `findFullyQualifiedClassName` — a discriminated union so the
747773 * type system enforces that callers handle the failure case without an
0 commit comments