Skip to content

Add verbose timing telemetry and improvements to terminal commands performance#257

Merged
wonderwhy-er merged 3 commits into
mainfrom
wonderwhy-er/terminal-process-tools
Oct 13, 2025
Merged

Add verbose timing telemetry and improvements to terminal commands performance#257
wonderwhy-er merged 3 commits into
mainfrom
wonderwhy-er/terminal-process-tools

Conversation

@wonderwhy-er

@wonderwhy-er wonderwhy-er commented Oct 10, 2025

Copy link
Copy Markdown
Owner

Summary

This PR adds comprehensive performance telemetry and fixes a critical bug where interact_with_process waited 8+ seconds despite output arriving in 2-6ms.

Problem Identified

Using the new telemetry, we discovered:

Exit Reason: timeout
Total Duration: 8037ms  ← WAITED 8 SECONDS!
Time to First Output: 2ms  ← OUTPUT ARRIVED IN 2MS!
Output Events (1 total):
  [1] +2ms | stdout | 18b
     "'Hello, World!'\n> "  ← PROMPT DETECTED IMMEDIATELY!

Root Causes & Fixes

1. Removed Broken Stream Listener

  • Problem: Duplicate stream listeners caused race conditions and delayed checks
  • Solution: Removed secondary listeners, rely on TerminalManager's accumulator

2. Optimized Polling Intervals

  • Before: 200ms polling interval
  • After: 50ms polling interval (4x faster)
  • Result: 50-100ms response time instead of 8s timeout (160x faster)

Features Added

Verbose Timing Telemetry

All three process tools now support verbose_timing: true parameter:

  • start_process
  • read_process_output
  • interact_with_process

Telemetry Output Includes:

  • Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_exit, timeout)
  • Total duration and time to first output
  • Complete timeline of output events with timestamps
  • Which detection mechanism triggered early exit

Example Output:

📊 Timing Information:
  Exit Reason: early_exit_periodic_check
  Total Duration: 52ms
  Time to First Output: 2ms
  Output Window: 0ms

  Output Events (1 total):
    [1] +2ms | periodic_poll | 18b
       "'Hello, World!'\n> "

Performance Impact

Scenario Before After Improvement
Simple REPL command ~8s timeout ~50-100ms 160x faster
Function definition ~8s timeout ~50-100ms 160x faster
Worst-case latency 200ms 50ms 4x faster

Files Changed

  • src/types.ts - Added TimingInfo and OutputEvent types
  • src/tools/schemas.ts - Added verbose_timing parameter to schemas
  • src/terminal-manager.ts - Added timing collection to executeCommand
  • src/tools/improved-process-tools.ts - Implemented telemetry and fixed polling
  • src/server.ts - Updated tool documentation

Test Plan

  • Build succeeds without errors
  • Verified timing telemetry with Node.js REPL
  • Test with Python REPL
  • Test with bash shell
  • Test with long-running commands

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Optional verbose timing reporting across process tools, showing exit reason, total duration, time-to-first-output, output window, and per-event details.
    • Execution results can include a timingInfo block when verbose timing is requested.
  • Bug Fixes

    • Clearer failure result when a process cannot be started, returning an explicit error status and output.
  • Documentation

    • Expanded tool descriptions and schemas to describe the verbose_timing flag and timing output structure.
  • Tests

    • Added an integration test to verify stable listener behavior across consecutive read operations.

This commit adds comprehensive performance telemetry and fixes a critical bug
where interact_with_process waited 8+ seconds despite output arriving in 2-6ms.

Changes:
- Add TimingInfo and OutputEvent types to track performance metrics
- Add verbose_timing parameter to start_process, read_process_output, and interact_with_process
- Implement detailed timing collection including exit reasons and output event timelines
- Fix interact_with_process bug: removed broken duplicate stream listeners
- Optimize polling: reduced interval from 200ms to 50ms (4x faster response)
- Update tool documentation with performance debugging instructions

Performance improvement: REPL interactions now complete in 50-100ms instead of 8s timeout (160x faster).

Telemetry shows:
- Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_exit, timeout)
- Total duration and time to first output
- Complete timeline of output events with timestamps and sources
- Which detection mechanism triggered early exit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds optional verbose timing telemetry across process tools: new TimingInfo/OutputEvent types and timingInfo on command results; TerminalManager collects and returns timing when requested; tools propagate and format verbose_timing; schemas gain verbose_timing flags; server docs expanded; new test ensures listener stability across consecutive reads.

