Skip to content

fix(sdk): tool call repair, CLI abort handling, fallback provider (BZ-665, BZ-667, BZ-1341) - #946

Merged
murdore merged 1 commit into
releasefrom
feat/pemnding-bugs
Apr 13, 2026
Merged

murdore merged 1 commit into
releasefrom
feat/pemnding-bugs

Conversation

@murdore

@murdore murdore commented Apr 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • BZ-665: Schema-driven tool call repair via experimental_repairToolCall — dynamically fixes wrong tool names and parameter names from LLMs using the tool's JSON schema (no static alias maps). Wired into all 11 providers.
  • BZ-667: Graceful CLI abort signal handling — Ctrl+C during streaming shows "Stream cancelled." and exits cleanly instead of killing the process abruptly.
  • BZ-1341: Fallback provider override via fallbackProvider/fallbackModel stream options or FALLBACK_PROVIDER/FALLBACK_MODEL env vars (cherry-picked from PR feat(provider): Add support to pass fallback provider through stream … #880 with fixes).

Changes

BZ-665: Tool Call Repair

  • New src/lib/utils/toolCallRepair.ts: Levenshtein distance, type coercion (string→number, JSON string→object/array), schema-driven param remapping
  • baseProvider.ts: getToolCallRepairFn() protected method with lazy import
  • All 11 providers: +1 line each wiring experimental_repairToolCall
  • streamTypes.ts: disableToolCallRepair opt-out option

BZ-667: CLI Abort Handling

  • New src/cli/utils/abortHandler.ts: SIGINT→AbortController bridge (first Ctrl+C cancels, second force-exits)
  • commandFactory.ts: Wire abort handler into processStreamWithTimeout()

BZ-1341: Fallback Provider

  • neurolink.ts: Override fallback provider/model via options or env vars with empty-string guard (.trim() || undefined)
  • streamTypes.ts: fallbackProvider and fallbackModel fields

Test plan

  • pnpm run check — 0 errors, 0 warnings
  • pnpm run lint — 0 errors (1 pre-existing warning)
  • pnpm run build — success
  • BZ-665 E2E: generate() and stream() with Vertex/Gemini 2.5 Flash — tools execute correctly, repair wired in 13 source files
  • BZ-667 runtime: neurolink stream + SIGINT → "Stream cancelled." message confirmed
  • Before/after evidence saved in /tmp/bz665-repro/ and /tmp/bz667-repro/

Summary by CodeRabbit

  • New Features

    • Graceful stream cancellation via Ctrl+C that returns partial results when aborted
    • Automatic tool-call repair: name matching, parameter reconciliation, and type coercion
    • Per-request fallback overrides for provider and model
    • Option to disable automatic tool-call repair
  • Improvements

    • Better cleanup of abort handlers and streaming resources on success, error, or abort
    • Enhanced fallback logging to show chosen fallback model and its source

Copilot AI review requested due to automatic review settings April 12, 2026 10:07
@vercel

vercel Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Apr 13, 2026 3:02am

@coderabbitai

coderabbitai Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a CLI SIGINT-driven stream abort handler, implements a schema-driven tool-call repair utility, wires the repair hook into BaseProvider and multiple providers, and extends StreamOptions with per-request fallback and disableToolCallRepair flags.

Changes

Cohort / File(s) Summary
CLI stream abort
src/cli/factories/commandFactory.ts, src/cli/utils/abortHandler.ts
Add createStreamAbortHandler(); race stream reads with an abort promise; ensure streamIterator.return() and cleanup() on abort/error; return partial content and optionally print newline on abort.
Tool-call repair utility
src/lib/utils/toolCallRepair.ts
New createToolCallRepair() implementing tool-name repair (case-insensitive, substring, Levenshtein), parameter key reconciliation, and schema-driven type coercion; returns repaired tool-call or null.
Base provider helper
src/lib/core/baseProvider.ts
Add protected getToolCallRepairFn(options?) that returns undefined when options.disableToolCallRepair is true, otherwise lazily imports and returns createToolCallRepair().
Stream options & fallback types
src/lib/types/streamTypes.ts
Extend StreamOptions with disableToolCallRepair?: boolean, fallbackProvider?: string, and fallbackModel?: string.
Fallback routing & logging
src/lib/neurolink.ts
Compute fallbackRoute by overriding modelConfig values with options.fallback* or env; add fallbackModel and fallbackSource (options/env/model_config) to warning metadata.
Provider integrations
src/lib/providers/...
src/lib/providers/anthropic.ts, anthropicBaseProvider.ts, azureOpenai.ts, googleAiStudio.ts, googleVertex.ts, huggingFace.ts, litellm.ts, mistral.ts, openAI.ts, openRouter.ts, openaiCompatible.ts
Pass experimental_repairToolCall: this.getToolCallRepairFn(options) into providers' streamText streaming configuration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant Iterator
    participant Provider
    User->>CLI: start streaming request / or press Ctrl+C
    CLI->>CLI: createStreamAbortHandler() -> provides AbortSignal
    CLI->>Provider: start streamText(..., signal)
    Provider->>Iterator: return async iterator of chunks
    CLI->>Iterator: await next() (raced with abortPromise)
    User->>CLI: SIGINT (Ctrl+C)
    CLI->>CLI: abortController.abort() -> abortPromise rejects
    CLI->>Iterator: pending next() unblocks (AbortError)
    CLI->>Iterator: call streamIterator.return() (if present)
    Iterator->>Provider: close stream/connection
    CLI->>CLI: abortHandler.cleanup(), print newline, return partial content
Loading
sequenceDiagram
    participant Provider
    participant ToolCall
    participant Repair
    participant Schema
    Provider->>ToolCall: detect tool-call error (NoSuchTool/InvalidToolInput)
    ToolCall->>Repair: invoke experimental_repairToolCall(error, toolCall, tools, inputSchema)
    Repair->>Repair: attempt name repair (exact, substring, Levenshtein)
    Repair->>Schema: request inputSchema({ toolName })
    Schema-->>Repair: schema or null
    Repair->>Repair: reconcile keys, coerce types per schema
    Repair-->>Provider: return repaired toolCall or null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

released

Suggested reviewers

  • pdogra1299
  • Pdogra2520

Poem

🐇 I nudged the stream with a gentle paw,
When Ctrl+C came, I stopped its draw.
I mended tool names and shaped each key,
Schema-whiskers fixed it — neat as can be.
Hop, repair, and stream on, happy as a hare! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the three main changes (tool call repair, CLI abort handling, fallback provider) with associated bug IDs, directly matching the changeset scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pemnding-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the SDK/CLI robustness by adding schema-driven tool-call repair across providers, graceful CLI stream cancellation on SIGINT, and configurable fallback provider/model overrides for streaming.

Changes:

  • Add schema-driven experimental_repairToolCall implementation and wire it into all providers (with an opt-out stream option).
  • Add CLI SIGINT→AbortController bridge to cancel streaming cleanly (and update streaming output handling).
  • Add per-call/env overrides for fallback provider/model selection during retries.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/lib/utils/toolCallRepair.ts New schema-driven tool name/arg repair + type coercion for AI SDK tool calls
src/lib/core/baseProvider.ts Adds getToolCallRepairFn() and lazy-import wiring for providers
src/lib/providers/openRouter.ts Wires experimental_repairToolCall into OpenRouter calls
src/lib/providers/openaiCompatible.ts Wires experimental_repairToolCall into OpenAI-compatible calls
src/lib/providers/openAI.ts Wires experimental_repairToolCall into OpenAI calls
src/lib/providers/mistral.ts Wires experimental_repairToolCall into Mistral calls
src/lib/providers/litellm.ts Wires experimental_repairToolCall into LiteLLM calls
src/lib/providers/huggingFace.ts Wires experimental_repairToolCall into HuggingFace calls
src/lib/providers/googleVertex.ts Wires experimental_repairToolCall into Google Vertex calls
src/lib/providers/googleAiStudio.ts Wires experimental_repairToolCall into Google AI Studio calls
src/lib/providers/azureOpenai.ts Wires experimental_repairToolCall into Azure OpenAI calls
src/lib/providers/anthropicBaseProvider.ts Wires experimental_repairToolCall into Anthropic V2 base provider
src/lib/providers/anthropic.ts Wires experimental_repairToolCall into Anthropic calls
src/lib/types/streamTypes.ts Adds disableToolCallRepair, fallbackProvider, fallbackModel to stream options
src/lib/types/configTypes.ts Updates MCP cache documentation to reflect new default behavior
src/lib/neurolink.ts Enables tool-result cache by default; adds fallback provider/model override logic; caches external MCP tool results
src/lib/core/modules/ToolsManager.ts Adds tool output truncation guard and wraps tool execution paths
src/cli/utils/typewriter.ts Adds animated stdout writing helper used by streaming output
src/cli/utils/abortHandler.ts New SIGINT abort handler for CLI streaming
src/cli/factories/commandFactory.ts Integrates abort handler + animatedWrite into streaming processing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +59 to +77
const called = toolCall.toolName;

// 1. Case-insensitive exact match
const ciMatch = availableTools.find(
(t) => t.toLowerCase() === called.toLowerCase(),
);
if (ciMatch) {
logger.debug(
`[ToolCallRepair] Name repair (case): "${called}" → "${ciMatch}"`,
);
return { ...toolCall, toolName: ciMatch };
}

// 2. Substring match: "search_file" is substring of "search_files" or vice versa
const calledLower = called.toLowerCase();
const subMatch = availableTools.find((t) => {
const tLower = t.toLowerCase();
return tLower.includes(calledLower) || calledLower.includes(tLower);
});

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repairToolName() can incorrectly "repair" an empty tool name because substring matching treats "" as a substring of every tool name (so the first available tool would be chosen). Add an early guard (e.g., if toolName.trim().length === 0 return null) before attempting case/substring/Levenshtein matching.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +163
const mapped = findMatchingKey(inputKey, expectedKeys);
if (mapped) {
logger.debug(
`[ToolCallRepair] Param repair: "${inputKey}" → "${mapped}" (tool: ${toolCall.toolName})`,
);
repaired[mapped] = inputObj[inputKey];
didRepair = true;
} else {
// Unknown key — pass through (schema may allow additionalProperties)
repaired[inputKey] = inputObj[inputKey];
}

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repairToolInput() always passes through unknown keys when it can't map them. For schemas with additionalProperties: false (used for OpenAI strict mode elsewhere in the codebase), those extra keys will keep failing validation, so the repair can't succeed. Consider dropping unmapped keys when schema.additionalProperties === false (and marking didRepair = true), while still preserving them when additional properties are allowed.

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +290
/**
* Coerce a value to match the expected schema type.
* Handles: string→number, JSON string→object, JSON string→array, value→[value].
*/
function coerceType(
value: unknown,
propSchema: Record<string, unknown>,
): unknown {
const expectedType = propSchema.type as string | undefined;
if (!expectedType || value === null || value === undefined) {
return value;
}

// String → Number
if (expectedType === "number" && typeof value === "string") {
const num = Number(value);
if (!isNaN(num)) {
return num;
}
}

// String → Integer
if (expectedType === "integer" && typeof value === "string") {
const num = parseInt(value, 10);
if (!isNaN(num)) {
return num;
}
}

// String → Boolean
if (expectedType === "boolean" && typeof value === "string") {
if (value.toLowerCase() === "true") {
return true;
}
if (value.toLowerCase() === "false") {
return false;
}
}

// JSON string → Object
if (expectedType === "object" && typeof value === "string") {
try {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed;
}
} catch {
// Not valid JSON — return as-is
}
}

// JSON string → Array
if (expectedType === "array" && typeof value === "string") {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
} catch {
// Not valid JSON — return as-is
}
}

