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

  1. Treat this as normal cancellation — re-issue the grep in a new request if it is still needed
  2. Don't reuse/forward an already-aborted AbortSignal to a new tool call; create a fresh controller per call
  3. 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

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)