Changes

Cohort / File(s) Summary
Core types & manager
src/types.ts, src/terminal-manager.ts
Add TimingInfo and OutputEvent types; add optional timingInfo to CommandExecutionResult; extend TerminalManager.executeCommand(..., collectTiming?: boolean) to collect start/end times, per-output events, first/last output times, totalDurationMs and exitReason; return results with timingInfo when requested.
Process tools & formatting
src/tools/improved-process-tools.ts
Propagate verbose_timing through startProcess, readProcessOutput, interactWithProcess; collect/assemble telemetry state; add formatTimingInfo() to format timing blocks and include them in user-facing messages when verbose_timing is enabled.
Schemas
src/tools/schemas.ts
Add optional verbose_timing: z.boolean().optional() to StartProcessArgsSchema, ReadProcessOutputArgsSchema, and InteractWithProcessArgsSchema.
Server docs
src/server.ts
Expand tool descriptions to document the new verbose_timing parameter and the timing-related outputs (exit reason, durations, event timeline). No control-flow changes.
Tests
test-listener-bug.js
New end-to-end script that starts a REPL, performs consecutive interact/read cycles, and validates that TerminalManager listeners and read_process_output behave correctly across multiple reads.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant S as Server/tools
  participant M as TerminalManager
  participant P as OS Process

  rect rgb(250,250,255)
    note over C,S: startProcess / read / interact with verbose_timing=true
    C->>S: startProcess(cmd, verbose_timing=true)
    S->>M: executeCommand(cmd, ..., collectTiming=true)
    activate M
    M->>P: spawn process
    M-->>M: record startTime
    par output capture
      P-->>M: stdout/stderr chunks
      M-->>M: push OutputEvent (timestamp, source, snippet)
      M-->>M: set firstOutputTime if unset
    and periodic checks
      M-->>M: periodic polling, may set exitReason (quick/periodic/timeout)
    end
    P-->>M: process exit
    M-->>M: record endTime, totalDurationMs
    M-->>S: CommandExecutionResult { output, timingInfo }
    deactivate M
    S-->>C: response + formatted timing block
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • serg33v

Poem

A rabbit taps the terminal, tick-tock timers sing,
Streams of stdout fireflies, each with timestamped wing.
We hop through polls and exits, noting every beat,
Listeners stay put, reads stay clean — debugging is sweet. 🐇⏱️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the two main changes by mentioning both the addition of verbose timing telemetry and the performance improvements to terminal commands, matching the core objectives of the PR without extraneous detail.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wonderwhy-er/terminal-process-tools

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fafe30c and e8df882.

📒 Files selected for processing (5)
  • src/server.ts (3 hunks)
  • src/terminal-manager.ts (8 hunks)
  • src/tools/improved-process-tools.ts (13 hunks)
  • src/tools/schemas.ts (2 hunks)
  • src/types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/tools/improved-process-tools.ts (2)
src/terminal-manager.ts (1)
  • terminalManager (325-325)
src/utils/process-detection.ts (1)
  • analyzeProcessState (54-130)
src/terminal-manager.ts (1)
src/types.ts (4)
  • CommandExecutionResult (24-29)
  • TerminalSession (16-22)
  • OutputEvent (42-49)
  • TimingInfo (31-40)

Comment thread src/tools/improved-process-tools.ts
@wonderwhy-er wonderwhy-er changed the title Add verbose timing telemetry and fix 8s timeout bug Add verbose timing telemetry and improvements to terminal commands performance Oct 10, 2025
Fixed critical bug where removeAllListeners('data') was removing
TerminalManager's persistent listeners, breaking all future calls.

Problem:
- read_process_output attached temporary listeners for immediate detection
- Used removeAllListeners('data') to clean up, which removed ALL listeners
- This broke TerminalManager's accumulator, causing future getNewOutput() to return empty
- Second call to read_process_output would always timeout

Solution:
- Store references to our specific detector functions
- Use .off('data', specificFunction) instead of .removeAllListeners('data')
- Only removes our temporary listeners, preserves TerminalManager's listeners

Testing:
- Added test-listener-bug.js that reproduces the issue
- Test verifies multiple sequential read_process_output calls work correctly
- All tests pass after fix