// Single value → Array (wrap)
if (
expectedType === "array" &&
!Array.isArray(value) &&
typeof value !== "string"
) {
return [value];
}

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Single value → Array (wrap)" but the implementation explicitly excludes string values (typeof value !== "string"). Either include strings in the wrap logic or update the comment/behavior to avoid this mismatch.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +28
const DEFAULT_CHAR_DELAY_MS = 8;

/**
* Write text to stdout with a per-character typewriter animation.
* Falls back to raw write when delay is 0 or negative.
*/
export async function typewriterWrite(
text: string,
delayMs: number = DEFAULT_CHAR_DELAY_MS,
): Promise<void> {
if (!text) {
return;
}
if (delayMs <= 0) {
process.stdout.write(text);
return;
}
// Use Array.from to handle surrogate pairs / emoji correctly
for (const ch of Array.from(text)) {
process.stdout.write(ch);
await new Promise((r) => setTimeout(r, delayMs));
}

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEFAULT_CHAR_DELAY_MS = 8 combined with animatedWrite() awaiting typewriterWrite() will add ~8ms per character whenever stdout is a TTY. For typical streaming outputs this can slow the CLI by orders of magnitude and can cause upstream stream buffering/backpressure. Consider making animation opt-in (CLI flag/env), defaulting delay to 0, or animating per-chunk without awaiting per-character delays.

Copilot uses AI. Check for mistakes.
Comment thread src/cli/factories/commandFactory.ts Outdated
Comment on lines +2996 to +3000
// BZ-667: Wire SIGINT to abort stream gracefully
const abortHandler = createStreamAbortHandler();
abortHandler.signal.addEventListener("abort", () => {
abortController.abort();
});

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abortHandler.signal.addEventListener("abort", () => abortController.abort()) uses an anonymous listener that can't be removed in abortHandler.cleanup(). If processStreamWithTimeout() is called multiple times in one process, this can accumulate listeners. Consider using { once: true } and/or storing the handler function and removing it during cleanup.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts
Comment on lines +1275 to +1281
// BZ-664: ToolCache — enabled by default to prevent duplicate tool calls.
// Callers can explicitly opt out via mcp.cache.enabled = false.
if (mcpConfig?.cache?.enabled !== false) {
this.mcpToolResultCache = new ToolResultCache({
ttl: mcpConfig.cache.ttl ?? 300_000,
maxSize: mcpConfig.cache.maxSize ?? 500,
strategy: mcpConfig.cache.strategy ?? "lru",
ttl: mcpConfig?.cache?.ttl ?? 300_000,
maxSize: mcpConfig?.cache?.maxSize ?? 500,
strategy: mcpConfig?.cache?.strategy ?? "lru",

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description/title focuses on BZ-665/BZ-667/BZ-1341, but this diff also changes MCP tool-result caching defaults (BZ-664: now enabled by default unless explicitly disabled). Since this is a behavior change that can affect callers (performance/side effects), it should be called out explicitly in the PR description/release notes and ideally behind a clearly documented config flag.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +46
export function createToolCallRepair(): ToolCallRepairFunction<ToolSet> {
return async ({ toolCall, tools, inputSchema, error }) => {
// Import error classes lazily to avoid circular deps at module level
const { NoSuchToolError: NoSuchTool, InvalidToolInputError: InvalidInput } =
await import("ai");

if (NoSuchTool.isInstance(error)) {
return repairToolName(toolCall, Object.keys(tools));
}

if (InvalidInput.isInstance(error)) {
try {
const schema = await inputSchema({ toolName: toolCall.toolName });
return repairToolInput(toolCall, schema);
} catch {
// inputSchema() failed — can't repair without schema
return null;
}
}

return null;
};

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New schema-driven tool-call repair logic is non-trivial (name matching + param remapping + type coercion) and currently appears untested in the repo’s automated test suites. Adding focused unit tests for the repair behavior (e.g., additionalProperties:false handling, key mapping, coercions) would help prevent regressions across providers.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/providers/googleAiStudio.ts (1)

647-662: ⚠️ Potential issue | 🟠 Major

Gemini 3's native tool path bypasses tool call repair logic.

The experimental_repairToolCall handler is only wired into the AI SDK streamText() path (line 661). Gemini 3 + tools route through executeNativeGemini3Stream() / executeNativeGemini3Generate() instead, which call buildNativeToolDeclarations() and then executeNativeToolCalls(). Neither of these functions apply any repair logic for wrong tool names or parameter schemas (BZ-665). Additionally, the disableToolCallRepair option is only checked in getToolCallRepairFn(), which is never invoked in the native path, so that option cannot control repair behavior there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/providers/googleAiStudio.ts` around lines 647 - 662, The native
Gemini 3 tool path never applies the tool-call repair logic because
getToolCallRepairFn() (and its disableToolCallRepair option) is only used with
streamText(), so executeNativeGemini3Stream(), executeNativeGemini3Generate(),
buildNativeToolDeclarations(), and executeNativeToolCalls() bypass repair; fix
by wiring the same repair handler into the native flow—either accept and pass
the repair function (this.getToolCallRepairFn(options)) into
buildNativeToolDeclarations()/executeNativeToolCalls() or call that repair
function inside executeNativeToolCalls() before executing a tool call, and
ensure the disableToolCallRepair option is respected when choosing whether to
apply repairs.
src/lib/neurolink.ts (1)

6797-6832: ⚠️ Potential issue | 🟠 Major

Centralize fallback-route resolution; this only fixes one stream fallback path.

The override merge lives only here. handleStreamError() (Lines 7440-7454) still builds its fallback from getBestProvider(options.provider) and ignores fallbackProvider / fallbackModel, so the new stream-level overrides will not apply when the primary provider throws. Also, if only the provider is overridden, Lines 6818-6820 keep modelConfigRoute.model, which can hand the new provider an incompatible model. Please move this into a shared resolver and drop or recompute the model whenever the provider changes without an explicit model.

Based on learnings: In neurolink stream fallback logging (src/lib/neurolink.ts:6191-6195), typical usage is to override both provider and model together, but logs should still accurately reflect one-sided overrides when only model or only provider is set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 6797 - 6832, The fallback override merge
is duplicated and incomplete: centralize the logic into a shared resolver (e.g.,
a new function like resolveFallbackRoute or reuse ModelRouter.getFallbackRoute)
and replace the in-place merge at the current block and inside handleStreamError
to call that resolver; ensure the resolver accepts (originalPrompt,
enhancedOptions, providerName, strategy) and returns {provider, model, source}
by applying optFallbackProvider/optFallbackModel, env
FALLBACK_PROVIDER/FALLBACK_MODEL, then fall back to
ModelRouter.getFallbackRoute; importantly, if the returned provider differs from
modelConfigRoute.provider and no explicit model override was provided, drop or
recompute the model from ModelRouter.getFallbackRoute (do not keep
modelConfigRoute.model blindly) so provider-model incompatibilities are avoided;
finally update the stream error logging to use the resolver’s source and
accurately reflect one-sided overrides (only provider or only model) in
fallbackSource.
🧹 Nitpick comments (2)
src/lib/core/baseProvider.ts (1)

1255-1268: Cache the repair factory inside the returned closure.

On Line 1264, createToolCallRepair() is rebuilt on every repair invocation. Cache it once per getToolCallRepairFn() call to reduce repeated setup work.

♻️ Proposed refactor
   protected getToolCallRepairFn(
     options?: StreamOptions | TextGenerationOptions,
   ): ToolCallRepairFunction<ToolSet> | undefined {
     if (
       (options as Record<string, unknown> | undefined)?.disableToolCallRepair
     ) {
       return undefined;
     }
     // Lazy import to avoid circular dependency at module load time
-    return (async (...args: Parameters<ToolCallRepairFunction<ToolSet>>) => {
-      const { createToolCallRepair } =
-        await import("../utils/toolCallRepair.js");
-      return createToolCallRepair()(...args);
-    }) as ToolCallRepairFunction<ToolSet>;
+    let repairFnPromise: Promise<ToolCallRepairFunction<ToolSet>> | undefined;
+    return (async (...args: Parameters<ToolCallRepairFunction<ToolSet>>) => {
+      const repairFn = await (repairFnPromise ??=
+        import("../utils/toolCallRepair.js").then(
+          ({ createToolCallRepair }) =>
+            createToolCallRepair() as ToolCallRepairFunction<ToolSet>,
+        ));
+      return repairFn(...args);
+    }) as ToolCallRepairFunction<ToolSet>;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/baseProvider.ts` around lines 1255 - 1268, getToolCallRepairFn
currently re-imports and calls createToolCallRepair on every invocation of the
returned async closure; change it to cache the produced repair factory once per
getToolCallRepairFn call: inside getToolCallRepairFn, declare a local variable
(e.g., repairFactory?: ReturnType<typeof createToolCallRepair>), then return an
async closure that, on first call, does the lazy import of
"../utils/toolCallRepair.js", sets repairFactory = createToolCallRepair(), and
thereafter calls repairFactory(...args) for subsequent invocations; keep the
same return type ToolCallRepairFunction<ToolSet> and preserve the
disableToolCallRepair check and lazy import behavior.
src/lib/utils/toolCallRepair.ts (1)

104-106: Guard debug-only serialization before constructing log payloads.

At Line 105, availableTools.join(", ") is computed even when debug logs are disabled.

Suggested fix
-  logger.debug(
-    `[ToolCallRepair] Could not repair tool name "${called}". Available: [${availableTools.join(", ")}]`,
-  );
+  if (logger.shouldLog("debug")) {
+    logger.debug(
+      `[ToolCallRepair] Could not repair tool name "${called}". Available: [${availableTools.join(", ")}]`,
+    );
+  }

As per coding guidelines: "Always wrap expensive serialization operations with logger.shouldLog("debug") guard before calling".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/toolCallRepair.ts` around lines 104 - 106, The debug log
currently computes availableTools.join(", ") unconditionally; wrap the expensive
serialization with a debug guard: check logger.shouldLog("debug") before calling
availableTools.join and only build the full message when true, then call
logger.debug with the constructed string (use the same symbols: logger.debug,
logger.shouldLog("debug"), availableTools.join, and called) so the join is not
executed when debug logging is disabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 2996-3000: The Ctrl+C handler currently only aborts a local
timeout (abortController) and is created after sdk.stream() resolves, so pending
streamIterator.next() calls never see the signal; fix by creating the abort
signal before calling sdk.stream() (use createStreamAbortHandler() early) and
pass that signal into sdk.stream(...) if the SDK supports it, otherwise wrap
each streamIterator.next() in a race with an abort promise and on abort call
streamIterator.return?.() to settle the pending read; update the abortHandler
listener to both abort the timeout controller and trigger
streamIterator.return?.() (or the SDK-provided cancel) so streaming stops
immediately.

In `@src/cli/utils/abortHandler.ts`:
- Around line 25-45: This file currently registers its own process.on("SIGINT")
and calls process.exit(0) inside sigintHandler, which conflicts with the
top-level signal coordinator; remove the internal process.on("SIGINT")
registration and the direct process.exit call and instead expose sigintHandler
(or provide attach/detach functions) so the top-level coordinator can call
sigintHandler to cancel the controller (using controller.abort()), manage
aborted/forceExitTimer state, and handle the "Stream cancelled." message; ensure
any existing callers (serve/loop/session/index) attach the handler centrally and
that forceExitTimer and aborted cleanup still occur when the coordinator invokes
the handler.

In `@src/cli/utils/typewriter.ts`:
- Around line 7-29: Change the typewriter path to be opt-in and abort-aware by
making the default behavior raw write: set DEFAULT_CHAR_DELAY_MS to 0 (or make
delayMs default to 0) so typewriterWrite only animates when a positive delay is
explicitly provided; add an optional AbortSignal parameter (e.g., signal?:
AbortSignal) to typewriterWrite and any caller like animatedWrite, check
signal?.aborted before starting and inside the character loop, and break/return
(or throw an AbortError) immediately if aborted so Ctrl+C can cancel
mid-animation; retain the existing Array.from(text) iteration and per-char delay
logic but replace await new Promise(r => setTimeout(r, delayMs)) with an
abort-aware wait (check signal before/after sleep) or short-circuit to avoid
hanging.

In `@src/lib/core/modules/ToolsManager.ts`:
- Around line 103-137: The issue is that returning nextObj immediately after
truncating content/data skips the subsequent whole-object JSON size check;
instead of returning from ToolsManager after setting nextObj, assign the
truncated object back into the variable used for the final size check (e.g.,
replace or set result = nextObj) and let execution fall through to the existing
JSON serialization/Buffer.byteLength logic so the 50KB budget check (and any
further trimming logic) runs on the full object; keep using
generateToolOutputPreview for per-field trimming but ensure the final
JSON.stringify uses the updated object (nextObj/result) before comparing to
51_200 bytes.
- Around line 133-150: The fallback currently replaces the tool result with only
{_truncated, _originalSize, _preview}, breaking callers expecting the tool's
original fields; change the return from ToolsManager's JSON-size branch to
preserve the original structured fields by merging them with the metadata (e.g.,
return { ...result, _truncated: true, _originalSize: originalSize, _preview:
preview }) so callers can still read fields like those produced by the tool; use
the same symbols generateToolOutputPreview, toolName, logger, and result to
locate the logic and ensure the preview and metadata are added under sentinel
names to avoid name collisions, or alternatively move this truncation logic out
of ToolsManager into the model-facing serialization layer if you prefer not to
ship the preview with the full object.

In `@src/lib/neurolink.ts`:
- Around line 10950-10954: getToolAnnotationsForExecution currently reads from
this.toolCache and then falls back to inferAnnotations, which can miss a
server-supplied destructiveHint; update it to first consult
externalServerManager.getServerTools(serverId) for the concrete tool metadata
(match by toolName and serverId) and use any server-provided destructiveHint
before consulting this.toolCache or calling inferAnnotations; ensure the cache
check that computes cacheEnabled (uses mcpToolResultCache and
_disableToolCacheForCurrentRequest) uses the annotations returned by the new
precedence so a server-marked destructive tool will disable caching immediately.
- Around line 10982-10985: The current cache branch unconditionally calls
this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result) which will
persist failed external-tool responses; change it to only cache successful
executions by checking the result flags used by handleSuccessfulToolExecution
(e.g. require result.success === true and result.isError !== true, or the
equivalent success predicate used later) before calling cacheResult, so only
truly successful tool responses are stored.

In `@src/lib/utils/toolCallRepair.ts`:
- Around line 145-163: A fuzzy-mapped key can overwrite a value that already
matched exactly earlier; in the loop inside the tool args repair logic (see
inputKeys, expectedKeys, findMatchingKey, repaired, inputObj, toolCall,
didRepair) add a guard before assigning repaired[mapped] so you do not overwrite
an existing repaired value: check if repaired already hasOwnProperty(mapped) (or
inputObj already provided the exact mapped key) and if so skip the fuzzy
assignment (optionally log a debug that the mapping was skipped); only assign
repaired[mapped] = inputObj[inputKey] and set didRepair = true when the target
key is not already present.
- Around line 241-247: The current coercion in the integer branch (inside the
function handling expectedType === "integer", using the local variable value and
num) uses parseInt which silently truncates inputs like "12abc" or "3.7";
replace it with a strict integer validation: first check that value is a string
and matches a whole-integer pattern (e.g. optional leading - and one or more
digits) before converting, then convert to a Number/parseInt and ensure
Number.isInteger on the result; only return the integer when the regex check and
integer check both pass, otherwise fall through (do not coerce).

---

Outside diff comments:
In `@src/lib/neurolink.ts`:
- Around line 6797-6832: The fallback override merge is duplicated and
incomplete: centralize the logic into a shared resolver (e.g., a new function
like resolveFallbackRoute or reuse ModelRouter.getFallbackRoute) and replace the
in-place merge at the current block and inside handleStreamError to call that
resolver; ensure the resolver accepts (originalPrompt, enhancedOptions,
providerName, strategy) and returns {provider, model, source} by applying
optFallbackProvider/optFallbackModel, env FALLBACK_PROVIDER/FALLBACK_MODEL, then
fall back to ModelRouter.getFallbackRoute; importantly, if the returned provider
differs from modelConfigRoute.provider and no explicit model override was
provided, drop or recompute the model from ModelRouter.getFallbackRoute (do not
keep modelConfigRoute.model blindly) so provider-model incompatibilities are
avoided; finally update the stream error logging to use the resolver’s source
and accurately reflect one-sided overrides (only provider or only model) in
fallbackSource.

In `@src/lib/providers/googleAiStudio.ts`:
- Around line 647-662: The native Gemini 3 tool path never applies the tool-call
repair logic because getToolCallRepairFn() (and its disableToolCallRepair
option) is only used with streamText(), so executeNativeGemini3Stream(),
executeNativeGemini3Generate(), buildNativeToolDeclarations(), and
executeNativeToolCalls() bypass repair; fix by wiring the same repair handler
into the native flow—either accept and pass the repair function
(this.getToolCallRepairFn(options)) into
buildNativeToolDeclarations()/executeNativeToolCalls() or call that repair
function inside executeNativeToolCalls() before executing a tool call, and
ensure the disableToolCallRepair option is respected when choosing whether to
apply repairs.

---

Nitpick comments:
In `@src/lib/core/baseProvider.ts`:
- Around line 1255-1268: getToolCallRepairFn currently re-imports and calls
createToolCallRepair on every invocation of the returned async closure; change
it to cache the produced repair factory once per getToolCallRepairFn call:
inside getToolCallRepairFn, declare a local variable (e.g., repairFactory?:
ReturnType<typeof createToolCallRepair>), then return an async closure that, on
first call, does the lazy import of "../utils/toolCallRepair.js", sets
repairFactory = createToolCallRepair(), and thereafter calls
repairFactory(...args) for subsequent invocations; keep the same return type
ToolCallRepairFunction<ToolSet> and preserve the disableToolCallRepair check and
lazy import behavior.

In `@src/lib/utils/toolCallRepair.ts`:
- Around line 104-106: The debug log currently computes availableTools.join(",
") unconditionally; wrap the expensive serialization with a debug guard: check
logger.shouldLog("debug") before calling availableTools.join and only build the
full message when true, then call logger.debug with the constructed string (use
the same symbols: logger.debug, logger.shouldLog("debug"), availableTools.join,
and called) so the join is not executed when debug logging is disabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 341eaed1-e1d9-4958-a58f-28ef0366ce0f

📥 Commits

Reviewing files that changed from the base of the PR and between b6b504b and d34903a.

📒 Files selected for processing (20)
  • src/cli/factories/commandFactory.ts
  • src/cli/utils/abortHandler.ts
  • src/cli/utils/typewriter.ts
  • src/lib/core/baseProvider.ts
  • src/lib/core/modules/ToolsManager.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/configTypes.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/toolCallRepair.ts

Comment thread src/cli/factories/commandFactory.ts Outdated
Comment on lines +25 to +45
const sigintHandler = () => {
if (aborted) {
// Second Ctrl+C — force exit
if (forceExitTimer) {
clearTimeout(forceExitTimer);
}
process.exit(0);
}

aborted = true;
controller.abort();
process.stderr.write(chalk.yellow("\nStream cancelled.\n"));

// Allow force exit on second Ctrl+C within 1 second
forceExitTimer = setTimeout(() => {
forceExitTimer = null;
}, 1000);
};

process.on("SIGINT", sigintHandler);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid owning another process-level SIGINT handler here.

src/cli/index.ts Lines 55-68 already exits on SIGINT, and src/cli/commands/serve.ts / src/cli/loop/session.ts install their own handlers too. Registering another process.on("SIGINT") here means the first Ctrl+C still races the existing exit paths instead of only canceling the active stream, while Line 31's process.exit(0) can bypass the async cleanup in src/cli/index.ts Lines 70-90. This needs one top-level signal coordinator instead of a second process-level owner inside the helper.

Also applies to: 46-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/utils/abortHandler.ts` around lines 25 - 45, This file currently
registers its own process.on("SIGINT") and calls process.exit(0) inside
sigintHandler, which conflicts with the top-level signal coordinator; remove the
internal process.on("SIGINT") registration and the direct process.exit call and
instead expose sigintHandler (or provide attach/detach functions) so the
top-level coordinator can call sigintHandler to cancel the controller (using
controller.abort()), manage aborted/forceExitTimer state, and handle the "Stream
cancelled." message; ensure any existing callers (serve/loop/session/index)
attach the handler centrally and that forceExitTimer and aborted cleanup still
occur when the coordinator invokes the handler.

Comment on lines +7 to +29
const DEFAULT_CHAR_DELAY_MS = 8;

/**
* Write text to stdout with a per-character typewriter animation.
* Falls back to raw write when delay is 0 or negative.
*/
export async function typewriterWrite(
text: string,
delayMs: number = DEFAULT_CHAR_DELAY_MS,
): Promise<void> {
if (!text) {
return;
}
if (delayMs <= 0) {
process.stdout.write(text);
return;
}
// Use Array.from to handle surrogate pairs / emoji correctly
for (const ch of Array.from(text)) {
process.stdout.write(ch);
await new Promise((r) => setTimeout(r, delayMs));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the typewriter path opt-in and abort-aware.

At 8ms per character this caps streamed output at about 125 chars/sec, so a 4k-char response adds ~32s of artificial latency. Because commandFactory awaits animatedWrite(...), Ctrl+C also cannot stop the current chunk until this sleep loop finishes. Default to raw writes unless a delay is explicitly requested, and thread an AbortSignal through the loop so cancellation can cut animation short.

Possible direction
-const DEFAULT_CHAR_DELAY_MS = 8;
+const DEFAULT_CHAR_DELAY_MS = 0;

 export async function typewriterWrite(
   text: string,
   delayMs: number = DEFAULT_CHAR_DELAY_MS,
+  signal?: AbortSignal,
 ): Promise<void> {
   if (!text) {
     return;
   }
   if (delayMs <= 0) {
     process.stdout.write(text);
     return;
   }
   // Use Array.from to handle surrogate pairs / emoji correctly
   for (const ch of Array.from(text)) {
+    if (signal?.aborted) {
+      return;
+    }
     process.stdout.write(ch);
-    await new Promise((r) => setTimeout(r, delayMs));
+    await new Promise((resolve) => {
+      const timer = setTimeout(resolve, delayMs);
+      signal?.addEventListener(
+        "abort",
+        () => {
+          clearTimeout(timer);
+          resolve(undefined);
+        },
+        { once: true },
+      );
+    });
   }
 }
 
 export async function animatedWrite(
   text: string,
+  signal?: AbortSignal,
 ): Promise<void> {
   if (shouldAnimate()) {
-    await typewriterWrite(text);
+    await typewriterWrite(text, DEFAULT_CHAR_DELAY_MS, signal);
   } else {
     process.stdout.write(text);
   }
 }

Also applies to: 41-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/utils/typewriter.ts` around lines 7 - 29, Change the typewriter path
to be opt-in and abort-aware by making the default behavior raw write: set
DEFAULT_CHAR_DELAY_MS to 0 (or make delayMs default to 0) so typewriterWrite
only animates when a positive delay is explicitly provided; add an optional
AbortSignal parameter (e.g., signal?: AbortSignal) to typewriterWrite and any
caller like animatedWrite, check signal?.aborted before starting and inside the
character loop, and break/return (or throw an AbortError) immediately if aborted
so Ctrl+C can cancel mid-animation; retain the existing Array.from(text)
iteration and per-char delay logic but replace await new Promise(r =>
setTimeout(r, delayMs)) with an abort-aware wait (check signal before/after
sleep) or short-circuit to avoid hanging.

Comment on lines +103 to +137
// Truncate "content" if present and oversized
if (typeof obj.content === "string") {
const { preview, truncated, originalSize } = generateToolOutputPreview(
obj.content,
);
if (truncated) {
logger.debug(
`[ToolsManager] Truncated '${toolName}' content field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`,
);
nextObj = { ...(nextObj ?? obj), content: preview };
}
}

// Truncate "data" if present and oversized — both fields can coexist
if (typeof obj.data === "string") {
const { preview, truncated, originalSize } = generateToolOutputPreview(
obj.data,
);
if (truncated) {
logger.debug(
`[ToolsManager] Truncated '${toolName}' data field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`,
);
nextObj = { ...(nextObj ?? obj), data: preview };
}
}

if (nextObj) {
return nextObj;
}

// For other objects, check if their JSON serialization is too large.
// Use UTF-8 byte length, not string length, to match the 50KB budget.
try {
const jsonStr = JSON.stringify(result);
if (Buffer.byteLength(jsonStr, "utf-8") > 51_200) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Field-level trimming skips the final size budget check.

nextObj is returned immediately after content / data truncation, so the whole-object check below never runs. A payload with both large content and data fields can still end up well over the 50 KB budget after each field was individually shortened.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/modules/ToolsManager.ts` around lines 103 - 137, The issue is
that returning nextObj immediately after truncating content/data skips the
subsequent whole-object JSON size check; instead of returning from ToolsManager
after setting nextObj, assign the truncated object back into the variable used
for the final size check (e.g., replace or set result = nextObj) and let
execution fall through to the existing JSON serialization/Buffer.byteLength
logic so the 50KB budget check (and any further trimming logic) runs on the full
object; keep using generateToolOutputPreview for per-field trimming but ensure
the final JSON.stringify uses the updated object (nextObj/result) before
comparing to 51_200 bytes.

Comment on lines +133 to +150
// For other objects, check if their JSON serialization is too large.
// Use UTF-8 byte length, not string length, to match the 50KB budget.
try {
const jsonStr = JSON.stringify(result);
if (Buffer.byteLength(jsonStr, "utf-8") > 51_200) {
const { preview, truncated, originalSize } =
generateToolOutputPreview(jsonStr);
if (truncated) {
logger.debug(
`[ToolsManager] Truncated '${toolName}' JSON output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`,
);
// Preserve object shape so callers reading structured fields don't
// get a type surprise. Attach the preview under a sentinel field.
return {
_truncated: true,
_originalSize: originalSize,
_preview: preview,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The JSON fallback still breaks structured tool outputs.

This branch replaces the original payload with { _truncated, _originalSize, _preview }, so any caller that reads fields from the real tool result gets a different object once the JSON-size path is hit. If the preview must travel downstream, keep the original fields alongside the metadata or move truncation to the model-facing serialization layer instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/modules/ToolsManager.ts` around lines 133 - 150, The fallback
currently replaces the tool result with only {_truncated, _originalSize,
_preview}, breaking callers expecting the tool's original fields; change the
return from ToolsManager's JSON-size branch to preserve the original structured
fields by merging them with the metadata (e.g., return { ...result, _truncated:
true, _originalSize: originalSize, _preview: preview }) so callers can still
read fields like those produced by the tool; use the same symbols
generateToolOutputPreview, toolName, logger, and result to locate the logic and
ensure the preview and metadata are added under sentinel names to avoid name
collisions, or alternatively move this truncation logic out of ToolsManager into
the model-facing serialization layer if you prefer not to ship the preview with
the full object.

Comment thread src/lib/neurolink.ts
Comment on lines +10950 to +10954
const toolAnnotations = this.getToolAnnotationsForExecution(toolName);
const cacheEnabled =
!!this.mcpToolResultCache &&
!this._disableToolCacheForCurrentRequest &&
!toolAnnotations?.destructiveHint;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve destructive annotations from the concrete external tool, not the cache helper.

getToolAnnotationsForExecution(toolName) only checks this.toolCache and then falls back to inferAnnotations({ name, description: "" }). On a cold cache, a server-supplied destructiveHint: true can be missed, and src/lib/mcp/toolAnnotations.ts:198-220 only infers destructive tools from keywords. That means the first call can still be cached even when the external server explicitly marked the tool unsafe to cache. Prefer the { serverId, toolName } metadata from externalServerManager.getServerTools(serverId) before falling back to inference.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10950 - 10954,
getToolAnnotationsForExecution currently reads from this.toolCache and then
falls back to inferAnnotations, which can miss a server-supplied
destructiveHint; update it to first consult
externalServerManager.getServerTools(serverId) for the concrete tool metadata
(match by toolName and serverId) and use any server-provided destructiveHint
before consulting this.toolCache or calling inferAnnotations; ensure the cache
check that computes cacheEnabled (uses mcpToolResultCache and
_disableToolCacheForCurrentRequest) uses the annotations returned by the new
precedence so a server-marked destructive tool will disable caching immediately.

Comment thread src/lib/neurolink.ts
Comment on lines +10982 to +10985
// BZ-664: Store result in cache after successful execution
if (cacheEnabled && this.mcpToolResultCache) {
this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only cache successful external-tool responses.

This branch caches every resolved payload, but handleSuccessfulToolExecution() later in this file (Lines 8872-8944) treats success: false / isError: true results as failures. Caching those here will pin transient MCP errors and short-circuit later retries.

Suggested fix
-      // BZ-664: Store result in cache after successful execution
-      if (cacheEnabled && this.mcpToolResultCache) {
-        this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result);
-      }
+      // BZ-664: Store only successful results in cache
+      const resultRecord =
+        result && typeof result === "object"
+          ? (result as Record<string, unknown>)
+          : undefined;
+      const isSuccessfulResult =
+        !resultRecord ||
+        (resultRecord.isError !== true && resultRecord.success !== false);
+      if (cacheEnabled && this.mcpToolResultCache && isSuccessfulResult) {
+        this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 10982 - 10985, The current cache branch
unconditionally calls this.mcpToolResultCache.cacheResult(toolName,
cacheKeyArgs, result) which will persist failed external-tool responses; change
it to only cache successful executions by checking the result flags used by
handleSuccessfulToolExecution (e.g. require result.success === true and
result.isError !== true, or the equivalent success predicate used later) before
calling cacheResult, so only truly successful tool responses are stored.

Comment thread src/lib/utils/toolCallRepair.ts
Comment thread src/lib/utils/toolCallRepair.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore
murdore force-pushed the feat/pemnding-bugs branch from af3f940 to 71e3394 Compare April 12, 2026 20:42
@github-actions

github-actions Bot commented Apr 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: 3510944cd0f636ab23cd25eb10c897d06c9cb099
  • Message: fix(sdk): schema-driven tool call repair, graceful abort, fallback provider (BZ-665, BZ-667, BZ-1341)
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@murdore

murdore commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Changes Made

src/lib/utils/toolCallRepair.ts (5 fixes):

  1. Empty tool name guard (Copilot) — Added early return when toolName is empty/whitespace to prevent false substring matches against every tool
  2. additionalProperties: false handling (Copilot) — Drop unmapped keys when schema forbids extra properties instead of passing them through
  3. Prevent remapped key overwrite (CodeRabbit) — Guard against fuzzy-mapped keys overwriting already-populated canonical keys
  4. Strict integer validation (CodeRabbit) — Replaced parseInt() with regex + Number.isSafeInteger() to reject partial parses like "12abc" → 12
  5. Comment fix (Copilot) — Updated "Single value → Array" comment to explain why strings are excluded from wrapping

src/cli/factories/commandFactory.ts (2 fixes):

  1. Abort listener leak (Copilot) — Added { once: true } to prevent listener accumulation across calls
  2. Stream iterator abort (CodeRabbit 🔴 Critical) — Race streamIterator.next() against abort promise so Ctrl+C actually unblocks pending reads; call streamIterator.return?.() on abort to release provider connection; hoisted iterator declaration outside try block for catch-block access

src/cli/utils/abortHandler.ts (1 fix):

  1. SIGINT handler conflict (CodeRabbit 🔴 Critical) — Changed from process.on() to process.prependListener() so stream handler fires before top-level SIGINT handler in cli/index.ts; removed process.exit(0) from second Ctrl+C path to let the global handler manage exit

Comments NOT in our scope (pre-existing code, not part of BZ-665/667/1341):

File Comment Why out of scope
typewriter.ts Animation delay + abort-awareness Not in this PR's diff
ToolsManager.ts Truncation size budget + structured output Not in this PR's diff
neurolink.ts:1356 BZ-664 cache default behavior BZ-664, different ticket
neurolink.ts:11058 Annotation resolution from external tool BZ-664, different ticket
neurolink.ts:11089 Cache only successful responses BZ-664, different ticket
toolCallRepair.ts:46 Unit tests request E2E tests exist in continuous-test-suite-tool-reliability.ts

Validation

  • TypeScript: ✅ 0 errors
  • Lint: ✅ 0 errors (6 pre-existing warnings)
  • Build: ✅ 0 errors, 0 warnings
  • Pre-commit hook: ✅ All checks passed

@coderabbitai review

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

@murdore Thank you for the thorough summary of addressed feedback! The fixes look comprehensive — especially the two critical ones:

  • Stream iterator abort in commandFactory.ts: Racing streamIterator.next() against the abort promise with streamIterator.return?.() cleanup is the correct pattern to prevent blocking on pending reads.
  • SIGINT handler conflict in abortHandler.ts: Using process.prependListener() and deferring force-exit to the global handler is the right approach to avoid handler ordering issues.

The out-of-scope justifications are well-reasoned — BZ-664 items belong to their own ticket, and the E2E test coverage in continuous-test-suite-tool-reliability.ts is a reasonable substitute for the unit tests request.

Triggering a fresh review now.


🧠 Learnings used
Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T03:54:40.299Z
Learning: Applies to src/lib/providers/**/*.ts : The `BaseProvider.stream()` method merges base tools with user-provided tools before calling provider-specific `executeStream()`. Do not bypass this pattern
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/cli/factories/commandFactory.ts (1)

2996-3004: ⚠️ Potential issue | 🟠 Major

Create the SIGINT bridge before sdk.stream() starts.

This handler is still created inside processStreamWithTimeout(), so the first Ctrl+C cannot cancel stalls while executeRealStream() is still awaiting sdk.stream(...) itself. That leaves auth/setup/connect hangs outside the new graceful-abort path; move createStreamAbortHandler() up into executeRealStream() and thread its signal through the sdk.stream(...) call path as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 2996 - 3004, Move creation
of the SIGINT bridge out of processStreamWithTimeout() into executeRealStream():
call createStreamAbortHandler() at the start of executeRealStream(), keep its
AbortSignal and add it to sdk.stream(...) call chain (pass it into
executeRealStream -> processStreamWithTimeout -> sdk.stream or into any
intermediate request options) so the first Ctrl+C triggers the same abort signal
used by abortController.abort(); remove the duplicate handler in
processStreamWithTimeout and ensure the created signal is wired through all
places that call or await sdk.stream(...) so auth/setup/connect stalls can be
cancelled immediately.
🧹 Nitpick comments (1)
src/lib/neurolink.ts (1)

6922-6927: Make fallback source logging precise for mixed-source overrides.

fallbackSource currently collapses mixed cases into "options" or "env" even when provider/model come from different sources. Logging provider/model source separately (or emitting "mixed") will make diagnostics more accurate.

♻️ Suggested logging refinement
-      fallbackSource:
-        optFallbackProvider || optFallbackModel
-          ? "options"
-          : envFallbackProvider || envFallbackModel
-            ? "env"
-            : "model_config",
+      fallbackProviderSource: optFallbackProvider
+        ? "options"
+        : envFallbackProvider
+          ? "env"
+          : "model_config",
+      fallbackModelSource: optFallbackModel
+        ? "options"
+        : envFallbackModel
+          ? "env"
+          : "model_config",
+      fallbackSource:
+        (optFallbackProvider
+          ? "options"
+          : envFallbackProvider
+            ? "env"
+            : "model_config") ===
+        (optFallbackModel
+          ? "options"
+          : envFallbackModel
+            ? "env"
+            : "model_config")
+          ? (optFallbackProvider
+              ? "options"
+              : envFallbackProvider
+                ? "env"
+                : "model_config")
+          : "mixed",

Based on learnings: “In neurolink stream fallback logging (src/lib/neurolink.ts handleStreamFallback), typical usage is to override both provider and model together, but logs should still accurately reflect one-sided overrides when only model or only provider is set.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 6922 - 6927, The current fallbackSource
calculation in handleStreamFallback collapses provider/model origins into one
value; instead determine provider and model origins separately using
optFallbackProvider, envFallbackProvider and optFallbackModel, envFallbackModel
(e.g., compute providerSource and modelSource) and then set fallbackSource to a
precise combined value (either "mixed" when providerSource !== modelSource or a
combined string like `${providerSource}/${modelSource}`) so logs accurately
reflect one-sided overrides; update any consumers of fallbackSource accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 3131-3134: The current cleanup calls abortController.abort(), then
awaits streamIterator.return?.(), which can itself throw and mask the original
stream error; change the sequence in the stream/error handling to preserve the
original error by: capture any original error/abort reason before cleanup, run
await streamIterator.return?.() inside its own try/catch so any exception from
streamIterator.return is caught/logged but does not replace the original error,
then call abortHandler.cleanup() and finally rethrow or return the original
error/result; follow the same pattern used in
src/lib/server/routes/claudeProxyRoutes.ts and update the code paths around
abortController.abort(), streamIterator.return?.(), and abortHandler.cleanup()
to ensure cleanup failures do not overwrite the original failure.

In `@src/lib/utils/toolCallRepair.ts`:
- Around line 250-256: In the branch that handles numeric coercion (where
expectedType === "number" and typeof value === "string"), trim the input string
first and reject empty/whitespace-only strings, then coerce using Number on the
trimmed string and only accept the result if it is finite (use isFinite on the
numeric result); i.e., use value.trim() to produce the input to Number(), check
trimmed !== "" and isFinite(num) before returning num (referencing expectedType,
value, and num in this block).

---

Duplicate comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 2996-3004: Move creation of the SIGINT bridge out of
processStreamWithTimeout() into executeRealStream(): call
createStreamAbortHandler() at the start of executeRealStream(), keep its
AbortSignal and add it to sdk.stream(...) call chain (pass it into
executeRealStream -> processStreamWithTimeout -> sdk.stream or into any
intermediate request options) so the first Ctrl+C triggers the same abort signal
used by abortController.abort(); remove the duplicate handler in
processStreamWithTimeout and ensure the created signal is wired through all
places that call or await sdk.stream(...) so auth/setup/connect stalls can be
cancelled immediately.

---

Nitpick comments:
In `@src/lib/neurolink.ts`:
- Around line 6922-6927: The current fallbackSource calculation in
handleStreamFallback collapses provider/model origins into one value; instead
determine provider and model origins separately using optFallbackProvider,
envFallbackProvider and optFallbackModel, envFallbackModel (e.g., compute
providerSource and modelSource) and then set fallbackSource to a precise
combined value (either "mixed" when providerSource !== modelSource or a combined
string like `${providerSource}/${modelSource}`) so logs accurately reflect
one-sided overrides; update any consumers of fallbackSource accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8df07219-25b9-4a5a-af44-a7a22cad1f74

📥 Commits

Reviewing files that changed from the base of the PR and between d34903a and 71e3394.

📒 Files selected for processing (17)
  • src/cli/factories/commandFactory.ts
  • src/cli/utils/abortHandler.ts
  • src/lib/core/baseProvider.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/toolCallRepair.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/providers/googleVertex.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/types/streamTypes.ts
  • src/cli/utils/abortHandler.ts

Comment thread src/cli/factories/commandFactory.ts
Comment thread src/lib/utils/toolCallRepair.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore

murdore commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed — Round 2

Fixes Applied (2 inline comments)

  1. commandFactory.ts:3131-3134 — streamIterator.return?.() now wrapped in its own try/catch so cleanup failures cannot mask the original stream error. The original error/abort propagates correctly.

  2. toolCallRepair.ts:250-256 — Number coercion now trims input first, rejects empty/whitespace strings (trimmed !== ""), and uses isFinite(num) instead of !isNaN(num). This blocks "" → 0, "Infinity", and "NaN" from being silently coerced.

Dismissed with rationale (2 comments)

  1. Duplicate: Move abort handler to executeRealStream (commandFactory.ts:2996-3004) — No change needed. The abort handler is scoped to the CLI output loop (processStreamWithTimeout), not the SDK stream itself. sdk.stream() already accepts its own abortSignal via StreamOptions for provider-level cancellation. Moving the handler up would conflate two concerns: CLI output cancellation vs provider request cancellation. The current placement correctly cancels the output loop and races streamIterator.next() with the abort promise.

  2. Nitpick: Split fallbackSource into provider/model origins (neurolink.ts:6922-6927) — No change needed. fallbackSource is a log-only diagnostic field in a logger.warn() call with no downstream consumers. The current "options" > "env" > "model_config" cascade accurately reflects the override precedence. Adding providerSource/modelSource granularity would increase complexity for zero runtime benefit.

Previously addressed (Round 1 — 8 fixes)

File Fix Source
toolCallRepair.ts Empty tool name guard Copilot
toolCallRepair.ts Drop unknown keys with additionalProperties: false Copilot
toolCallRepair.ts Prevent remapped key overwrite CodeRabbit
toolCallRepair.ts Strict integer validation (parseInt → regex + isSafeInteger) CodeRabbit
toolCallRepair.ts Comment fix for string exclusion in array wrap Copilot
commandFactory.ts { once: true } on abort listener Copilot
commandFactory.ts Race streamIterator.next() with abort promise CodeRabbit Critical
abortHandler.ts prependListener + remove process.exit CodeRabbit Critical

Out of scope (pre-existing code, not part of BZ-665/667/1341)

  • typewriter.ts — Not in this PR's diff
  • ToolsManager.ts — Not in this PR's diff
  • neurolink.ts lines 1356, 11058, 11089 — BZ-664 (different ticket)

Validation

  • TypeScript: ✅ 0 errors
  • Lint: ✅ 0 errors
  • Build: ✅ 0 errors
  • Rebased on latest origin/release

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

@murdore Thanks for the thorough Round 2 summary. All looks good:

Fixes applied — LGTM:

  1. commandFactory.ts — streamIterator.return?.() try/catch isolation: Correct approach. Cleanup errors are now absorbed locally, ensuring the original error/abort continues to propagate up the call stack without being masked.

  2. toolCallRepair.ts — Number coercion hardening: The triple improvement (trim() → empty string guard via trimmed !== "" → isFinite()) is solid. isFinite() correctly blocks "Infinity", "-Infinity", and "NaN" that !isNaN() would have silently passed, and the empty string guard prevents "" → 0.

Dismissals — rationale accepted:

  1. Abort handler placement: The distinction between CLI output loop cancellation (processStreamWithTimeout) and provider request cancellation (via abortSignal in StreamOptions) is a valid separation of concerns. Merging them into executeRealStream would conflate two independent cancellation signals.

  2. fallbackSource granularity: Agreed — adding providerSource/modelSource fields adds structural complexity to a log-only diagnostic with no downstream consumers. The existing cascade string is readable and sufficient.

Triggering a fresh review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 3054-3068: Declare nextResult with an explicit iterator result
type instead of implicit any: replace "let nextResult;" with a typed declaration
using Awaited<ReturnType<typeof streamIterator.next>> (e.g. let nextResult:
Awaited<ReturnType<typeof streamIterator.next>>>;) so TypeScript knows the shape
(.done and .value) returned by streamIterator.next() even when used inside
Promise.race with timeoutPromise and abortPromise; reference symbol: nextResult,
streamIterator.next, timeoutPromise, abortPromise.

In `@src/lib/utils/toolCallRepair.ts`:
- Around line 77-88: The substring-match branch currently uses
availableTools.find(...) which picks the first match and makes repairs
order-dependent; change it to collect all substring candidates (e.g., filter
into a candidates array using the same tLower.includes(calledLower) ||
calledLower.includes(tLower) logic) and only apply the repair (return updated
toolCall and logger.debug) if candidates.length === 1; if there are 0 or >1
candidates, do not modify toolCall and let the stricter match logic below handle
it. Ensure you reference calledLower, availableTools, subMatch (rename to
candidates or similar), toolCall, and logger.debug in the updated
implementation.
- Around line 147-180: The current repaired object is created with a plain {}
which allows keys like "__proto__" or "constructor" to be treated as prototype
properties and thus lost; change the initialization of repaired (the const
repaired: Record<string, unknown> = {} in toolCallRepair.ts) to use
Object.create(null) so it has no prototype (e.g., const repaired =
Object.create(null) as Record<string, unknown>), leaving the rest of the logic
(checks using Object.prototype.hasOwnProperty.call(repaired, mapped),
findMatchingKey(inputKey), expectedKeys, inputObj, toolCall.toolName) unchanged
so unmapped keys are forwarded safely when additionalProperties is allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea936847-ce0f-4f89-b861-7160ed767932

📥 Commits

Reviewing files that changed from the base of the PR and between 71e3394 and 4fa41ed.

📒 Files selected for processing (17)
  • src/cli/factories/commandFactory.ts
  • src/cli/utils/abortHandler.ts
  • src/lib/core/baseProvider.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/toolCallRepair.ts
✅ Files skipped from review due to trivial changes (4)
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/googleAiStudio.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/lib/providers/openRouter.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/litellm.ts
  • src/cli/utils/abortHandler.ts
  • src/lib/neurolink.ts

Comment on lines 3054 to +3068
let nextResult;

if (timeoutActive && !contentReceived) {
// Race between next chunk and timeout for first chunk only
// Race between next chunk, timeout, and abort signal
nextResult = await Promise.race([
streamIterator.next(),
timeoutPromise,
abortPromise,
]);
} else {
// No timeout for subsequent chunks
nextResult = await streamIterator.next();
// Race between next chunk and abort signal
nextResult = await Promise.race([
streamIterator.next(),
abortPromise,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm the implicit-any declaration site
rg -n 'let nextResult;' src/cli/factories/commandFactory.ts

# Re-run Biome on the file to verify the lint finding
pnpm biome check src/cli/factories/commandFactory.ts

Repository: juspay/neurolink

Length of output: 16099


🏁 Script executed:

# Get context around line 3054 to understand the function and streamIterator
sed -n '3000,3100p' src/cli/factories/commandFactory.ts | head -100

Repository: juspay/neurolink

Length of output: 3535


🏁 Script executed:

# Find streamIterator declaration/usage to understand its type
rg -n 'streamIterator' src/cli/factories/commandFactory.ts | head -20

Repository: juspay/neurolink

Length of output: 345


🏁 Script executed:

# Get the function signature containing line 3054
sed -n '2980,3060p' src/cli/factories/commandFactory.ts

Repository: juspay/neurolink

Length of output: 3016


🏁 Script executed:

# Show how nextResult is used after line 3054
sed -n '3054,3120p' src/cli/factories/commandFactory.ts

Repository: juspay/neurolink

Length of output: 2201


Type nextResult to avoid implicit any at Line 3054.

let nextResult; is implicitly any and triggers Biome noImplicitAnyLet. The variable is assigned the result of Promise.race([streamIterator.next(), ...]) and used with .done and .value properties, so it must have an explicit IteratorResult type.

💡 Recommended fix
-        let nextResult;
+        let nextResult: IteratorResult<
+          | { content: string }
+          | { type: "audio" }
+          | { type: "image"; imageOutput: { base64: string } }
+        >;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let nextResult;
if (timeoutActive && !contentReceived) {
// Race between next chunk and timeout for first chunk only
// Race between next chunk, timeout, and abort signal
nextResult = await Promise.race([
streamIterator.next(),
timeoutPromise,
abortPromise,
]);
} else {
// No timeout for subsequent chunks
nextResult = await streamIterator.next();
// Race between next chunk and abort signal
nextResult = await Promise.race([
streamIterator.next(),
abortPromise,
]);
let nextResult: IteratorResult<
| { content: string }
| { type: "audio" }
| { type: "image"; imageOutput: { base64: string } }
>;
if (timeoutActive && !contentReceived) {
// Race between next chunk, timeout, and abort signal
nextResult = await Promise.race([
streamIterator.next(),
timeoutPromise,
abortPromise,
]);
} else {
// Race between next chunk and abort signal
nextResult = await Promise.race([
streamIterator.next(),
abortPromise,
]);
🧰 Tools
🪛 Biome (2.4.10)

[error] 3054-3054: This variable implicitly has the any type.

(lint/suspicious/noImplicitAnyLet)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 3054 - 3068, Declare
nextResult with an explicit iterator result type instead of implicit any:
replace "let nextResult;" with a typed declaration using
Awaited<ReturnType<typeof streamIterator.next>> (e.g. let nextResult:
Awaited<ReturnType<typeof streamIterator.next>>>;) so TypeScript knows the shape
(.done and .value) returned by streamIterator.next() even when used inside
Promise.race with timeoutPromise and abortPromise; reference symbol: nextResult,
streamIterator.next, timeoutPromise, abortPromise.

Comment thread src/lib/utils/toolCallRepair.ts Outdated
Comment thread src/lib/utils/toolCallRepair.ts Outdated
@murdore
murdore force-pushed the feat/pemnding-bugs branch from 4fa41ed to b7157a9 Compare April 12, 2026 21:33
@murdore

murdore commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed — Round 3

Fixes Applied (2 inline comments)

  1. toolCallRepair.ts:77-88 — Substring match now collects all candidates via .filter() and only applies the repair when candidates.length === 1. Ambiguous matches (multiple substring hits) are skipped, letting the stricter Levenshtein logic below handle them.

  2. toolCallRepair.ts:147 — repaired object now initialized with Object.create(null) instead of {} to prevent prototype pollution — keys like __proto__ or constructor are safely handled as regular properties.

Dismissed with rationale (1 inline comment)

  1. commandFactory.ts:3054-3068 — Explicit type for nextResult — No change needed. TypeScript correctly infers the type from assignment: Promise.race with Promise<never> (timeout/abort) resolves to the iterator result type. nextResult.done and nextResult.value compile without error. This is a style nit with no type-safety benefit.

Rebased

Branch rebased on latest origin/release (7e4ea623).

Cumulative fixes across all rounds

Round File Fix
1 toolCallRepair.ts Empty tool name guard
1 toolCallRepair.ts Drop unknown keys with additionalProperties: false
1 toolCallRepair.ts Prevent remapped key overwrite
1 toolCallRepair.ts Strict integer validation (regex + isSafeInteger)
1 toolCallRepair.ts Comment fix for string exclusion
1 commandFactory.ts { once: true } on abort listener
1 commandFactory.ts Race iterator with abort promise
1 abortHandler.ts prependListener + remove process.exit
2 commandFactory.ts try/catch around streamIterator.return?.()
2 toolCallRepair.ts Number coercion: trim + isFinite
3 toolCallRepair.ts Unambiguous substring match (single candidate only)
3 toolCallRepair.ts Object.create(null) for prototype-safe repaired object

Validation

  • TypeScript: ✅ 0 errors
  • Lint: ✅ 0 errors
  • Build: ✅ (pre-commit hook passed)

@coderabbitai review

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

@murdore Thanks for the Round 3 summary. Both fixes are well-executed:

Fixes applied — LGTM:

  1. toolCallRepair.ts — Unambiguous substring match: Collecting all candidates with .filter() and gating on candidates.length === 1 is the right approach. Ambiguous substring matches are now correctly deferred to Levenshtein, which uses a bounded distance threshold and provides a more principled ranking.

  2. toolCallRepair.ts — Object.create(null) for repaired object: Good security hardening. A null-prototype object ensures that adversarially crafted keys like __proto__, constructor, or toString are treated as plain data properties rather than prototype chain entries, preventing any prototype pollution during the key reconciliation loop.

Dismissal — rationale accepted:

  1. nextResult explicit type annotation: Agreed — TypeScript's narrowing of Promise.race<IteratorResult<...> | never> to IteratorResult<...> is correct by the language spec. Accessing .done and .value compiles cleanly. It's purely a style preference with no safety delta.

The cumulative 12 fixes across 3 rounds represent thorough coverage of all raised concerns. Triggering a fresh review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/cli/factories/commandFactory.ts (1)

3054-3054: ⚠️ Potential issue | 🟠 Major

Type nextResult explicitly to avoid implicit any lint failure.

let nextResult; at Line 3054 is still implicit any and can fail Biome (noImplicitAnyLet) in this path.

💡 Minimal fix
-        let nextResult;
+        let nextResult: Awaited<ReturnType<typeof streamIterator.next>>;
#!/bin/bash
set -euo pipefail

# Verify declaration site
rg -n 'let nextResult;' src/cli/factories/commandFactory.ts

# Verify lint status for this file
pnpm biome check src/cli/factories/commandFactory.ts

Expected result: no lint/suspicious/noImplicitAnyLet error at Line 3054.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` at line 3054, The local variable
nextResult is declared without a type (let nextResult;) causing an implicit any
lint failure; update its declaration to an explicit type that matches how it's
assigned/used (e.g., the IteratorResult/Promise return type or a domain-specific
type) — if unsure, use a safe typed placeholder like unknown or the appropriate
union (e.g., IteratorResult<YourItemType, void> | undefined) and narrow it where
used; modify the single declaration of nextResult in commandFactory.ts to
include that explicit type and re-run the Biome check to ensure the
noImplicitAnyLet error is resolved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/utils/abortHandler.ts`:
- Around line 8-10: Update the doc comment to match the actual API usage:
replace the claim that the code uses `prependOnceListener` with accurate wording
that it uses `process.prependListener` (a persistent listener) and clarify that
the SIGINT handler remains registered until `cleanup()` removes it—so the first
Ctrl+C triggers graceful stream cancellation and only a subsequent rapid Ctrl+C
will exit; also update the similar text around the second occurrence (lines
~49-52) to the same wording.

In `@src/lib/utils/toolCallRepair.ts`:
- Around line 160-166: When findMatchingKey(inputKey, expectedKeys) returns
mapped but the canonical key is already present in repaired, don't silently
continue — mark that a repair occurred so didRepair becomes true; update the
logic in the loop inside toolCallRepair (the block that checks
Object.prototype.hasOwnProperty.call(repaired, mapped)) to set didRepair = true
(and optionally record the discarded fuzzy duplicate in a diagnostics/repairs
collection if one exists) before skipping the duplicate, so the function will
return the repaired version instead of null.
- Around line 91-108: The loop that selects a replacement tool uses raw edit
distance (bestScore = dist) to pick the winner but filters by normalized
distance, causing wrong picks when lengths differ; change the selection to track
and compare the normalized score instead (e.g., replace bestScore to represent
the lowest normalized value and compare normalized < bestScore) while still
ensuring candidates pass the normalized < 0.3 filter; update references to
bestScore/bestMatch in the block (variables: bestMatch, bestScore, dist,
normalized, calledLower, availableTools, toolCall) so the returned toolName is
the candidate with the smallest normalized distance rather than the smallest raw
dist.

---

Duplicate comments:
In `@src/cli/factories/commandFactory.ts`:
- Line 3054: The local variable nextResult is declared without a type (let
nextResult;) causing an implicit any lint failure; update its declaration to an
explicit type that matches how it's assigned/used (e.g., the
IteratorResult/Promise return type or a domain-specific type) — if unsure, use a
safe typed placeholder like unknown or the appropriate union (e.g.,
IteratorResult<YourItemType, void> | undefined) and narrow it where used; modify
the single declaration of nextResult in commandFactory.ts to include that
explicit type and re-run the Biome check to ensure the noImplicitAnyLet error is
resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6671b8c-3e35-4d1d-bb2e-f1b2f1257109

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa41ed and b7157a9.

📒 Files selected for processing (17)
  • src/cli/factories/commandFactory.ts
  • src/cli/utils/abortHandler.ts
  • src/lib/core/baseProvider.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/toolCallRepair.ts
✅ Files skipped from review due to trivial changes (4)
  • src/lib/providers/openRouter.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/googleAiStudio.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/providers/anthropic.ts

Comment thread src/cli/utils/abortHandler.ts Outdated
Comment thread src/lib/utils/toolCallRepair.ts
Comment thread src/lib/utils/toolCallRepair.ts
…ovider (BZ-665, BZ-667, BZ-1341)

BZ-665 — Schema-driven tool call repair via experimental_repairToolCall:
- Dynamic repair function reads JSON schema at repair time (zero static maps)
- Tool name repair: case-insensitive → substring → Levenshtein (< 30% normalized)
- Param name repair: case-insensitive → Levenshtein (≤ 2 edits) against schema properties
- Type coercion: string→number, string→boolean, JSON string→object/array per schema type
- Empty tool name guard to prevent false substring matches
- Strict integer validation (regex + isSafeInteger) to reject partial parses like "12abc"
- Drop unmapped keys when schema has additionalProperties: false
- Prevent remapped keys from overwriting already-valid canonical keys
- Wired into all 11 providers via BaseProvider.getToolCallRepairFn()
- Opt-out via disableToolCallRepair option on StreamOptions

BZ-667 — Graceful abort signal handling for CLI stream commands:
- createStreamAbortHandler() bridges SIGINT → AbortController
- Uses prependListener to fire before top-level SIGINT handler in cli/index.ts
- Abort promise races against streamIterator.next() to unblock pending reads
- streamIterator.return() called on abort to release provider connection
- First Ctrl+C cancels stream + returns partial content; second Ctrl+C force-exits
- Listener cleanup via { once: true } to prevent accumulation across calls

BZ-1341 — Fallback provider override via options and env vars:
- fallbackProvider/fallbackModel on StreamOptions for per-call override
- FALLBACK_PROVIDER/FALLBACK_MODEL env vars as second-priority fallback
- Empty string guard (trim || undefined) to treat "" as unset
- fallbackSource tracking: "options" > "env" > "model_config"
@murdore
murdore force-pushed the feat/pemnding-bugs branch from b7157a9 to 3510944 Compare April 13, 2026 03:02
@murdore

murdore commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed — Round 4

Fixes Applied (3 inline comments)

  1. abortHandler.ts:8-10, 49-52 — Doc comments corrected: prependOnceListener → prependListener. Clarified that the listener remains registered until cleanup() removes it.

  2. toolCallRepair.ts:160-166 — When a fuzzy-mapped key collides with an already-populated canonical key, didRepair is now set to true before skipping. This ensures the function returns the repaired object instead of null when the only repair action was discarding a duplicate mapping.

  3. toolCallRepair.ts:91-108 — Levenshtein selection now compares by normalized score (normalized < bestNormalized) instead of raw edit distance (dist < bestScore). This prevents length-dependent skew where a long tool name with 2 edits (normalized 0.15) would incorrectly beat a short tool name with 1 edit (normalized 0.25).

Dismissed with rationale (1 duplicate comment)

  1. commandFactory.ts:3054 — Explicit type for nextResult — No change needed. No lint rule flags this (pnpm run lint clean). TypeScript infers the correct type from Promise.race — the Promise<never> branches (timeout/abort) don't widen the type. .done and .value access compiles without error.

Validation

  • TypeScript: ✅ 0 errors
  • Lint: ✅ 0 errors
  • Build: ✅ (pre-commit hook passed)
  • Rebased on latest origin/release

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

@murdore Thanks for the Round 4 summary. All three fixes are solid:

Fixes applied — LGTM:

  1. abortHandler.ts — Doc comment correction: Clean clarification. prependListener (not prependOnceListener) is accurate — the listener persists until cleanup() explicitly removes it, which is the correct lifecycle for this pattern.

  2. toolCallRepair.ts — didRepair = true on fuzzy collision skip: This is an important correctness fix. Previously, if the only repair action was discarding a conflicting fuzzy-mapped key (to preserve the already-populated canonical key), didRepair would remain false and the function would return null — silently treating an actual repair as a no-op. Setting didRepair = true before the skip ensures the repaired object is returned in that case.

  3. toolCallRepair.ts — Normalized Levenshtein for tool name selection: Good algorithmic improvement. Raw edit distance is length-biased — a 2-edit match on a 20-character name (normalized ≈ 0.10) would incorrectly rank higher than a 1-edit match on a 4-character name (normalized = 0.25) when using raw scoring. Comparing by normalized < bestNormalized gives length-independent similarity ranking.

Dismissal — rationale accepted:

  1. nextResult explicit type: Consistent with the Round 3 dismissal. TypeScript's inference from Promise.race with Promise<never> branches is sound, .done/.value access compiles cleanly, and lint is clean.

The cumulative 15 fixes across 4 rounds are comprehensive. Triggering a fresh review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/lib/utils/toolCallRepair.ts (1)

93-109: ⚠️ Potential issue | 🟠 Major

Treat equal Levenshtein winners as ambiguous.

The normalized-score change fixed the raw-distance bias, but Line 99 still keeps the first tool when two candidates tie on the best score. That makes repair order-dependent again in a path that can execute the wrong tool.

Suggested fix
   let bestMatch: string | null = null;
   let bestNormalized = Infinity;
+  let bestDistance = Infinity;
+  let ambiguous = false;
   for (const t of availableTools) {
     const dist = levenshtein(calledLower, t.toLowerCase());
     const maxLen = Math.max(called.length, t.length);
     const normalized = maxLen === 0 ? 0 : dist / maxLen;
-    if (normalized < 0.3 && normalized < bestNormalized) {
+    if (normalized >= 0.3) {
+      continue;
+    }
+    if (
+      normalized < bestNormalized ||
+      (normalized === bestNormalized && dist < bestDistance)
+    ) {
       bestNormalized = normalized;
+      bestDistance = dist;
       bestMatch = t;
+      ambiguous = false;
+    } else if (
+      normalized === bestNormalized &&
+      dist === bestDistance
+    ) {
+      ambiguous = true;
     }
   }
-  if (bestMatch) {
+  if (bestMatch && !ambiguous) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/toolCallRepair.ts` around lines 93 - 109, The current loop over
availableTools can pick the first tool on equal normalized Levenshtein scores,
making repair order-dependent; change the comparison logic in the loop that
computes dist/normalized (using levenshtein, calledLower, availableTools) to
detect ties using a small epsilon (e.g. 1e-6): only accept a candidate when
normalized < bestNormalized - eps, and if Math.abs(normalized - bestNormalized)
<= eps mark the result as ambiguous (e.g. set bestMatch to null or set an
ambiguous flag). After the loop, only perform the repair and logger.debug (use
bestNormalized and toolCall/toolName) when a unique bestMatch exists (not
ambiguous); otherwise skip repair to avoid order-dependent selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/utils/toolCallRepair.ts`:
- Around line 226-236: The fuzzy-match loop in toolCallRepair.ts (variables
schemaKeys, best, bestDist, levenshtein, inputLower) silently keeps the first
key when a later key has the same edit distance; change the logic to detect ties
and return null in that case: introduce a tie flag (or set best = null) and when
you find dist < bestDist set best = k, bestDist = dist and clear the tie; when
you find dist === bestDist set best = null (or set tie = true) so tied distances
do not select a winner; at the end return best only if no tie (otherwise null),
preserving the ≤2 threshold.

---

Duplicate comments:
In `@src/lib/utils/toolCallRepair.ts`:
- Around line 93-109: The current loop over availableTools can pick the first
tool on equal normalized Levenshtein scores, making repair order-dependent;
change the comparison logic in the loop that computes dist/normalized (using
levenshtein, calledLower, availableTools) to detect ties using a small epsilon
(e.g. 1e-6): only accept a candidate when normalized < bestNormalized - eps, and
if Math.abs(normalized - bestNormalized) <= eps mark the result as ambiguous
(e.g. set bestMatch to null or set an ambiguous flag). After the loop, only
perform the repair and logger.debug (use bestNormalized and toolCall/toolName)
when a unique bestMatch exists (not ambiguous); otherwise skip repair to avoid
order-dependent selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f122fb96-0aba-46e8-afb8-1b0f182f19ce

📥 Commits

Reviewing files that changed from the base of the PR and between b7157a9 and 3510944.

📒 Files selected for processing (17)
  • src/cli/factories/commandFactory.ts
  • src/cli/utils/abortHandler.ts
  • src/lib/core/baseProvider.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/toolCallRepair.ts
✅ Files skipped from review due to trivial changes (4)
  • src/lib/providers/openAI.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/googleVertex.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/lib/providers/openRouter.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/types/streamTypes.ts
  • src/lib/core/baseProvider.ts
  • src/cli/utils/abortHandler.ts

Comment on lines +226 to +236
// Levenshtein — threshold ≤2 edits
let best: string | null = null;
let bestDist = Infinity;
for (const k of schemaKeys) {
const dist = levenshtein(inputLower, k.toLowerCase());
if (dist <= 2 && dist < bestDist) {
bestDist = dist;
best = k;
}
}
return best;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Skip fuzzy param repair on tied distances.

Line 231 keeps the first schema key when two params are equally close. For short or similar names, that can silently rewrite the payload to the wrong argument instead of letting validation fail.

Suggested fix
   let best: string | null = null;
   let bestDist = Infinity;
+  let ambiguous = false;
   for (const k of schemaKeys) {
     const dist = levenshtein(inputLower, k.toLowerCase());
-    if (dist <= 2 && dist < bestDist) {
+    if (dist > 2) {
+      continue;
+    }
+    if (dist < bestDist) {
       bestDist = dist;
       best = k;
+      ambiguous = false;
+    } else if (dist === bestDist) {
+      ambiguous = true;
     }
   }
-  return best;
+  return ambiguous ? null : best;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/toolCallRepair.ts` around lines 226 - 236, The fuzzy-match loop
in toolCallRepair.ts (variables schemaKeys, best, bestDist, levenshtein,
inputLower) silently keeps the first key when a later key has the same edit
distance; change the logic to detect ties and return null in that case:
introduce a tie flag (or set best = null) and when you find dist < bestDist set
best = k, bestDist = dist and clear the tie; when you find dist === bestDist set
best = null (or set tie = true) so tied distances do not select a winner; at the
end return best only if no tie (otherwise null), preserving the ≤2 threshold.

@murdore

murdore commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback — Round 5

Dismissed (1 comment, already resolved in prior rounds)

abortHandler.ts:25-45 — "process.exit(0) conflict + expose sigintHandler"

This comment describes the state of the code before Round 1 fixes. The current code already addresses both concerns:

  1. process.exit(0) removed — The second Ctrl+C path now simply return;s (line 36), letting the top-level SIGINT handler in cli/index.ts manage process exit.

  2. process.prependListener (not process.on) — The handler fires before the global SIGINT handler, not alongside it. cleanup() removes it after the stream ends, so no persistent listener accumulation.

  3. "Expose sigintHandler for central coordinator" — This would require refactoring the entire CLI signal architecture across cli/index.ts, serve.ts, and session.ts into a unified signal coordinator. That's a separate initiative beyond the scope of BZ-667 (graceful stream cancellation). The current design is scoped, ordered correctly via prependListener, and cleaned up via cleanup().

No changes made this round.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

@murdore Confirmed — the stale comment was describing pre-Round-1 behavior. The current implementation already handles all three concerns correctly:

  1. process.exit(0) removed: The second Ctrl+C path correctly defers to the global handler via return, as established in Round 1.
  2. prependListener ordering: The stream handler fires before the top-level SIGINT handler, with cleanup() removing it after the stream ends — no listener accumulation.
  3. Central signal coordinator: Agreed this is out of scope for BZ-667. Refactoring signal architecture across cli/index.ts, serve.ts, and session.ts would be a standalone initiative. The current scoped design is clean and correct for graceful stream cancellation.

Triggering a fresh review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@murdore
murdore merged commit fd74ae4 into release Apr 13, 2026
16 checks passed
@murdore
murdore deleted the feat/pemnding-bugs branch April 13, 2026 03:20
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.54.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 3510944c Deployed Apr 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants