feat(ai): save partial results on timeout#429
Conversation
aaee6ec to
6987af3
Compare
6987af3 to
b9d710a
Compare
|
@cursoragent please /aidd-review |
|
You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor |
|
@cursoragent please /aidd-review |
|
Now I'll compile my comprehensive code review following the project's review process. 🔬 Code Review: Partial Results on Timeout🎯 RestateThis PR implements saving partial results when AI test runs time out, preserving completed run data and capturing partial output from timed-out runs with a clear 📊 Hotspot Analysis
Recommendation: These files are prime candidates for refactoring to reduce complexity. Consider extracting cohesive sub-modules, especially from ✅ Test CoverageStatus: Excellent
💡 Code Quality AssessmentStrengths1. Clean Separation of Concerns const collectProcessOutput = (proc, partialOutput) =>
new Promise((resolve, reject) => {
let stderr = '';
proc.stdout.on('data', (data) => { partialOutput.stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('close', (code) => { resolve({ stdout: partialOutput.stdout, stderr, code }); });
proc.on('error', reject);
});The shared 2. Proper Error Propagation let result;
try {
result = await withTimeout(
promise,
timeout,
() => ({
...TimeoutError,
message: `Agent process timed out after ${timeout}ms. Command: ${command} ${args.join(' ')}`,
command,
args: args.join(' '),
timeout,
partialStdout: partialOutput.stdout
})
);
} catch (error) {
try { proc.kill(); } catch { /* best-effort */ }
throw error;
}Timeout errors include 3. Robust Aggregation Logic try {
const allRunResults = await runsPromise;
const aggregated = aggregateResults({ assertions, allRunResults, threshold, runs });
const responses = allRunResults.map(({ response }) => response);
return { ...aggregated, responses };
} catch (error) {
const timedOutPartialStdout = getTimedOutPartialStdout();
const hasPartialData = completedRuns.length > 0 || timedOutPartialStdout !== undefined;
if (hasPartialData) {
const responses = completedRuns.map(({ response }) => response);
if (timedOutPartialStdout !== undefined) {
const timeoutMs = error.cause?.timeout ?? timeout;
responses.push(
timedOutPartialStdout +
`\n\n---\n[RITEWAY TIMEOUT] Agent was terminated after ${timeoutMs}ms. Output above is partial.\n---\n`
);
}
const aggregated = completedRuns.length > 0
? aggregateResults({
assertions,
allRunResults: completedRuns,
threshold,
runs: completedRuns.length
})
: { passed: false, assertions: [] };
throw createError({
name: error.cause?.name ?? error.name,
code: error.cause?.code ?? error.code,
message: error.cause?.message ?? error.message,
partialResults: { ...aggregated, responses },
cause: error
});
}
throw error;
}Handles multiple edge cases:
4. Proper Variable Hoisting const testFilename = basename(filePath);
try {The 🪞 Areas for Improvement1. Variable Naming ClarityIn let timedOutPartialStdout;Issue: This captures only the last timed-out run's stdout, not all of them. Name doesn't convey the "last one wins" semantics. Suggestion: Rename to 2. Closure Documentation getTimedOutPartialStdout: () => timedOutPartialStdoutIssue: The closure pattern isn't immediately obvious from the function signature. Suggestion: Add a JSDoc comment: /**
* Returns the most recently captured partial stdout from any timed-out run.
* @returns {string|undefined} Partial output or undefined if no timeouts occurred
*/
getTimedOutPartialStdout: () => timedOutPartialStdout3. Comment Verbosity * TODO (follow-up): console.log calls are mixed with orchestration here. A future
* refactor could return a structured result and let the CLI layer own all output,
* aligning with the saga pattern's IO-separation principle. Not critical for this PR.Issue: While the TODO is helpful, it's overly detailed for what's essentially "TODO: separate IO from orchestration logic" Suggestion: Condense to: * TODO: Separate console.log calls from orchestration (saga pattern IO-separation)4. Magic String FormatThe responses.push(
timedOutPartialStdout +
`\n\n---\n[RITEWAY TIMEOUT] Agent was terminated after ${timeoutMs}ms. Output above is partial.\n---\n`
);Suggestion: Extract to a constant at the module level: const TIMEOUT_MARKER_TEMPLATE = (timeoutMs) =>
`\n\n---\n[RITEWAY TIMEOUT] Agent was terminated after ${timeoutMs}ms. Output above is partial.\n---\n`;🔭 Architectural ObservationsPositive Patterns
Future ConsiderationsThe high complexity score (22) in
Into separate composed functions to reduce complexity and improve testability. 🔒 Security ReviewOWASP Top 10 Check: ✅ No violations detected
Security Note: The ⚖️ Adherence to StandardsJavaScript Best Practices ✅
TDD Best Practices ✅
Documentation ✅
💬 Final RecommendationsMust Address Before MergeNone - code is production-ready. Should Consider (Non-blocking)
Overall AssessmentStatus: ✅ APPROVE This is a high-quality implementation that:
The changes touch hotspot files but are minimally invasive and well-isolated. The timeout handling logic is elegant and maintainable. Great work! The only concerns are minor naming/documentation improvements and the ongoing need to reduce complexity in |
ianwhitedeveloper
left a comment
There was a problem hiding this comment.
✅ APPROVE
Solid PR. Solves a real problem (data loss on timeout), has comprehensive test coverage (4 new tests, 229 total pass), and follows the project's error-causes and functional patterns cleanly.
Key Findings
The completedRuns tracking is more general than the PR title suggests — it captures completed runs for any error type (not just timeouts), which also guards against scenarios like the Cursor CLI ENOENT race condition where a stray process failure discards all results.
One race condition under concurrency > 1: completedRuns is read in the catch block immediately when Promise.all rejects, but other runs may still be in-flight. With concurrency: 4, the ENOENT fires during the concurrent judge burst before any run fully completes, so completedRuns is empty and no partial results are captured. Fix: await Promise.allSettled(runPromises) before reading completedRuns. (Tested locally — all 229 tests pass with this fix.)
Unbounded judge concurrency: Within each run, Promise.all spawns all judge calls simultaneously. With 4 runs × N assertions, this can produce 4×N concurrent agent processes — the direct trigger for the Cursor CLI's cli-config.json rename race. Throttling judge calls with limitConcurrency(judgeTasks, concurrency) reduces the blast radius.
Both enhancements are uncommitted on my local branch and available for a follow-up PR if desired.
Minor non-blocking suggestions
- CI flakiness risk: Integration tests use 2s timeouts with real process spawning. Monitor CI; increase to 5s if flakes appear.
- TODO comment verbosity: The
TODO (follow-up)block inai-command.jscould be condensed to a single line:TODO: Separate console.log calls from orchestration (saga pattern IO-separation). completedRunsordering: Underconcurrency > 1, the responses array order depends on which runs finish first, not run index. Acceptable for diagnostic output but worth noting.
OWASP Top 10 & security check
All clear. No injection vectors (spawn uses array args), no auth changes, no secrets in diff, no SSRF. Path traversal prevented by existing validateFilePath. proc.kill() on timeout is appropriate with best-effort error handling.
Hotspot analysis
All three modified source files are top-10 hotspots (ai-command.js #1 at 15972, ai-runner.js #3 at 7530, execute-agent.js #8 at 3204). Changes are well-scoped and don't meaningfully increase complexity. The long-term recommendation to extract orchestration sub-functions from ai-command.js (complexity 22) still stands as future work.
b9d710a to
28754b8
Compare
When some runs complete before another times out, attach the completed runs' data as partialResults on the error. The orchestrator writes these to disk before re-throwing, so CI artifacts capture whatever completed even on timeout failures.
28754b8 to
2c3eb63
Compare


Summary
[RITEWAY TIMEOUT]marker showing the timeout duration, so you can see exactly how far the agent got before being killedHow it works
execute-agent.js:spawnProcessandcollectProcessOutputshare a singlepartialOutputbuffer for stdout. On timeout, the buffer contents are attached aspartialStdouton theTimeoutError, and the orphaned child process is killed.ai-runner.js:executeRunstracks completed run results as each run settles. If a run times out, its partial stdout is captured.runAITestsbuildspartialResultsfrom all available data: completed runs' full responses, plus the timed-out run's partial output appended with a[RITEWAY TIMEOUT] Agent was terminated after Xms. Output above is partial.marker. This is attached to the re-thrown error.ai-command.js: The catch block checks forpartialResultson caught errors and callsrecordTestOutputbefore re-throwing.testFilenamehoisted out oftryso it's accessible incatch. The original error type is preserved sohandleAIErrorsstill routes correctly.README.md: Documents partial results on timeout behavior.Test plan
recordTestOutputcalled with partial results before re-throwrecordTestOutputnot called when error has no partial results