Credit: Fix suggested by code reviewer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
test-listener-bug.js (3)

22-41: Verify server process lifecycle.

The test spawns a server but doesn't verify it started successfully before proceeding with tests. If the server fails to start or crashes during initialization, the test will hang waiting for responses.

Apply this diff to add startup verification:

  const server = spawn('node', [serverPath, '--no-onboarding'], {
    stdio: ['pipe', 'pipe', 'pipe']
  });
+
+  // Verify server started
+  let serverReady = false;
+  server.on('error', (error) => {
+    console.error('❌ Failed to start server:', error);
+    process.exit(1);
+  });

  let responseBuffer = '';
  let requestId = 1;

  server.stdout.on('data', (data) => {
    responseBuffer += data.toString();
+    serverReady = true;
  });

136-137: Race condition: fixed delay may be insufficient.

The 500ms sleep assumes output will arrive within that window, but under load or in slower environments, the command might not complete in time, causing a false failure.

Apply this diff to poll for output availability instead:

-  // Wait a bit for output to arrive
-  await new Promise(resolve => setTimeout(resolve, 500));
+  // Poll for output with timeout
+  const maxWait = 2000;
+  const pollInterval = 100;
+  let waited = 0;
+  while (waited < maxWait) {
+    const checkResult = await callTool('read_process_output', {
+      pid,
+      timeout_ms: 100
+    });
+    if (!checkResult.content[0].text.includes('No new output')) {
+      break;
+    }
+    await new Promise(resolve => setTimeout(resolve, pollInterval));
+    waited += pollInterval;
+  }

169-171: Handle cleanup errors gracefully.

server.kill() can throw if the process already exited. This could mask the actual test failure with an unhandled exception.

Apply this diff to handle cleanup errors:

  } finally {
-    server.kill();
+    try {
+      if (server && !server.killed) {
+        server.kill();
+      }
+    } catch (error) {
+      // Process may have already exited
+    }
  }
src/tools/improved-process-tools.ts (4)

181-193: Extract timing collection into a reusable helper.

Timing collection logic (capturing timestamp, deltaMs, source, length, snippet) is duplicated across initial poll, stdout/stderr detectors, and periodic poll sites. This violates DRY and makes maintenance error-prone.

Apply this diff to create a shared helper:

+function recordOutputEvent(
+  outputEvents: any[],
+  startTime: number,
+  source: string,
+  text: string
+): void {
+  const now = Date.now();
+  outputEvents.push({
+    timestamp: now,
+    deltaMs: now - startTime,
+    source,
+    length: text.length,
+    snippet: text.slice(0, 50).replace(/\n/g, '\\n')
+  });
+}

Then replace each duplication site with:

-        if (verbose_timing) {
-          outputEvents.push({
-            timestamp: now,
-            deltaMs: now - startTime,
-            source: 'initial_poll',
-            length: initialOutput.length,
-            snippet: initialOutput.slice(0, 50).replace(/\n/g, '\\n')
-          });
-        }
+        if (verbose_timing) {
+          recordOutputEvent(outputEvents, startTime, 'initial_poll', initialOutput);
+        }

Apply similar changes to the other three duplication sites at lines 237-245, 299-307, and 503-511.

Also applies to: 237-245, 299-307, 503-511


211-211: Deduplicate quick prompt pattern definitions.

The regex />>>\s*$|>\s*$|\$\s*$|#\s*$/ is defined identically at two locations (readProcessOutput and interactWithProcess). Consider extracting to a module-level constant for maintainability.

Apply this diff to extract the pattern:

