@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.
@@ -27561,8 +27561,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27561
27561
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
27562
27562
  "- NEVER repeat the same search query \u2014 you will get the same results. Changing the path does NOT change this.",
27563
27563
  "- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful \u2014 probe handles it.",
27564
- "- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.",
27565
- "- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.",
27564
+ "",
27565
+ "When a search returns no results:",
27566
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
27567
+ " Try searching from the workspace root (omit the path parameter) or a different directory.",
27568
+ " But do NOT retry the same subfolder with different quoting \u2014 that will not help.",
27569
+ "- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.",
27570
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
27571
+ "- These are ALL the same failed search, NOT different searches:",
27572
+ ' search("func ctxGetData") \u2192 no results',
27573
+ ' search("ctxGetData") \u2192 no results \u2190 WASTED, same concept, different quoting',
27574
+ " search(ctxGetData) \u2192 no results \u2190 WASTED, same concept, no quotes",
27575
+ ' search("ctx.GetData") \u2192 no results \u2190 WASTED, method syntax of same concept',
27576
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
27577
+ " a fundamentally different approach: search for a broader concept, use listFiles",
27578
+ " to discover actual function names, or extract a known file to read real code.",
27579
+ "- If 2 searches return no results for a concept (across different scopes), the code likely",
27580
+ " uses different naming than you expect \u2014 discover the real names via extract or listFiles.",
27566
27581
  "",
27567
27582
  "When to use exact=true:",
27568
27583
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
@@ -27615,6 +27630,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27615
27630
  ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
27616
27631
  ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
27617
27632
  "",
27633
+ " WORST pattern \u2014 retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):",
27634
+ ' \u2192 search "func ctxGetData" \u2192 no results',
27635
+ ' \u2192 search "ctxGetData" \u2192 no results \u2190 WRONG: same term without "func" prefix',
27636
+ ' \u2192 search "ctx.GetData" \u2192 no results \u2190 WRONG: method syntax of same concept',
27637
+ ' \u2192 search "ctx.SetData" \u2192 no results \u2190 WRONG: Set variant of same concept',
27638
+ " \u2192 search ctxGetData \u2192 no results \u2190 WRONG: unquoted version of same term",
27639
+ " \u2192 extract api.go \u2192 extract api.go \u2192 extract api.go (8 times!) \u2190 WRONG: re-reading same file",
27640
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
27641
+ " Option A: Widen scope \u2014 search from the workspace root (omit path) in case the",
27642
+ " function is defined in a different package (e.g., apidef/, user/, config/).",
27643
+ " Option B: Discover real names \u2014 extract a file you KNOW uses context (e.g., a",
27644
+ " middleware file) and READ what functions it actually calls.",
27645
+ " Option C: Browse \u2014 use listFiles to see what files exist and extract the relevant ones.",
27646
+ " NEVER: retry the same concept with different quoting in the same directory.",
27647
+ "",
27618
27648
  "Keyword tips:",
27619
27649
  "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
27620
27650
  '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -27653,7 +27683,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27653
27683
  " - Type references and imports \u2192 include type definitions.",
27654
27684
  " - Registered handlers/middleware \u2192 include all registered items.",
27655
27685
  "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
27656
- "7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.",
27686
+ "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.",
27657
27687
  "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
27658
27688
  "",
27659
27689
  `Query: ${searchQuery}`,
