@probelabs/probe 0.6.0-rc208 → 0.6.0-rc210

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.
Files changed (30) hide show
  1. package/bin/binaries/probe-v0.6.0-rc210-aarch64-apple-darwin.tar.gz +0 -0
  2. package/bin/binaries/probe-v0.6.0-rc210-aarch64-unknown-linux-musl.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc210-x86_64-apple-darwin.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc210-x86_64-pc-windows-msvc.zip +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc210-x86_64-unknown-linux-musl.tar.gz +0 -0
  6. package/build/agent/ProbeAgent.d.ts +4 -2
  7. package/build/agent/ProbeAgent.js +87 -5
  8. package/build/agent/bashCommandUtils.js +98 -12
  9. package/build/agent/bashPermissions.js +207 -1
  10. package/build/agent/index.js +283 -23
  11. package/build/agent/mcp/client.js +2 -1
  12. package/build/delegate.js +11 -2
  13. package/build/tools/vercel.js +5 -2
  14. package/cjs/agent/ProbeAgent.cjs +277 -17
  15. package/cjs/index.cjs +277 -17
  16. package/index.d.ts +4 -2
  17. package/package.json +1 -1
  18. package/src/agent/ProbeAgent.d.ts +4 -2
  19. package/src/agent/ProbeAgent.js +87 -5
  20. package/src/agent/bashCommandUtils.js +98 -12
  21. package/src/agent/bashPermissions.js +207 -1
  22. package/src/agent/index.js +5 -5
  23. package/src/agent/mcp/client.js +2 -1
  24. package/src/delegate.js +11 -2
  25. package/src/tools/vercel.js +5 -2
  26. package/bin/binaries/probe-v0.6.0-rc208-aarch64-apple-darwin.tar.gz +0 -0
  27. package/bin/binaries/probe-v0.6.0-rc208-aarch64-unknown-linux-musl.tar.gz +0 -0
  28. package/bin/binaries/probe-v0.6.0-rc208-x86_64-apple-darwin.tar.gz +0 -0
  29. package/bin/binaries/probe-v0.6.0-rc208-x86_64-pc-windows-msvc.zip +0 -0
  30. package/bin/binaries/probe-v0.6.0-rc208-x86_64-unknown-linux-musl.tar.gz +0 -0
package/cjs/index.cjs CHANGED
@@ -82879,7 +82879,7 @@ var init_client2 = __esm({
82879
82879
  clientInfo.client.callTool({
82880
82880
  name: tool4.originalName,
82881
82881
  arguments: args
82882
- }),
82882
+ }, void 0, { timeout }),
82883
82883
  timeoutPromise
82884
82884
  ]);
82885
82885
  const durationMs = Date.now() - startTime;
