openairev 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/codex.js +1 -1
- package/src/cli/wait.js +3 -0
- package/src/mcp/mcp-server.js +6 -3
- package/src/review/input-stager.js +102 -0
- package/src/review/review-runner.js +33 -3
package/package.json
CHANGED
package/src/agents/codex.js
CHANGED
package/src/cli/wait.js
CHANGED
package/src/mcp/mcp-server.js
CHANGED
|
@@ -76,13 +76,16 @@ server.tool(
|
|
|
76
76
|
session.status = 'completed';
|
|
77
77
|
saveSession(session, cwd);
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
status: 'completed',
|
|
79
|
+
const progressData = {
|
|
80
|
+
status: review.error ? 'error' : 'completed',
|
|
81
81
|
reviewer: reviewerName,
|
|
82
82
|
progress: review.progress || [],
|
|
83
83
|
verdict: review.verdict,
|
|
84
84
|
executor_feedback: review.executor_feedback,
|
|
85
|
-
}
|
|
85
|
+
};
|
|
86
|
+
if (review.error) progressData.error = review.error;
|
|
87
|
+
if (review.partial_notice) progressData.partial_notice = review.partial_notice;
|
|
88
|
+
writeProgress(progressData);
|
|
86
89
|
activeReview = null;
|
|
87
90
|
activeAbort = null;
|
|
88
91
|
return review;
|
|
@@ -3,6 +3,108 @@ import { join } from 'path';
|
|
|
3
3
|
|
|
4
4
|
const INLINE_THRESHOLD = 8_000; // characters — inline if under this
|
|
5
5
|
|
|
6
|
+
// Rough estimate: ~4 characters per token for code/diffs
|
|
7
|
+
const CHARS_PER_TOKEN = 4;
|
|
8
|
+
|
|
9
|
+
// Reviewer context budgets (in tokens). Leave room for prompt, schema, and output.
|
|
10
|
+
// These are conservative — better to compact than to blow the budget.
|
|
11
|
+
const REVIEWER_BUDGETS = {
|
|
12
|
+
codex: 100_000, // Codex has ~200k context, reserve half for output + tools
|
|
13
|
+
claude_code: 150_000, // Claude has ~200k context, needs less reserve
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BUDGET = 100_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Estimate token count from character length.
|
|
20
|
+
*/
|
|
21
|
+
export function estimateTokens(text) {
|
|
22
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the token budget for a reviewer.
|
|
27
|
+
*/
|
|
28
|
+
export function getReviewerBudget(reviewerName) {
|
|
29
|
+
return REVIEWER_BUDGETS[reviewerName] || DEFAULT_BUDGET;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compact a diff to fit within a token budget.
|
|
34
|
+
* Strategy: parse into per-file hunks, sort by size ascending,
|
|
35
|
+
* drop the largest files first until it fits.
|
|
36
|
+
* Returns { content, compacted, stats }.
|
|
37
|
+
*/
|
|
38
|
+
export function compactDiff(diff, { maxTokens }) {
|
|
39
|
+
const tokens = estimateTokens(diff);
|
|
40
|
+
if (tokens <= maxTokens) {
|
|
41
|
+
return { content: diff, compacted: false, partial: false, stats: { originalTokens: tokens, finalTokens: tokens, filesDropped: 0 } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const files = parseDiffFiles(diff);
|
|
45
|
+
// Sort by size descending — drop largest first
|
|
46
|
+
files.sort((a, b) => b.content.length - a.content.length);
|
|
47
|
+
|
|
48
|
+
// Reserve chars for the omission notice (worst case)
|
|
49
|
+
const noticeOverhead = 500;
|
|
50
|
+
const maxChars = (maxTokens * CHARS_PER_TOKEN) - noticeOverhead;
|
|
51
|
+
let totalChars = diff.length;
|
|
52
|
+
const dropped = [];
|
|
53
|
+
|
|
54
|
+
while (totalChars > maxChars && files.length > 1) {
|
|
55
|
+
const largest = files.shift();
|
|
56
|
+
totalChars -= largest.content.length;
|
|
57
|
+
dropped.push(largest.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If single file still too large, truncate it
|
|
61
|
+
if (files.length === 1 && totalChars > maxChars) {
|
|
62
|
+
const file = files[0];
|
|
63
|
+
const truncateAt = maxChars - 200;
|
|
64
|
+
file.content = file.content.slice(0, Math.max(0, truncateAt)) + `\n\n... [TRUNCATED — file too large for reviewer context] ...\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let compactedDiff = files.map(f => f.content).join('\n');
|
|
68
|
+
if (dropped.length > 0) {
|
|
69
|
+
const notice = `\n\n--- FILES OMITTED (too large for reviewer context) ---\n${dropped.map(f => ` ${f}`).join('\n')}\n--- This is a PARTIAL review. Only the files below were included. ---\n`;
|
|
70
|
+
compactedDiff = notice + compactedDiff;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const finalTokens = estimateTokens(compactedDiff);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: compactedDiff,
|
|
77
|
+
compacted: true,
|
|
78
|
+
partial: dropped.length > 0,
|
|
79
|
+
stats: {
|
|
80
|
+
originalTokens: tokens,
|
|
81
|
+
finalTokens,
|
|
82
|
+
filesDropped: dropped.length,
|
|
83
|
+
droppedFiles: dropped,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse a unified diff into per-file sections.
|
|
90
|
+
*/
|
|
91
|
+
function parseDiffFiles(diff) {
|
|
92
|
+
const files = [];
|
|
93
|
+
// Split on diff headers (diff --git, or --- a/ lines preceded by blank)
|
|
94
|
+
const parts = diff.split(/(?=^diff --git )/m);
|
|
95
|
+
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
if (!part.trim()) continue;
|
|
98
|
+
const nameMatch = part.match(/^diff --git a\/(.+?) b\//m)
|
|
99
|
+
|| part.match(/^---\s+a\/(.+)/m)
|
|
100
|
+
|| part.match(/^\+\+\+\s+b\/(.+)/m);
|
|
101
|
+
const name = nameMatch ? nameMatch[1] : 'unknown';
|
|
102
|
+
files.push({ name, content: part });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
|
|
6
108
|
/**
|
|
7
109
|
* Stage review input. For small content, returns it inline.
|
|
8
110
|
* For large content, writes to .openairev/tmp/ and returns a file reference.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { createAdapter } from '../agents/registry.js';
|
|
4
|
-
import { stageInput, buildInputReference } from './input-stager.js';
|
|
4
|
+
import { stageInput, buildInputReference, compactDiff, getReviewerBudget, estimateTokens } from './input-stager.js';
|
|
5
5
|
import { loadPromptFile } from './prompt-loader.js';
|
|
6
6
|
|
|
7
7
|
export async function runReview(content, {
|
|
@@ -22,7 +22,12 @@ export async function runReview(content, {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const reviewerPrompt = loadPromptFile(promptFile, cwd);
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
// Compact diff if it exceeds the reviewer's context budget
|
|
27
|
+
const budget = getReviewerBudget(reviewerName);
|
|
28
|
+
const { content: compactedContent, compacted, stats } = compactDiff(content, { maxTokens: budget });
|
|
29
|
+
|
|
30
|
+
const staged = stageInput(compactedContent, { cwd });
|
|
26
31
|
const inputRef = buildInputReference(staged);
|
|
27
32
|
|
|
28
33
|
let prompt = reviewerPrompt;
|
|
@@ -51,7 +56,7 @@ export async function runReview(content, {
|
|
|
51
56
|
|
|
52
57
|
logReviewerOutput(rawOutput, reviewerName, cwd);
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
const reviewResult = {
|
|
55
60
|
reviewer: reviewerName,
|
|
56
61
|
verdict,
|
|
57
62
|
executor_feedback: executorFeedback,
|
|
@@ -59,6 +64,31 @@ export async function runReview(content, {
|
|
|
59
64
|
progress: result?.progress || [],
|
|
60
65
|
session_id: adapter.sessionName || adapter.sessionId,
|
|
61
66
|
};
|
|
67
|
+
|
|
68
|
+
if (compacted) {
|
|
69
|
+
reviewResult.context_stats = stats;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Mark partial reviews so callers know files were omitted
|
|
73
|
+
if (compacted && stats.filesDropped > 0 && verdict) {
|
|
74
|
+
reviewResult.partial = true;
|
|
75
|
+
reviewResult.partial_notice = `PARTIAL REVIEW: ${stats.filesDropped} files were omitted to fit the reviewer's context budget (${stats.droppedFiles.join(', ')}). Re-run with a smaller diff to cover them.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Diagnose null verdicts — check for explicit errors before blaming context budget
|
|
79
|
+
if (!verdict) {
|
|
80
|
+
const explicitError = result?.error;
|
|
81
|
+
if (explicitError) {
|
|
82
|
+
reviewResult.error = `Reviewer (${reviewerName}) failed: ${explicitError}`;
|
|
83
|
+
} else {
|
|
84
|
+
reviewResult.error = `Reviewer (${reviewerName}) produced no verdict. ` +
|
|
85
|
+
`Diff was ~${stats.originalTokens} tokens` +
|
|
86
|
+
(compacted ? `, compacted to ~${stats.finalTokens} tokens (${stats.filesDropped} files dropped)` : '') +
|
|
87
|
+
`. Possible causes: context budget exceeded, auth failure, or schema mismatch. Check .openairev/logs/ for details.`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return reviewResult;
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
function logReviewerOutput(rawOutput, reviewerName, cwd) {
|