@probelabs/probe 0.6.0-rc285 → 0.6.0-rc287

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.
@@ -93,7 +93,7 @@ import { formatAvailableSkillsXml as formatAvailableSkills } from './skills/form
93
93
  import { createSkillToolInstances } from './skills/tools.js';
94
94
  import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
95
95
  import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
96
- import { handleContextLimitError } from './contextCompactor.js';
96
+ import { handleContextLimitError, compactMessages, calculateCompactionStats } from './contextCompactor.js';
97
97
  import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
98
98
  import { getCommonPrefix, toRelativePath, safeRealpath } from '../utils/path-validation.js';
99
99
  import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
@@ -3227,6 +3227,25 @@ Follow these instructions carefully:
3227
3227
  ];
3228
3228
  }
3229
3229
 
3230
+ // Proactively compact for multi-turn conversations.
3231
+ // On turn 2+, previous turns contain full tool call/result history which can
3232
+ // be 50K+ tokens. This drowns out the new user message and causes the model to
3233
+ // focus on prior context rather than the new question.
3234
+ // compactMessages strips intermediate monologue from completed segments,
3235
+ // keeping user messages + final answers from prior turns.
3236
+ // Must run AFTER adding the new user message so the compactor sees 2+ segments
3237
+ // (completed prior turns + the new incomplete turn), preserving the latest segment.
3238
+ if (this.history.length > 0) {
3239
+ const compacted = compactMessages(currentMessages, { keepLastSegment: true, minSegmentsToKeep: 1 });
3240
+ if (compacted.length < currentMessages.length) {
3241
+ const stats = calculateCompactionStats(currentMessages, compacted);
3242
+ if (this.debug) {
3243
+ console.log(`[DEBUG] Proactive history compaction: ${currentMessages.length} → ${compacted.length} messages (${stats.reductionPercent}% reduction, ~${stats.tokensSaved} tokens saved)`);
3244
+ }
3245
+ currentMessages = compacted;
3246
+ }
3247
+ }
3248
+
3230
3249
  let currentIteration = 0;
3231
3250
  let finalResult = 'I was unable to complete your request due to reaching the maximum number of tool iterations.';
3232
3251
 
@@ -4141,8 +4160,6 @@ Double-check your response based on the criteria above. If everything looks good
4141
4160
  * @returns {Object} Compaction statistics
4142
4161
  */
