dmtrKovalenko/fff · warning · Error
Operation aborted
Error message
Operation aborted
What it means
The grep tool's execute function checks the caller-supplied AbortSignal at the start and throws if the request was already cancelled. This is an eager bail-out so no picker work is done for a cancelled request.
Solutions
- Treat this as normal cancellation — re-issue the grep in a new request if it is still needed
- Don't reuse/forward an already-aborted AbortSignal to a new tool call; create a fresh controller per call
- Check upstream timeout configuration if signals abort too early
Example fix
// before controller.abort(); await grepTool.execute(id, params, controller.signal); // after const fresh = new AbortController(); await grepTool.execute(id, params, fresh.signal);
Defensive patterns
Strategy: retry
Validate before calling
if (controller.signal.aborted) controller = new AbortController();
Try / catch
try { await grepTool.execute(id, params, signal); } catch (e) { if (String(e).includes('aborted')) return null; throw e; } Prevention
- Create a fresh AbortController per tool call
- Never forward already-aborted signals
- Set realistic upstream timeouts so signals don't fire prematurely
When it happens
Trigger: The host (agent harness or user pressing Ctrl-C) aborts the signal before/while the grep tool handler starts; `signal.aborted` is true on entry.
Common situations: Agent run cancelled between planning and tool execution; slow UI causing the user to cancel a pending grep; timeouts in the calling framework aborting the signal.
AI-assisted analysis of dmtrKovalenko/fff@7f8537e70f (2026-09-10).
Data as JSON: /api/errors/8d3c7daabf620620.
Report an issue: GitHub.
Appendix: source
Thrown at packages/pi-fff/src/index.ts:867
),
cursor: Type.Optional(
Type.String({ description: "Pagination cursor from previous result" }),
),
});
queueTool(() => toolNames.grep, {
description: `Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${DEFAULT_GREP_LIMIT}.`,
promptSnippet: "Grep contents",
promptGuidelines: (names) => [
`${names.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
`${names.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
`${names.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
`${names.grep}: after 1-2 greps, read the top match instead of more greps.`,
],
parameters: grepSchema,
async execute(_toolCallId, params, signal) {
if (signal?.aborted) throw new Error("Operation aborted");
const pattern = params.pattern;
const aux = await resolveFinderForPath(params.path, pattern, params.exclude);
const picker = aux ? aux.finder : await ensureFinder(activeCwd);
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
// pageSize caps TOTAL matches across all files (soft cap: the current file
// is always finished first). maxMatchesPerFile stays decoupled at the engine
// default so same-file overflow remains reachable via cursor (#825).
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
const context = clampContext(params.context);
const query = aux
? aux.query
: buildQuery(params.path, pattern, params.exclude, activeCwd);
// Auto-detect: regex if the pattern has regex metacharacters AND parses
// as a valid regex, otherwise plain literal. The fuzzy fallback below
// only kicks in for plain mode — regex queries are intentional.View on GitHub (pinned to 7f8537e70f)