+// Quick prompt patterns for immediate detection across tools
+const QUICK_PROMPT_PATTERNS = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
+
 /**
  * Start a new process (renamed from execute_command)

Then replace both occurrences:

-      const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;

With:

+      const quickPromptPatterns = QUICK_PROMPT_PATTERNS;

Also applies to: 476-476


116-124: Optimize output event rendering for large event lists.

When outputEvents contains hundreds of entries from long-running processes, rendering all events in the timing message could produce massive output that overwhelms the user. Consider limiting the number of events displayed or providing summary statistics.

Apply this diff to cap and summarize events:

  if (timing.outputEvents && timing.outputEvents.length > 0) {
-    msg += `\n  Output Events (${timing.outputEvents.length} total):\n`;
-    timing.outputEvents.forEach((event, idx: number) => {
+    const maxEventsToShow = 10;
+    const eventsToShow = timing.outputEvents.slice(0, maxEventsToShow);
+    const remaining = timing.outputEvents.length - maxEventsToShow;
+    
+    msg += `\n  Output Events (showing ${eventsToShow.length} of ${timing.outputEvents.length}):\n`;
+    eventsToShow.forEach((event, idx: number) => {
      msg += `    [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
      if (event.matchedPattern) {
        msg += ` | 🎯 ${event.matchedPattern}`;
      }
      msg += `\n       "${event.snippet}"\n`;
    });
+    
+    if (remaining > 0) {
+      msg += `    ... and ${remaining} more events\n`;
+    }
  }

338-338: Document rationale for 50ms polling interval.

The comment states "Check every 50ms for faster response" but doesn't explain why 50ms was chosen over the previous 200ms, or the trade-offs (CPU usage vs. responsiveness). According to the PR objectives, this change produced ~160× improvement, which is a significant finding worth documenting.

Consider adding a more detailed comment:

-      }, 50); // Check every 50ms for faster response
+      }, 50); // Poll every 50ms (optimized from 200ms) to detect prompts/completion quickly.
+             // This reduces typical response times from ~8s timeouts to ~50-100ms,
+             // while keeping CPU overhead acceptable (~2% per active process).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8df882 and 89ec964.

📒 Files selected for processing (2)
  • src/tools/improved-process-tools.ts (13 hunks)
  • test-listener-bug.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test-listener-bug.js (2)
test/test-read-completed-process.js (3)
  • testReadCompletedProcessOutput (11-42)
  • testImmediateCompletion (47-73)
  • runTests (76-89)
test/test-enhanced-repl.js (1)
  • testEnhancedREPL (29-136)
src/tools/improved-process-tools.ts (2)
src/terminal-manager.ts (1)
  • terminalManager (325-325)
src/utils/process-detection.ts (1)
  • analyzeProcessState (54-130)
🔇 Additional comments (1)
src/tools/improved-process-tools.ts (1)

266-289: LGTM: Listener cleanup fix addresses critical bug.

The code now correctly removes only the specific detectors it registered by keeping named references (stdoutDetector, stderrDetector) and using .off() instead of removeAllListeners(). This preserves TerminalManager's persistent listeners and fixes the race condition that caused 8s timeouts.

Based on past review comments, this implementation matches the recommended fix pattern and should resolve the listener cleanup bug validated by test-listener-bug.js.

Comment on lines +103 to +128
function formatTimingInfo(timing: any): string {
let msg = '\n\n📊 Timing Information:\n';
msg += ` Exit Reason: ${timing.exitReason}\n`;
msg += ` Total Duration: ${timing.totalDurationMs}ms\n`;

if (timing.timeToFirstOutputMs !== undefined) {
msg += ` Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
}

if (timing.firstOutputTime && timing.lastOutputTime) {
msg += ` Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
}

if (timing.outputEvents && timing.outputEvents.length > 0) {
msg += `\n Output Events (${timing.outputEvents.length} total):\n`;
timing.outputEvents.forEach((event: any, idx: number) => {
msg += ` [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
if (event.matchedPattern) {
msg += ` | 🎯 ${event.matchedPattern}`;
}
msg += `\n "${event.snippet}"\n`;
});
}

return msg;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use strict typing instead of any.

The timing parameter and event objects use any, which defeats TypeScript's type safety and makes refactoring error-prone. Based on the types introduced in src/types.ts (TimingInfo, OutputEvent), these should be strongly typed.

Apply this diff to add proper typing:

-function formatTimingInfo(timing: any): string {
+import type { TimingInfo } from '../types.js';
+
+function formatTimingInfo(timing: TimingInfo): string {
  let msg = '\n\n📊 Timing Information:\n';
  msg += `  Exit Reason: ${timing.exitReason}\n`;
  msg += `  Total Duration: ${timing.totalDurationMs}ms\n`;

  if (timing.timeToFirstOutputMs !== undefined) {
    msg += `  Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
  }

  if (timing.firstOutputTime && timing.lastOutputTime) {
    msg += `  Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
  }

  if (timing.outputEvents && timing.outputEvents.length > 0) {
    msg += `\n  Output Events (${timing.outputEvents.length} total):\n`;
-    timing.outputEvents.forEach((event: any, idx: number) => {
+    timing.outputEvents.forEach((event, idx: number) => {
      msg += `    [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
      if (event.matchedPattern) {
        msg += ` | 🎯 ${event.matchedPattern}`;
      }
      msg += `\n       "${event.snippet}"\n`;
    });
  }

  return msg;
}
📝 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
function formatTimingInfo(timing: any): string {
let msg = '\n\n📊 Timing Information:\n';
msg += ` Exit Reason: ${timing.exitReason}\n`;
msg += ` Total Duration: ${timing.totalDurationMs}ms\n`;
if (timing.timeToFirstOutputMs !== undefined) {
msg += ` Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
}
if (timing.firstOutputTime && timing.lastOutputTime) {
msg += ` Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
}
if (timing.outputEvents && timing.outputEvents.length > 0) {
msg += `\n Output Events (${timing.outputEvents.length} total):\n`;
timing.outputEvents.forEach((event: any, idx: number) => {
msg += ` [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
if (event.matchedPattern) {
msg += ` | 🎯 ${event.matchedPattern}`;
}
msg += `\n "${event.snippet}"\n`;
});
}
return msg;
}
// at the top of src/tools/improved-process-tools.ts
import type { TimingInfo } from '../types.js';
function formatTimingInfo(timing: TimingInfo): string {
let msg = '\n\n📊 Timing Information:\n';
msg += ` Exit Reason: ${timing.exitReason}\n`;
msg += ` Total Duration: ${timing.totalDurationMs}ms\n`;
if (timing.timeToFirstOutputMs !== undefined) {
msg += ` Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
}
if (timing.firstOutputTime && timing.lastOutputTime) {
msg += ` Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
}
if (timing.outputEvents && timing.outputEvents.length > 0) {
msg += `\n Output Events (${timing.outputEvents.length} total):\n`;
timing.outputEvents.forEach((event, idx: number) => {
msg += ` [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
if (event.matchedPattern) {
msg += ` | 🎯 ${event.matchedPattern}`;
}
msg += `\n "${event.snippet}"\n`;
});
}
return msg;
}
🤖 Prompt for AI Agents
In src/tools/improved-process-tools.ts around lines 103 to 128, the
formatTimingInfo function and its loop use `any` for the timing parameter and
event objects; replace those with the proper exported types (TimingInfo for the
timing parameter and OutputEvent for each event) from src/types.ts, update the
function signature to `function formatTimingInfo(timing: TimingInfo): string`,
type the forEach callback as `(event: OutputEvent, idx: number)`, and adjust any
property checks to match the typed optional properties (e.g., use === undefined
or !== undefined for optional fields) so the code compiles under strict
TypeScript and leverages compile-time type safety.

Comment on lines +170 to +176
// Timing telemetry
const startTime = Date.now();
let firstOutputTime: number | undefined;
let lastOutputTime: number | undefined;
const outputEvents: any[] = [];
let exitReason: 'early_exit_quick_pattern' | 'early_exit_periodic_check' | 'process_finished' | 'timeout' = 'timeout';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Type exitReason with a union type.

The exitReason variable is manually typed as a string union in two places, but it should reference a shared type definition from src/types.ts to ensure consistency and prevent drift.

If not already defined in src/types.ts, add:

export type ExitReason = 
  | 'early_exit_quick_pattern' 
  | 'early_exit_periodic_check' 
  | 'process_finished' 
  | 'timeout' 
  | 'no_wait';

Then import and use it:

+import type { ExitReason } from '../types.js';
...
-  let exitReason: 'early_exit_quick_pattern' | 'early_exit_periodic_check' | 'process_exit' | 'timeout' = 'timeout';
+  let exitReason: ExitReason = 'timeout';

Apply the same change at line 427 in interactWithProcess.

Also applies to: 422-427

🤖 Prompt for AI Agents
In src/tools/improved-process-tools.ts around lines 170–176, exitReason is
currently declared inline as a string union; replace that inline type with a
shared ExitReason type imported from src/types.ts (add the ExitReason export to
src/types.ts if missing with the five variants 'early_exit_quick_pattern' |
'early_exit_periodic_check' | 'process_finished' | 'timeout' | 'no_wait'),
import ExitReason into improved-process-tools.ts, and type the exitReason
variable as ExitReason; also apply the same change where exitReason (or its
type) is used at lines ~422–427 in the interactWithProcess function so both
declarations reference the centralized ExitReason type.

Comment thread test-listener-bug.js Outdated
Improved test-listener-bug.js to validate output content, not just presence:

Changes:
- Added validation that output contains expected result "4" from "2 + 2"
- Added validation that output contains REPL prompt ">"
- More detailed success messages showing what was validated
- Better error messages if validation fails

This prevents false positives where the test could pass even if:
- Output is corrupt or incomplete
- REPL prompt detection stops working
- Listeners are partially broken

All validations pass, confirming:
✓ Output contains correct computation result
✓ REPL prompt is properly detected
✓ Multiple sequential calls work correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
test-listener-bug.js (2)

103-105: Add defensive null check for regex match.

Line 103 assumes the regex will always match, but if the output format changes, pidMatch[1] will throw. Add a null check and specify radix in parseInt.

Apply this diff:

-   const pidMatch = startResult.content[0].text.match(/PID (\d+)/);
-   const pid = parseInt(pidMatch[1]);
+   const pidMatch = startResult.content[0].text.match(/PID (\d+)/);
+   if (!pidMatch) {
+     throw new Error('Failed to extract PID from start_process output');
+   }
+   const pid = parseInt(pidMatch[1], 10);

136-137: Hardcoded wait may cause flakiness.

The 500ms wait is arbitrary and assumes output arrives within that window. On slow systems or under load, this could cause intermittent failures.

Consider polling for output availability or making the delay configurable via environment variable:

-   // Wait a bit for output to arrive
-   await new Promise(resolve => setTimeout(resolve, 500));
+   // Wait for output to arrive (configurable for slow CI environments)
+   const outputDelay = parseInt(process.env.TEST_OUTPUT_DELAY || '500', 10);
+   await new Promise(resolve => setTimeout(resolve, outputDelay));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89ec964 and 0d26e50.

📒 Files selected for processing (1)
  • test-listener-bug.js (1 hunks)
🔇 Additional comments (2)
test-listener-bug.js (2)

157-169: Output validation addresses previous review concern.

The addition of content validation for "4" and ">" prompt directly addresses the previous review comment. The test now verifies not just presence of output, but correctness of content.

Optional enhancement: Consider using more precise pattern matching to avoid false positives:

   // Validate output contains expected result from "2 + 2"
-  if (!outputText.includes('4')) {
+  if (!/[>\s]4\s/.test(outputText)) {
     console.error('❌ BUG DETECTED: Second read_process_output has corrupt output!');
     console.error(`   Expected result "4" from "2 + 2" but got: ${outputText}`);
     process.exit(1);
   }

183-188: LGTM: Proper error handling and cleanup.

The try-catch-finally structure ensures the server is always killed, preventing resource leaks. Error messages are clear and actionable.

Comment thread test-listener-bug.js
Comment on lines +22 to +29
async function runTest() {
console.log('🧪 Testing listener cleanup bug...\n');

// Start the MCP server
const serverPath = path.join(__dirname, 'dist', 'index.js');
const server = spawn('node', [serverPath, '--no-onboarding'], {
stdio: ['pipe', 'pipe', 'pipe']
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add overall test timeout to prevent hangs.

The test has no maximum execution time. If any step hangs (e.g., server never responds), the test will run indefinitely. This is problematic for CI/CD pipelines.

Add a test timeout at the start of runTest:

 async function runTest() {
   console.log('🧪 Testing listener cleanup bug...\n');
+
+  // Set overall test timeout to prevent hanging
+  const testTimeout = setTimeout(() => {
+    console.error('❌ Test timeout: Exceeded maximum execution time');
+    process.exit(1);
+  }, 30000); // 30 seconds
+
+  // Clear timeout on completion
+  process.on('exit', () => clearTimeout(testTimeout));

   // Start the MCP server
📝 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
async function runTest() {
console.log('🧪 Testing listener cleanup bug...\n');
// Start the MCP server
const serverPath = path.join(__dirname, 'dist', 'index.js');
const server = spawn('node', [serverPath, '--no-onboarding'], {
stdio: ['pipe', 'pipe', 'pipe']
});
async function runTest() {
console.log('🧪 Testing listener cleanup bug...\n');
// Set overall test timeout to prevent hanging
const testTimeout = setTimeout(() => {
console.error('❌ Test timeout: Exceeded maximum execution time');
process.exit(1);
}, 30000); // 30 seconds
// Clear timeout on completion
process.on('exit', () => clearTimeout(testTimeout));
// Start the MCP server
const serverPath = path.join(__dirname, 'dist', 'index.js');
const server = spawn('node', [serverPath, '--no-onboarding'], {
stdio: ['pipe', 'pipe', 'pipe']
});
// …rest of runTest…
}
🤖 Prompt for AI Agents
In test-listener-bug.js around lines 22 to 29, the runTest function lacks a
global timeout so the test can hang forever; add an overall timeout at the start
of runTest that triggers after a reasonable duration (e.g., 2–5 minutes) to
cleanly fail the test: start a timer that on expiry kills the spawned server,
performs any necessary cleanup, logs a timeout error, and exits/throws with a
non-zero status; implement this either by wrapping the main run logic in a
Promise.race with a timeout promise or by creating a setTimeout handle that is
cleared on successful completion.

Comment thread test-listener-bug.js
Comment on lines +52 to +78
function waitForResponse(expectedId, timeout = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkInterval = setInterval(() => {
const lines = responseBuffer.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
if (response.id === expectedId) {
clearInterval(checkInterval);
responseBuffer = responseBuffer.replace(line, '');
resolve(response);
return;
}
} catch (e) {
// Not valid JSON yet, keep waiting
}
}

if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
reject(new Error(`Timeout waiting for response ${expectedId}`));
}
}, 50);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix buffer mutation to avoid removing wrong response.

Line 63 uses string.replace(line, '') which only removes the first occurrence. If duplicate JSON lines exist in the buffer, this could remove the wrong response or leave stale data.

Apply this diff to properly remove the matched line:

  function waitForResponse(expectedId, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      const checkInterval = setInterval(() => {
-       const lines = responseBuffer.split('\n');
-       for (const line of lines) {
+       const lines = responseBuffer.split('\n');
+       for (let i = 0; i < lines.length; i++) {
+         const line = lines[i];
          if (!line.trim()) continue;
          try {
            const response = JSON.parse(line);
            if (response.id === expectedId) {
              clearInterval(checkInterval);
-             responseBuffer = responseBuffer.replace(line, '');
+             // Remove this specific line by reconstructing buffer without it
+             lines.splice(i, 1);
+             responseBuffer = lines.join('\n');
              resolve(response);
              return;
            }
          } catch (e) {
            // Not valid JSON yet, keep waiting
          }
        }

        if (Date.now() - startTime > timeout) {
          clearInterval(checkInterval);
          reject(new Error(`Timeout waiting for response ${expectedId}`));
        }
      }, 50);
    });
  }
📝 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
function waitForResponse(expectedId, timeout = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkInterval = setInterval(() => {
const lines = responseBuffer.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
if (response.id === expectedId) {
clearInterval(checkInterval);
responseBuffer = responseBuffer.replace(line, '');
resolve(response);
return;
}
} catch (e) {
// Not valid JSON yet, keep waiting
}
}
if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
reject(new Error(`Timeout waiting for response ${expectedId}`));
}
}, 50);
});
}
function waitForResponse(expectedId, timeout = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkInterval = setInterval(() => {
const lines = responseBuffer.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
if (response.id === expectedId) {
clearInterval(checkInterval);
// Remove this specific line by reconstructing buffer without it
lines.splice(i, 1);
responseBuffer = lines.join('\n');
resolve(response);
return;
}
} catch (e) {
// Not valid JSON yet, keep waiting
}
}
if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
reject(new Error(`Timeout waiting for response ${expectedId}`));
}
}, 50);
});
}
🤖 Prompt for AI Agents
In test-listener-bug.js around lines 52–78, the code uses
responseBuffer.replace(line, '') to remove the matched JSON line which only
removes the first occurrence and can delete the wrong entry when duplicate lines
exist; replace that mutation by splitting responseBuffer into lines, remove the
exact matched line by index (e.g., find its index and splice it out or filter
out only the single matched element), then rejoin with '\n' to assign back to
responseBuffer so you remove the exact instance and preserve other identical
lines.

@wonderwhy-er wonderwhy-er merged commit 90f1687 into main Oct 13, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant