Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions npm/src/tools/vercel.js
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,12 @@
// ── Delegate-level semantic dedup ────────────────────────────
// Each delegate is a full flash agent session (minutes, not seconds).
// Use LLM to detect semantic duplicates and suggest rewrites.
// Compare against ALL previous delegations (not filtered by path) because
// the parent model often narrows the path while asking the same concept
// (e.g., "dedup" at /src → "deduplicate" at /src/search.js).
const delegatePath = searchPath || '';

Check warning on line 722 in npm/src/tools/vercel.js

View check run for this annotation

probelabs / Visor: performance

performance Issue

The new `samePathDelegations` filter creates a new array on every delegate search by iterating through the entire `previousDelegations` array. This is O(n) per delegation, which becomes inefficient as the session grows with many delegations across different paths. For long-running sessions with hundreds of delegations, this repeated filtering could cause measurable performance degradation.
Raw output
Consider maintaining a Map or object indexed by path for O(1) lookups instead of O(n) filtering. For example: `const delegationsByPath = new Map(); delegationsByPath.set(delegatePath, delegationsByPath.get(delegatePath) || []);` This would eliminate the linear scan on every delegation.
const samePathDelegations = previousDelegations.filter(d => d.path === delegatePath);

let effectiveQuery = searchQuery;

if (previousDelegations.length > 0) {
if (samePathDelegations.length > 0) {
const dedupProvider = options.searchDelegateProvider || process.env.PROBE_SEARCH_DELEGATE_PROVIDER || options.provider || process.env.FORCE_PROVIDER || null;
const dedupModelName = options.searchDelegateModel || process.env.PROBE_SEARCH_DELEGATE_MODEL || options.model || process.env.MODEL_NAME || null;
// Lazily create the dedup model (same provider/model as delegate)
Expand All @@ -742,16 +740,16 @@

const dedupSpanAttrs = {
'dedup.query': searchQuery,
'dedup.previous_count': String(previousDelegations.length),
'dedup.previous_queries': previousDelegations.map(d => d.query).join(' | '),
'dedup.previous_count': String(samePathDelegations.length),
'dedup.previous_queries': samePathDelegations.map(d => d.query).join(' | '),
'dedup.provider': dedupProvider || '',
'dedup.model': dedupModelName || '',
'dedup.model_available': cachedDedupModel ? 'true' : 'false',
};

const dedup = options.tracer?.withSpan
? await options.tracer.withSpan('search.delegate.dedup', async () => {
return await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
return await checkDelegateDedup(searchQuery, samePathDelegations, cachedDedupModel, debug);
}, dedupSpanAttrs, (span, result) => {
span.setAttributes({
'dedup.action': result.action,
Expand All @@ -760,14 +758,14 @@
'dedup.error': result.error || '',
});
})
: await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
: await checkDelegateDedup(searchQuery, samePathDelegations, cachedDedupModel, debug);

if (debug) {
console.error(`[DEDUP-LLM] Query: "${searchQuery}" → ${dedup.action}: ${dedup.reason}${dedup.rewritten ? ` → "${dedup.rewritten}"` : ''}`);
}

if (dedup.action === 'block') {
const prevQueries = previousDelegations.map(d => `"${d.query}"`).join(', ');
const prevQueries = samePathDelegations.map(d => `"${d.query}"`).join(', ');
return `DELEGATE BLOCKED: "${searchQuery}" is semantically duplicate of previous delegation(s) [${prevQueries}]. ${dedup.reason}\n\nDo NOT re-delegate the same concept. Use extract() on files already found, or synthesize your answer from existing results.`;
}

Expand Down
61 changes: 61 additions & 0 deletions npm/tests/unit/search-delegate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,67 @@ describe('searchDelegate behavior', () => {
expect(dedupSpan.resultAttrs['dedup.error']).not.toBe('');
});

test('scopes delegate semantic dedup to the same path', async () => {
mockCreateLanguageModel.mockResolvedValue(null);
mockDelegate.mockResolvedValue(JSON.stringify({
targets: ['crypto/fips.js#validateFips']
}));

const spans = [];
const tracer = {
withSpan: jest.fn(async (name, fn, attrs, onResult) => {
const result = await fn();
const span = {
attrs: {},
setAttributes(values) {
this.attrs = { ...this.attrs, ...values };
}
};
if (onResult) onResult(span, result);
spans.push({ name, attrs, resultAttrs: span.attrs });
return result;
})
};

const tool = searchTool({
searchDelegate: true,
cwd: '/workspace',
allowedFolders: ['/workspace'],
provider: 'google',
model: 'gemini-3-flash-preview',
tracer
});

const first = await tool.execute({ query: 'FIPS validation', path: '/workspace/repo-a' });
const second = await tool.execute({ query: 'FIPS validation', path: '/workspace/repo-b' });

expect(first).not.toContain('DELEGATE BLOCKED');
expect(second).not.toContain('DELEGATE BLOCKED');
expect(mockDelegate).toHaveBeenCalledTimes(2);
expect(spans.filter(s => s.name === 'search.delegate.dedup')).toHaveLength(0);

const third = await tool.execute({ query: 'FIPS validation', path: '/workspace/repo-a' });

expect(third).not.toContain('DELEGATE BLOCKED');
expect(mockDelegate).toHaveBeenCalledTimes(3);
expect(mockGenerateText).not.toHaveBeenCalled();

const dedupSpan = spans.find(s => s.name === 'search.delegate.dedup');
expect(dedupSpan).toBeDefined();
expect(dedupSpan.attrs).toEqual(
expect.objectContaining({
'dedup.previous_count': '1',
'dedup.previous_queries': 'FIPS validation'
})
);
expect(dedupSpan.resultAttrs).toEqual(
expect.objectContaining({
'dedup.action': 'allow',
'dedup.reason': 'dedup model unavailable'
})
);
});

test('delegates search and returns structured JSON when searchDelegate=true', async () => {
// Delegate returns paths relative to the search directory (searchPaths[0]),
// not relative to cwd
Expand Down
Loading