@probelabs/probe 0.6.0-rc301 → 0.6.0-rc303

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
@@ -50610,6 +50610,10 @@ var init_parser4 = __esm({
50610
50610
  });
50611
50611
 
50612
50612
  // node_modules/@probelabs/maid/out/diagrams/sequence/semantics.js
50613
+ function isEscapedEntitySemicolon(image, semicolonIdx) {
50614
+ const uptoSemicolon = image.slice(0, semicolonIdx + 1);
50615
+ return /(?:#\d+|&#\d+|&[A-Za-z][A-Za-z0-9]+);$/.test(uptoSemicolon);
50616
+ }
50613
50617
  function analyzeSequence(_cst, _tokens) {
50614
50618
  const ctx = { tokens: _tokens };
50615
50619
  const v = new SequenceSemanticsVisitor(ctx);
@@ -50710,6 +50714,35 @@ function analyzeSequence(_cst, _tokens) {
50710
50714
  if (arrowIdx > 0) {
50711
50715
  const from = grabActorRef(arr, 0);
50712
50716
  const to = grabActorRef(arr, arrowIdx + 1);
50717
+ const colonIdx = arr.findIndex((tk, idx) => idx > arrowIdx && tk.tokenType === Colon3);
50718
+ if (colonIdx !== -1) {
50719
+ let semicolonColumn = null;
50720
+ for (let i = colonIdx + 1; i < arr.length && semicolonColumn == null; i++) {
50721
+ const tk = arr[i];
50722
+ const img = tk.image || "";
50723
+ if (!img.includes(";"))
50724
+ continue;
50725
+ for (let j = 0; j < img.length; j++) {
50726
+ if (img[j] !== ";")
50727
+ continue;
50728
+ if (isEscapedEntitySemicolon(img, j))
50729
+ continue;
50730
+ semicolonColumn = (tk.startColumn ?? 1) + j;
50731
+ break;
50732
+ }
50733
+ }
50734
+ if (semicolonColumn != null) {
50735
+ errs.push({
50736
+ line: ln,
50737
+ column: semicolonColumn,
50738
+ severity: "error",
50739
+ code: "SE-MSG-SEMICOLON-UNESCAPED",
50740
+ message: "Semicolons in sequence message text must be escaped as '#59;'.",
50741
+ hint: "Replace ';' with '#59;' in the message text.",
50742
+ length: 1
50743
+ });
50744
+ }
50745
+ }
50713
50746
  if (from || to) {
50714
50747
  const plusTok = arr.find((tk) => tk.tokenType === Plus);
50715
50748
  const minusTok = arr.find((tk) => tk.tokenType === Minus);
@@ -52284,6 +52317,20 @@ function computeFixes(text, errors, level = "safe") {
52284
52317
  out = out.split(SENT_Q).join("&quot;");
52285
52318
  return out;
52286
52319
  }
52320
+ function escapeUnescapedSemicolons(textPart) {
52321
+ let out = "";
52322
+ for (let i = 0; i < textPart.length; i++) {
52323
+ const ch = textPart[i];
52324
+ if (ch !== ";") {
52325
+ out += ch;
52326
+ continue;
52327
+ }
52328
+ const upto = textPart.slice(0, i + 1);
52329
+ const isEntity = /(?:#\d+|&#\d+|&[A-Za-z][A-Za-z0-9]+);$/.test(upto);
52330
+ out += isEntity ? ";" : "#59;";
52331
+ }
52332
+ return out;
52333
+ }
52287
52334
  for (const e of errors) {
52288
52335
  const key = `${e.code}@${e.line}:${e.column}:${e.length ?? 1}`;
52289
52336
  if (seen.has(key))
@@ -53191,6 +53238,35 @@ function computeFixes(text, errors, level = "safe") {
53191
53238
  }
53192
53239
  continue;
53193
53240
  }
53241
+ if (is("SE-MSG-SEMICOLON-UNESCAPED", e)) {
53242
+ const lineText = lineTextAt(text, e.line);
53243
+ const arrows = ["<<-->>", "<<->>", "-->>", "->>", "-->", "->", "--x", "-x", "--)", "-)"];
53244
+ let ai = -1;
53245
+ let alen = 0;
53246
+ for (const a of arrows) {
53247
+ const idx = lineText.indexOf(a);
53248
+ if (idx !== -1 && (ai === -1 || idx < ai)) {
53249
+ ai = idx;
53250
+ alen = a.length;
53251
+ }
53252
+ }
53253
+ if (ai !== -1) {
53254
+ const colonIdx = lineText.indexOf(":", ai + alen);
53255
+ if (colonIdx !== -1) {
53256
+ const head2 = lineText.slice(0, colonIdx + 1);
53257
+ const tail = lineText.slice(colonIdx + 1);
53258
+ const fixedTail = escapeUnescapedSemicolons(tail);
53259
+ if (fixedTail !== tail) {
53260
+ edits.push({
53261
+ start: { line: e.line, column: 1 },
53262
+ end: { line: e.line, column: lineText.length + 1 },
53263
+ newText: head2 + fixedTail
53264
+ });
53265
+ }
53266
+ }
53267
+ }
53268
+ continue;
53269
+ }
53194
53270
  if (is("SE-NOTE-MALFORMED", e)) {
53195
53271
  const lineText = lineTextAt(text, e.line);
53196
53272
  const mLR = /^(\s*)Note\s+(left|right)\s+of\s+(.+?)\s+(.+)$/.exec(lineText);
@@ -93984,10 +94060,9 @@ function parseSimpleCommand(command) {
93984
94060
  // Command substitution $()
93985
94061
  /`/,
93986
94062
  // Command substitution ``
93987
- />/,
93988
- // Redirection >
93989
- /</,
93990
- // Redirection <
94063
+ // Note: > and < (redirection) are intentionally NOT in this list.
94064
+ // They are not command separators — they redirect I/O on a single command.
94065
+ // The base command is still checked against allow/deny lists.
93991
94066
  /\*\*/,
93992
94067
  // Glob patterns (potentially dangerous)
93993
94068
  /^\s*\{.*,.*\}|\{.*\.\.\.*\}/
@@ -94090,12 +94165,8 @@ function isComplexPattern(pattern) {
94090
94165
  // Background execution
94091
94166
  /\$\(/,
94092
94167
  // Command substitution $()
94093
- /`/,
94168
+ /`/
94094
94169
  // Command substitution ``
94095
- />/,
94096
- // Redirection >
94097
- /</
94098
- // Redirection <
94099
94170
  ];
94100
94171
  return operatorPatterns.some((p) => p.test(pattern));
94101
94172
  }
@@ -94204,12 +94275,14 @@ var init_bashPermissions = __esm({
94204
94275
  * @param {string[]} [config.deny] - Additional deny patterns (always win)
94205
94276
  * @param {boolean} [config.disableDefaultAllow] - Disable default allow list
94206
94277
  * @param {boolean} [config.disableDefaultDeny] - Disable default deny list
94278
+ * @param {boolean} [config.allowEdit] - Whether file editing is allowed (controls output redirection)
94207
94279
  * @param {boolean} [config.debug] - Enable debug logging
94208
94280
  * @param {Object} [config.tracer] - Optional tracer for telemetry
94209
94281
  */
94210
94282
  constructor(config2 = {}) {
94211
94283
  this.debug = config2.debug || false;
94212
94284
  this.tracer = config2.tracer || null;
94285
+ this.allowEdit = config2.allowEdit || false;
94213
94286
  this.defaultAllowPatterns = config2.disableDefaultAllow ? [] : [...DEFAULT_ALLOW_PATTERNS];
94214
94287
  this.customAllowPatterns = config2.allow && Array.isArray(config2.allow) ? [...config2.allow] : [];
94215
94288
  this.allowPatterns = [...this.defaultAllowPatterns, ...this.customAllowPatterns];
@@ -94295,6 +94368,24 @@ var init_bashPermissions = __esm({
94295
94368
  console.log(`[BashPermissions] Checking simple command: "${command}"`);
94296
94369
  console.log(`[BashPermissions] Parsed: ${parsed.command} with args: [${parsed.args.join(", ")}]`);
94297
94370
  }
94371
+ if (!this.allowEdit && parsed.args.some((arg) => arg === ">" || arg === ">>")) {
94372
+ const result2 = {
94373
+ allowed: false,
94374
+ reason: "Output redirection (> or >>) requires edit permissions (allowEdit)",
94375
+ command,
94376
+ parsed
94377
+ };
94378
+ if (this.debug) {
94379
+ console.log(`[BashPermissions] DENIED - output redirection without allowEdit`);
94380
+ }
94381
+ this.recordBashEvent("permission.denied", {
94382
+ command,
94383
+ parsedCommand: parsed.command,
94384
+ reason: "output_redirection_without_allow_edit",
94385
+ isComplex: false
94386
+ });
94387
+ return result2;
94388
+ }
94298
94389
  if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
94299
94390
  const matchedPatterns = this.customDenyPatterns.filter((pattern) => matchesPattern(parsed, pattern));
94300
94391
  if (this.debug) {
@@ -94564,6 +94655,15 @@ var init_bashPermissions = __esm({
94564
94655
  deniedReason = parsed.error || "Component contains nested complex constructs";
94565
94656
  break;
94566
94657
  }
94658
+ if (!this.allowEdit && parsed.args && parsed.args.some((arg) => arg === ">" || arg === ">>")) {
94659
+ if (this.debug) {
94660
+ console.log(`[BashPermissions] Component "${component}" has output redirection without allowEdit`);
94661
+ }
94662
+ allAllowed = false;
94663
+ deniedComponent = component;
94664
+ deniedReason = "Output redirection (> or >>) requires edit permissions (allowEdit)";
94665
+ break;
94666
+ }
94567
94667
  if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
94568
94668
  if (this.debug) {
94569
94669
  console.log(`[BashPermissions] Component "${component}" matches custom deny pattern`);
@@ -95093,6 +95193,7 @@ var init_bash = __esm({
95093
95193
  debug = false,
95094
95194
  cwd,
95095
95195
  allowedFolders = [],
95196
+ allowEdit = false,
95096
95197
  workspaceRoot: providedWorkspaceRoot,
95097
95198
  tracer = null
95098
95199
  } = options;
@@ -95102,6 +95203,7 @@ var init_bash = __esm({
95102
95203
  deny: bashConfig.deny,
95103
95204
  disableDefaultAllow: bashConfig.disableDefaultAllow,
95104
95205
  disableDefaultDeny: bashConfig.disableDefaultDeny,
95206
+ allowEdit,
95105
95207
  debug,
95106
95208
  tracer
95107
95209
  });
@@ -100231,19 +100333,23 @@ ${activeToolsDesc}
100231
100333
  - Time budget remaining: ${Math.round(remainingBudgetMs / 6e4)} minutes
100232
100334
  - Max per extension: ${maxPerReqMin} minutes
100233
100335
 
100234
- Decide whether the agent should get more time. EXTEND if:
100336
+ Your DEFAULT should be to EXTEND. The agent is working on a task and deserves time to finish. Only decline if you see clear evidence of a problem.
100337
+
100338
+ EXTEND (this should be your default) if:
100235
100339
  - Tools are actively running (especially delegates or complex analysis) \u2014 they need time to finish
100236
- - The agent is making clear progress on a complex task
100340
+ - The agent is making progress, even if slowly \u2014 complex tasks take time
100237
100341
  - New information is being gathered that will improve the final answer
100342
+ - The agent is iterating on an approach (trying different searches, refining queries) \u2014 this is normal problem-solving, not a loop
100343
+ - There is remaining budget and the task is not yet complete
100344
+ - When in doubt, extend \u2014 it's better to give the agent a chance than to cut it off prematurely
100238
100345
 
100239
- DO NOT EXTEND if:
100240
- - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
100241
- - The conversation shows the agent retrying failed operations without changing approach
100242
- - The agent has enough information to answer but keeps searching for more
100243
- - Tool calls are returning empty or error results repeatedly
100244
- - The agent is doing redundant work (searching for things it already found)
100346
+ DO NOT EXTEND only if you see CLEAR evidence of:
100347
+ - The agent is stuck in an obvious loop \u2014 repeating the EXACT same tool calls with the EXACT same arguments and getting the same errors back-to-back (3+ times)
100348
+ - The agent is retrying a fundamentally broken operation without changing its approach at all
100349
+ - Tool calls are consistently returning errors or empty results AND the agent is not adapting
100350
+ - The conversation clearly shows the agent has all the information it needs and is just making redundant calls
100245
100351
 
100246
- A stuck agent will not recover with more time \u2014 it will just burn the budget. Better to force it to answer with what it has.
100352
+ IMPORTANT: Iterating, refining, or trying variations is NOT the same as being stuck in a loop. A loop means identical repeated calls with no variation. Be generous with time \u2014 a slightly longer response time is much better than a prematurely cut-off incomplete answer.
100247
100353
 
100248
100354
  Respond with ONLY valid JSON (no markdown, no explanation):
100249
100355
  {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
@@ -100287,38 +100393,38 @@ or
100287
100393
  const decision = JSON.parse(jsonStr);
100288
100394
  if (decision.extend && decision.minutes > 0) {
100289
100395
  const requestedMs = Math.min(decision.minutes, maxPerReqMin) * 6e4;
100290
- const grantedMs = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
100291
- const grantedMin = Math.round(grantedMs / 6e4 * 10) / 10;
100396
+ const grantedMs2 = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
100397
+ const grantedMin2 = Math.round(grantedMs2 / 6e4 * 10) / 10;
100292
100398
  negotiatedTimeoutState.extensionsUsed++;
100293
- negotiatedTimeoutState.totalExtraTimeMs += grantedMs;
100294
- negotiatedTimeoutState.extensionMessage = `\u23F0 Time limit was reached. The timeout observer granted ${grantedMin} more minute(s) (reason: ${decision.reason || "work in progress"}). Extensions remaining: ${negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed}. Continue your work efficiently.`;
100399
+ negotiatedTimeoutState.totalExtraTimeMs += grantedMs2;
100400
+ negotiatedTimeoutState.extensionMessage = `\u23F0 Time limit was reached. The timeout observer granted ${grantedMin2} more minute(s) (reason: ${decision.reason || "work in progress"}). Extensions remaining: ${negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed}. Continue your work efficiently.`;
100295
100401
  negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
100296
100402
  runTimeoutObserver();
100297
- }, grantedMs);
100403
+ }, grantedMs2);
100298
100404
  if (this.debug) {
100299
- console.log(`[DEBUG] Timeout observer: granted ${grantedMin} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
100405
+ console.log(`[DEBUG] Timeout observer: granted ${grantedMin2} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
100300
100406
  }
100301
100407
  if (this.tracer) {
100302
100408
  this.tracer.addEvent("negotiated_timeout.observer_extended", {
100303
100409
  decision_reason: decision.reason,
100304
100410
  requested_minutes: decision.minutes,
100305
- granted_ms: grantedMs,
100306
- granted_min: grantedMin,
100411
+ granted_ms: grantedMs2,
100412
+ granted_min: grantedMin2,
100307
100413
  extensions_used: negotiatedTimeoutState.extensionsUsed,
100308
100414
  max_requests: negotiatedTimeoutState.maxRequests,
100309
100415
  total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
100310
- budget_remaining_ms: remainingBudgetMs - grantedMs,
100416
+ budget_remaining_ms: remainingBudgetMs - grantedMs2,
100311
100417
  active_tools: activeToolsList.map((t) => t.name),
100312
100418
  active_tools_count: activeToolsList.length
100313
100419
  });
100314
100420
  }
100315
100421
  this.events.emit("timeout.extended", {
100316
- grantedMs,
100422
+ grantedMs: grantedMs2,
100317
100423
  reason: decision.reason || "work in progress",
100318
100424
  extensionsUsed: negotiatedTimeoutState.extensionsUsed,
100319
100425
  extensionsRemaining: negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed,
100320
100426
  totalExtraTimeMs: negotiatedTimeoutState.totalExtraTimeMs,
100321
- budgetRemainingMs: remainingBudgetMs - grantedMs
100427
+ budgetRemainingMs: remainingBudgetMs - grantedMs2
100322
100428
  });
100323
100429
  } else {
100324
100430
  if (this.debug) {
@@ -100340,6 +100446,18 @@ or
100340
100446
  });
100341
100447
  await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
100342
100448
  }
100449
+ return {
100450
+ decision: decision.extend ? "extended" : "declined",
100451
+ reason: decision.reason || "",
100452
+ ...decision.extend ? {
100453
+ granted_ms: grantedMs,
100454
+ granted_min: grantedMin,
100455
+ budget_remaining_ms: remainingBudgetMs - grantedMs
100456
+ } : {},
100457
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
100458
+ max_requests: negotiatedTimeoutState.maxRequests,
100459
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs
100460
+ };
100343
100461
  };
100344
100462
  try {
100345
100463
  if (this.tracer) {
@@ -100348,6 +100466,23 @@ or
100348
100466
  "timeout.extensions_used": negotiatedTimeoutState.extensionsUsed,
100349
100467
  "timeout.active_tools_count": activeToolsList.length,
100350
100468
  "timeout.remaining_budget_ms": remainingBudgetMs
100469
+ }, (span, result) => {
100470
+ if (result) {
100471
+ span.setAttributes({
100472
+ "observer.decision": result.decision,
100473
+ "observer.reason": result.reason,
100474
+ "observer.extensions_used": result.extensions_used,
100475
+ "observer.max_requests": result.max_requests,
100476
+ "observer.total_extra_time_ms": result.total_extra_time_ms
100477
+ });
100478
+ if (result.decision === "extended") {
100479
+ span.setAttributes({
100480
+ "observer.granted_ms": result.granted_ms,
100481
+ "observer.granted_min": result.granted_min,
100482
+ "observer.budget_remaining_ms": result.budget_remaining_ms
100483
+ });
100484
+ }
100485
+ }
100351
100486
  });
100352
100487
  } else {
100353
100488
  await observerFn();
@@ -100455,7 +100590,13 @@ or
100455
100590
  }
100456
100591
  return {
100457
100592
  toolChoice: "none",
100458
- userMessage: `\u26A0\uFE0F TIME LIMIT REACHED. You are running out of time. You have ${remaining} step(s) remaining. Provide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
100593
+ userMessage: `\u26A0\uFE0F TIME BUDGET EXHAUSTED. Your allocated time for this task has run out. You have ${remaining} step(s) remaining to provide your answer.
100594
+
100595
+ IMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly \u2014 you simply used all your allocated time.
100596
+
100597
+ Do NOT say things like "the system is shutting down" or "try again later" \u2014 the user submitted a request and is waiting for YOUR answer right now.
100598
+
100599
+ Provide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
100459
100600
  };
100460
100601
  }
100461
100602
  if (this.debug) {
@@ -100883,7 +101024,9 @@ Respond with ONLY valid JSON \u2014 no markdown, no explanation, no text outside
100883
101024
  } catch {
100884
101025
  }
100885
101026
  }
100886
- const summaryPrompt = `Your operation was interrupted by a timeout observer because the time limit was reached. Some of your tool calls were cancelled mid-execution.
101027
+ const summaryPrompt = `Your allocated time budget for this task has been exhausted. Some of your tool calls were cancelled mid-execution because the timeout observer determined the time limit was reached.
101028
+
101029
+ IMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly \u2014 you simply used all your allocated time. Do NOT say things like "the system is shutting down" or "try again later." The user is waiting for your answer RIGHT NOW.
100887
101030
 
100888
101031
  Please provide a DETAILED summary of:
100889
101032
  1. What you were asked to do (the original task)
@@ -100918,7 +101061,14 @@ Be thorough \u2014 this is the user's only response. Include all useful informat
100918
101061
  let summaryText;
100919
101062
  if (this.tracer) {
100920
101063
  summaryText = await this.tracer.withSpan("negotiated_timeout.abort_summary", summaryFn, {
100921
- "summary.conversation_messages": currentMessages.length
101064
+ "summary.conversation_messages": currentMessages.length,
101065
+ "observer.was_timeout": true
101066
+ }, (span, result) => {
101067
+ if (result) {
101068
+ span.setAttributes({
101069
+ "observer.summary_length": result.length
101070
+ });
101071
+ }
100922
101072
  });
100923
101073
  } else {
100924
101074
  summaryText = await summaryFn();
@@ -102930,8 +103080,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
102930
103080
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
102931
103081
  "- NEVER repeat the same search query \u2014 you will get the same results. Changing the path does NOT change this.",
102932
103082
  "- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful \u2014 probe handles it.",
102933
- "- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.",
102934
- "- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.",
103083
+ "",
103084
+ "When a search returns no results:",
103085
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
103086
+ " Try searching from the workspace root (omit the path parameter) or a different directory.",
103087
+ " But do NOT retry the same subfolder with different quoting \u2014 that will not help.",
103088
+ "- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.",
103089
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
103090
+ "- These are ALL the same failed search, NOT different searches:",
103091
+ ' search("func ctxGetData") \u2192 no results',
103092
+ ' search("ctxGetData") \u2192 no results \u2190 WASTED, same concept, different quoting',
103093
+ " search(ctxGetData) \u2192 no results \u2190 WASTED, same concept, no quotes",
103094
+ ' search("ctx.GetData") \u2192 no results \u2190 WASTED, method syntax of same concept',
103095
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
103096
+ " a fundamentally different approach: search for a broader concept, use listFiles",
103097
+ " to discover actual function names, or extract a known file to read real code.",
103098
+ "- If 2 searches return no results for a concept (across different scopes), the code likely",
103099
+ " uses different naming than you expect \u2014 discover the real names via extract or listFiles.",
102935
103100
  "",
102936
103101
  "When to use exact=true:",
102937
103102
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
@@ -102984,6 +103149,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
102984
103149
  ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
102985
103150
  ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
102986
103151
  "",
103152
+ " WORST pattern \u2014 retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):",
103153
+ ' \u2192 search "func ctxGetData" \u2192 no results',
103154
+ ' \u2192 search "ctxGetData" \u2192 no results \u2190 WRONG: same term without "func" prefix',
103155
+ ' \u2192 search "ctx.GetData" \u2192 no results \u2190 WRONG: method syntax of same concept',
103156
+ ' \u2192 search "ctx.SetData" \u2192 no results \u2190 WRONG: Set variant of same concept',
103157
+ " \u2192 search ctxGetData \u2192 no results \u2190 WRONG: unquoted version of same term",
103158
+ " \u2192 extract api.go \u2192 extract api.go \u2192 extract api.go (8 times!) \u2190 WRONG: re-reading same file",
103159
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
103160
+ " Option A: Widen scope \u2014 search from the workspace root (omit path) in case the",
103161
+ " function is defined in a different package (e.g., apidef/, user/, config/).",
103162
+ " Option B: Discover real names \u2014 extract a file you KNOW uses context (e.g., a",
103163
+ " middleware file) and READ what functions it actually calls.",
103164
+ " Option C: Browse \u2014 use listFiles to see what files exist and extract the relevant ones.",
103165
+ " NEVER: retry the same concept with different quoting in the same directory.",
103166
+ "",
102987
103167
  "Keyword tips:",
102988
103168
  "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
102989
103169
  '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -103022,7 +103202,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
103022
103202
  " - Type references and imports \u2192 include type definitions.",
103023
103203
  " - Registered handlers/middleware \u2192 include all registered items.",
103024
103204
  "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
103025
- "7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.",
103205
+ "7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations \u2014 they search the same index.",
103026
103206
  "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
103027
103207
  "",
103028
103208
  `Query: ${searchQuery}`,
@@ -103082,7 +103262,14 @@ var init_vercel = __esm({
103082
103262
  const previousSearches = /* @__PURE__ */ new Map();
103083
103263
  const dupBlockCounts = /* @__PURE__ */ new Map();
103084
103264
  const paginationCounts = /* @__PURE__ */ new Map();
103265
+ let consecutiveNoResults = 0;
103266
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
103267
+ const failedConcepts = /* @__PURE__ */ new Map();
103085
103268
  const MAX_PAGES_PER_QUERY = 3;
103269
+ function normalizeQueryConcept(query2) {
103270
+ if (!query2) return "";
103271
+ return query2.replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
103272
+ }
103086
103273
  return (0, import_ai5.tool)({
103087
103274
  name: "search",
103088
103275
  description: searchDelegate ? searchDelegateDescription : searchDescription,
@@ -103151,6 +103338,41 @@ var init_vercel = __esm({
103151
103338
  }
103152
103339
  previousSearches.set(searchKey, { hadResults: false });
103153
103340
  paginationCounts.set(searchKey, 0);
103341
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
103342
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
103343
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
103344
+ failedConcepts.set(normalizedKey, conceptCount);
103345
+ if (debug) {
103346
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
103347
+ }
103348
+ const isSubfolder = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103349
+ const scopeHint = isSubfolder ? `
103350
+ - Try searching from the workspace root (omit the path parameter) \u2014 the term may exist in a different directory` : `
103351
+ - The term does not exist in this codebase at any path`;
103352
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.
103353
+
103354
+ Change your strategy:${scopeHint}
103355
+ - Use extract on a file you ALREADY found to read actual code and discover real function/type names
103356
+ - Use listFiles to browse directories and find what functions actually exist
103357
+ - Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")
103358
+ - If you have enough information from prior searches, provide your final answer NOW`;
103359
+ }
103360
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
103361
+ if (debug) {
103362
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
103363
+ }
103364
+ const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103365
+ const cbScopeHint = isSubfolderCB ? `
103366
+ - You have been searching in "${path9}" \u2014 try searching from the workspace root or a different directory` : "";
103367
+ return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.
103368
+
103369
+ Change your approach:${cbScopeHint}
103370
+ 1. Use extract on files you already found \u2014 read the actual code to discover real function names
103371
+ 2. Use listFiles to browse directories and see what files/functions actually exist
103372
+ 3. If you found some results earlier, those are likely sufficient \u2014 provide your final answer
103373
+
103374
+ Retrying search query variations will not help. Discover real names from real code instead.`;
103375
+ }
103154
103376
  } else {
103155
103377
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
103156
103378
  paginationCounts.set(searchKey, pageCount);
@@ -103164,10 +103386,24 @@ var init_vercel = __esm({
103164
103386
  try {
103165
103387
  const result = maybeAnnotate(await runRawSearch());
103166
103388
  if (typeof result === "string" && result.includes("No results found")) {
103389
+ consecutiveNoResults++;
103390
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
103391
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
103392
+ if (debug) {
103393
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
103394
+ }
103167
103395
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
103168
103396
  return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
103169
103397
  }
103398
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
103399
+ const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103400
+ const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
103401
+ return result + `
103402
+
103403
+ \u26A0\uFE0F WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
103404
+ }
103170
103405
  } else if (typeof result === "string") {
103406
+ consecutiveNoResults = 0;
103171
103407
  const entry = previousSearches.get(searchKey);
103172
103408
  if (entry) entry.hadResults = true;
103173
103409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc301",
3
+ "version": "0.6.0-rc303",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -81,7 +81,7 @@
81
81
  "@anthropic-ai/claude-agent-sdk": "^0.1.46",
82
82
  "@modelcontextprotocol/sdk": "^1.0.0",
83
83
  "@nyariv/sandboxjs": "github:probelabs/SandboxJS",
84
- "@probelabs/maid": "^0.0.28",
84
+ "@probelabs/maid": "^0.0.29",
85
85
  "acorn": "^8.15.0",
86
86
  "acorn-walk": "^8.3.4",
87
87
  "adm-zip": "^0.5.16",
@@ -3746,19 +3746,23 @@ ${activeToolsDesc}
3746
3746
  - Time budget remaining: ${Math.round(remainingBudgetMs / 60000)} minutes
3747
3747
  - Max per extension: ${maxPerReqMin} minutes
3748
3748
 
3749
- Decide whether the agent should get more time. EXTEND if:
3749
+ Your DEFAULT should be to EXTEND. The agent is working on a task and deserves time to finish. Only decline if you see clear evidence of a problem.
3750
+
3751
+ EXTEND (this should be your default) if:
3750
3752
  - Tools are actively running (especially delegates or complex analysis) — they need time to finish
3751
- - The agent is making clear progress on a complex task
3753
+ - The agent is making progress, even if slowly — complex tasks take time
3752
3754
  - New information is being gathered that will improve the final answer
3755
+ - The agent is iterating on an approach (trying different searches, refining queries) — this is normal problem-solving, not a loop
3756
+ - There is remaining budget and the task is not yet complete
3757
+ - When in doubt, extend — it's better to give the agent a chance than to cut it off prematurely
3753
3758
 
3754
- DO NOT EXTEND if:
3755
- - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
3756
- - The conversation shows the agent retrying failed operations without changing approach
3757
- - The agent has enough information to answer but keeps searching for more
3758
- - Tool calls are returning empty or error results repeatedly
3759
- - The agent is doing redundant work (searching for things it already found)
3759
+ DO NOT EXTEND only if you see CLEAR evidence of:
3760
+ - The agent is stuck in an obvious loop repeating the EXACT same tool calls with the EXACT same arguments and getting the same errors back-to-back (3+ times)
3761
+ - The agent is retrying a fundamentally broken operation without changing its approach at all
3762
+ - Tool calls are consistently returning errors or empty results AND the agent is not adapting
3763
+ - The conversation clearly shows the agent has all the information it needs and is just making redundant calls
3760
3764
 
3761
- A stuck agent will not recover with more time — it will just burn the budget. Better to force it to answer with what it has.
3765
+ IMPORTANT: Iterating, refining, or trying variations is NOT the same as being stuck in a loop. A loop means identical repeated calls with no variation. Be generous with time — a slightly longer response time is much better than a prematurely cut-off incomplete answer.
3762
3766
 
3763
3767
  Respond with ONLY valid JSON (no markdown, no explanation):
3764
3768
  {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
@@ -3885,6 +3889,20 @@ or
3885
3889
 
3886
3890
  await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
3887
3891
  }
3892
+
3893
+ // Return decision data for span enrichment
3894
+ return {
3895
+ decision: decision.extend ? 'extended' : 'declined',
3896
+ reason: decision.reason || '',
3897
+ ...(decision.extend ? {
3898
+ granted_ms: grantedMs,
3899
+ granted_min: grantedMin,
3900
+ budget_remaining_ms: remainingBudgetMs - grantedMs,
3901
+ } : {}),
3902
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
3903
+ max_requests: negotiatedTimeoutState.maxRequests,
3904
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
3905
+ };
3888
3906
  };
3889
3907
 
3890
3908
  try {
@@ -3894,6 +3912,23 @@ or
3894
3912
  'timeout.extensions_used': negotiatedTimeoutState.extensionsUsed,
3895
3913
  'timeout.active_tools_count': activeToolsList.length,
3896
3914
  'timeout.remaining_budget_ms': remainingBudgetMs,
3915
+ }, (span, result) => {
3916
+ if (result) {
3917
+ span.setAttributes({
3918
+ 'observer.decision': result.decision,
3919
+ 'observer.reason': result.reason,
3920
+ 'observer.extensions_used': result.extensions_used,
3921
+ 'observer.max_requests': result.max_requests,
3922
+ 'observer.total_extra_time_ms': result.total_extra_time_ms,
3923
+ });
3924
+ if (result.decision === 'extended') {
3925
+ span.setAttributes({
3926
+ 'observer.granted_ms': result.granted_ms,
3927
+ 'observer.granted_min': result.granted_min,
3928
+ 'observer.budget_remaining_ms': result.budget_remaining_ms,
3929
+ });
3930
+ }
3931
+ }
3897
3932
  });
3898
3933
  } else {
3899
3934
  await observerFn();
@@ -4033,7 +4068,7 @@ or
4033
4068
  }
4034
4069
  return {
4035
4070
  toolChoice: 'none',
4036
- userMessage: `⚠️ TIME LIMIT REACHED. You are running out of time. You have ${remaining} step(s) remaining. Provide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
4071
+ userMessage: `⚠️ TIME BUDGET EXHAUSTED. Your allocated time for this task has run out. You have ${remaining} step(s) remaining to provide your answer.\n\nIMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly — you simply used all your allocated time.\n\nDo NOT say things like "the system is shutting down" or "try again later" — the user submitted a request and is waiting for YOUR answer right now.\n\nProvide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
4037
4072
  };
4038
4073
  }
4039
4074
 
@@ -4571,8 +4606,10 @@ Double-check your response based on the criteria above. If everything looks good
4571
4606
  } catch {}
4572
4607
  }
4573
4608
 
4574
- const summaryPrompt = `Your operation was interrupted by a timeout observer because the time limit was reached. ` +
4575
- `Some of your tool calls were cancelled mid-execution.\n\n` +
4609
+ const summaryPrompt = `Your allocated time budget for this task has been exhausted. ` +
4610
+ `Some of your tool calls were cancelled mid-execution because the timeout observer determined the time limit was reached.\n\n` +
4611
+ `IMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly — you simply used all your allocated time. ` +
4612
+ `Do NOT say things like "the system is shutting down" or "try again later." The user is waiting for your answer RIGHT NOW.\n\n` +
4576
4613
  `Please provide a DETAILED summary of:\n` +
4577
4614
  `1. What you were asked to do (the original task)\n` +
4578
4615
  `2. What you accomplished — include ALL findings, code snippets, data, and conclusions you gathered\n` +
@@ -4615,6 +4652,13 @@ Double-check your response based on the criteria above. If everything looks good
4615
4652
  if (this.tracer) {
4616
4653
  summaryText = await this.tracer.withSpan('negotiated_timeout.abort_summary', summaryFn, {
4617
4654
  'summary.conversation_messages': currentMessages.length,
4655
+ 'observer.was_timeout': true,
4656
+ }, (span, result) => {
4657
+ if (result) {
4658
+ span.setAttributes({
4659
+ 'observer.summary_length': result.length,
4660
+ });
4661
+ }
4618
4662
  });
4619
4663
  } else {
4620
4664
  summaryText = await summaryFn();