fix(extension): diff_review steps=[] race condition — in-memory metadata cache (v0.3.13)

Root cause: Collector deletes pending file before Extension reads edit_step_indices.
Fix: diffReviewMetadata Map caches step indices in Extension memory.
Known issue added. Devlog entry 003.
This commit is contained in:
Variet Worker
2026-03-16 16:09:04 +09:00
parent 12a1cf8692
commit 9ef2c3f07c
9 changed files with 163 additions and 43 deletions

View File

@@ -53,6 +53,9 @@ const sentPendingIds = new Set<string>();
// Map<string, number> = `${conversationId}:${stepIndex}` → creation timestamp
const recentPendingSteps = new Map<string, number>();
const PENDING_MEMORY_TTL_MS = 60_000; // 60 seconds memory retention
// In-memory cache for diff_review metadata (survives pending file deletion by Collector).
// Map<request_id, { edit_step_indices, modified_files }>
const diffReviewMetadata = new Map<string, { edit_step_indices: number[]; modified_files: string[] }>();
// ─── Project Detection ───
@@ -2431,7 +2434,7 @@ function setupMonitor() {
],
modified_files: capturedModFiles,
edit_step_indices: capturedEditSteps,
} as any);
});
}, 8000);
}
wasRunning = isRunning;
@@ -2592,21 +2595,32 @@ async function processResponseFile(filePath: string) {
let diffReviewDone = false;
const targetSession = sessionId || activeSessionId;
let modifiedFiles: string[] = []; // shared between Strategy 1 and 2
// ── Strategy 1: AcknowledgeCascadeCodeEdit RPC ──
// Accept/reject all pending code edits via protocol (no UI interaction needed)
if (sdk) {
try {
// Get tracked step indices from pending data (or use all recent edit steps)
// Get tracked step indices from in-memory cache FIRST (pending file may be deleted by Collector)
const trackedSteps: number[] = [];
const pendingDir = path.join(bridgePath, 'pending');
try {
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
}
} catch { }
const memMeta = diffReviewMetadata.get(resp.request_id);
if (memMeta) {
trackedSteps.push(...memMeta.edit_step_indices);
modifiedFiles = memMeta.modified_files;
diffReviewMetadata.delete(resp.request_id); // cleanup
logToFile(`[DIFF-REVIEW-RPC] loaded from memory: steps=[${trackedSteps.join(',')}] files=${modifiedFiles.length}`);
} else {
// Fallback: try pending file (may already be deleted)
const pendingDir = path.join(bridgePath, 'pending');
try {
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
}
// If no tracked steps, use the step_index from the pending
if (trackedSteps.length === 0 && pendingStepIndex > 0) {
@@ -2636,15 +2650,7 @@ async function processResponseFile(filePath: string) {
await new Promise(r => setTimeout(r, 500));
} catch { }
// Step 2b: Find modified files from pending data
let modifiedFiles: string[] = [];
try {
const pendingFile = path.join(bridgePath, 'pending', `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
// Step 2b: Use modifiedFiles from Strategy 1 (already loaded from memory/file above)
// Step 2c: Open and focus each modified file, then execute
if (modifiedFiles.length > 0) {
@@ -2833,7 +2839,7 @@ function extractToolDescription(stepData: any, sessionTitle: string, stepIndex:
}
/** Write a pending approval file matching Bot's ApprovalRequest dataclass. */
function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number; source?: string; buttons?: Array<{text: string; index: number}> }) {
function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number; source?: string; buttons?: Array<{text: string; index: number}>; modified_files?: string[]; edit_step_indices?: number[] }) {
try {
const pendingDir = path.join(bridgePath, 'pending');
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
@@ -2922,9 +2928,19 @@ function writePendingApproval(data: { conversation_id: string; command: string;
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
...(data.source ? { source: data.source } : {}),
...(buttons ? { buttons } : {}),
...(data.modified_files ? { modified_files: data.modified_files } : {}),
...(data.edit_step_indices && data.edit_step_indices.length > 0 ? { edit_step_indices: data.edit_step_indices } : {}),
};
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
// Cache diff_review metadata in-memory (survives pending file deletion by Collector/Bot)
if (data.step_type === 'diff_review' && (data.edit_step_indices?.length || data.modified_files?.length)) {
diffReviewMetadata.set(id, {
edit_step_indices: data.edit_step_indices || [],
modified_files: data.modified_files || [],
});
logToFile(`[DIFF-REVIEW-CACHE] stored metadata for rid=${id}: steps=[${(data.edit_step_indices || []).join(',')}] files=${(data.modified_files || []).length}`);
}
// Record in memory dedup cache (survives file deletion by Collector/Bot)
if (data.step_index !== undefined && data.conversation_id) {
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);