@probelabs/probe 0.6.0-rc269 → 0.6.0-rc271

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cjs/index.cjs CHANGED
@@ -108797,9 +108797,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
108797
108797
  }
108798
108798
  if (completionAttempted && this.completionPrompt && !options._completionPromptProcessed) {
108799
108799
  if (this.debug) {
108800
- console.log("[DEBUG] Running completion prompt for post-completion validation/review...");
108800
+ console.log("[DEBUG] Running completion prompt as continuation of current session...");
108801
108801
  }
108802
108802
  try {
108803
+ const originalResult = finalResult;
108803
108804
  if (this.tracer) {
108804
108805
  this.tracer.recordEvent("completion_prompt.started", {
108805
108806
  "completion_prompt.original_result_length": finalResult?.length || 0
@@ -108812,24 +108813,66 @@ Here is the result to review:
108812
108813
  ${finalResult}
108813
108814
  </result>
108814
108815
 
108815
- After reviewing, provide your final answer using attempt_completion.`;
108816
- const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
108817
- const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
108818
- const completionResult2 = await this.answer(completionPromptMessage, [], {
108819
- ...options,
108820
- _completionPromptProcessed: true
108821
- });
108822
- if (this._outputBuffer) {
108823
- this._outputBuffer.items = savedOutputItems;
108816
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is using attempt_completion. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix) using attempt_completion.`;
108817
+ currentMessages.push({ role: "user", content: completionPromptMessage });
108818
+ completionResult = null;
108819
+ completionAttempted = false;
108820
+ const completionMaxIterations = 5;
108821
+ const completionStreamOptions = {
108822
+ model: this.provider ? this.provider(this.model) : this.model,
108823
+ messages: this.prepareMessagesWithImages(currentMessages),
108824
+ tools: tools2,
108825
+ stopWhen: (0, import_ai4.stepCountIs)(completionMaxIterations),
108826
+ maxTokens: maxResponseTokens,
108827
+ temperature: 0.3,
108828
+ onStepFinish: ({ toolResults, text, finishReason, usage }) => {
108829
+ if (usage) {
108830
+ this.tokenCounter.recordUsage(usage);
108831
+ }
108832
+ if (options.onStream && text) {
108833
+ options.onStream(text);
108834
+ }
108835
+ if (this.debug) {
108836
+ console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
108837
+ }
108838
+ }
108839
+ };
108840
+ const providerOpts = this._buildThinkingProviderOptions(maxResponseTokens);
108841
+ if (providerOpts) {
108842
+ completionStreamOptions.providerOptions = providerOpts;
108843
+ }
108844
+ const cpResult = await this.streamTextWithRetryAndFallback(completionStreamOptions);
108845
+ const cpFinalText = await cpResult.text;
108846
+ const cpUsage = await cpResult.usage;
108847
+ if (cpUsage) {
108848
+ this.tokenCounter.recordUsage(cpUsage, cpResult.experimental_providerMetadata);
108849
+ }
108850
+ const cpMessages = await cpResult.response?.messages;
108851
+ if (cpMessages) {
108852
+ for (const msg of cpMessages) {
108853
+ currentMessages.push(msg);
108854
+ }
108855
+ }
108856
+ if (completionResult) {
108857
+ finalResult = completionResult;
108858
+ completionAttempted = true;
108859
+ } else if (cpFinalText && cpFinalText.trim().length > 0) {
108860
+ finalResult = cpFinalText;
108861
+ completionAttempted = true;
108862
+ } else {
108863
+ finalResult = originalResult;
108864
+ completionAttempted = true;
108865
+ if (this.debug) {
108866
+ console.log("[DEBUG] Completion prompt returned empty result, keeping original.");
108867
+ }
108824
108868
  }
108825
- this._extractedRawBlocks = savedExtractedBlocks;
108826
- finalResult = completionResult2;
108827
108869
  if (this.debug) {
108828
- console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
108870
+ console.log(`[DEBUG] Completion prompt finished. Final result length: ${finalResult?.length || 0}`);
108829
108871
  }
108830
108872
  if (this.tracer) {
108831
108873
  this.tracer.recordEvent("completion_prompt.completed", {
108832
- "completion_prompt.final_result_length": finalResult?.length || 0
108874
+ "completion_prompt.final_result_length": finalResult?.length || 0,
108875
+ "completion_prompt.used_original": finalResult === originalResult
108833
108876
  });
108834
108877
  }
108835
108878
  } catch (error2) {
@@ -110587,6 +110630,17 @@ function parseDelegatedTargets(rawResponse) {
110587
110630
  }
110588
110631
  return normalizeTargets(fallbackTargetsFromText(trimmed));
110589
110632
  }
110633
+ function splitTargetSuffix(target) {
110634
+ const searchStart = target.length > 2 && target[1] === ":" && /[a-zA-Z]/.test(target[0]) ? 2 : 0;
110635
+ const colonIdx = target.indexOf(":", searchStart);
110636
+ const hashIdx = target.indexOf("#");
110637
+ if (colonIdx !== -1 && (hashIdx === -1 || colonIdx < hashIdx)) {
110638
+ return { filePart: target.substring(0, colonIdx), suffix: target.substring(colonIdx) };
110639
+ } else if (hashIdx !== -1) {
110640
+ return { filePart: target.substring(0, hashIdx), suffix: target.substring(hashIdx) };
110641
+ }
110642
+ return { filePart: target, suffix: "" };
110643
+ }
110590
110644
  function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, allowTests }) {
110591
110645
  return [
110592
110646
  "You are a code-search subagent. Your job is to find ALL relevant code locations for the given query.",
@@ -110615,7 +110669,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
110615
110669
  "Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets."
110616
110670
  ].join("\n");
110617
110671
  }
110618
- var import_ai5, CODE_SEARCH_SCHEMA, searchTool, queryTool, extractTool, delegateTool, analyzeAllTool;
110672
+ var import_ai5, import_fs11, CODE_SEARCH_SCHEMA, searchTool, queryTool, extractTool, delegateTool, analyzeAllTool;
110619
110673
  var init_vercel = __esm({
110620
110674
  "src/tools/vercel.js"() {
110621
110675
  "use strict";
@@ -110626,6 +110680,7 @@ var init_vercel = __esm({
110626
110680
  init_delegate();
110627
110681
  init_analyzeAll();
110628
110682
  init_common2();
110683
+ import_fs11 = require("fs");
110629
110684
  init_error_types();
110630
110685
  init_hashline();
110631
110686
  CODE_SEARCH_SCHEMA = {
@@ -110751,10 +110806,47 @@ var init_vercel = __esm({
110751
110806
  }
110752
110807
  return fallbackResult;
110753
110808
  }
110809
+ const delegateBase = options.allowedFolders?.[0] || options.cwd || ".";
110754
110810
  const resolutionBase = searchPaths[0] || options.cwd || ".";
110755
- const resolvedTargets = targets.map((target) => resolveTargetPath(target, resolutionBase));
110811
+ const resolvedTargets = targets.map((target) => resolveTargetPath(target, delegateBase));
110812
+ const validatedTargets = [];
110813
+ for (const target of resolvedTargets) {
110814
+ const { filePart, suffix } = splitTargetSuffix(target);
110815
+ if ((0, import_fs11.existsSync)(filePart)) {
110816
+ validatedTargets.push(target);
110817
+ continue;
110818
+ }
110819
+ let fixed = false;
110820
+ const parts = filePart.split("/").filter(Boolean);
110821
+ for (let i5 = 0; i5 < parts.length - 1; i5++) {
110822
+ if (parts[i5] === parts[i5 + 1]) {
110823
+ const candidate = "/" + [...parts.slice(0, i5), ...parts.slice(i5 + 1)].join("/");
110824
+ if ((0, import_fs11.existsSync)(candidate)) {
110825
+ validatedTargets.push(candidate + suffix);
110826
+ if (debug) console.error(`[search-delegate] Fixed doubled path segment: ${filePart} \u2192 ${candidate}`);
110827
+ fixed = true;
110828
+ break;
110829
+ }
110830
+ }
110831
+ }
110832
+ if (fixed) continue;
110833
+ for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
110834
+ if (altBase === delegateBase) continue;
110835
+ const altResolved = resolveTargetPath(target, altBase);
110836
+ const { filePart: altFile } = splitTargetSuffix(altResolved);
110837
+ if ((0, import_fs11.existsSync)(altFile)) {
110838
+ validatedTargets.push(altResolved);
110839
+ if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} \u2192 ${altFile}`);
110840
+ fixed = true;
110841
+ break;
110842
+ }
110843
+ }
110844
+ if (fixed) continue;
110845
+ if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
110846
+ validatedTargets.push(target);
110847
+ }
110756
110848
  const extractOptions = {
110757
- files: resolvedTargets,
110849
+ files: validatedTargets,
110758
110850
  cwd: resolutionBase,
110759
110851
  allowTests: allow_tests ?? true
110760
110852
  };
@@ -111307,7 +111399,7 @@ Example: <edit><file_path>${file_path}</file_path><symbol>${allMatches[0].qualif
111307
111399
  Example: <extract><targets>${file_path}#${symbol15}</targets></extract>`;
111308
111400
  }
111309
111401
  }
111310
- const content = await import_fs11.promises.readFile(resolvedPath2, "utf-8");
111402
+ const content = await import_fs12.promises.readFile(resolvedPath2, "utf-8");
111311
111403
  const lines = content.split("\n");
111312
111404
  if (position) {
111313
111405
  const refIndent = detectBaseIndent(symbolInfo.code);
@@ -111318,7 +111410,7 @@ Example: <extract><targets>${file_path}#${symbol15}</targets></extract>`;
111318
111410
  } else {
111319
111411
  lines.splice(symbolInfo.startLine - 1, 0, ...newLines, "");
111320
111412
  }
111321
- await import_fs11.promises.writeFile(resolvedPath2, lines.join("\n"), "utf-8");
111413
+ await import_fs12.promises.writeFile(resolvedPath2, lines.join("\n"), "utf-8");
111322
111414
  if (fileTracker) {
111323
111415
  const updated = await findSymbol(resolvedPath2, symbol15, cwd || process.cwd());
111324
111416
  if (updated) {
@@ -111336,7 +111428,7 @@ Example: <extract><targets>${file_path}#${symbol15}</targets></extract>`;
111336
111428
  const reindented = reindent(new_string, originalIndent);
111337
111429
  const newLines = reindented.split("\n");
111338
111430
  lines.splice(symbolInfo.startLine - 1, symbolInfo.endLine - symbolInfo.startLine + 1, ...newLines);
111339
- await import_fs11.promises.writeFile(resolvedPath2, lines.join("\n"), "utf-8");
111431
+ await import_fs12.promises.writeFile(resolvedPath2, lines.join("\n"), "utf-8");
111340
111432
  if (fileTracker) {
111341
111433
  const updated = await findSymbol(resolvedPath2, symbol15, cwd || process.cwd());
111342
111434
  if (updated) {
@@ -111391,7 +111483,7 @@ async function handleLineEdit({ resolvedPath: resolvedPath2, file_path, start_li
111391
111483
  if (position !== void 0 && position !== null && position !== "before" && position !== "after") {
111392
111484
  return 'Error editing file: Invalid position - must be "before" or "after". Use position="before" to insert before the line, or position="after" to insert after it.';
111393
111485
  }
111394
- const content = await import_fs11.promises.readFile(resolvedPath2, "utf-8");
111486
+ const content = await import_fs12.promises.readFile(resolvedPath2, "utf-8");
111395
111487
  const fileLines = content.split("\n");
111396
111488
  if (startLine > fileLines.length) {
111397
111489
  return `Error editing file: Line ${startLine} is beyond file length (${fileLines.length} lines). Use 'extract' to read the current file content.`;
@@ -111420,20 +111512,20 @@ async function handleLineEdit({ resolvedPath: resolvedPath2, file_path, start_li
111420
111512
  const newLines = cleaned === "" ? [] : cleaned.split("\n");
111421
111513
  if (position === "after") {
111422
111514
  fileLines.splice(startLine, 0, ...newLines);
111423
- await import_fs11.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111515
+ await import_fs12.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111424
111516
  if (fileTracker) await fileTracker.trackFileAfterWrite(resolvedPath2);
111425
111517
  const action = `${newLines.length} line${newLines.length !== 1 ? "s" : ""} inserted after line ${startLine}`;
111426
111518
  return buildLineEditResponse(file_path, startLine, startLine, newLines.length, fileLines, startLine, action, modifications);
111427
111519
  } else if (position === "before") {
111428
111520
  fileLines.splice(startLine - 1, 0, ...newLines);
111429
- await import_fs11.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111521
+ await import_fs12.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111430
111522
  if (fileTracker) await fileTracker.trackFileAfterWrite(resolvedPath2);
111431
111523
  const action = `${newLines.length} line${newLines.length !== 1 ? "s" : ""} inserted before line ${startLine}`;
111432
111524
  return buildLineEditResponse(file_path, startLine, startLine, newLines.length, fileLines, startLine - 1, action, modifications);
111433
111525
  } else {
111434
111526
  const replacedCount = endLine - startLine + 1;
111435
111527
  fileLines.splice(startLine - 1, replacedCount, ...newLines);
111436
- await import_fs11.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111528
+ await import_fs12.promises.writeFile(resolvedPath2, fileLines.join("\n"), "utf-8");
111437
111529
  if (fileTracker) await fileTracker.trackFileAfterWrite(resolvedPath2);
111438
111530
  let action;
111439
111531
  if (newLines.length === 0) {
@@ -111446,14 +111538,14 @@ async function handleLineEdit({ resolvedPath: resolvedPath2, file_path, start_li
111446
111538
  return buildLineEditResponse(file_path, startLine, endLine, newLines.length, fileLines, startLine - 1, action, modifications);
111447
111539
  }
111448
111540
  }
111449
- var import_ai6, import_fs11, import_path16, import_fs12, editTool, createTool, multiEditTool, editSchema, createSchema, multiEditSchema, editDescription, createDescription, multiEditDescription, editToolDefinition, createToolDefinition, multiEditToolDefinition;
111541
+ var import_ai6, import_fs12, import_path16, import_fs13, editTool, createTool, multiEditTool, editSchema, createSchema, multiEditSchema, editDescription, createDescription, multiEditDescription, editToolDefinition, createToolDefinition, multiEditToolDefinition;
111450
111542
  var init_edit = __esm({
111451
111543
  "src/tools/edit.js"() {
111452
111544
  "use strict";
111453
111545
  import_ai6 = require("ai");
111454
- import_fs11 = require("fs");
111455
- import_path16 = require("path");
111456
111546
  import_fs12 = require("fs");
111547
+ import_path16 = require("path");
111548
+ import_fs13 = require("fs");
111457
111549
  init_path_validation();
111458
111550
  init_fuzzyMatch();
111459
111551
  init_symbolEdit();
@@ -111537,7 +111629,7 @@ Parameters:
111537
111629
  const relativePath = toRelativePath(resolvedPath2, workspaceRoot);
111538
111630
  return `Error editing file: Permission denied - ${relativePath} is outside allowed directories. Use a file path within the project workspace.`;
111539
111631
  }
111540
- if (!(0, import_fs12.existsSync)(resolvedPath2)) {
111632
+ if (!(0, import_fs13.existsSync)(resolvedPath2)) {
111541
111633
  return `Error editing file: File not found - ${file_path}. Verify the path is correct and the file exists. Use 'search' to find files by name, or 'create' to make a new file.`;
111542
111634
  }
111543
111635
  if (options.fileTracker && !options.fileTracker.isFileSeen(resolvedPath2)) {
@@ -111567,7 +111659,7 @@ Example: <extract><targets>${displayPath}</targets></extract>`;
111567
111659
  Example: <extract><targets>${displayPath}</targets></extract>`;
111568
111660
  }
111569
111661
  }
111570
- const content = await import_fs11.promises.readFile(resolvedPath2, "utf-8");
111662
+ const content = await import_fs12.promises.readFile(resolvedPath2, "utf-8");
111571
111663
  let matchTarget = old_string;
111572
111664
  let matchStrategy = "exact";
111573
111665
  if (!content.includes(old_string)) {
@@ -111599,7 +111691,7 @@ Example: <extract><targets>${displayPath}</targets></extract>`;
111599
111691
  if (newContent === content) {
111600
111692
  return `Error editing file: No changes made - the replacement result is identical to the original. Verify that old_string and new_string are actually different. If fuzzy matching was used, the matched text may already equal new_string.`;
111601
111693
  }
111602
- await import_fs11.promises.writeFile(resolvedPath2, newContent, "utf-8");
111694
+ await import_fs12.promises.writeFile(resolvedPath2, newContent, "utf-8");
111603
111695
  if (options.fileTracker) {
111604
111696
  await options.fileTracker.trackFileAfterWrite(resolvedPath2);
111605
111697
  options.fileTracker.recordTextEdit(resolvedPath2);
@@ -111670,13 +111762,13 @@ Important:
111670
111762
  const relativePath = toRelativePath(resolvedPath2, workspaceRoot);
111671
111763
  return `Error creating file: Permission denied - ${relativePath} is outside allowed directories. Use a file path within the project workspace.`;
111672
111764
  }
111673
- if ((0, import_fs12.existsSync)(resolvedPath2) && !overwrite) {
111765
+ if ((0, import_fs13.existsSync)(resolvedPath2) && !overwrite) {
111674
111766
  return `Error creating file: File already exists - ${file_path}. Use overwrite: true to replace it.`;
111675
111767
  }
111676
- const existed = (0, import_fs12.existsSync)(resolvedPath2);
111768
+ const existed = (0, import_fs13.existsSync)(resolvedPath2);
111677
111769
  const dir = (0, import_path16.dirname)(resolvedPath2);
111678
- await import_fs11.promises.mkdir(dir, { recursive: true });
111679
- await import_fs11.promises.writeFile(resolvedPath2, content, "utf-8");
111770
+ await import_fs12.promises.mkdir(dir, { recursive: true });
111771
+ await import_fs12.promises.writeFile(resolvedPath2, content, "utf-8");
111680
111772
  if (options.fileTracker) await options.fileTracker.trackFileAfterWrite(resolvedPath2);
111681
111773
  const action = existed && overwrite ? "overwrote" : "created";
111682
111774
  const bytes = Buffer.byteLength(content, "utf-8");
@@ -112318,10 +112410,10 @@ async function listFilesByLevel(options) {
112318
112410
  maxFiles = 100,
112319
112411
  respectGitignore = true
112320
112412
  } = options;
112321
- if (!import_fs13.default.existsSync(directory)) {
112413
+ if (!import_fs14.default.existsSync(directory)) {
112322
112414
  throw new Error(`Directory does not exist: ${directory}`);
112323
112415
  }
112324
- const gitDirExists = import_fs13.default.existsSync(import_path17.default.join(directory, ".git"));
112416
+ const gitDirExists = import_fs14.default.existsSync(import_path17.default.join(directory, ".git"));
112325
112417
  if (gitDirExists && respectGitignore) {
112326
112418
  try {
112327
112419
  return await listFilesUsingGit(directory, maxFiles);
@@ -112352,7 +112444,7 @@ async function listFilesByLevelManually(directory, maxFiles, respectGitignore) {
112352
112444
  while (queue.length > 0 && result.length < maxFiles) {
112353
112445
  const { dir, level } = queue.shift();
112354
112446
  try {
112355
- const entries = import_fs13.default.readdirSync(dir, { withFileTypes: true });
112447
+ const entries = import_fs14.default.readdirSync(dir, { withFileTypes: true });
112356
112448
  const files = entries.filter((entry) => {
112357
112449
  const fullPath = import_path17.default.join(dir, entry.name);
112358
112450
  return getEntryTypeSync(entry, fullPath).isFile;
@@ -112383,11 +112475,11 @@ async function listFilesByLevelManually(directory, maxFiles, respectGitignore) {
112383
112475
  }
112384
112476
  function loadGitignorePatterns(directory) {
112385
112477
  const gitignorePath = import_path17.default.join(directory, ".gitignore");
112386
- if (!import_fs13.default.existsSync(gitignorePath)) {
112478
+ if (!import_fs14.default.existsSync(gitignorePath)) {
112387
112479
  return [];
112388
112480
  }
112389
112481
  try {
112390
- const content = import_fs13.default.readFileSync(gitignorePath, "utf8");
112482
+ const content = import_fs14.default.readFileSync(gitignorePath, "utf8");
112391
112483
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
112392
112484
  } catch (error2) {
112393
112485
  console.error(`Warning: Could not read .gitignore: ${error2.message}`);
@@ -112405,11 +112497,11 @@ function shouldIgnore(filePath, ignorePatterns) {
112405
112497
  }
112406
112498
  return false;
112407
112499
  }
112408
- var import_fs13, import_path17, import_util12, import_child_process10, execAsync3;
112500
+ var import_fs14, import_path17, import_util12, import_child_process10, execAsync3;
112409
112501
  var init_file_lister = __esm({
112410
112502
  "src/utils/file-lister.js"() {
112411
112503
  "use strict";
112412
- import_fs13 = __toESM(require("fs"), 1);
112504
+ import_fs14 = __toESM(require("fs"), 1);
112413
112505
  import_path17 = __toESM(require("path"), 1);
112414
112506
  import_util12 = require("util");
112415
112507
  import_child_process10 = require("child_process");
@@ -112428,11 +112520,11 @@ function initializeSimpleTelemetryFromOptions(options) {
112428
112520
  });
112429
112521
  return telemetry;
112430
112522
  }
112431
- var import_fs14, import_path18, SimpleTelemetry, SimpleAppTracer;
112523
+ var import_fs15, import_path18, SimpleTelemetry, SimpleAppTracer;
112432
112524
  var init_simpleTelemetry = __esm({
112433
112525
  "src/agent/simpleTelemetry.js"() {
112434
112526
  "use strict";
112435
- import_fs14 = require("fs");
112527
+ import_fs15 = require("fs");
112436
112528
  import_path18 = require("path");
112437
112529
  SimpleTelemetry = class {
112438
112530
  constructor(options = {}) {
@@ -112448,10 +112540,10 @@ var init_simpleTelemetry = __esm({
112448
112540
  initializeFileExporter() {
112449
112541
  try {
112450
112542
  const dir = (0, import_path18.dirname)(this.filePath);
112451
- if (!(0, import_fs14.existsSync)(dir)) {
112452
- (0, import_fs14.mkdirSync)(dir, { recursive: true });
112543
+ if (!(0, import_fs15.existsSync)(dir)) {
112544
+ (0, import_fs15.mkdirSync)(dir, { recursive: true });
112453
112545
  }
112454
- this.stream = (0, import_fs14.createWriteStream)(this.filePath, { flags: "a" });
112546
+ this.stream = (0, import_fs15.createWriteStream)(this.filePath, { flags: "a" });
112455
112547
  this.stream.on("error", (error2) => {
112456
112548
  console.error(`[SimpleTelemetry] Stream error: ${error2.message}`);
112457
112549
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc269",
3
+ "version": "0.6.0-rc271",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -3582,23 +3582,24 @@ Follow these instructions carefully:
3582
3582
  // Continue even if storage fails
3583
3583
  }
3584
3584
 
3585
- // Completion prompt handling - run a follow-up prompt after attempt_completion for validation/review
3586
- // This runs BEFORE mermaid validation and JSON schema validation
3587
- // Skip if we're already in a completion prompt follow-up call or if no completion prompt is configured
3585
+ // Completion prompt handling - inject one more user message into the existing conversation
3586
+ // This continues the SAME agentic session (same tools, same TaskManager, same history)
3587
+ // rather than spawning a recursive this.answer() call which would reset state
3588
3588
  if (completionAttempted && this.completionPrompt && !options._completionPromptProcessed) {
3589
3589
  if (this.debug) {
3590
- console.log('[DEBUG] Running completion prompt for post-completion validation/review...');
3590
+ console.log('[DEBUG] Running completion prompt as continuation of current session...');
3591
3591
  }
3592
3592
 
3593
3593
  try {
3594
- // Record completion prompt start in telemetry
3594
+ const originalResult = finalResult;
3595
+
3595
3596
  if (this.tracer) {
3596
3597
  this.tracer.recordEvent('completion_prompt.started', {
3597
3598
  'completion_prompt.original_result_length': finalResult?.length || 0
3598
3599
  });
3599
3600
  }
3600
3601
 
3601
- // Create the completion prompt with the current result as context
3602
+ // Append completion prompt as a user message to the existing conversation
3602
3603
  const completionPromptMessage = `${this.completionPrompt}
3603
3604
 
3604
3605
  Here is the result to review:
@@ -3606,34 +3607,82 @@ Here is the result to review:
3606
3607
  ${finalResult}
3607
3608
  </result>
3608
3609
 
3609
- After reviewing, provide your final answer using attempt_completion.`;
3610
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is using attempt_completion. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix) using attempt_completion.`;
3610
3611
 
3611
- // Make a follow-up call with the completion prompt
3612
- // Pass _completionPromptProcessed to prevent infinite loops
3613
- // Save output buffers the recursive answer() must not destroy DSL output() content
3614
- const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
3615
- const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
3616
- const completionResult = await this.answer(completionPromptMessage, [], {
3617
- ...options,
3618
- _completionPromptProcessed: true
3619
- });
3620
- // Restore output buffers so the parent call can append them to the final result
3621
- if (this._outputBuffer) {
3622
- this._outputBuffer.items = savedOutputItems;
3612
+ currentMessages.push({ role: 'user', content: completionPromptMessage });
3613
+
3614
+ // Reset completion tracking for the follow-up turn
3615
+ completionResult = null;
3616
+ completionAttempted = false;
3617
+
3618
+ // Run one more streamText pass with the same tools and conversation context
3619
+ // Give a small number of extra iterations for the follow-up
3620
+ const completionMaxIterations = 5;
3621
+ const completionStreamOptions = {
3622
+ model: this.provider ? this.provider(this.model) : this.model,
3623
+ messages: this.prepareMessagesWithImages(currentMessages),
3624
+ tools,
3625
+ stopWhen: stepCountIs(completionMaxIterations),
3626
+ maxTokens: maxResponseTokens,
3627
+ temperature: 0.3,
3628
+ onStepFinish: ({ toolResults, text, finishReason, usage }) => {
3629
+ if (usage) {
3630
+ this.tokenCounter.recordUsage(usage);
3631
+ }
3632
+ if (options.onStream && text) {
3633
+ options.onStream(text);
3634
+ }
3635
+ if (this.debug) {
3636
+ console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
3637
+ }
3638
+ }
3639
+ };
3640
+
3641
+ const providerOpts = this._buildThinkingProviderOptions(maxResponseTokens);
3642
+ if (providerOpts) {
3643
+ completionStreamOptions.providerOptions = providerOpts;
3644
+ }
3645
+
3646
+ const cpResult = await this.streamTextWithRetryAndFallback(completionStreamOptions);
3647
+ const cpFinalText = await cpResult.text;
3648
+ const cpUsage = await cpResult.usage;
3649
+ if (cpUsage) {
3650
+ this.tokenCounter.recordUsage(cpUsage, cpResult.experimental_providerMetadata);
3651
+ }
3652
+
3653
+ // Append follow-up messages to conversation history
3654
+ const cpMessages = await cpResult.response?.messages;
3655
+ if (cpMessages) {
3656
+ for (const msg of cpMessages) {
3657
+ currentMessages.push(msg);
3658
+ }
3623
3659
  }
3624
- this._extractedRawBlocks = savedExtractedBlocks;
3625
3660
 
3626
- // Update finalResult with the result from the completion prompt
3627
- finalResult = completionResult;
3661
+ // Use new completion result if the agent called attempt_completion again,
3662
+ // otherwise keep the original result (the follow-up may have just done side-effects)
3663
+ if (completionResult) {
3664
+ finalResult = completionResult;
3665
+ completionAttempted = true;
3666
+ } else if (cpFinalText && cpFinalText.trim().length > 0) {
3667
+ finalResult = cpFinalText;
3668
+ completionAttempted = true;
3669
+ } else {
3670
+ // Follow-up produced nothing useful — keep the original
3671
+ finalResult = originalResult;
3672
+ completionAttempted = true;
3673
+ if (this.debug) {
3674
+ console.log('[DEBUG] Completion prompt returned empty result, keeping original.');
3675
+ }
3676
+ }
3628
3677
 
3629
3678
  if (this.debug) {
3630
- console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
3679
+ console.log(`[DEBUG] Completion prompt finished. Final result length: ${finalResult?.length || 0}`);
3631
3680
  }
3632
3681
 
3633
- // Record completion prompt completion in telemetry
3634
3682
  if (this.tracer) {
3635
3683
  this.tracer.recordEvent('completion_prompt.completed', {
3636
- 'completion_prompt.final_result_length': finalResult?.length || 0
3684
+ 'completion_prompt.final_result_length': finalResult?.length || 0,
3685
+ 'completion_prompt.used_original': finalResult === originalResult
3637
3686
  });
3638
3687
  }
3639
3688
  } catch (error) {
@@ -10,6 +10,7 @@ import { extract } from '../extract.js';
10
10
  import { delegate } from '../delegate.js';
11
11
  import { analyzeAll } from './analyzeAll.js';
12
12
  import { searchSchema, querySchema, extractSchema, delegateSchema, analyzeAllSchema, searchDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, parseTargets, parseAndResolvePaths, resolveTargetPath } from './common.js';
13
+ import { existsSync } from 'fs';
13
14
  import { formatErrorForAI } from '../utils/error-types.js';
14
15
  import { annotateOutputWithHashes } from './hashline.js';
15
16
 
@@ -118,6 +119,18 @@ function parseDelegatedTargets(rawResponse) {
118
119
  return normalizeTargets(fallbackTargetsFromText(trimmed));
119
120
  }
120
121
 
122
+ function splitTargetSuffix(target) {
123
+ const searchStart = (target.length > 2 && target[1] === ':' && /[a-zA-Z]/.test(target[0])) ? 2 : 0;
124
+ const colonIdx = target.indexOf(':', searchStart);
125
+ const hashIdx = target.indexOf('#');
126
+ if (colonIdx !== -1 && (hashIdx === -1 || colonIdx < hashIdx)) {
127
+ return { filePart: target.substring(0, colonIdx), suffix: target.substring(colonIdx) };
128
+ } else if (hashIdx !== -1) {
129
+ return { filePart: target.substring(0, hashIdx), suffix: target.substring(hashIdx) };
130
+ }
131
+ return { filePart: target, suffix: '' };
132
+ }
133
+
121
134
  function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, allowTests }) {
122
135
  return [
123
136
  'You are a code-search subagent. Your job is to find ALL relevant code locations for the given query.',
@@ -286,13 +299,61 @@ export const searchTool = (options = {}) => {
286
299
  return fallbackResult;
287
300
  }
288
301
 
289
- // Resolve relative paths against the actual search directory, not the general cwd.
290
- // The delegate returns paths relative to where the search was performed (searchPaths[0]),
291
- // which may differ from options.cwd when the user specifies a path parameter.
302
+ // The delegate runs from workspace root (allowedFolders[0] or cwd), NOT from searchPaths[0].
303
+ // It returns paths relative to that workspace root. Resolve against the same base.
304
+ const delegateBase = options.allowedFolders?.[0] || options.cwd || '.';
292
305
  const resolutionBase = searchPaths[0] || options.cwd || '.';
293
- const resolvedTargets = targets.map(target => resolveTargetPath(target, resolutionBase));
306
+ const resolvedTargets = targets.map(target => resolveTargetPath(target, delegateBase));
307
+
308
+ // Auto-fix: detect and repair invalid paths (doubled segments, AI hallucinations)
309
+ const validatedTargets = [];
310
+ for (const target of resolvedTargets) {
311
+ const { filePart, suffix } = splitTargetSuffix(target);
312
+
313
+ // 1. Path exists as-is
314
+ if (existsSync(filePart)) {
315
+ validatedTargets.push(target);
316
+ continue;
317
+ }
318
+
319
+ // 2. Detect doubled directory segments: /ws/proj/proj/src → /ws/proj/src
320
+ let fixed = false;
321
+ const parts = filePart.split('/').filter(Boolean);
322
+ for (let i = 0; i < parts.length - 1; i++) {
323
+ if (parts[i] === parts[i + 1]) {
324
+ const candidate = '/' + [...parts.slice(0, i), ...parts.slice(i + 1)].join('/');
325
+ if (existsSync(candidate)) {
326
+ validatedTargets.push(candidate + suffix);
327
+ if (debug) console.error(`[search-delegate] Fixed doubled path segment: ${filePart} → ${candidate}`);
328
+ fixed = true;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ if (fixed) continue;
334
+
335
+ // 3. Try resolving against alternative bases (searchPaths[0], cwd)
336
+ for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
337
+ if (altBase === delegateBase) continue;
338
+ const altResolved = resolveTargetPath(target, altBase);
339
+ const { filePart: altFile } = splitTargetSuffix(altResolved);
340
+ if (existsSync(altFile)) {
341
+ validatedTargets.push(altResolved);
342
+ if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} → ${altFile}`);
343
+ fixed = true;
344
+ break;
345
+ }
346
+ }
347
+ if (fixed) continue;
348
+
349
+ // 4. Keep target anyway (probe binary will report the error)
350
+ // but log a warning
351
+ if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
352
+ validatedTargets.push(target);
353
+ }
354
+
294
355
  const extractOptions = {
295
- files: resolvedTargets,
356
+ files: validatedTargets,
296
357
  cwd: resolutionBase,
297
358
  allowTests: allow_tests ?? true
298
359
  };