@@ -93433,7 +93433,7 @@ var init_ProbeAgent = __esm({
93433
93433
  this.maxIterations = options.maxIterations || null;
93434
93434
  this.disableMermaidValidation = !!options.disableMermaidValidation;
93435
93435
  this.disableJsonValidation = !!options.disableJsonValidation;
93436
- this.enableSkills = options.disableSkills ? false : options.enableSkills !== void 0 ? !!options.enableSkills : true;
93436
+ this.enableSkills = options.disableSkills ? false : !!(options.allowSkills || options.enableSkills);
93437
93437
  if (Array.isArray(options.skillDirs)) {
93438
93438
  this.skillDirs = options.skillDirs;
93439
93439
  } else if (typeof options.skillDirs === "string") {
@@ -95328,6 +95328,9 @@ You are working with a repository located at: ${searchDirectory}
95328
95328
  let lastFormatErrorType = null;
95329
95329
  let sameFormatErrorCount = 0;
95330
95330
  const MAX_REPEATED_FORMAT_ERRORS = 3;
95331
+ let lastNoToolResponse = null;
95332
+ let sameResponseCount = 0;
95333
+ const MAX_REPEATED_IDENTICAL_RESPONSES = 3;
95331
95334
  while (currentIteration < maxIterations && !completionAttempted) {
95332
95335
  currentIteration++;
95333
95336
  if (this.cancelled) throw new Error("Request was cancelled by the user");
@@ -95809,6 +95812,26 @@ ${errorXml}
95809
95812
  }
95810
95813
  break;
95811
95814
  }
95815
+ if (lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse) {
95816
+ sameResponseCount++;
95817
+ if (sameResponseCount >= MAX_REPEATED_IDENTICAL_RESPONSES) {
95818
+ let cleanedResponse = assistantResponseContent;
95819
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
95820
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, "").trim();
95821
+ const hasSubstantialContent = cleanedResponse.length > 50 && !cleanedResponse.includes("<api_call>") && !cleanedResponse.includes("<tool_name>") && !cleanedResponse.includes("<function>");
95822
+ if (hasSubstantialContent) {
95823
+ if (this.debug) {
95824
+ console.log(`[DEBUG] Same response repeated ${sameResponseCount} times - accepting as final answer (${cleanedResponse.length} chars)`);
95825
+ }
95826
+ finalResult = cleanedResponse;
95827
+ completionAttempted = true;
95828
+ break;
95829
+ }
95830
+ }
95831
+ } else {
95832
+ lastNoToolResponse = assistantResponseContent;
95833
+ sameResponseCount = 1;
95834
+ }
95812
95835
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
95813
95836
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
95814
95837
  let reminderContent;
@@ -95881,10 +95904,35 @@ Or if your previous response already contains a complete, direct answer (not a t
95881
95904
 
95882
95905
  Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
95883
95906
  }
95884
- currentMessages.push({
95885
- role: "user",
95886
- content: reminderContent
95887
- });
95907
+ const prevUserMsgIndex = currentMessages.length - 2;
95908
+ const prevUserMsg = currentMessages[prevUserMsgIndex];
95909
+ const isExistingReminder = prevUserMsg && prevUserMsg.role === "user" && (prevUserMsg.content.includes("Please use one of the available tools") || prevUserMsg.content.includes("<tool_result>"));
95910
+ if (isExistingReminder && sameResponseCount > 1) {
95911
+ const prevAssistantIndex = prevUserMsgIndex - 1;
95912
+ const hasSystemMessage2 = currentMessages.length > 0 && currentMessages[0].role === "system";
95913
+ const minValidIndex = hasSystemMessage2 ? 1 : 0;
95914
+ const canSafelyRemove = prevAssistantIndex >= minValidIndex && currentMessages[prevAssistantIndex] && currentMessages[prevAssistantIndex].role === "assistant" && currentMessages.length - 2 >= (hasSystemMessage2 ? 2 : 1);
95915
+ if (canSafelyRemove) {
95916
+ currentMessages.splice(prevAssistantIndex, 2);
95917
+ if (this.debug) {
95918
+ console.log(`[DEBUG] Removed duplicate assistant+reminder pair (iteration ${currentIteration}, same response #${sameResponseCount})`);
95919
+ }
95920
+ } else if (this.debug) {
95921
+ console.log(`[DEBUG] Skipped deduplication: pattern validation failed (prevAssistantIndex=${prevAssistantIndex}, arrayLength=${currentMessages.length})`);
95922
+ }
95923
+ const iterationHint = `
95924
+
95925
+ (Attempt #${sameResponseCount}: Your previous ${sameResponseCount} responses were identical. If you have a complete answer, use <attempt_complete></attempt_complete> to finalize it.)`;
95926
+ currentMessages.push({
95927
+ role: "user",
95928
+ content: reminderContent + iterationHint
95929
+ });
95930
+ } else {
95931
+ currentMessages.push({
95932
+ role: "user",
95933
+ content: reminderContent
95934
+ });
95935
+ }
95888
95936
  if (this.debug) {
95889
95937
  if (unrecognizedTool) {
95890
95938
  console.log(`[DEBUG] Unrecognized tool '${unrecognizedTool}' used. Providing error feedback.`);
@@ -96656,7 +96704,10 @@ async function delegate({
96656
96704
  disableTools = false,
96657
96705
  searchDelegate = void 0,
96658
96706
  schema = null,
96659
- enableTasks = false
96707
+ enableTasks = false,
96708
+ enableMcp = false,
96709
+ mcpConfig = null,
96710
+ mcpConfigPath = null
96660
96711
  }) {
96661
96712
  if (!task || typeof task !== "string") {
96662
96713
  throw new Error("Task parameter is required and must be a string");
@@ -96712,8 +96763,14 @@ async function delegate({
96712
96763
  allowedTools,
96713
96764
  disableTools,
96714
96765
  searchDelegate,
96715
- enableTasks
96766
+ enableTasks,
96716
96767
  // Inherit from parent (subagent gets isolated TaskManager)
96768
+ enableMcp,
96769
+ // Inherit from parent (subagent creates own MCPXmlBridge)
96770
+ mcpConfig,
96771
+ // Inherit from parent
96772
+ mcpConfigPath
96773
+ // Inherit from parent
96717
96774
  });
96718
96775
  if (debug) {
96719
96776
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
@@ -97271,7 +97328,7 @@ var init_vercel = __esm({
97271
97328
  });
97272
97329
  };
97273
97330
  delegateTool = (options = {}) => {
97274
- const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName } = options;
97331
+ const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null } = options;
97275
97332
  return (0, import_ai4.tool)({
97276
97333
  name: "delegate",
97277
97334
  description: delegateDescription,
@@ -97330,7 +97387,10 @@ var init_vercel = __esm({
97330
97387
  enableBash,
97331
97388
  bashConfig,
97332
97389
  architectureFileName,
97333
- searchDelegate
97390
+ searchDelegate,
97391
+ enableMcp,
97392
+ mcpConfig,
97393
+ mcpConfigPath
97334
97394
  });
97335
97395
  return result;
97336
97396
  }
@@ -97857,6 +97917,38 @@ function parseSimpleCommand(command) {
97857
97917
  isComplex: false
97858
97918
  };
97859
97919
  }
97920
+ const stripQuotedContent = (str) => {
97921
+ let result = "";
97922
+ let inQuotes2 = false;
97923
+ let quoteChar2 = "";
97924
+ for (let i4 = 0; i4 < str.length; i4++) {
97925
+ const char = str[i4];
97926
+ const nextChar = str[i4 + 1];
97927
+ if (!inQuotes2 && char === "\\" && nextChar !== void 0) {
97928
+ i4++;
97929
+ continue;
97930
+ }
97931
+ if (inQuotes2 && quoteChar2 === '"' && char === "\\" && nextChar !== void 0) {
97932
+ i4++;
97933
+ continue;
97934
+ }
97935
+ if (!inQuotes2 && (char === '"' || char === "'")) {
97936
+ inQuotes2 = true;
97937
+ quoteChar2 = char;
97938
+ continue;
97939
+ }
97940
+ if (inQuotes2 && char === quoteChar2) {
97941
+ inQuotes2 = false;
97942
+ quoteChar2 = "";
97943
+ continue;
97944
+ }
97945
+ if (!inQuotes2) {
97946
+ result += char;
97947
+ }
97948
+ }
97949
+ return result;
97950
+ };
97951
+ const strippedForOperators = stripQuotedContent(trimmed);
97860
97952
  const complexPatterns = [
97861
97953
  /\|/,
97862
97954
  // Pipes
@@ -97882,7 +97974,7 @@ function parseSimpleCommand(command) {
97882
97974
  // Brace expansion like {a,b} or {1..10} (but not find {} placeholders)
97883
97975
  ];
97884
97976
  for (const pattern of complexPatterns) {
97885
- if (pattern.test(trimmed)) {
97977
+ if (pattern.test(strippedForOperators)) {
97886
97978
  return {
97887
97979
  success: false,
97888
97980
  error: "Complex shell commands with pipes, operators, or redirections are not supported for security reasons",
@@ -97897,17 +97989,17 @@ function parseSimpleCommand(command) {
97897
97989
  let current = "";
97898
97990
  let inQuotes = false;
97899
97991
  let quoteChar = "";
97900
- let escaped = false;
97901
97992
  for (let i4 = 0; i4 < trimmed.length; i4++) {
97902
97993
  const char = trimmed[i4];
97903
97994
  const nextChar = i4 + 1 < trimmed.length ? trimmed[i4 + 1] : "";
97904
- if (escaped) {
97905
- current += char;
97906
- escaped = false;
97995
+ if (!inQuotes && char === "\\" && nextChar) {
97996
+ current += nextChar;
97997
+ i4++;
97907
97998
  continue;
97908
97999
  }
97909
- if (char === "\\" && !inQuotes) {
97910
- escaped = true;
98000
+ if (inQuotes && quoteChar === '"' && char === "\\" && nextChar) {
98001
+ current += nextChar;
98002
+ i4++;
97911
98003
  continue;
97912
98004
  }
97913
98005
  if (!inQuotes && (char === '"' || char === "'")) {
@@ -98240,8 +98332,97 @@ var init_bashPermissions = __esm({
98240
98332
  });
98241
98333
  return result;
98242
98334
  }
98335
+ /**
98336
+ * Split a complex command into component commands by operators
98337
+ *
98338
+ * ## Escape Handling (Security-Critical)
98339
+ *
98340
+ * This function intentionally PRESERVES escape sequences (both backslash AND
98341
+ * escaped character) in the output. This is step 1 of a 2-step parsing process:
98342
+ *
98343
+ * 1. _splitComplexCommand: Splits by operators, PRESERVES escapes → `echo "test\" && b"`
98344
+ * 2. parseCommand: Interprets escapes in each component → args: ['test" && b']
98345
+ *
98346
+ * This differs from stripQuotedContent() in bashCommandUtils.js which REMOVES
98347
+ * escapes entirely (for operator detection only).
98348
+ *
98349
+ * The security rationale: if we stripped escapes here, `\"` would become `"`,
98350
+ * potentially causing incorrect quote boundary detection and allowing operator
98351
+ * injection. By preserving escapes, parseCommand() can correctly interpret them.
98352
+ *
98353
+ * See bashCommandUtils.js module header for the full escape handling architecture.
98354
+ *
98355
+ * @private
98356
+ * @param {string} command - Complex command to split
98357
+ * @returns {string[]} Array of component commands (with escapes preserved)
98358
+ */
98359
+ _splitComplexCommand(command) {
98360
+ const components = [];
98361
+ let current = "";
98362
+ let inQuotes = false;
98363
+ let quoteChar = "";
98364
+ let i4 = 0;
98365
+ while (i4 < command.length) {
98366
+ const char = command[i4];
98367
+ const nextChar = command[i4 + 1] || "";
98368
+ if (!inQuotes && char === "\\") {
98369
+ current += char;
98370
+ if (nextChar) {
98371
+ current += nextChar;
98372
+ i4 += 2;
98373
+ } else {
98374
+ i4++;
98375
+ }
98376
+ continue;
98377
+ }
98378
+ if (inQuotes && quoteChar === '"' && char === "\\" && nextChar) {
98379
+ current += char + nextChar;
98380
+ i4 += 2;
98381
+ continue;
98382
+ }
98383
+ if (!inQuotes && (char === '"' || char === "'")) {
98384
+ inQuotes = true;
98385
+ quoteChar = char;
98386
+ current += char;
98387
+ i4++;
98388
+ continue;
98389
+ }
98390
+ if (inQuotes && char === quoteChar) {
98391
+ inQuotes = false;
98392
+ quoteChar = "";
98393
+ current += char;
98394
+ i4++;
98395
+ continue;
98396
+ }
98397
+ if (!inQuotes) {
98398
+ if (char === "&" && nextChar === "&" || char === "|" && nextChar === "|") {
98399
+ if (current.trim()) {
98400
+ components.push(current.trim());
98401
+ }
98402
+ current = "";
98403
+ i4 += 2;
98404
+ continue;
98405
+ }
98406
+ if (char === "|") {
98407
+ if (current.trim()) {
98408
+ components.push(current.trim());
98409
+ }
98410
+ current = "";
98411
+ i4++;
98412
+ continue;
98413
+ }
98414
+ }
98415
+ current += char;
98416
+ i4++;
98417
+ }
98418
+ if (current.trim()) {
98419
+ components.push(current.trim());
98420
+ }
98421
+ return components;
98422
+ }
98243
98423
  /**
98244
98424
  * Check a complex command against complex patterns in allow/deny lists
98425
+ * Also supports auto-allowing commands where all components are individually allowed
98245
98426
  * @private
98246
98427
  * @param {string} command - Complex command to check
98247
98428
  * @returns {Object} Permission result
@@ -98296,6 +98477,85 @@ var init_bashPermissions = __esm({
98296
98477
  return result;
98297
98478
  }
98298
98479
  }
98480
+ const components = this._splitComplexCommand(command);
98481
+ if (this.debug) {
98482
+ console.log(`[BashPermissions] Checking ${components.length} command components: ${JSON.stringify(components)}`);
98483
+ }
98484
+ if (components.length > 1) {
98485
+ const componentResults = [];
98486
+ let allAllowed = true;
98487
+ let deniedComponent = null;
98488
+ let deniedReason = null;
98489
+ for (const component of components) {
98490
+ const parsed = parseCommand(component);
98491
+ if (parsed.error || parsed.isComplex) {
98492
+ if (this.debug) {
98493
+ console.log(`[BashPermissions] Component "${component}" is complex or has error: ${parsed.error}`);
98494
+ }
98495
+ allAllowed = false;
98496
+ deniedComponent = component;
98497
+ deniedReason = parsed.error || "Component contains nested complex constructs";
98498
+ break;
98499
+ }
98500
+ if (matchesAnyPattern(parsed, this.denyPatterns)) {
98501
+ if (this.debug) {
98502
+ console.log(`[BashPermissions] Component "${component}" matches deny pattern`);
98503
+ }
98504
+ allAllowed = false;
98505
+ deniedComponent = component;
98506
+ deniedReason = "Component matches deny pattern";
98507
+ break;
98508
+ }
98509
+ if (!matchesAnyPattern(parsed, this.allowPatterns)) {
98510
+ if (this.debug) {
98511
+ console.log(`[BashPermissions] Component "${component}" not in allow list`);
98512
+ }
98513
+ allAllowed = false;
98514
+ deniedComponent = component;
98515
+ deniedReason = "Component not in allow list";
98516
+ break;
98517
+ }
98518
+ componentResults.push({ component, parsed, allowed: true });
98519
+ }
98520
+ if (allAllowed) {
98521
+ if (this.debug) {
98522
+ console.log(`[BashPermissions] ALLOWED - all ${components.length} components passed individual checks`);
98523
+ }
98524
+ const result = {
98525
+ allowed: true,
98526
+ command,
98527
+ isComplex: true,
98528
+ allowedByComponents: true,
98529
+ components: componentResults
98530
+ };
98531
+ this.recordBashEvent("permission.allowed", {
98532
+ command,
98533
+ isComplex: true,
98534
+ allowedByComponents: true,
98535
+ componentCount: components.length
98536
+ });
98537
+ return result;
98538
+ } else {
98539
+ if (this.debug) {
98540
+ console.log(`[BashPermissions] DENIED - component "${deniedComponent}" failed: ${deniedReason}`);
98541
+ }
98542
+ const result = {
98543
+ allowed: false,
98544
+ reason: `Component "${deniedComponent}" not allowed: ${deniedReason}`,
98545
+ command,
98546
+ isComplex: true,
98547
+ failedComponent: deniedComponent
98548
+ };
98549
+ this.recordBashEvent("permission.denied", {
98550
+ command,
98551
+ reason: "component_not_allowed",
98552
+ failedComponent: deniedComponent,
98553
+ componentReason: deniedReason,
98554
+ isComplex: true
98555
+ });
98556
+ return result;
98557
+ }
98558
+ }
98299
98559
  if (this.debug) {
98300
98560
  console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
98301
98561
  }
package/index.d.ts CHANGED
@@ -49,9 +49,11 @@ export interface ProbeAgentOptions {
49
49
  disableMermaidValidation?: boolean;
50
50
  /** Disable automatic JSON validation and fixing (prevents infinite recursion in JsonFixingAgent) */
51
51
  disableJsonValidation?: boolean;
52
- /** Enable agent skills discovery and activation */
52
+ /** Enable agent skills discovery and activation (disabled by default) */
53
+ allowSkills?: boolean;
54
+ /** @deprecated Use allowSkills instead. Enable agent skills discovery and activation (disabled by default) */
53
55
  enableSkills?: boolean;
54
- /** Disable agent skills (overrides enableSkills) */
56
+ /** Disable agent skills (overrides allowSkills/enableSkills) */
55
57
  disableSkills?: boolean;
56
58
  /** Skill directories to scan relative to repo root */
57
59
  skillDirs?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc208",
3
+ "version": "0.6.0-rc210",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -68,9 +68,11 @@ export interface ProbeAgentOptions {
68
68
  disableMermaidValidation?: boolean;
69
69
  /** Disable automatic JSON validation and fixing (prevents infinite recursion in JsonFixingAgent) */
70
70
  disableJsonValidation?: boolean;
71
- /** Enable agent skills discovery and activation */
71
+ /** Enable agent skills discovery and activation (disabled by default) */
72
+ allowSkills?: boolean;
73
+ /** @deprecated Use allowSkills instead. Enable agent skills discovery and activation (disabled by default) */
72
74
  enableSkills?: boolean;
73
- /** Disable agent skills (overrides enableSkills) */
75
+ /** Disable agent skills (overrides allowSkills/enableSkills) */
74
76
  disableSkills?: boolean;
75
77
  /** Skill directories to scan relative to repo root */
76
78
  skillDirs?: string[];
@@ -210,7 +210,8 @@ export class ProbeAgent {
210
210
  this.maxIterations = options.maxIterations || null;
211
211
  this.disableMermaidValidation = !!options.disableMermaidValidation;
212
212
  this.disableJsonValidation = !!options.disableJsonValidation;
213
- this.enableSkills = options.disableSkills ? false : (options.enableSkills !== undefined ? !!options.enableSkills : true);
213
+ // Skills are disabled by default; enable via allowSkills or enableSkills
214
+ this.enableSkills = options.disableSkills ? false : !!(options.allowSkills || options.enableSkills);
214
215
  if (Array.isArray(options.skillDirs)) {
215
216
  this.skillDirs = options.skillDirs;
216
217
  } else if (typeof options.skillDirs === 'string') {
@@ -2586,6 +2587,11 @@ Follow these instructions carefully:
2586
2587
  let sameFormatErrorCount = 0;
2587
2588
  const MAX_REPEATED_FORMAT_ERRORS = 3;
2588
2589
 
2590
+ // Circuit breaker for repeated identical responses without tool calls
2591
+ let lastNoToolResponse = null;
2592
+ let sameResponseCount = 0;
2593
+ const MAX_REPEATED_IDENTICAL_RESPONSES = 3;
2594
+
2589
2595
  // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
2590
2596
  while (currentIteration < maxIterations && !completionAttempted) {
2591
2597
  currentIteration++;
@@ -3223,6 +3229,36 @@ Follow these instructions carefully:
3223
3229
  break;
3224
3230
  }
3225
3231
 
3232
+ // Check for repeated identical responses - if AI gives same response 3 times,
3233
+ // accept it as the final answer instead of continuing the loop
3234
+ if (lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse) {
3235
+ sameResponseCount++;
3236
+ if (sameResponseCount >= MAX_REPEATED_IDENTICAL_RESPONSES) {
3237
+ // Clean up the response - remove thinking tags
3238
+ let cleanedResponse = assistantResponseContent;
3239
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
3240
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
3241
+
3242
+ const hasSubstantialContent = cleanedResponse.length > 50 &&
3243
+ !cleanedResponse.includes('<api_call>') &&
3244
+ !cleanedResponse.includes('<tool_name>') &&
3245
+ !cleanedResponse.includes('<function>');
3246
+
3247
+ if (hasSubstantialContent) {
3248
+ if (this.debug) {
3249
+ console.log(`[DEBUG] Same response repeated ${sameResponseCount} times - accepting as final answer (${cleanedResponse.length} chars)`);
3250
+ }
3251
+ finalResult = cleanedResponse;
3252
+ completionAttempted = true;
3253
+ break;
3254
+ }
3255
+ }
3256
+ } else {
3257
+ // Different response, reset counter
3258
+ lastNoToolResponse = assistantResponseContent;
3259
+ sameResponseCount = 1;
3260
+ }
3261
+
3226
3262
  // Add assistant response and ask for tool usage
3227
3263
  currentMessages.push({ role: 'assistant', content: assistantResponseContent });
3228
3264
 
@@ -3312,10 +3348,56 @@ Or if your previous response already contains a complete, direct answer (not a t
3312
3348
  Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
3313
3349
  }
3314
3350
 
3315
- currentMessages.push({
3316
- role: 'user',
3317
- content: reminderContent
3318
- });
3351
+ // Check if we should replace the previous reminder instead of appending
3352
+ // After pushing assistant message, the previous user message (if a reminder) is at length - 2
3353
+ // Message pattern: [..., prev_assistant, prev_user_reminder, current_assistant]
3354
+ const prevUserMsgIndex = currentMessages.length - 2;
3355
+ const prevUserMsg = currentMessages[prevUserMsgIndex];
3356
+ const isExistingReminder = prevUserMsg && prevUserMsg.role === 'user' &&
3357
+ (prevUserMsg.content.includes('Please use one of the available tools') ||
3358
+ prevUserMsg.content.includes('<tool_result>'));
3359
+
3360
+ if (isExistingReminder && sameResponseCount > 1) {
3361
+ // Replace the previous reminder with updated content and remove duplicated assistant message
3362
+ // This prevents context bloat from repeated identical exchanges
3363
+ // Pattern: [..., prev_assistant, prev_user_reminder, current_assistant] -> [..., current_assistant, new_reminder]
3364
+ const prevAssistantIndex = prevUserMsgIndex - 1;
3365
+
3366
+ // Validate the expected pattern before splicing:
3367
+ // 1. prevAssistantIndex must be valid (>= 0)
3368
+ // 2. If there's a system message at index 0, don't remove it (prevAssistantIndex > 0)
3369
+ // 3. Must be an assistant message at prevAssistantIndex
3370
+ // 4. After removal, array should have at least 2 messages (current assistant + new reminder)
3371
+ const hasSystemMessage = currentMessages.length > 0 && currentMessages[0].role === 'system';
3372
+ const minValidIndex = hasSystemMessage ? 1 : 0;
3373
+ const canSafelyRemove = prevAssistantIndex >= minValidIndex &&
3374
+ currentMessages[prevAssistantIndex] &&
3375
+ currentMessages[prevAssistantIndex].role === 'assistant' &&
3376
+ (currentMessages.length - 2) >= (hasSystemMessage ? 2 : 1); // After removal: at least system+assistant or just assistant
3377
+
3378
+ if (canSafelyRemove) {
3379
+ // Remove the duplicate assistant and old reminder (2 messages starting at prevAssistantIndex)
3380
+ currentMessages.splice(prevAssistantIndex, 2);
3381
+ if (this.debug) {
3382
+ console.log(`[DEBUG] Removed duplicate assistant+reminder pair (iteration ${currentIteration}, same response #${sameResponseCount})`);
3383
+ }
3384
+ } else if (this.debug) {
3385
+ console.log(`[DEBUG] Skipped deduplication: pattern validation failed (prevAssistantIndex=${prevAssistantIndex}, arrayLength=${currentMessages.length})`);
3386
+ }
3387
+
3388
+ // Add iteration context to help the AI understand this is a repeated attempt
3389
+ const iterationHint = `\n\n(Attempt #${sameResponseCount}: Your previous ${sameResponseCount} responses were identical. If you have a complete answer, use <attempt_complete></attempt_complete> to finalize it.)`;
3390
+ currentMessages.push({
3391
+ role: 'user',
3392
+ content: reminderContent + iterationHint
3393
+ });
3394
+ } else {
3395
+ currentMessages.push({
3396
+ role: 'user',
3397
+ content: reminderContent
3398
+ });
3399
+ }
3400
+
3319
3401
  if (this.debug) {
3320
3402
  if (unrecognizedTool) {
3321
3403
  console.log(`[DEBUG] Unrecognized tool '${unrecognizedTool}' used. Providing error feedback.`);