4143
4162
  async compactHistory(options = {}) {
4144
- const { compactMessages, calculateCompactionStats } = await import('./contextCompactor.js');
4145
-
4146
4163
  if (this.history.length === 0) {
4147
4164
  if (this.debug) {
4148
4165
  console.log(`[DEBUG] No history to compact for session ${this.sessionId}`);
@@ -2755,7 +2755,11 @@ async function extractBinary(assetPath, outputDir) {
2755
2755
  if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
2756
2756
  console.log(`Extracting zip to ${extractDir}...`);
2757
2757
  }
2758
- await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
2758
+ if (isWindows) {
2759
+ await exec(`powershell -NoProfile -Command "Expand-Archive -Path '${assetPath}' -DestinationPath '${extractDir}' -Force"`);
2760
+ } else {
2761
+ await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
2762
+ }
2759
2763
  } else {
2760
2764
  if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
2761
2765
  console.log(`Copying binary directly to ${binaryPath}`);
@@ -8929,7 +8933,14 @@ function parseTargets(targets) {
8929
8933
  }
8930
8934
  function parseAndResolvePaths(pathStr, cwd) {
8931
8935
  if (!pathStr) return [];
8932
- const paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
8936
+ let paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
8937
+ paths = paths.flatMap((p) => {
8938
+ if (!/\s/.test(p)) return [p];
8939
+ const parts = p.split(/\s+/).filter(Boolean);
8940
+ if (parts.length <= 1) return [p];
8941
+ const allLookLikePaths = parts.every((part) => /[/\\]/.test(part) || /\.\w+/.test(part));
8942
+ return allLookLikePaths ? parts : [p];
8943
+ });
8933
8944
  return paths.map((p) => {
8934
8945
  if (isAbsolute(p)) {
8935
8946
  return p;
@@ -9163,11 +9174,24 @@ function normalizeTargets(targets) {
9163
9174
  if (typeof target !== "string") continue;
9164
9175
  const trimmed = target.trim();
9165
9176
  if (!trimmed || seen.has(trimmed)) continue;
9166
- seen.add(trimmed);
9167
- normalized.push(trimmed);
9177
+ const subTargets = splitSpaceSeparatedPaths(trimmed);
9178
+ for (const sub of subTargets) {
9179
+ if (!seen.has(sub)) {
9180
+ seen.add(sub);
9181
+ normalized.push(sub);
9182
+ }
9183
+ }
9168
9184
  }
9169
9185
  return normalized;
9170
9186
  }
9187
+ function splitSpaceSeparatedPaths(target) {
9188
+ if (!/\s/.test(target)) return [target];
9189
+ const parts = target.split(/\s+/).filter(Boolean);
9190
+ if (parts.length <= 1) return [target];
9191
+ const allLookLikePaths = parts.every((p) => /[/\\]/.test(p) || /\.\w+/.test(p));
9192
+ if (allLookLikePaths) return parts;
9193
+ return [target];
9194
+ }
9171
9195
  function extractJsonSnippet(text) {
9172
9196
  const jsonBlockMatch = text.match(/```json\s*([\s\S]*?)```/i);
9173
9197
  if (jsonBlockMatch) {
@@ -9651,14 +9675,15 @@ var init_vercel = __esm({
9651
9675
  const parsedTargets = parseTargets(targets);
9652
9676
  extractFiles = parsedTargets.map((target) => resolveTargetPath(target, effectiveCwd));
9653
9677
  if (options.allowedFolders && options.allowedFolders.length > 0) {
9678
+ const { join: pathJoin, sep: pathSep } = await import("path");
9654
9679
  extractFiles = extractFiles.map((target) => {
9655
9680
  const { filePart, suffix } = splitTargetSuffix(target);
9656
9681
  if (existsSync(filePart)) return target;
9657
- const cwdPrefix = effectiveCwd.endsWith("/") ? effectiveCwd : effectiveCwd + "/";
9682
+ const cwdPrefix = effectiveCwd.endsWith(pathSep) ? effectiveCwd : effectiveCwd + pathSep;
9658
9683
  const relativePart = filePart.startsWith(cwdPrefix) ? filePart.slice(cwdPrefix.length) : null;
9659
9684
  if (relativePart) {
9660
9685
  for (const folder of options.allowedFolders) {
9661
- const candidate = folder + "/" + relativePart;
9686
+ const candidate = pathJoin(folder, relativePart);
9662
9687
  if (existsSync(candidate)) {
9663
9688
  if (debug) console.error(`[extract] Auto-fixed path: ${filePart} \u2192 ${candidate}`);
9664
9689
  return candidate + suffix;
@@ -9666,11 +9691,12 @@ var init_vercel = __esm({
9666
9691
  }
9667
9692
  }
9668
9693
  for (const folder of options.allowedFolders) {
9669
- const folderPrefix = folder.endsWith("/") ? folder : folder + "/";
9670
- const wsParent = folderPrefix.replace(/[^/]+\/$/, "");
9694
+ const folderPrefix = folder.endsWith(pathSep) ? folder : folder + pathSep;
9695
+ const sepEscaped = pathSep === "\\" ? "\\\\" : pathSep;
9696
+ const wsParent = folderPrefix.replace(new RegExp("[^" + sepEscaped + "]+" + sepEscaped + "$"), "");
9671
9697
  if (filePart.startsWith(wsParent)) {
9672
9698
  const tail = filePart.slice(wsParent.length);
9673
- const candidate = folderPrefix + tail;
9699
+ const candidate = pathJoin(folderPrefix, tail);
9674
9700
  if (candidate !== filePart && existsSync(candidate)) {
9675
9701
  if (debug) console.error(`[extract] Auto-fixed path via workspace: ${filePart} \u2192 ${candidate}`);
9676
9702
  return candidate + suffix;
@@ -44301,6 +44327,7 @@ var init_parser2 = __esm({
44301
44327
  { ALT: () => this.CONSUME(Identifier) },
44302
44328
  { ALT: () => this.CONSUME(Text) },
44303
44329
  { ALT: () => this.CONSUME(NumberLiteral) },
44330
+ { ALT: () => this.CONSUME(ColorValue) },
44304
44331
  // Note: RoundOpen and RoundClose (parentheses) are NOT allowed in unquoted labels
44305
44332
  // to match Mermaid's behavior - use quoted labels like ["text (with parens)"] instead
44306
44333
  // Allow HTML-like tags (e.g., <br/>) inside labels
@@ -44392,6 +44419,7 @@ var init_parser2 = __esm({
44392
44419
  { ALT: () => this.CONSUME(Identifier) },
44393
44420
  { ALT: () => this.CONSUME(Text) },
44394
44421
  { ALT: () => this.CONSUME(NumberLiteral) },
44422
+ { ALT: () => this.CONSUME(ColorValue) },
44395
44423
  // Allow HTML-like angle brackets and slashes for <br/>, <i>, etc.
44396
44424
  { ALT: () => this.CONSUME(AngleLess) },
44397
44425
  { ALT: () => this.CONSUME(AngleOpen) },
@@ -45567,13 +45595,24 @@ function mapFlowchartParserError(err, text) {
45567
45595
  length: len
45568
45596
  };
45569
45597
  }
45570
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
45598
+ if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose" || tokType === "DiamondOpen" || tokType === "DiamondClose") {
45571
45599
  const context = err?.context;
45572
45600
  const inLinkRule = context?.ruleStack?.includes("linkTextInline") || context?.ruleStack?.includes("link") || false;
45573
45601
  const lineContent = allLines[Math.max(0, line - 1)] || "";
45574
45602
  const beforeQuote = lineContent.slice(0, Math.max(0, column - 1));
45575
45603
  const hasLinkBefore = beforeQuote.match(/--\s*$|==\s*$|-\.\s*$|-\.-\s*$|\[\s*$/);
45576
45604
  if (inLinkRule || hasLinkBefore) {
45605
+ if (tokType === "DiamondOpen" || tokType === "DiamondClose") {
45606
+ return {
45607
+ line,
45608
+ column,
45609
+ severity: "error",
45610
+ code: "FL-EDGE-LABEL-CURLY-IN-PIPES",
45611
+ message: "Curly braces { } are not supported inside pipe-delimited edge labels.",
45612
+ hint: "Use HTML entities &#123; and &#125; inside |...|, e.g., --|Resolve $&#123;VAR&#125;|-->",
45613
+ length: len
45614
+ };
45615
+ }
45577
45616
  if (tokType === "SquareOpen" || tokType === "SquareClose") {
45578
45617
  return {
45579
45618
  line,
@@ -45635,6 +45674,17 @@ function mapFlowchartParserError(err, text) {
45635
45674
  length: len
45636
45675
  };
45637
45676
  }
45677
+ if (tokType === "DiamondOpen" || tokType === "DiamondClose") {
45678
+ return {
45679
+ line,
45680
+ column,
45681
+ severity: "error",
45682
+ code: "FL-LABEL-CURLY-IN-UNQUOTED",
45683
+ message: "Curly braces are not supported inside unquoted node labels.",
45684
+ hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;].",
45685
+ length: len
45686
+ };
45687
+ }
45638
45688
  {
45639
45689
  const caret0 = Math.max(0, column - 1);
45640
45690
  const openIdx = lineStr.lastIndexOf("[", caret0);
@@ -45676,9 +45726,20 @@ function mapFlowchartParserError(err, text) {
45676
45726
  length: len
45677
45727
  };
45678
45728
  }
45729
+ if (seg.includes("{") || seg.includes("}")) {
45730
+ return {
45731
+ line,
45732
+ column,
45733
+ severity: "error",
45734
+ code: "FL-LABEL-CURLY-IN-UNQUOTED",
45735
+ message: "Curly braces are not supported inside unquoted node labels.",
45736
+ hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;].",
45737
+ length: len
45738
+ };
45739
+ }
45679
45740
  }
45680
45741
  }
45681
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
45742
+ if (tokType === "QuotedString") {
45682
45743
  return {
45683
45744
  line,
45684
45745
  column,
@@ -45689,6 +45750,17 @@ function mapFlowchartParserError(err, text) {
45689
45750
  length: len
45690
45751
  };
45691
45752
  }
45753
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
45754
+ return {
45755
+ line,
45756
+ column,
45757
+ severity: "error",
45758
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
45759
+ message: "Square brackets are not supported inside unquoted node labels.",
45760
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
45761
+ length: len
45762
+ };
45763
+ }
45692
45764
  const q = findInnerQuoteIssue("[");
45693
45765
  if (q?.kind === "escaped") {
45694
45766
  return { line, column: q.column, severity: "error", code: "FL-LABEL-ESCAPED-QUOTE", message: 'Escaped quotes (\\") in node labels are not supported by Mermaid. Use &quot; instead.', hint: 'Prefer "He said &quot;Hi&quot;".', length: 2 };
@@ -45699,7 +45771,7 @@ function mapFlowchartParserError(err, text) {
45699
45771
  return { line, column, severity: "error", code: "FL-NODE-UNCLOSED-BRACKET", message: "Unclosed '['. Add a matching ']' before the arrow or newline.", hint: "Example: A[Label] --> B", length: 1 };
45700
45772
  }
45701
45773
  if (expecting(err, "RoundClose")) {
45702
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
45774
+ if (tokType === "QuotedString") {
45703
45775
  return {
45704
45776
  line,
45705
45777
  column,
@@ -45710,6 +45782,17 @@ function mapFlowchartParserError(err, text) {
45710
45782
  length: len
45711
45783
  };
45712
45784
  }
45785
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
45786
+ return {
45787
+ line,
45788
+ column,
45789
+ severity: "error",
45790
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
45791
+ message: "Square brackets are not supported inside unquoted node labels.",
45792
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
45793
+ length: len
45794
+ };
45795
+ }
45713
45796
  {
45714
45797
  const caret0 = Math.max(0, column - 1);
45715
45798
  const openIdx = lineStr.lastIndexOf("(", caret0);
@@ -45738,7 +45821,7 @@ function mapFlowchartParserError(err, text) {
45738
45821
  return { line, column, severity: "error", code: "FL-NODE-UNCLOSED-BRACKET", message: "Unclosed '('. Add a matching ')'.", hint: "Example: B(Label)", length: 1 };
45739
45822
  }
45740
45823
  if (expecting(err, "DiamondClose")) {
45741
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
45824
+ if (tokType === "QuotedString") {
45742
45825
  return {
45743
45826
  line,
45744
45827
  column,
@@ -45749,6 +45832,17 @@ function mapFlowchartParserError(err, text) {
45749
45832
  length: len
45750
45833
  };
45751
45834
  }
45835
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
45836
+ return {
45837
+ line,
45838
+ column,
45839
+ severity: "error",
45840
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
45841
+ message: "Square brackets are not supported inside unquoted node labels.",
45842
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
45843
+ length: len
45844
+ };
45845
+ }
45752
45846
  {
45753
45847
  const caret0 = Math.max(0, column - 1);
45754
45848
  const openIdx = lineStr.lastIndexOf("{", caret0);
@@ -46591,7 +46685,7 @@ function validateFlowchart(text, options = {}) {
46591
46685
  const byLine = /* @__PURE__ */ new Map();
46592
46686
  const collect = (arr) => {
46593
46687
  for (const e of arr || []) {
46594
- if (e && (e.code === "FL-LABEL-PARENS-UNQUOTED" || e.code === "FL-LABEL-AT-IN-UNQUOTED" || e.code === "FL-LABEL-QUOTE-IN-UNQUOTED" || e.code === "FL-LABEL-SLASH-UNQUOTED")) {
46688
+ if (e && (e.code === "FL-LABEL-PARENS-UNQUOTED" || e.code === "FL-LABEL-AT-IN-UNQUOTED" || e.code === "FL-LABEL-QUOTE-IN-UNQUOTED" || e.code === "FL-LABEL-SLASH-UNQUOTED" || e.code === "FL-LABEL-CURLY-IN-UNQUOTED" || e.code === "FL-LABEL-BRACKET-IN-UNQUOTED")) {
46595
46689
  const ln = e.line ?? 0;
46596
46690
  const col = e.column ?? 1;
46597
46691
  const list = byLine.get(ln) || [];
@@ -46673,6 +46767,9 @@ function validateFlowchart(text, options = {}) {
46673
46767
  const hasParens = seg.includes("(") || seg.includes(")");
46674
46768
  const hasAt = seg.includes("@");
46675
46769
  const hasQuote = seg.includes('"');
46770
+ const hasCurly = seg.includes("{") || seg.includes("}");
46771
+ const hasBracket = seg.includes("[") || seg.includes("]");
46772
+ const isDoubleSquare = raw.slice(i, i + 2) === "[[" && raw.slice(j - 2, j) === "]]";
46676
46773
  const isSingleQuoted = /^'[^]*'$/.test(trimmed);
46677
46774
  const hasLeadingSlash = lsp === "/" || lsp === "\\";
46678
46775
  if (!covered && !isQuoted && !isParenWrapped && hasParens) {
@@ -46690,6 +46787,16 @@ function validateFlowchart(text, options = {}) {
46690
46787
  existing.push({ start: startCol, end: endCol });
46691
46788
  byLine.set(ln, existing);
46692
46789
  }
46790
+ if (!covered && !isQuoted && !isSlashPair && hasCurly) {
46791
+ errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-CURLY-IN-UNQUOTED", message: "Curly braces are not supported inside unquoted node labels.", hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;]." });
46792
+ existing.push({ start: startCol, end: endCol });
46793
+ byLine.set(ln, existing);
46794
+ }
46795
+ if (!covered && !isQuoted && !isSlashPair && !isDoubleSquare && hasBracket) {
46796
+ errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-BRACKET-IN-UNQUOTED", message: "Square brackets are not supported inside unquoted node labels.", hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]' });
46797
+ existing.push({ start: startCol, end: endCol });
46798
+ byLine.set(ln, existing);
46799
+ }
46693
46800
  if (!covered && !isQuoted && !isSlashPair && hasQuote && !isSingleQuoted) {
46694
46801
  errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-QUOTE-IN-UNQUOTED", message: "Quotes are not allowed inside unquoted node labels. Use &quot; for quotes or wrap the entire label in quotes.", hint: 'Example: C["HTML Output: data-trigger-visibility=&quot;true&quot;"]' });
46695
46802
  existing.push({ start: startCol, end: endCol });
@@ -49050,7 +49157,7 @@ function computeFixes(text, errors, level = "safe") {
49050
49157
  }
49051
49158
  continue;
49052
49159
  }
49053
- if (is("FL-EDGE-LABEL-BRACKET", e)) {
49160
+ if (is("FL-EDGE-LABEL-BRACKET", e) || is("FL-EDGE-LABEL-CURLY-IN-PIPES", e)) {
49054
49161
  const lineText = lineTextAt(text, e.line);
49055
49162
  const firstBar = lineText.indexOf("|");
49056
49163
  const secondBar = firstBar >= 0 ? lineText.indexOf("|", firstBar + 1) : -1;
@@ -49058,7 +49165,8 @@ function computeFixes(text, errors, level = "safe") {
49058
49165
  const before = lineText.slice(0, firstBar + 1);
49059
49166
  const label = lineText.slice(firstBar + 1, secondBar);
49060
49167
  const after = lineText.slice(secondBar);
49061
- const fixedLabel = label.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
49168
+ let fixedLabel = label.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
49169
+ fixedLabel = fixedLabel.replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
49062
49170
  const fixedLine = before + fixedLabel + after;
49063
49171
  const finalLine = fixedLine.replace(/\[([^\]]*)\]/g, (m, seg) => "[" + String(seg).replace(/`/g, "") + "]");
49064
49172
  edits.push({ start: { line: e.line, column: 1 }, end: { line: e.line, column: lineText.length + 1 }, newText: finalLine });
@@ -49698,7 +49806,7 @@ function computeFixes(text, errors, level = "safe") {
49698
49806
  }
49699
49807
  continue;
49700
49808
  }
49701
- if (is("FL-LABEL-PARENS-UNQUOTED", e) || is("FL-LABEL-AT-IN-UNQUOTED", e) || is("FL-LABEL-SLASH-UNQUOTED", e)) {
49809
+ if (is("FL-LABEL-PARENS-UNQUOTED", e) || is("FL-LABEL-AT-IN-UNQUOTED", e) || is("FL-LABEL-SLASH-UNQUOTED", e) || is("FL-LABEL-CURLY-IN-UNQUOTED", e) || is("FL-LABEL-BRACKET-IN-UNQUOTED", e)) {
49702
49810
  if (level === "safe" || level === "all") {
49703
49811
  if (patchedLines.has(e.line))
49704
49812
  continue;
@@ -49739,7 +49847,7 @@ function computeFixes(text, errors, level = "safe") {
49739
49847
  if (openIdx === -1)
49740
49848
  break;
49741
49849
  const contentStart = openIdx + shape.open.length;
49742
- const closeIdx = shape.open === "(" && shape.close === ")" ? findMatchingCloser(lineText, openIdx, shape.open, shape.close) : lineText.indexOf(shape.close, contentStart);
49850
+ const closeIdx = shape.open === "(" && shape.close === ")" || shape.open === "[" && shape.close === "]" ? findMatchingCloser(lineText, openIdx, shape.open, shape.close) : lineText.indexOf(shape.close, contentStart);
49743
49851
  if (closeIdx === -1)
49744
49852
  break;
49745
49853
  if (openIdx <= caret0 && caret0 < closeIdx) {
@@ -49759,11 +49867,21 @@ function computeFixes(text, errors, level = "safe") {
49759
49867
  const isSlashPair = (l, r) => l === "/" && r === "/" || l === "\\" && r === "\\" || l === "/" && r === "\\" || l === "\\" && r === "/";
49760
49868
  const isParallelogramShape = core.length >= 2 && isSlashPair(left, right);
49761
49869
  let replaced;
49762
- if (!isParallelogramShape) {
49763
- const escaped = inner.replace(/`/g, "").replace(/\"/g, "&quot;").replace(/"/g, "&quot;");
49870
+ if (is("FL-LABEL-BRACKET-IN-UNQUOTED", e) && !isParallelogramShape) {
49871
+ const hasOtherHazards = /[(){}@]/.test(inner) || inner.includes('"') || inner.includes('\\"');
49872
+ if (hasOtherHazards) {
49873
+ const escaped = inner.replace(/`/g, "").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
49874
+ replaced = '"' + escaped + '"';
49875
+ } else {
49876
+ replaced = inner.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
49877
+ }
49878
+ } else if (is("FL-LABEL-CURLY-IN-UNQUOTED", e)) {
49879
+ replaced = inner.replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
49880
+ } else if (!isParallelogramShape) {
49881
+ const escaped = inner.replace(/`/g, "").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
49764
49882
  replaced = '"' + escaped + '"';
49765
49883
  } else {
49766
- replaced = inner.replace(/`/g, "").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/\"/g, "&quot;").replace(/"/g, "&quot;");
49884
+ replaced = inner.replace(/`/g, "").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/\[/g, "&#91;").replace(/\]/g, "&#93;").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
49767
49885
  }
49768
49886
  if (replaced !== inner) {
49769
49887
  edits.push({ start: { line: e.line, column: contentStart + 1 }, end: { line: e.line, column: closeIdx + 1 }, newText: replaced });
@@ -79910,14 +80028,6 @@ var init_FallbackManager = __esm({
79910
80028
  });
79911
80029
 
79912
80030
  // src/agent/contextCompactor.js
79913
- var contextCompactor_exports = {};
79914
- __export(contextCompactor_exports, {
79915
- calculateCompactionStats: () => calculateCompactionStats,
79916
- compactMessages: () => compactMessages,
79917
- handleContextLimitError: () => handleContextLimitError,
79918
- identifyMessageSegments: () => identifyMessageSegments,
79919
- isContextLimitError: () => isContextLimitError
79920
- });
79921
80031
  function isContextLimitError(error) {
79922
80032
  if (!error) return false;
79923
80033
  const errorMessage = (typeof error === "string" ? error : error?.message || "").toLowerCase();
@@ -84234,6 +84344,16 @@ You are working with a workspace. Available paths: ${workspaceDesc}
84234
84344
  userMessage
84235
84345
  ];
84236
84346
  }
84347
+ if (this.history.length > 0) {
84348
+ const compacted = compactMessages(currentMessages, { keepLastSegment: true, minSegmentsToKeep: 1 });
84349
+ if (compacted.length < currentMessages.length) {
84350
+ const stats = calculateCompactionStats(currentMessages, compacted);
84351
+ if (this.debug) {
84352
+ console.log(`[DEBUG] Proactive history compaction: ${currentMessages.length} \u2192 ${compacted.length} messages (${stats.reductionPercent}% reduction, ~${stats.tokensSaved} tokens saved)`);
84353
+ }
84354
+ currentMessages = compacted;
84355
+ }
84356
+ }
84237
84357
  let currentIteration = 0;
84238
84358
  let finalResult = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
84239
84359
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
@@ -84934,7 +85054,6 @@ Double-check your response based on the criteria above. If everything looks good
84934
85054
  * @returns {Object} Compaction statistics
84935
85055
  */
84936
85056
  async compactHistory(options = {}) {
84937
- const { compactMessages: compactMessages2, calculateCompactionStats: calculateCompactionStats2 } = await Promise.resolve().then(() => (init_contextCompactor(), contextCompactor_exports));
84938
85057
  if (this.history.length === 0) {
84939
85058
  if (this.debug) {
84940
85059
  console.log(`[DEBUG] No history to compact for session ${this.sessionId}`);
@@ -84949,8 +85068,8 @@ Double-check your response based on the criteria above. If everything looks good
84949
85068
  tokensSaved: 0
84950
85069
  };
84951
85070
  }
84952
- const compactedMessages = compactMessages2(this.history, options);
84953
- const stats = calculateCompactionStats2(this.history, compactedMessages);
85071
+ const compactedMessages = compactMessages(this.history, options);
85072
+ const stats = calculateCompactionStats(this.history, compactedMessages);
84954
85073
  this.history = compactedMessages;
84955
85074
  try {
84956
85075
  await this.storageAdapter.clearHistory(this.sessionId);
@@ -743,7 +743,11 @@ async function extractBinary(assetPath, outputDir) {
743
743
  if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
744
744
  console.log(`Extracting zip to ${extractDir}...`);
745
745
  }
746
- await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
746
+ if (isWindows) {
747
+ await exec(`powershell -NoProfile -Command "Expand-Archive -Path '${assetPath}' -DestinationPath '${extractDir}' -Force"`);
748
+ } else {
749
+ await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
750
+ }
747
751
  } else {
748
752
  // Assume it's a direct binary
749
753
  if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
@@ -228,7 +228,17 @@ export function parseAndResolvePaths(pathStr, cwd) {
228
228
  if (!pathStr) return [];
229
229
 
230
230
  // Split on comma and trim whitespace
231
- const paths = pathStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
231
+ let paths = pathStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
232
+
233
+ // Auto-fix: model sometimes passes space-separated file paths as one string
234
+ // e.g. "src/ranking.rs src/simd_ranking.rs" — split if each part looks like a path
235
+ paths = paths.flatMap(p => {
236
+ if (!/\s/.test(p)) return [p];
237
+ const parts = p.split(/\s+/).filter(Boolean);
238
+ if (parts.length <= 1) return [p];
239
+ const allLookLikePaths = parts.every(part => /[/\\]/.test(part) || /\.\w+/.test(part));
240
+ return allLookLikePaths ? parts : [p];
241
+ });
232
242
 
233
243
  // Resolve relative paths against cwd
234
244
  return paths.map(p => {
@@ -105,13 +105,42 @@ function normalizeTargets(targets) {
105
105
  if (typeof target !== 'string') continue;
106
106
  const trimmed = target.trim();
107
107
  if (!trimmed || seen.has(trimmed)) continue;
108
- seen.add(trimmed);
109
- normalized.push(trimmed);
108
+
109
+ // Auto-fix: model sometimes puts multiple space-separated file paths in one string
110
+ // e.g. "src/ranking.rs src/simd_ranking.rs" — split them apart
111
+ const subTargets = splitSpaceSeparatedPaths(trimmed);
112
+ for (const sub of subTargets) {
113
+ if (!seen.has(sub)) {
114
+ seen.add(sub);
115
+ normalized.push(sub);
116
+ }
117
+ }
110
118
  }
111
119
 
112
120
  return normalized;
113
121
  }
114
122
 
123
+ /**
124
+ * Split a string that may contain multiple space-separated file paths.
125
+ * Detects patterns like "path/file.ext path2/file2.ext" and splits them.
126
+ * Preserves single paths and paths with suffixes like ":10-20" or "#Symbol".
127
+ */
128
+ function splitSpaceSeparatedPaths(target) {
129
+ // If no spaces, it's a single target
130
+ if (!/\s/.test(target)) return [target];
131
+
132
+ // Split on whitespace and check if parts look like file paths
133
+ const parts = target.split(/\s+/).filter(Boolean);
134
+ if (parts.length <= 1) return [target];
135
+
136
+ // Check if each part looks like a file path (has a dot extension or path separator)
137
+ const allLookLikePaths = parts.every(p => /[/\\]/.test(p) || /\.\w+/.test(p));
138
+ if (allLookLikePaths) return parts;
139
+
140
+ // Not confident these are separate paths — return as-is
141
+ return [target];
142
+ }
143
+
115
144
  function extractJsonSnippet(text) {
116
145
  const jsonBlockMatch = text.match(/```json\s*([\s\S]*?)```/i);
117
146
  if (jsonBlockMatch) {
@@ -691,19 +720,20 @@ export const extractTool = (options = {}) => {
691
720
  // model constructs wrong absolute paths (e.g., /workspace/gateway/file.go
692
721
  // instead of /workspace/tyk/gateway/file.go)
693
722
  if (options.allowedFolders && options.allowedFolders.length > 0) {
723
+ const { join: pathJoin, sep: pathSep } = await import('path');
694
724
  extractFiles = extractFiles.map(target => {
695
725
  const { filePart, suffix } = splitTargetSuffix(target);
696
726
  if (existsSync(filePart)) return target;
697
727
 
698
728
  // Try resolving the relative tail against each allowedFolder
699
- const cwdPrefix = (effectiveCwd.endsWith('/') ? effectiveCwd : effectiveCwd + '/');
729
+ const cwdPrefix = effectiveCwd.endsWith(pathSep) ? effectiveCwd : effectiveCwd + pathSep;
700
730
  const relativePart = filePart.startsWith(cwdPrefix)
701
731
  ? filePart.slice(cwdPrefix.length)
702
732
  : null;
703
733
 
704
734
  if (relativePart) {
705
735
  for (const folder of options.allowedFolders) {
706
- const candidate = folder + '/' + relativePart;
736
+ const candidate = pathJoin(folder, relativePart);
707
737
  if (existsSync(candidate)) {
708
738
  if (debug) console.error(`[extract] Auto-fixed path: ${filePart} → ${candidate}`);
709
739
  return candidate + suffix;
@@ -714,11 +744,12 @@ export const extractTool = (options = {}) => {
714
744
  // Try stripping workspace prefix and resolving against allowedFolders
715
745
  // e.g., /tmp/visor-workspaces/abc/gateway/file.go → try each folder + gateway/file.go
716
746
  for (const folder of options.allowedFolders) {
717
- const folderPrefix = folder.endsWith('/') ? folder : folder + '/';
718
- const wsParent = folderPrefix.replace(/[^/]+\/$/, '');
747
+ const folderPrefix = folder.endsWith(pathSep) ? folder : folder + pathSep;
748
+ const sepEscaped = pathSep === '\\' ? '\\\\' : pathSep;
749
+ const wsParent = folderPrefix.replace(new RegExp('[^' + sepEscaped + ']+' + sepEscaped + '$'), '');
719
750
  if (filePart.startsWith(wsParent)) {
720
751
  const tail = filePart.slice(wsParent.length);
721
- const candidate = folderPrefix + tail;
752
+ const candidate = pathJoin(folderPrefix, tail);
722
753
  if (candidate !== filePart && existsSync(candidate)) {
723
754
  if (debug) console.error(`[extract] Auto-fixed path via workspace: ${filePart} → ${candidate}`);
724
755
  return candidate + suffix;