Skip to content

Commit 73d3eb2

Browse files
author
Copilot CLI
committed
Merge remote-tracking branch 'origin/main' into feat/lmt-telemetry-diagnostic-fields
# Conflicts: # src/languageModelTool.ts
2 parents 0f4f372 + 809056d commit 73d3eb2

1 file changed

Lines changed: 89 additions & 63 deletions

File tree

src/languageModelTool.ts

Lines changed: 89 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
678730
function 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

Comments
 (0)