@probelabs/probe 0.6.0-rc292 → 0.6.0-rc294
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/bin/binaries/{probe-v0.6.0-rc292-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc294-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc292-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc294-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc292-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc294-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc292-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc294-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc292-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc294-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/ProbeAgent.js +156 -48
- package/build/agent/shared/prompts.js +33 -3
- package/build/tools/fileTracker.js +33 -17
- package/build/tools/vercel.js +13 -10
- package/cjs/agent/ProbeAgent.cjs +211 -78
- package/cjs/index.cjs +218 -85
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +156 -48
- package/src/agent/shared/prompts.js +33 -3
- package/src/tools/fileTracker.js +33 -17
- package/src/tools/vercel.js +13 -10
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1341,9 +1341,16 @@ export class ProbeAgent {
|
|
|
1341
1341
|
// Use fallback manager with retry for each provider
|
|
1342
1342
|
return await this.fallbackManager.executeWithFallback(
|
|
1343
1343
|
async (provider, model, config) => {
|
|
1344
|
+
// Wrap fallback model with per-call concurrency limiter if configured.
|
|
1345
|
+
// The original options.model was wrapped in streamTextWithRetryAndFallback,
|
|
1346
|
+
// but fallback replaces it with a new model that needs wrapping too.
|
|
1347
|
+
let fallbackModel = provider(model);
|
|
1348
|
+
if (this.concurrencyLimiter) {
|
|
1349
|
+
fallbackModel = ProbeAgent._wrapModelWithLimiter(fallbackModel, this.concurrencyLimiter, this.debug);
|
|
1350
|
+
}
|
|
1344
1351
|
const fallbackOptions = {
|
|
1345
1352
|
...options,
|
|
1346
|
-
model:
|
|
1353
|
+
model: fallbackModel,
|
|
1347
1354
|
abortSignal: controller.signal
|
|
1348
1355
|
};
|
|
1349
1356
|
|
|
@@ -1377,6 +1384,143 @@ export class ProbeAgent {
|
|
|
1377
1384
|
);
|
|
1378
1385
|
}
|
|
1379
1386
|
|
|
1387
|
+
/**
|
|
1388
|
+
* Wrap a LanguageModelV1 model so each doStream/doGenerate call acquires and
|
|
1389
|
+
* releases a concurrency limiter slot. This gates individual LLM API calls
|
|
1390
|
+
* (seconds each) instead of entire multi-step agent sessions (minutes).
|
|
1391
|
+
*
|
|
1392
|
+
* @param {Object} model - LanguageModelV1 model instance
|
|
1393
|
+
* @param {Object} limiter - Concurrency limiter with acquire/release/getStats
|
|
1394
|
+
* @param {boolean} debug - Enable debug logging
|
|
1395
|
+
* @returns {Object} Wrapped model with per-call concurrency gating
|
|
1396
|
+
* @private
|
|
1397
|
+
*/
|
|
1398
|
+
static _wrapModelWithLimiter(model, limiter, debug) {
|
|
1399
|
+
return new Proxy(model, {
|
|
1400
|
+
get(target, prop) {
|
|
1401
|
+
if (prop === 'doStream') {
|
|
1402
|
+
return async function (...args) {
|
|
1403
|
+
await limiter.acquire(null);
|
|
1404
|
+
if (debug) {
|
|
1405
|
+
const stats = limiter.getStats();
|
|
1406
|
+
console.log(`[DEBUG] Acquired AI slot for LLM call (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const result = await target.doStream(...args);
|
|
1410
|
+
|
|
1411
|
+
// Wrap the ReadableStream to release the slot when it completes,
|
|
1412
|
+
// errors, or is cancelled — covering all stream termination paths.
|
|
1413
|
+
// Guard against double-release: if cancel() races with an in-flight
|
|
1414
|
+
// pull() that is awaiting originalReader.read(), both paths could
|
|
1415
|
+
// try to release. The flag ensures exactly one release.
|
|
1416
|
+
const originalStream = result.stream;
|
|
1417
|
+
const originalReader = originalStream.getReader();
|
|
1418
|
+
let released = false;
|
|
1419
|
+
const releaseOnce = () => {
|
|
1420
|
+
if (released) return;
|
|
1421
|
+
released = true;
|
|
1422
|
+
limiter.release(null);
|
|
1423
|
+
};
|
|
1424
|
+
const wrappedStream = new ReadableStream({
|
|
1425
|
+
async pull(controller) {
|
|
1426
|
+
try {
|
|
1427
|
+
const { done, value } = await originalReader.read();
|
|
1428
|
+
if (done) {
|
|
1429
|
+
controller.close();
|
|
1430
|
+
releaseOnce();
|
|
1431
|
+
if (debug) {
|
|
1432
|
+
const stats = limiter.getStats();
|
|
1433
|
+
console.log(`[DEBUG] Released AI slot after LLM stream complete (${stats.globalActive}/${stats.maxConcurrent})`);
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
controller.enqueue(value);
|
|
1437
|
+
}
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
releaseOnce();
|
|
1440
|
+
if (debug) {
|
|
1441
|
+
console.log(`[DEBUG] Released AI slot on LLM stream error`);
|
|
1442
|
+
}
|
|
1443
|
+
controller.error(err);
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
cancel() {
|
|
1447
|
+
releaseOnce();
|
|
1448
|
+
if (debug) {
|
|
1449
|
+
console.log(`[DEBUG] Released AI slot on LLM stream cancel`);
|
|
1450
|
+
}
|
|
1451
|
+
originalReader.cancel();
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
return { ...result, stream: wrappedStream };
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
limiter.release(null);
|
|
1458
|
+
if (debug) {
|
|
1459
|
+
console.log(`[DEBUG] Released AI slot on doStream error`);
|
|
1460
|
+
}
|
|
1461
|
+
throw err;
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (prop === 'doGenerate') {
|
|
1467
|
+
return async function (...args) {
|
|
1468
|
+
await limiter.acquire(null);
|
|
1469
|
+
if (debug) {
|
|
1470
|
+
const stats = limiter.getStats();
|
|
1471
|
+
console.log(`[DEBUG] Acquired AI slot for LLM generate (${stats.globalActive}/${stats.maxConcurrent})`);
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
const result = await target.doGenerate(...args);
|
|
1475
|
+
return result;
|
|
1476
|
+
} finally {
|
|
1477
|
+
limiter.release(null);
|
|
1478
|
+
if (debug) {
|
|
1479
|
+
const stats = limiter.getStats();
|
|
1480
|
+
console.log(`[DEBUG] Released AI slot after LLM generate (${stats.globalActive}/${stats.maxConcurrent})`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const value = target[prop];
|
|
1487
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Wrap an engine stream result so its textStream async generator acquires
|
|
1494
|
+
* and releases a concurrency limiter slot. Acquire happens when iteration
|
|
1495
|
+
* begins; release happens in finally (completion, error, or break).
|
|
1496
|
+
*
|
|
1497
|
+
* @param {Object} result - Engine result with { textStream, usage, ... }
|
|
1498
|
+
* @param {Object} limiter - Concurrency limiter with acquire/release/getStats
|
|
1499
|
+
* @param {boolean} debug - Enable debug logging
|
|
1500
|
+
* @returns {Object} Result with wrapped textStream
|
|
1501
|
+
* @private
|
|
1502
|
+
*/
|
|
1503
|
+
static _wrapEngineStreamWithLimiter(result, limiter, debug) {
|
|
1504
|
+
const originalStream = result.textStream;
|
|
1505
|
+
async function* gatedStream() {
|
|
1506
|
+
await limiter.acquire(null);
|
|
1507
|
+
if (debug) {
|
|
1508
|
+
const stats = limiter.getStats();
|
|
1509
|
+
console.log(`[DEBUG] Acquired AI slot for engine stream (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
yield* originalStream;
|
|
1513
|
+
} finally {
|
|
1514
|
+
limiter.release(null);
|
|
1515
|
+
if (debug) {
|
|
1516
|
+
const stats = limiter.getStats();
|
|
1517
|
+
console.log(`[DEBUG] Released AI slot after engine stream (${stats.globalActive}/${stats.maxConcurrent})`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return { ...result, textStream: gatedStream() };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1380
1524
|
/**
|
|
1381
1525
|
* Execute streamText with retry and fallback support
|
|
1382
1526
|
* @param {Object} options - streamText options
|
|
@@ -1384,14 +1528,12 @@ export class ProbeAgent {
|
|
|
1384
1528
|
* @private
|
|
1385
1529
|
*/
|
|
1386
1530
|
async streamTextWithRetryAndFallback(options) {
|
|
1387
|
-
//
|
|
1531
|
+
// Wrap the model with per-call concurrency gating if limiter is configured.
|
|
1532
|
+
// This acquires/releases the slot around each individual LLM API call (doStream/doGenerate)
|
|
1533
|
+
// instead of holding it for the entire multi-step agent session.
|
|
1388
1534
|
const limiter = this.concurrencyLimiter;
|
|
1389
|
-
if (limiter) {
|
|
1390
|
-
|
|
1391
|
-
if (this.debug) {
|
|
1392
|
-
const stats = limiter.getStats();
|
|
1393
|
-
console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
|
|
1394
|
-
}
|
|
1535
|
+
if (limiter && options.model) {
|
|
1536
|
+
options = { ...options, model: ProbeAgent._wrapModelWithLimiter(options.model, limiter, this.debug) };
|
|
1395
1537
|
}
|
|
1396
1538
|
|
|
1397
1539
|
// Create AbortController for overall operation timeout
|
|
@@ -1430,6 +1572,12 @@ export class ProbeAgent {
|
|
|
1430
1572
|
if (useClaudeCode || useCodex) {
|
|
1431
1573
|
try {
|
|
1432
1574
|
result = await this._tryEngineStreamPath(options, controller, timeoutState);
|
|
1575
|
+
// Gate engine stream with concurrency limiter if configured.
|
|
1576
|
+
// Engine paths bypass the Vercel model wrapper, so we wrap the
|
|
1577
|
+
// textStream async generator with acquire/release instead.
|
|
1578
|
+
if (result && limiter) {
|
|
1579
|
+
result = ProbeAgent._wrapEngineStreamWithLimiter(result, limiter, this.debug);
|
|
1580
|
+
}
|
|
1433
1581
|
} catch (error) {
|
|
1434
1582
|
if (this.debug) {
|
|
1435
1583
|
const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
|
|
@@ -1444,47 +1592,7 @@ export class ProbeAgent {
|
|
|
1444
1592
|
result = await this._executeWithVercelProvider(options, controller);
|
|
1445
1593
|
}
|
|
1446
1594
|
|
|
1447
|
-
// Wrap textStream so limiter slot is held until stream completes.
|
|
1448
|
-
// result.textStream is a read-only getter on DefaultStreamTextResult,
|
|
1449
|
-
// so we wrap the result in a Proxy that intercepts the textStream property.
|
|
1450
|
-
if (limiter && result.textStream) {
|
|
1451
|
-
const originalStream = result.textStream;
|
|
1452
|
-
const debug = this.debug;
|
|
1453
|
-
const wrappedStream = (async function* () {
|
|
1454
|
-
try {
|
|
1455
|
-
for await (const chunk of originalStream) {
|
|
1456
|
-
yield chunk;
|
|
1457
|
-
}
|
|
1458
|
-
} finally {
|
|
1459
|
-
limiter.release(null);
|
|
1460
|
-
if (debug) {
|
|
1461
|
-
const stats = limiter.getStats();
|
|
1462
|
-
console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
})();
|
|
1466
|
-
return new Proxy(result, {
|
|
1467
|
-
get(target, prop) {
|
|
1468
|
-
if (prop === 'textStream') return wrappedStream;
|
|
1469
|
-
const value = target[prop];
|
|
1470
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
1471
|
-
}
|
|
1472
|
-
});
|
|
1473
|
-
} else if (limiter) {
|
|
1474
|
-
// No textStream (shouldn't happen, but release just in case)
|
|
1475
|
-
limiter.release(null);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
1595
|
return result;
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
// Release on error if limiter was acquired
|
|
1481
|
-
if (limiter) {
|
|
1482
|
-
limiter.release(null);
|
|
1483
|
-
if (this.debug) {
|
|
1484
|
-
console.log(`[DEBUG] Released global AI concurrency slot on error`);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
throw error;
|
|
1488
1596
|
} finally {
|
|
1489
1597
|
// Clean up timeout (for non-engine paths; engine paths clean up in the generator)
|
|
1490
1598
|
if (timeoutState.timeoutId) {
|
|
@@ -90,9 +90,9 @@ If the solution is clear, you can jump to implementation right away. If not, ask
|
|
|
90
90
|
- Do not add code comments unless the logic is genuinely complex and non-obvious.
|
|
91
91
|
|
|
92
92
|
# Before Implementation
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
93
|
+
- Read tests first — find existing test files for the module you're changing. They reveal expected behavior, edge cases, and the project's testing patterns.
|
|
94
|
+
- Read neighboring files — understand naming conventions, error handling patterns, import style, and existing utilities before creating new ones.
|
|
95
|
+
- Trace the call chain — follow how the code you're changing is called and what depends on it. Check interfaces, types, and consumers.
|
|
96
96
|
- Focus on backward compatibility
|
|
97
97
|
- Consider scalability, maintainability, and extensibility in your analysis
|
|
98
98
|
|
|
@@ -117,6 +117,20 @@ Before building or testing, determine the project's toolchain:
|
|
|
117
117
|
- Read README for build/test instructions if the above are unclear
|
|
118
118
|
- Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
|
|
119
119
|
|
|
120
|
+
# File Editing Rules
|
|
121
|
+
You have access to the \`edit\`, \`create\`, and \`multi_edit\` tools for modifying files. You MUST use these tools for ALL code changes. They are purpose-built, atomic, and safe.
|
|
122
|
+
|
|
123
|
+
DO NOT use sed, awk, echo/cat redirection, or heredocs to modify source code. These commands cause real damage in practice: truncated lines, duplicate code blocks, broken syntax. Every bad edit wastes iterations on fix-up commits.
|
|
124
|
+
|
|
125
|
+
Use the right tool:
|
|
126
|
+
1. To MODIFY existing code → \`edit\` tool (old_string → new_string, or start_line/end_line)
|
|
127
|
+
2. To CREATE a new file → \`create\` tool
|
|
128
|
+
3. To CHANGE multiple files at once → \`multi_edit\` tool
|
|
129
|
+
4. To READ code → \`extract\` or \`search\` tools
|
|
130
|
+
5. If \`edit\` fails with "file has not been read yet" → use \`extract\` with the EXACT same file path you will pass to \`edit\`. Relative vs absolute path mismatch causes this error. Use the same path format consistently. If it still fails, use bash \`cat\` to read the file, then use \`create\` to write the entire modified file. Do NOT fall back to sed.
|
|
131
|
+
|
|
132
|
+
Bash is fine for: formatters (gofmt, prettier, black), build/test/lint commands, git operations, and read-only file inspection (cat, head, tail). sed/awk should ONLY be used for trivial non-code tasks (e.g., config file tweaks) where the replacement is a simple literal string swap.
|
|
133
|
+
|
|
120
134
|
# During Implementation
|
|
121
135
|
- Always create a new branch before making changes to the codebase.
|
|
122
136
|
- Fix problems at the root cause, not with surface-level patches. Prefer general solutions over special cases.
|
|
@@ -143,6 +157,22 @@ Before committing or creating a PR, run through this checklist:
|
|
|
143
157
|
|
|
144
158
|
Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
|
|
145
159
|
|
|
160
|
+
# Output Integrity
|
|
161
|
+
Your final output MUST accurately reflect what ACTUALLY happened. Do NOT fabricate, hallucinate, or report aspirational results.
|
|
162
|
+
|
|
163
|
+
- Only report PR URLs you actually created or updated with \`gh pr create\` or \`git push\`. If you checked out an existing PR but did NOT push changes to it, do NOT claim you updated it.
|
|
164
|
+
- Describe what you ACTUALLY DID, not what you planned or intended to do. If you ran out of iterations, say so. If tests failed, say so.
|
|
165
|
+
- Only list files you actually modified AND committed.
|
|
166
|
+
- If you could not complete the task — ran out of iterations, tests failed, build broken, push rejected — report the real reason honestly.
|
|
167
|
+
|
|
168
|
+
NEVER claim success when:
|
|
169
|
+
- You did not run \`git push\` successfully
|
|
170
|
+
- Tests failed and you did not fix them
|
|
171
|
+
- You hit the iteration limit before completing the work
|
|
172
|
+
- You only analyzed/investigated but did not implement changes
|
|
173
|
+
|
|
174
|
+
A false success report is WORSE than an honest failure — it misleads the user into thinking work is done when it is not.
|
|
175
|
+
|
|
146
176
|
# GitHub Integration
|
|
147
177
|
- Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
|
|
148
178
|
- To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
|
|
@@ -12,9 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
|
-
import { resolve, isAbsolute } from 'path';
|
|
15
|
+
import { resolve, isAbsolute, normalize } from 'path';
|
|
16
16
|
import { findSymbol } from './symbolEdit.js';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a file path for consistent storage and lookup.
|
|
20
|
+
* Resolves '.', '..', double slashes, and ensures absolute paths are canonical.
|
|
21
|
+
* Does NOT resolve symlinks (that would be expensive and might fail for non-existent files).
|
|
22
|
+
* @param {string} filePath - Path to normalize
|
|
23
|
+
* @returns {string} Normalized path
|
|
24
|
+
*/
|
|
25
|
+
function normalizePath(filePath) {
|
|
26
|
+
if (!filePath) return filePath;
|
|
27
|
+
// resolve() handles '.', '..', double slashes, and makes the path absolute
|
|
28
|
+
return resolve(filePath);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/**
|
|
19
32
|
* Compute a SHA-256 content hash for a code block.
|
|
20
33
|
* Normalizes trailing whitespace per line for robustness against editor formatting.
|
|
@@ -106,10 +119,11 @@ export class FileTracker {
|
|
|
106
119
|
* @param {string} resolvedPath - Absolute path to the file
|
|
107
120
|
*/
|
|
108
121
|
markFileSeen(resolvedPath) {
|
|
109
|
-
|
|
110
|
-
this.
|
|
122
|
+
const normalized = normalizePath(resolvedPath);
|
|
123
|
+
this._seenFiles.add(normalized);
|
|
124
|
+
this._textEditCounts.set(normalized, 0);
|
|
111
125
|
if (this.debug) {
|
|
112
|
-
console.error(`[FileTracker] Marked as seen: ${
|
|
126
|
+
console.error(`[FileTracker] Marked as seen: ${normalized}`);
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
|
|
@@ -119,7 +133,7 @@ export class FileTracker {
|
|
|
119
133
|
* @returns {boolean}
|
|
120
134
|
*/
|
|
121
135
|
isFileSeen(resolvedPath) {
|
|
122
|
-
return this._seenFiles.has(resolvedPath);
|
|
136
|
+
return this._seenFiles.has(normalizePath(resolvedPath));
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/**
|
|
@@ -132,7 +146,7 @@ export class FileTracker {
|
|
|
132
146
|
* @param {string} [source='extract'] - How the content was obtained
|
|
133
147
|
*/
|
|
134
148
|
trackSymbolContent(resolvedPath, symbolName, code, startLine, endLine, source = 'extract') {
|
|
135
|
-
const key = `${resolvedPath}#${symbolName}`;
|
|
149
|
+
const key = `${normalizePath(resolvedPath)}#${symbolName}`;
|
|
136
150
|
const contentHash = computeContentHash(code);
|
|
137
151
|
this._contentRecords.set(key, {
|
|
138
152
|
contentHash,
|
|
@@ -154,7 +168,7 @@ export class FileTracker {
|
|
|
154
168
|
* @returns {Object|null} The stored record or null
|
|
155
169
|
*/
|
|
156
170
|
getSymbolRecord(resolvedPath, symbolName) {
|
|
157
|
-
return this._contentRecords.get(`${resolvedPath}#${symbolName}`) || null;
|
|
171
|
+
return this._contentRecords.get(`${normalizePath(resolvedPath)}#${symbolName}`) || null;
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
/**
|
|
@@ -165,7 +179,7 @@ export class FileTracker {
|
|
|
165
179
|
* @returns {{ok: boolean, reason?: string, message?: string}}
|
|
166
180
|
*/
|
|
167
181
|
checkSymbolContent(resolvedPath, symbolName, currentCode) {
|
|
168
|
-
const key = `${resolvedPath}#${symbolName}`;
|
|
182
|
+
const key = `${normalizePath(resolvedPath)}#${symbolName}`;
|
|
169
183
|
const record = this._contentRecords.get(key);
|
|
170
184
|
|
|
171
185
|
if (!record) {
|
|
@@ -253,7 +267,7 @@ export class FileTracker {
|
|
|
253
267
|
* @returns {{ok: boolean, reason?: string, message?: string}}
|
|
254
268
|
*/
|
|
255
269
|
checkBeforeEdit(resolvedPath) {
|
|
256
|
-
if (!this._seenFiles.has(resolvedPath)) {
|
|
270
|
+
if (!this._seenFiles.has(normalizePath(resolvedPath))) {
|
|
257
271
|
return {
|
|
258
272
|
ok: false,
|
|
259
273
|
reason: 'untracked',
|
|
@@ -269,8 +283,9 @@ export class FileTracker {
|
|
|
269
283
|
* @param {string} resolvedPath - Absolute path to the file
|
|
270
284
|
*/
|
|
271
285
|
async trackFileAfterWrite(resolvedPath) {
|
|
272
|
-
|
|
273
|
-
this.
|
|
286
|
+
const normalized = normalizePath(resolvedPath);
|
|
287
|
+
this._seenFiles.add(normalized);
|
|
288
|
+
this.invalidateFileRecords(normalized);
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
/**
|
|
@@ -279,10 +294,11 @@ export class FileTracker {
|
|
|
279
294
|
* @param {string} resolvedPath - Absolute path to the file
|
|
280
295
|
*/
|
|
281
296
|
recordTextEdit(resolvedPath) {
|
|
282
|
-
const
|
|
283
|
-
this._textEditCounts.
|
|
297
|
+
const normalized = normalizePath(resolvedPath);
|
|
298
|
+
const count = (this._textEditCounts.get(normalized) || 0) + 1;
|
|
299
|
+
this._textEditCounts.set(normalized, count);
|
|
284
300
|
if (this.debug) {
|
|
285
|
-
console.error(`[FileTracker] Text edit #${count} for ${
|
|
301
|
+
console.error(`[FileTracker] Text edit #${count} for ${normalized}`);
|
|
286
302
|
}
|
|
287
303
|
}
|
|
288
304
|
|
|
@@ -292,7 +308,7 @@ export class FileTracker {
|
|
|
292
308
|
* @returns {{ok: boolean, editCount?: number, message?: string}}
|
|
293
309
|
*/
|
|
294
310
|
checkTextEditStaleness(resolvedPath) {
|
|
295
|
-
const count = this._textEditCounts.get(resolvedPath) || 0;
|
|
311
|
+
const count = this._textEditCounts.get(normalizePath(resolvedPath)) || 0;
|
|
296
312
|
if (count >= this.maxConsecutiveTextEdits) {
|
|
297
313
|
return {
|
|
298
314
|
ok: false,
|
|
@@ -323,7 +339,7 @@ export class FileTracker {
|
|
|
323
339
|
* @param {string} resolvedPath - Absolute path to the file
|
|
324
340
|
*/
|
|
325
341
|
invalidateFileRecords(resolvedPath) {
|
|
326
|
-
const prefix = resolvedPath + '#';
|
|
342
|
+
const prefix = normalizePath(resolvedPath) + '#';
|
|
327
343
|
for (const key of this._contentRecords.keys()) {
|
|
328
344
|
if (key.startsWith(prefix)) {
|
|
329
345
|
this._contentRecords.delete(key);
|
|
@@ -340,7 +356,7 @@ export class FileTracker {
|
|
|
340
356
|
* @returns {boolean}
|
|
341
357
|
*/
|
|
342
358
|
isTracked(resolvedPath) {
|
|
343
|
-
return this.isFileSeen(resolvedPath);
|
|
359
|
+
return this.isFileSeen(normalizePath(resolvedPath));
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
/**
|
package/build/tools/vercel.js
CHANGED
|
@@ -385,7 +385,7 @@ export const searchTool = (options = {}) => {
|
|
|
385
385
|
? searchDelegateDescription
|
|
386
386
|
: searchDescription,
|
|
387
387
|
inputSchema: searchSchema,
|
|
388
|
-
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage }) => {
|
|
388
|
+
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
|
|
389
389
|
// Auto-quote mixed-case and underscore terms to prevent unwanted stemming/splitting
|
|
390
390
|
// Skip when exact=true since that already preserves the literal string
|
|
391
391
|
if (!exact && searchQuery) {
|
|
@@ -399,15 +399,18 @@ export const searchTool = (options = {}) => {
|
|
|
399
399
|
// Use parameter maxTokens if provided, otherwise use the default
|
|
400
400
|
const effectiveMaxTokens = paramMaxTokens || maxTokens;
|
|
401
401
|
|
|
402
|
+
// Use workingDirectory (injected by _buildNativeTools at runtime) > cwd from config > fallback
|
|
403
|
+
const effectiveSearchCwd = workingDirectory || options.cwd || '.';
|
|
404
|
+
|
|
402
405
|
// Parse and resolve paths (supports comma-separated and relative paths)
|
|
403
406
|
let searchPaths;
|
|
404
407
|
if (path) {
|
|
405
|
-
searchPaths = parseAndResolvePaths(path,
|
|
408
|
+
searchPaths = parseAndResolvePaths(path, effectiveSearchCwd);
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
// Default to cwd or '.' if no paths provided
|
|
409
412
|
if (!searchPaths || searchPaths.length === 0) {
|
|
410
|
-
searchPaths = [
|
|
413
|
+
searchPaths = [effectiveSearchCwd];
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
// Join paths with space for CLI (probe search supports multiple paths)
|
|
@@ -416,7 +419,7 @@ export const searchTool = (options = {}) => {
|
|
|
416
419
|
const searchOptions = {
|
|
417
420
|
query: searchQuery,
|
|
418
421
|
path: searchPath,
|
|
419
|
-
cwd:
|
|
422
|
+
cwd: effectiveSearchCwd, // Working directory for resolving relative paths
|
|
420
423
|
allowTests: allow_tests ?? true,
|
|
421
424
|
exact,
|
|
422
425
|
json: false,
|
|
@@ -473,7 +476,7 @@ export const searchTool = (options = {}) => {
|
|
|
473
476
|
const result = maybeAnnotate(await runRawSearch());
|
|
474
477
|
// Track files found in search results for staleness detection
|
|
475
478
|
if (options.fileTracker && typeof result === 'string') {
|
|
476
|
-
options.fileTracker.trackFilesFromOutput(result,
|
|
479
|
+
options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {});
|
|
477
480
|
}
|
|
478
481
|
return result;
|
|
479
482
|
} catch (error) {
|
|
@@ -532,7 +535,7 @@ export const searchTool = (options = {}) => {
|
|
|
532
535
|
}
|
|
533
536
|
const fallbackResult = maybeAnnotate(await runRawSearch());
|
|
534
537
|
if (options.fileTracker && typeof fallbackResult === 'string') {
|
|
535
|
-
options.fileTracker.trackFilesFromOutput(fallbackResult,
|
|
538
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult, effectiveSearchCwd).catch(() => {});
|
|
536
539
|
}
|
|
537
540
|
return fallbackResult;
|
|
538
541
|
}
|
|
@@ -614,7 +617,7 @@ export const searchTool = (options = {}) => {
|
|
|
614
617
|
try {
|
|
615
618
|
const fallbackResult2 = maybeAnnotate(await runRawSearch());
|
|
616
619
|
if (options.fileTracker && typeof fallbackResult2 === 'string') {
|
|
617
|
-
options.fileTracker.trackFilesFromOutput(fallbackResult2,
|
|
620
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult2, effectiveSearchCwd).catch(() => {});
|
|
618
621
|
}
|
|
619
622
|
return fallbackResult2;
|
|
620
623
|
} catch (fallbackError) {
|
|
@@ -693,10 +696,10 @@ export const extractTool = (options = {}) => {
|
|
|
693
696
|
name: 'extract',
|
|
694
697
|
description: extractDescription,
|
|
695
698
|
inputSchema: extractSchema,
|
|
696
|
-
execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
|
|
699
|
+
execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format, workingDirectory }) => {
|
|
697
700
|
try {
|
|
698
|
-
// Use
|
|
699
|
-
const effectiveCwd = options.cwd || '.';
|
|
701
|
+
// Use workingDirectory (injected by _buildNativeTools at runtime) > cwd from config > fallback
|
|
702
|
+
const effectiveCwd = workingDirectory || options.cwd || '.';
|
|
700
703
|
|
|
701
704
|
if (debug) {
|
|
702
705
|
if (targets) {
|