@@ -27713,7 +27743,14 @@ var init_vercel = __esm({
27713
27743
  const previousSearches = /* @__PURE__ */ new Map();
27714
27744
  const dupBlockCounts = /* @__PURE__ */ new Map();
27715
27745
  const paginationCounts = /* @__PURE__ */ new Map();
27746
+ let consecutiveNoResults = 0;
27747
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
27748
+ const failedConcepts = /* @__PURE__ */ new Map();
27716
27749
  const MAX_PAGES_PER_QUERY = 3;
27750
+ function normalizeQueryConcept(query2) {
27751
+ if (!query2) return "";
27752
+ return query2.replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
27753
+ }
27717
27754
  return (0, import_ai.tool)({
27718
27755
  name: "search",
27719
27756
  description: searchDelegate ? searchDelegateDescription : searchDescription,
@@ -27782,6 +27819,41 @@ var init_vercel = __esm({
27782
27819
  }
27783
27820
  previousSearches.set(searchKey, { hadResults: false });
27784
27821
  paginationCounts.set(searchKey, 0);
27822
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
27823
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
27824
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
27825
+ failedConcepts.set(normalizedKey, conceptCount);
27826
+ if (debug) {
27827
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
27828
+ }
27829
+ const isSubfolder = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27830
+ const scopeHint = isSubfolder ? `
27831
+ - Try searching from the workspace root (omit the path parameter) \u2014 the term may exist in a different directory` : `
27832
+ - The term does not exist in this codebase at any path`;
27833
+ 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.
27834
+
27835
+ Change your strategy:${scopeHint}
27836
+ - Use extract on a file you ALREADY found to read actual code and discover real function/type names
27837
+ - Use listFiles to browse directories and find what functions actually exist
27838
+ - Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")
27839
+ - If you have enough information from prior searches, provide your final answer NOW`;
27840
+ }
27841
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
27842
+ if (debug) {
27843
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
27844
+ }
27845
+ const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27846
+ const cbScopeHint = isSubfolderCB ? `
27847
+ - You have been searching in "${path9}" \u2014 try searching from the workspace root or a different directory` : "";
27848
+ 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.
27849
+
27850
+ Change your approach:${cbScopeHint}
27851
+ 1. Use extract on files you already found \u2014 read the actual code to discover real function names
27852
+ 2. Use listFiles to browse directories and see what files/functions actually exist
27853
+ 3. If you found some results earlier, those are likely sufficient \u2014 provide your final answer
27854
+
27855
+ Retrying search query variations will not help. Discover real names from real code instead.`;
27856
+ }
27785
27857
  } else {
27786
27858
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
27787
27859
  paginationCounts.set(searchKey, pageCount);
@@ -27795,10 +27867,24 @@ var init_vercel = __esm({
27795
27867
  try {
27796
27868
  const result = maybeAnnotate(await runRawSearch());
27797
27869
  if (typeof result === "string" && result.includes("No results found")) {
27870
+ consecutiveNoResults++;
27871
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
27872
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
27873
+ if (debug) {
27874
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
27875
+ }
27798
27876
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27799
27877
  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).";
27800
27878
  }
27879
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
27880
+ const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27881
+ const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
27882
+ return result + `
27883
+
27884
+ \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.`;
27885
+ }
27801
27886
  } else if (typeof result === "string") {
27887
+ consecutiveNoResults = 0;
27802
27888
  const entry = previousSearches.get(searchKey);
27803
27889
  if (entry) entry.hadResults = true;
27804
27890
  }
@@ -28828,10 +28914,9 @@ function parseSimpleCommand(command) {
28828
28914
  // Command substitution $()
28829
28915
  /`/,
28830
28916
  // Command substitution ``
28831
- />/,
28832
- // Redirection >
28833
- /</,
28834
- // Redirection <
28917
+ // Note: > and < (redirection) are intentionally NOT in this list.
28918
+ // They are not command separators — they redirect I/O on a single command.
28919
+ // The base command is still checked against allow/deny lists.
28835
28920
  /\*\*/,
28836
28921
  // Glob patterns (potentially dangerous)
28837
28922
  /^\s*\{.*,.*\}|\{.*\.\.\.*\}/
@@ -28934,12 +29019,8 @@ function isComplexPattern(pattern) {
28934
29019
  // Background execution
28935
29020
  /\$\(/,
28936
29021
  // Command substitution $()
28937
- /`/,
29022
+ /`/
28938
29023
  // Command substitution ``
28939
- />/,
28940
- // Redirection >
28941
- /</
28942
- // Redirection <
28943
29024
  ];
28944
29025
  return operatorPatterns.some((p) => p.test(pattern));
28945
29026
  }
@@ -29048,12 +29129,14 @@ var init_bashPermissions = __esm({
29048
29129
  * @param {string[]} [config.deny] - Additional deny patterns (always win)
29049
29130
  * @param {boolean} [config.disableDefaultAllow] - Disable default allow list
29050
29131
  * @param {boolean} [config.disableDefaultDeny] - Disable default deny list
29132
+ * @param {boolean} [config.allowEdit] - Whether file editing is allowed (controls output redirection)
29051
29133
  * @param {boolean} [config.debug] - Enable debug logging
29052
29134
  * @param {Object} [config.tracer] - Optional tracer for telemetry
29053
29135
  */
29054
29136
  constructor(config2 = {}) {
29055
29137
  this.debug = config2.debug || false;
29056
29138
  this.tracer = config2.tracer || null;
29139
+ this.allowEdit = config2.allowEdit || false;
29057
29140
  this.defaultAllowPatterns = config2.disableDefaultAllow ? [] : [...DEFAULT_ALLOW_PATTERNS];
29058
29141
  this.customAllowPatterns = config2.allow && Array.isArray(config2.allow) ? [...config2.allow] : [];
29059
29142
  this.allowPatterns = [...this.defaultAllowPatterns, ...this.customAllowPatterns];
@@ -29139,6 +29222,24 @@ var init_bashPermissions = __esm({
29139
29222
  console.log(`[BashPermissions] Checking simple command: "${command}"`);
29140
29223
  console.log(`[BashPermissions] Parsed: ${parsed.command} with args: [${parsed.args.join(", ")}]`);
29141
29224
  }
29225
+ if (!this.allowEdit && parsed.args.some((arg) => arg === ">" || arg === ">>")) {
29226
+ const result2 = {
29227
+ allowed: false,
29228
+ reason: "Output redirection (> or >>) requires edit permissions (allowEdit)",
29229
+ command,
29230
+ parsed
29231
+ };
29232
+ if (this.debug) {
29233
+ console.log(`[BashPermissions] DENIED - output redirection without allowEdit`);
29234
+ }
29235
+ this.recordBashEvent("permission.denied", {
29236
+ command,
29237
+ parsedCommand: parsed.command,
29238
+ reason: "output_redirection_without_allow_edit",
29239
+ isComplex: false
29240
+ });
29241
+ return result2;
29242
+ }
29142
29243
  if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
29143
29244
  const matchedPatterns = this.customDenyPatterns.filter((pattern) => matchesPattern(parsed, pattern));
29144
29245
  if (this.debug) {
@@ -29408,6 +29509,15 @@ var init_bashPermissions = __esm({
29408
29509
  deniedReason = parsed.error || "Component contains nested complex constructs";
29409
29510
  break;
29410
29511
  }
29512
+ if (!this.allowEdit && parsed.args && parsed.args.some((arg) => arg === ">" || arg === ">>")) {
29513
+ if (this.debug) {
29514
+ console.log(`[BashPermissions] Component "${component}" has output redirection without allowEdit`);
29515
+ }
29516
+ allAllowed = false;
29517
+ deniedComponent = component;
29518
+ deniedReason = "Output redirection (> or >>) requires edit permissions (allowEdit)";
29519
+ break;
29520
+ }
29411
29521
  if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
29412
29522
  if (this.debug) {
29413
29523
  console.log(`[BashPermissions] Component "${component}" matches custom deny pattern`);
@@ -29937,6 +30047,7 @@ var init_bash = __esm({
29937
30047
  debug = false,
29938
30048
  cwd,
29939
30049
  allowedFolders = [],
30050
+ allowEdit = false,
29940
30051
  workspaceRoot: providedWorkspaceRoot,
29941
30052
  tracer = null
29942
30053
  } = options;
@@ -29946,6 +30057,7 @@ var init_bash = __esm({
29946
30057
  deny: bashConfig.deny,
29947
30058
  disableDefaultAllow: bashConfig.disableDefaultAllow,
29948
30059
  disableDefaultDeny: bashConfig.disableDefaultDeny,
30060
+ allowEdit,
29949
30061
  debug,
29950
30062
  tracer
29951
30063
  });
@@ -65612,6 +65724,10 @@ var init_parser4 = __esm({
65612
65724
  });
65613
65725
 
65614
65726
  // node_modules/@probelabs/maid/out/diagrams/sequence/semantics.js
65727
+ function isEscapedEntitySemicolon(image, semicolonIdx) {
65728
+ const uptoSemicolon = image.slice(0, semicolonIdx + 1);
65729
+ return /(?:#\d+|&#\d+|&[A-Za-z][A-Za-z0-9]+);$/.test(uptoSemicolon);
65730
+ }
65615
65731
  function analyzeSequence(_cst, _tokens) {
65616
65732
  const ctx = { tokens: _tokens };
65617
65733
  const v = new SequenceSemanticsVisitor(ctx);
@@ -65712,6 +65828,35 @@ function analyzeSequence(_cst, _tokens) {
65712
65828
  if (arrowIdx > 0) {
65713
65829
  const from = grabActorRef(arr, 0);
65714
65830
  const to = grabActorRef(arr, arrowIdx + 1);
65831
+ const colonIdx = arr.findIndex((tk, idx) => idx > arrowIdx && tk.tokenType === Colon3);
65832
+ if (colonIdx !== -1) {
65833
+ let semicolonColumn = null;
65834
+ for (let i = colonIdx + 1; i < arr.length && semicolonColumn == null; i++) {
65835
+ const tk = arr[i];
65836
+ const img = tk.image || "";
65837
+ if (!img.includes(";"))
65838
+ continue;
65839
+ for (let j = 0; j < img.length; j++) {
65840
+ if (img[j] !== ";")
65841
+ continue;
65842
+ if (isEscapedEntitySemicolon(img, j))
65843
+ continue;
65844
+ semicolonColumn = (tk.startColumn ?? 1) + j;
65845
+ break;
65846
+ }
65847
+ }
65848
+ if (semicolonColumn != null) {
65849
+ errs.push({
65850
+ line: ln,
65851
+ column: semicolonColumn,
65852
+ severity: "error",
65853
+ code: "SE-MSG-SEMICOLON-UNESCAPED",
65854
+ message: "Semicolons in sequence message text must be escaped as '#59;'.",
65855
+ hint: "Replace ';' with '#59;' in the message text.",
65856
+ length: 1
65857
+ });
65858
+ }
65859
+ }
65715
65860
  if (from || to) {
65716
65861
  const plusTok = arr.find((tk) => tk.tokenType === Plus);
65717
65862
  const minusTok = arr.find((tk) => tk.tokenType === Minus);
@@ -67286,6 +67431,20 @@ function computeFixes(text, errors, level = "safe") {
67286
67431
  out = out.split(SENT_Q).join("&quot;");
67287
67432
  return out;
67288
67433
  }
67434
+ function escapeUnescapedSemicolons(textPart) {
67435
+ let out = "";
67436
+ for (let i = 0; i < textPart.length; i++) {
67437
+ const ch = textPart[i];
67438
+ if (ch !== ";") {
67439
+ out += ch;
67440
+ continue;
67441
+ }
67442
+ const upto = textPart.slice(0, i + 1);
67443
+ const isEntity = /(?:#\d+|&#\d+|&[A-Za-z][A-Za-z0-9]+);$/.test(upto);
67444
+ out += isEntity ? ";" : "#59;";
67445
+ }
67446
+ return out;
67447
+ }
67289
67448
  for (const e of errors) {
67290
67449
  const key = `${e.code}@${e.line}:${e.column}:${e.length ?? 1}`;
67291
67450
  if (seen.has(key))
@@ -68193,6 +68352,35 @@ function computeFixes(text, errors, level = "safe") {
68193
68352
  }
68194
68353
  continue;
68195
68354
  }
68355
+ if (is("SE-MSG-SEMICOLON-UNESCAPED", e)) {
68356
+ const lineText = lineTextAt(text, e.line);
68357
+ const arrows = ["<<-->>", "<<->>", "-->>", "->>", "-->", "->", "--x", "-x", "--)", "-)"];
68358
+ let ai = -1;
68359
+ let alen = 0;
68360
+ for (const a of arrows) {
68361
+ const idx = lineText.indexOf(a);
68362
+ if (idx !== -1 && (ai === -1 || idx < ai)) {
68363
+ ai = idx;
68364
+ alen = a.length;
68365
+ }
68366
+ }
68367
+ if (ai !== -1) {
68368
+ const colonIdx = lineText.indexOf(":", ai + alen);
68369
+ if (colonIdx !== -1) {
68370
+ const head2 = lineText.slice(0, colonIdx + 1);
68371
+ const tail = lineText.slice(colonIdx + 1);
68372
+ const fixedTail = escapeUnescapedSemicolons(tail);
68373
+ if (fixedTail !== tail) {
68374
+ edits.push({
68375
+ start: { line: e.line, column: 1 },
68376
+ end: { line: e.line, column: lineText.length + 1 },
68377
+ newText: head2 + fixedTail
68378
+ });
68379
+ }
68380
+ }
68381
+ }
68382
+ continue;
68383
+ }
68196
68384
  if (is("SE-NOTE-MALFORMED", e)) {
68197
68385
  const lineText = lineTextAt(text, e.line);
68198
68386
  const mLR = /^(\s*)Note\s+(left|right)\s+of\s+(.+?)\s+(.+)$/.exec(lineText);
@@ -102912,19 +103100,23 @@ ${activeToolsDesc}
102912
103100
  - Time budget remaining: ${Math.round(remainingBudgetMs / 6e4)} minutes
102913
103101
  - Max per extension: ${maxPerReqMin} minutes
102914
103102
 
102915
- Decide whether the agent should get more time. EXTEND if:
103103
+ 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.
103104
+
103105
+ EXTEND (this should be your default) if:
102916
103106
  - Tools are actively running (especially delegates or complex analysis) \u2014 they need time to finish
102917
- - The agent is making clear progress on a complex task
103107
+ - The agent is making progress, even if slowly \u2014 complex tasks take time
102918
103108
  - New information is being gathered that will improve the final answer
103109
+ - The agent is iterating on an approach (trying different searches, refining queries) \u2014 this is normal problem-solving, not a loop
103110
+ - There is remaining budget and the task is not yet complete
103111
+ - When in doubt, extend \u2014 it's better to give the agent a chance than to cut it off prematurely
102919
103112
 
102920
- DO NOT EXTEND if:
102921
- - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
102922
- - The conversation shows the agent retrying failed operations without changing approach
102923
- - The agent has enough information to answer but keeps searching for more
102924
- - Tool calls are returning empty or error results repeatedly
102925
- - The agent is doing redundant work (searching for things it already found)
103113
+ DO NOT EXTEND only if you see CLEAR evidence of:
103114
+ - 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)
103115
+ - The agent is retrying a fundamentally broken operation without changing its approach at all
103116
+ - Tool calls are consistently returning errors or empty results AND the agent is not adapting
103117
+ - The conversation clearly shows the agent has all the information it needs and is just making redundant calls
102926
103118
 
102927
- 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.
103119
+ 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.
102928
103120
 
102929
103121
  Respond with ONLY valid JSON (no markdown, no explanation):
102930
103122
  {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
@@ -102968,38 +103160,38 @@ or
102968
103160
  const decision = JSON.parse(jsonStr);
102969
103161
  if (decision.extend && decision.minutes > 0) {
102970
103162
  const requestedMs = Math.min(decision.minutes, maxPerReqMin) * 6e4;
102971
- const grantedMs = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
102972
- const grantedMin = Math.round(grantedMs / 6e4 * 10) / 10;
103163
+ const grantedMs2 = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
103164
+ const grantedMin2 = Math.round(grantedMs2 / 6e4 * 10) / 10;
102973
103165
  negotiatedTimeoutState.extensionsUsed++;
102974
- negotiatedTimeoutState.totalExtraTimeMs += grantedMs;
102975
- 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.`;
103166
+ negotiatedTimeoutState.totalExtraTimeMs += grantedMs2;
103167
+ 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.`;
102976
103168
  negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
102977
103169
  runTimeoutObserver();
102978
- }, grantedMs);
103170
+ }, grantedMs2);
102979
103171
  if (this.debug) {
102980
- console.log(`[DEBUG] Timeout observer: granted ${grantedMin} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
103172
+ console.log(`[DEBUG] Timeout observer: granted ${grantedMin2} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
102981
103173
  }
102982
103174
  if (this.tracer) {
102983
103175
  this.tracer.addEvent("negotiated_timeout.observer_extended", {
102984
103176
  decision_reason: decision.reason,
102985
103177
  requested_minutes: decision.minutes,
102986
- granted_ms: grantedMs,
102987
- granted_min: grantedMin,
103178
+ granted_ms: grantedMs2,
103179
+ granted_min: grantedMin2,
102988
103180
  extensions_used: negotiatedTimeoutState.extensionsUsed,
102989
103181
  max_requests: negotiatedTimeoutState.maxRequests,
102990
103182
  total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102991
- budget_remaining_ms: remainingBudgetMs - grantedMs,
103183
+ budget_remaining_ms: remainingBudgetMs - grantedMs2,
102992
103184
  active_tools: activeToolsList.map((t) => t.name),
102993
103185
  active_tools_count: activeToolsList.length
102994
103186
  });
102995
103187
  }
102996
103188
  this.events.emit("timeout.extended", {
102997
- grantedMs,
103189
+ grantedMs: grantedMs2,
102998
103190
  reason: decision.reason || "work in progress",
102999
103191
  extensionsUsed: negotiatedTimeoutState.extensionsUsed,
103000
103192
  extensionsRemaining: negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed,
103001
103193
  totalExtraTimeMs: negotiatedTimeoutState.totalExtraTimeMs,
103002
- budgetRemainingMs: remainingBudgetMs - grantedMs
103194
+ budgetRemainingMs: remainingBudgetMs - grantedMs2
103003
103195
  });
103004
103196
  } else {
103005
103197
  if (this.debug) {
@@ -103021,6 +103213,18 @@ or
103021
103213
  });
103022
103214
  await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
103023
103215
  }
103216
+ return {
103217
+ decision: decision.extend ? "extended" : "declined",
103218
+ reason: decision.reason || "",
103219
+ ...decision.extend ? {
103220
+ granted_ms: grantedMs,
103221
+ granted_min: grantedMin,
103222
+ budget_remaining_ms: remainingBudgetMs - grantedMs
103223
+ } : {},
103224
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
103225
+ max_requests: negotiatedTimeoutState.maxRequests,
103226
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs
103227
+ };
103024
103228
  };
103025
103229
  try {
103026
103230
  if (this.tracer) {
@@ -103029,6 +103233,23 @@ or
103029
103233
  "timeout.extensions_used": negotiatedTimeoutState.extensionsUsed,
103030
103234
  "timeout.active_tools_count": activeToolsList.length,
103031
103235
  "timeout.remaining_budget_ms": remainingBudgetMs
103236
+ }, (span, result) => {
103237
+ if (result) {
103238
+ span.setAttributes({
103239
+ "observer.decision": result.decision,
103240
+ "observer.reason": result.reason,
103241
+ "observer.extensions_used": result.extensions_used,
103242
+ "observer.max_requests": result.max_requests,
103243
+ "observer.total_extra_time_ms": result.total_extra_time_ms
103244
+ });
103245
+ if (result.decision === "extended") {
103246
+ span.setAttributes({
103247
+ "observer.granted_ms": result.granted_ms,
103248
+ "observer.granted_min": result.granted_min,
103249
+ "observer.budget_remaining_ms": result.budget_remaining_ms
103250
+ });
103251
+ }
103252
+ }
103032
103253
  });
103033
103254
  } else {
103034
103255
  await observerFn();
@@ -103136,7 +103357,13 @@ or
103136
103357
  }
103137
103358
  return {
103138
103359
  toolChoice: "none",
103139
- 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.`
103360
+ 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.
103361
+
103362
+ 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.
103363
+
103364
+ 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.
103365
+
103366
+ 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.`
103140
103367
  };
103141
103368
  }
103142
103369
  if (this.debug) {
@@ -103564,7 +103791,9 @@ Respond with ONLY valid JSON \u2014 no markdown, no explanation, no text outside
103564
103791
  } catch {
103565
103792
  }
103566
103793
  }
103567
- 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.
103794
+ 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.
103795
+
103796
+ 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.
103568
103797
 
103569
103798
  Please provide a DETAILED summary of:
103570
103799
  1. What you were asked to do (the original task)
@@ -103599,7 +103828,14 @@ Be thorough \u2014 this is the user's only response. Include all useful informat
103599
103828
  let summaryText;
103600
103829
  if (this.tracer) {
103601
103830
  summaryText = await this.tracer.withSpan("negotiated_timeout.abort_summary", summaryFn, {
103602
- "summary.conversation_messages": currentMessages.length
103831
+ "summary.conversation_messages": currentMessages.length,
103832
+ "observer.was_timeout": true
103833
+ }, (span, result) => {
103834
+ if (result) {
103835
+ span.setAttributes({
103836
+ "observer.summary_length": result.length
103837
+ });
103838
+ }
103603
103839
  });
103604
103840
  } else {
103605
103841
  summaryText = await summaryFn();