@aabadin/project-memory-context 0.2.3 → 0.2.4
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/cli/enrich-queue.mjs +68 -0
- package/cli/retry-errors.mjs +131 -187
- package/package.json +1 -1
- package/src/retry-errors-runner.mjs +120 -0
- package/templates/opencode/commands/retry-errors.md +7 -14
package/cli/enrich-queue.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
3
4
|
import { resolve, dirname, basename } from 'node:path';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
6
8
|
|
|
7
9
|
import { appendProviderEvent, withRecordedAttempt } from '../src/enrichment-attempts.mjs';
|
|
8
10
|
import { PMC_ENRICHMENT_CONFIG_FILE, resolveEnrichmentConfig } from '../src/enrichment-config.mjs';
|
|
@@ -571,6 +573,19 @@ async function main() {
|
|
|
571
573
|
summary,
|
|
572
574
|
});
|
|
573
575
|
|
|
576
|
+
const retryLaunch = await maybeLaunchRetryErrors({
|
|
577
|
+
projectRoot: PROJECT_ROOT,
|
|
578
|
+
enrichmentDir,
|
|
579
|
+
summary,
|
|
580
|
+
loadRetryState: async () => loadOptionalJson(resolve(enrichmentDir, 'retry-state.json')),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
if (retryLaunch.launched) {
|
|
584
|
+
console.error(`[queue] Auto-launched retry-errors in background -> ${retryLaunch.stdoutPath}`);
|
|
585
|
+
} else if (retryLaunch.reason === 'already-running') {
|
|
586
|
+
console.error('[queue] Retry-errors already running; skipping second launch');
|
|
587
|
+
}
|
|
588
|
+
|
|
574
589
|
console.log(JSON.stringify({
|
|
575
590
|
complete: summary.pending === 0,
|
|
576
591
|
total: worklist.length,
|
|
@@ -584,6 +599,59 @@ async function main() {
|
|
|
584
599
|
}, null, 2));
|
|
585
600
|
}
|
|
586
601
|
|
|
602
|
+
export async function maybeLaunchRetryErrors({
|
|
603
|
+
projectRoot,
|
|
604
|
+
enrichmentDir,
|
|
605
|
+
summary,
|
|
606
|
+
loadRetryState = async () => null,
|
|
607
|
+
spawnRetryProcess = launchRetryProcess,
|
|
608
|
+
}) {
|
|
609
|
+
if ((summary?.errors ?? 0) === 0) {
|
|
610
|
+
return { launched: false, reason: 'no-errors' };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const retryState = await loadRetryState();
|
|
614
|
+
if (retryState?.status === 'running') {
|
|
615
|
+
return { launched: false, reason: 'already-running', retryState };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const scriptPath = resolve(PROJECT_ROOT, 'tools/project-memory-context/cli/retry-errors.mjs');
|
|
619
|
+
const stdoutPath = resolve(enrichmentDir, 'retry-stdout.log');
|
|
620
|
+
const stderrPath = resolve(enrichmentDir, 'retry-stderr.log');
|
|
621
|
+
const startedAt = new Date().toISOString();
|
|
622
|
+
const pid = process.pid;
|
|
623
|
+
|
|
624
|
+
await writeRetryState({
|
|
625
|
+
enrichmentDir,
|
|
626
|
+
status: 'running',
|
|
627
|
+
pid,
|
|
628
|
+
projectRoot,
|
|
629
|
+
startedAt,
|
|
630
|
+
heartbeatAt: startedAt,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
await spawnRetryProcess({ projectRoot, scriptPath, stdoutPath, stderrPath });
|
|
634
|
+
return { launched: true, reason: 'spawned', stdoutPath, stderrPath };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function launchRetryProcess({ projectRoot, scriptPath, stdoutPath, stderrPath }) {
|
|
638
|
+
const stdout = await open(stdoutPath, 'a');
|
|
639
|
+
const stderr = await open(stderrPath, 'a');
|
|
640
|
+
const child = spawn(process.execPath, [scriptPath, projectRoot, '--concurrency', '1', '--timeout', String(TIMEOUT_MS)], {
|
|
641
|
+
detached: true,
|
|
642
|
+
stdio: ['ignore', stdout.fd, stderr.fd],
|
|
643
|
+
cwd: projectRoot,
|
|
644
|
+
});
|
|
645
|
+
child.unref();
|
|
646
|
+
stdout.close();
|
|
647
|
+
stderr.close();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function writeRetryState({ enrichmentDir, status, pid, projectRoot, startedAt, heartbeatAt, finishedAt = null, lastError = null }) {
|
|
651
|
+
const retryStateFile = resolve(enrichmentDir, 'retry-state.json');
|
|
652
|
+
await saveJson(retryStateFile, { status, pid, projectRoot, startedAt, heartbeatAt, finishedAt, lastError });
|
|
653
|
+
}
|
|
654
|
+
|
|
587
655
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
588
656
|
main().catch(async (err) => {
|
|
589
657
|
try {
|
package/cli/retry-errors.mjs
CHANGED
|
@@ -4,11 +4,13 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
|
4
4
|
import { resolve, dirname, basename } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
import { appendProviderEvent
|
|
7
|
+
import { appendProviderEvent } from '../src/enrichment-attempts.mjs';
|
|
8
8
|
import { resolveEnrichmentConfig, PMC_ENRICHMENT_CONFIG_FILE } from '../src/enrichment-config.mjs';
|
|
9
9
|
import { runEnrichmentWithFallback } from '../src/enrichment-driver.mjs';
|
|
10
10
|
import { createLocalModelProvider } from '../src/providers/local-model-provider.mjs';
|
|
11
|
+
import { createCloudApiProvider } from '../src/providers/cloud-api-provider.mjs';
|
|
11
12
|
import { appendSyncEntry, createSyncEntry } from '../src/sync-manifest.mjs';
|
|
13
|
+
import { MAX_RETRY_ITERATIONS, runRetryLoop } from '../src/retry-errors-runner.mjs';
|
|
12
14
|
import { homedir } from 'node:os';
|
|
13
15
|
|
|
14
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -52,11 +54,10 @@ async function buildSymbolPrompt(symbol, projectRoot, maxCodeLines = 80) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function parseArgs(argv) {
|
|
55
|
-
const args = {
|
|
57
|
+
const args = { timeoutMs: 600000, model: null, baseUrl: null, reportFile: null, limit: null };
|
|
56
58
|
for (let i = 2; i < argv.length; i++) {
|
|
57
59
|
const arg = argv[i];
|
|
58
|
-
if (arg === '--
|
|
59
|
-
else if (arg === '--timeout' && argv[i + 1]) { args.timeoutMs = parseInt(argv[++i], 10); }
|
|
60
|
+
if (arg === '--timeout' && argv[i + 1]) { args.timeoutMs = parseInt(argv[++i], 10); }
|
|
60
61
|
else if (arg === '--model' && argv[i + 1]) { args.model = argv[++i]; }
|
|
61
62
|
else if (arg === '--base-url' && argv[i + 1]) { args.baseUrl = argv[++i]; }
|
|
62
63
|
else if (arg === '--report' && argv[i + 1]) { args.reportFile = argv[++i]; }
|
|
@@ -68,18 +69,17 @@ function parseArgs(argv) {
|
|
|
68
69
|
|
|
69
70
|
function printHelp() {
|
|
70
71
|
console.log(`
|
|
71
|
-
PMC Retry Errors — Re-enriches symbols that failed using
|
|
72
|
+
PMC Retry Errors — Re-enriches symbols that failed using full provider fallback chain
|
|
72
73
|
|
|
73
|
-
Usage: node tools/project-memory-context/cli/retry-errors.mjs [options]
|
|
74
|
+
Usage: node tools/project-memory-context/cli/retry-errors.mjs [options] [project-root]
|
|
74
75
|
|
|
75
76
|
Options:
|
|
76
|
-
--
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
--
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
-h, --help Show this help
|
|
77
|
+
--timeout MS Timeout per symbol in ms (default: 600000 = 10min)
|
|
78
|
+
--model NAME Ollama model name (default: from config)
|
|
79
|
+
--base-url URL Ollama base URL (default: http://localhost:11434)
|
|
80
|
+
--limit N Only retry first N error entries (default: all)
|
|
81
|
+
--report PATH Save JSON report to file (default: .planning/.../retry-report.json)
|
|
82
|
+
-h, --help Show this help
|
|
83
83
|
`);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -121,8 +121,6 @@ async function main() {
|
|
|
121
121
|
const globalConfig = await loadOptionalJson(resolve(homedir(), '.config', 'opencode', PMC_ENRICHMENT_CONFIG_FILE));
|
|
122
122
|
let config = resolveEnrichmentConfig({ projectConfig, globalConfig, env: process.env });
|
|
123
123
|
|
|
124
|
-
config.preferredModes = ['local-model'];
|
|
125
|
-
|
|
126
124
|
if (args.model) {
|
|
127
125
|
config.localModel.model = args.model;
|
|
128
126
|
}
|
|
@@ -132,219 +130,165 @@ async function main() {
|
|
|
132
130
|
|
|
133
131
|
const projectSlug = process.env.PMC_PROJECT_SLUG || basename(projectRoot);
|
|
134
132
|
|
|
135
|
-
const providers = [
|
|
133
|
+
const providers = [
|
|
134
|
+
createLocalModelProvider(),
|
|
135
|
+
createCloudApiProvider(),
|
|
136
|
+
];
|
|
136
137
|
|
|
137
138
|
console.error(`\n[retry] ═════════════════════════════════════════════════════`);
|
|
138
|
-
console.error(`[retry] Retrying ${errorEntries.length} error symbols`);
|
|
139
|
-
console.error(`[retry]
|
|
140
|
-
console.error(`[retry]
|
|
141
|
-
console.error(`[retry]
|
|
139
|
+
console.error(`[retry] Retrying ${errorEntries.length} error symbols using full provider chain`);
|
|
140
|
+
console.error(`[retry] Local Model: ${config.localModel.provider} | ${config.localModel.model} @ ${config.localModel.baseUrl}`);
|
|
141
|
+
console.error(`[retry] Cloud API: ${config.cloudApi.provider} @ ${config.cloudApi.baseUrl}`);
|
|
142
|
+
console.error(`[retry] Max iterations: ${MAX_RETRY_ITERATIONS} | Timeout: ${args.timeoutMs / 1000}s`);
|
|
142
143
|
console.error(`[retry] ═════════════════════════════════════════════════════\n`);
|
|
143
144
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
results: [],
|
|
149
|
-
summary: { succeeded: 0, failed: 0, skipped: 0 },
|
|
150
|
-
};
|
|
145
|
+
const retrySymbol = async (candidate, iteration) => {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
const entry = worklist.find(e => e.symbolKey === candidate.symbolKey);
|
|
148
|
+
if (!entry) return { status: 'failed', elapsedMs: 0, attempts: [], failureReason: 'not found in worklist' };
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
name: entry.name,
|
|
156
|
-
filePath: entry.filePath,
|
|
157
|
-
kind: entry.kind,
|
|
158
|
-
previousError: entry.error,
|
|
159
|
-
localModelError: lastLocalAttempt?.errorMessage ?? null,
|
|
160
|
-
localModelErrorType: lastLocalAttempt?.errorType ?? null,
|
|
161
|
-
attemptCount: entry.attempts?.length ?? 0,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
150
|
+
entry.status = 'pending';
|
|
151
|
+
delete entry.error;
|
|
152
|
+
delete entry.failedAt;
|
|
164
153
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
154
|
+
try {
|
|
155
|
+
const prompt = await buildSymbolPrompt(entry, projectRoot);
|
|
156
|
+
const result = await runEnrichmentWithFallback({
|
|
157
|
+
request: { prompt, timeoutMs: args.timeoutMs },
|
|
158
|
+
config,
|
|
159
|
+
providers,
|
|
160
|
+
env: process.env,
|
|
161
|
+
});
|
|
169
162
|
|
|
170
|
-
|
|
171
|
-
while (queue.length > 0) {
|
|
172
|
-
const entry = queue.shift();
|
|
163
|
+
const elapsed = Date.now() - startTime;
|
|
173
164
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
for (const attempt of result.attempts ?? []) {
|
|
166
|
+
await appendProviderEvent(enrichmentDir, { symbolKey: entry.symbolKey, name: entry.name, ...attempt });
|
|
167
|
+
}
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
if (result.status === 'succeeded') {
|
|
170
|
+
const memoryId = `queue-${safeKey(entry.symbolKey)}`;
|
|
171
|
+
const enrichedAt = new Date().toISOString();
|
|
172
|
+
const memoryFile = resolve(enrichmentDir, `${safeKey(entry.symbolKey)}.memory.json`);
|
|
180
173
|
|
|
181
|
-
|
|
174
|
+
await saveJson(memoryFile, {
|
|
175
|
+
content: result.content,
|
|
176
|
+
category: 'architecture',
|
|
177
|
+
tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`],
|
|
178
|
+
});
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
|
|
181
|
+
Object.assign(entry, {
|
|
182
|
+
status: 'enriched',
|
|
183
|
+
memoryId,
|
|
184
|
+
enrichedAt,
|
|
185
|
+
error: undefined,
|
|
186
|
+
failedAt: undefined,
|
|
187
|
+
attempts: newAttempts,
|
|
190
188
|
});
|
|
191
189
|
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
symbolIndex[entry.symbolKey] = {
|
|
191
|
+
memoryId,
|
|
192
|
+
graphNodeId: entry.graphNodeId ?? null,
|
|
193
|
+
codeHash: entry.codeHash,
|
|
194
|
+
status: 'enriched',
|
|
195
|
+
lastEnrichedAt: enrichedAt,
|
|
196
|
+
};
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
await
|
|
198
|
+
try {
|
|
199
|
+
await appendSyncEntry(enrichmentDir, createSyncEntry({
|
|
200
|
+
action: 'upsert',
|
|
201
|
+
keyTag: `key:symbol:${safeKey(entry.symbolKey)}`,
|
|
202
|
+
content: `## ${entry.name}\n\n${result.content}`,
|
|
203
|
+
category: 'architecture',
|
|
204
|
+
tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`, 'enriched-by-retry'],
|
|
205
|
+
source: 'retry-errors',
|
|
206
|
+
symbolKey: entry.symbolKey,
|
|
207
|
+
}));
|
|
208
|
+
} catch (syncErr) {
|
|
209
|
+
console.error(`[retry] WARN: sync append failed for ${entry.name}: ${syncErr.message}`);
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
|
|
200
|
-
symbolKey: entry.symbolKey,
|
|
201
|
-
name: entry.name,
|
|
202
|
-
filePath: entry.filePath,
|
|
203
|
-
kind: entry.kind,
|
|
204
|
-
language: entry.language,
|
|
205
|
-
elapsedMs: elapsed,
|
|
206
|
-
attempts: result.attempts ?? [],
|
|
207
|
-
};
|
|
212
|
+
console.error(` [OK] iter ${iteration} | ${entry.name} (${entry.filePath}:${entry.range.startLine}) — ${Math.round(elapsed / 1000)}s`);
|
|
208
213
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const enrichedAt = new Date().toISOString();
|
|
212
|
-
const memoryFile = resolve(enrichmentDir, `${safeKey(entry.symbolKey)}.memory.json`);
|
|
214
|
+
await saveJson(worklistFile, worklist);
|
|
215
|
+
await saveJson(symbolIndexFile, symbolIndex);
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
enrichedAt,
|
|
225
|
-
error: undefined,
|
|
226
|
-
failedAt: undefined,
|
|
227
|
-
attempts: newAttempts,
|
|
228
|
-
lastModeUsed: 'local-model',
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
symbolIndex[entry.symbolKey] = {
|
|
232
|
-
memoryId,
|
|
233
|
-
graphNodeId: entry.graphNodeId ?? null,
|
|
234
|
-
codeHash: entry.codeHash,
|
|
235
|
-
status: 'enriched',
|
|
236
|
-
lastEnrichedAt: enrichedAt,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
await appendSyncEntry(enrichmentDir, createSyncEntry({
|
|
241
|
-
action: 'upsert',
|
|
242
|
-
keyTag: `key:symbol:${safeKey(entry.symbolKey)}`,
|
|
243
|
-
content: `## ${entry.name}\n\n${result.content}`,
|
|
244
|
-
category: 'architecture',
|
|
245
|
-
tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`, 'enriched-by-retry'],
|
|
246
|
-
source: 'retry-errors',
|
|
247
|
-
symbolKey: entry.symbolKey,
|
|
248
|
-
}));
|
|
249
|
-
} catch (syncErr) {
|
|
250
|
-
console.error(`[retry] WARN: sync append failed for ${entry.name}: ${syncErr.message}`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
reportEntry.status = 'succeeded';
|
|
254
|
-
reportEntry.contentPreview = result.content?.substring(0, 200);
|
|
255
|
-
reportEntry.memoryId = memoryId;
|
|
256
|
-
report.summary.succeeded++;
|
|
257
|
-
|
|
258
|
-
const done = report.summary.succeeded + report.summary.failed;
|
|
259
|
-
console.error(` [OK] ${entry.name} (${entry.filePath}:${entry.range.startLine}) — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
|
|
260
|
-
} else {
|
|
261
|
-
const lastError = result.attempts?.find(a => a.errorMessage)?.errorMessage ?? 'all providers failed';
|
|
262
|
-
|
|
263
|
-
const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
|
|
264
|
-
Object.assign(entry, {
|
|
265
|
-
status: 'error',
|
|
266
|
-
error: lastError,
|
|
267
|
-
failedAt: new Date().toISOString(),
|
|
268
|
-
attempts: newAttempts,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
symbolIndex[entry.symbolKey] = {
|
|
272
|
-
memoryId: null,
|
|
273
|
-
graphNodeId: entry.graphNodeId ?? null,
|
|
274
|
-
codeHash: entry.codeHash,
|
|
275
|
-
status: 'error',
|
|
276
|
-
lastEnrichedAt: new Date().toISOString(),
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
reportEntry.status = 'failed';
|
|
280
|
-
reportEntry.failureReason = lastError;
|
|
281
|
-
report.summary.failed++;
|
|
282
|
-
|
|
283
|
-
const done = report.summary.succeeded + report.summary.failed;
|
|
284
|
-
console.error(` [FAIL] ${entry.name} (${entry.filePath}:${entry.range.startLine}) — ${lastError} — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
|
|
285
|
-
}
|
|
217
|
+
return { status: 'succeeded', elapsedMs: elapsed, attempts: result.attempts ?? [], memoryId, contentPreview: result.content?.substring(0, 200) };
|
|
218
|
+
} else {
|
|
219
|
+
const lastError = result.attempts?.find(a => a.errorMessage)?.errorMessage ?? 'all providers failed';
|
|
220
|
+
const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
|
|
221
|
+
Object.assign(entry, {
|
|
222
|
+
status: 'error',
|
|
223
|
+
error: lastError,
|
|
224
|
+
failedAt: new Date().toISOString(),
|
|
225
|
+
attempts: newAttempts,
|
|
226
|
+
});
|
|
286
227
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
entry.error = err.message;
|
|
294
|
-
entry.failedAt = new Date().toISOString();
|
|
295
|
-
|
|
296
|
-
const reportEntry = {
|
|
297
|
-
symbolKey: entry.symbolKey,
|
|
298
|
-
name: entry.name,
|
|
299
|
-
filePath: entry.filePath,
|
|
300
|
-
kind: entry.kind,
|
|
301
|
-
language: entry.language,
|
|
302
|
-
elapsedMs: elapsed,
|
|
303
|
-
status: 'failed',
|
|
304
|
-
failureReason: err.message,
|
|
305
|
-
attempts: [],
|
|
228
|
+
symbolIndex[entry.symbolKey] = {
|
|
229
|
+
memoryId: null,
|
|
230
|
+
graphNodeId: entry.graphNodeId ?? null,
|
|
231
|
+
codeHash: entry.codeHash,
|
|
232
|
+
status: 'error',
|
|
233
|
+
lastEnrichedAt: new Date().toISOString(),
|
|
306
234
|
};
|
|
307
235
|
|
|
308
|
-
|
|
309
|
-
|
|
236
|
+
console.error(` [FAIL] iter ${iteration} | ${entry.name} — ${lastError} — ${Math.round(elapsed / 1000)}s`);
|
|
237
|
+
|
|
238
|
+
await saveJson(worklistFile, worklist);
|
|
239
|
+
await saveJson(symbolIndexFile, symbolIndex);
|
|
310
240
|
|
|
311
|
-
|
|
312
|
-
console.error(` [ERR] ${entry.name} — ${err.message} — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
|
|
241
|
+
return { status: 'failed', elapsedMs: elapsed, attempts: result.attempts ?? [], failureReason: lastError };
|
|
313
242
|
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const elapsed = Date.now() - startTime;
|
|
245
|
+
entry.status = 'error';
|
|
246
|
+
entry.error = err.message;
|
|
247
|
+
entry.failedAt = new Date().toISOString();
|
|
248
|
+
|
|
249
|
+
console.error(` [ERR] iter ${iteration} | ${entry.name} — ${err.message} — ${Math.round(elapsed / 1000)}s`);
|
|
314
250
|
|
|
315
251
|
await saveJson(worklistFile, worklist);
|
|
316
252
|
await saveJson(symbolIndexFile, symbolIndex);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
253
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
await Promise.all(slots);
|
|
254
|
+
return { status: 'failed', elapsedMs: elapsed, attempts: [], failureReason: err.message };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
325
257
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
258
|
+
const startedAt = new Date().toISOString();
|
|
259
|
+
const result = await runRetryLoop({
|
|
260
|
+
worklist,
|
|
261
|
+
maxIterations: MAX_RETRY_ITERATIONS,
|
|
262
|
+
retrySymbol,
|
|
263
|
+
});
|
|
329
264
|
|
|
330
265
|
const reportPath = args.reportFile || resolve(enrichmentDir, 'retry-report.json');
|
|
331
|
-
await saveJson(reportPath,
|
|
266
|
+
await saveJson(reportPath, {
|
|
267
|
+
startedAt,
|
|
268
|
+
finishedAt: new Date().toISOString(),
|
|
269
|
+
iterations: result.iterations,
|
|
270
|
+
config: { model: config.localModel.model, baseUrl: config.localModel.baseUrl, timeoutMs: args.timeoutMs },
|
|
271
|
+
symbols: result.symbols,
|
|
272
|
+
summary: result.summary,
|
|
273
|
+
});
|
|
332
274
|
|
|
333
275
|
console.error(`\n[retry] ═════════════════════════════════════════════════════`);
|
|
334
|
-
console.error(`[retry] DONE — ${
|
|
276
|
+
console.error(`[retry] DONE — ${result.summary.symbolsRecovered} recovered, ${result.summary.symbolsStillFailing} still failing in ${result.iterations} iterations`);
|
|
335
277
|
console.error(`[retry] Report: ${reportPath}`);
|
|
336
278
|
console.error(`[retry] ═════════════════════════════════════════════════════\n`);
|
|
337
279
|
|
|
338
280
|
console.log(JSON.stringify({
|
|
339
281
|
ok: true,
|
|
340
282
|
command: 'retry-errors',
|
|
341
|
-
total:
|
|
342
|
-
succeeded:
|
|
343
|
-
failed:
|
|
283
|
+
total: result.summary.symbolsRetried,
|
|
284
|
+
succeeded: result.summary.symbolsRecovered,
|
|
285
|
+
failed: result.summary.symbolsStillFailing,
|
|
286
|
+
iterations: result.iterations,
|
|
287
|
+
maxIterationsReached: result.summary.maxIterationsReached,
|
|
344
288
|
reportPath,
|
|
345
289
|
}, null, 2));
|
|
346
290
|
|
|
347
|
-
return
|
|
291
|
+
return result.summary.symbolsStillFailing > 0 ? 1 : 0;
|
|
348
292
|
}
|
|
349
293
|
|
|
350
294
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const MAX_RETRY_ITERATIONS = 5;
|
|
2
|
+
|
|
3
|
+
function collectPreviousErrors(entry) {
|
|
4
|
+
const fromAttempts = (entry.attempts ?? [])
|
|
5
|
+
.filter((attempt) => attempt.status === 'failed' && attempt.errorMessage)
|
|
6
|
+
.map((attempt) => ({
|
|
7
|
+
provider: attempt.provider ?? null,
|
|
8
|
+
errorType: attempt.errorType ?? null,
|
|
9
|
+
message: attempt.errorMessage,
|
|
10
|
+
failedAt: attempt.endedAt ?? attempt.startedAt ?? null,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
if (fromAttempts.length > 0) {
|
|
14
|
+
return fromAttempts;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return entry.error
|
|
18
|
+
? [{ provider: null, errorType: null, message: entry.error, failedAt: entry.failedAt ?? null }]
|
|
19
|
+
: [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function collectRetryCandidates(worklist) {
|
|
23
|
+
const bySymbol = new Map();
|
|
24
|
+
|
|
25
|
+
for (const entry of worklist) {
|
|
26
|
+
if (entry.status !== 'error') continue;
|
|
27
|
+
const existing = bySymbol.get(entry.symbolKey);
|
|
28
|
+
const previousErrors = collectPreviousErrors(entry);
|
|
29
|
+
|
|
30
|
+
if (!existing) {
|
|
31
|
+
bySymbol.set(entry.symbolKey, {
|
|
32
|
+
...entry,
|
|
33
|
+
previousErrors: [...previousErrors],
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
existing.previousErrors.push(...previousErrors);
|
|
39
|
+
if ((entry.attempts?.length ?? 0) >= (existing.attempts?.length ?? 0)) {
|
|
40
|
+
Object.assign(existing, entry, { previousErrors: existing.previousErrors });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...bySymbol.values()];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildRetryState({ status, pid, projectRoot, startedAt, heartbeatAt, finishedAt = null, lastError = null }) {
|
|
48
|
+
return {
|
|
49
|
+
status,
|
|
50
|
+
pid,
|
|
51
|
+
projectRoot,
|
|
52
|
+
startedAt,
|
|
53
|
+
heartbeatAt,
|
|
54
|
+
finishedAt,
|
|
55
|
+
lastError,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runRetryLoop({
|
|
60
|
+
worklist,
|
|
61
|
+
maxIterations = MAX_RETRY_ITERATIONS,
|
|
62
|
+
retrySymbol,
|
|
63
|
+
}) {
|
|
64
|
+
const reportBySymbol = new Map(
|
|
65
|
+
collectRetryCandidates(worklist).map((candidate) => [
|
|
66
|
+
candidate.symbolKey,
|
|
67
|
+
{
|
|
68
|
+
symbolKey: candidate.symbolKey,
|
|
69
|
+
name: candidate.name,
|
|
70
|
+
filePath: candidate.filePath,
|
|
71
|
+
kind: candidate.kind,
|
|
72
|
+
language: candidate.language,
|
|
73
|
+
previousErrors: candidate.previousErrors,
|
|
74
|
+
iterationResults: [],
|
|
75
|
+
finalStatus: 'error',
|
|
76
|
+
memoryId: null,
|
|
77
|
+
contentPreview: null,
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
let iterations = 0;
|
|
83
|
+
|
|
84
|
+
while (iterations < maxIterations) {
|
|
85
|
+
const currentCandidates = collectRetryCandidates(worklist);
|
|
86
|
+
if (currentCandidates.length === 0) break;
|
|
87
|
+
|
|
88
|
+
iterations += 1;
|
|
89
|
+
for (const symbol of currentCandidates) {
|
|
90
|
+
const outcome = await retrySymbol(symbol, iterations);
|
|
91
|
+
const reportEntry = reportBySymbol.get(symbol.symbolKey);
|
|
92
|
+
reportEntry.iterationResults.push({ iteration: iterations, ...outcome });
|
|
93
|
+
reportEntry.finalStatus = outcome.status === 'succeeded' ? 'enriched' : 'error';
|
|
94
|
+
reportEntry.memoryId = outcome.memoryId ?? reportEntry.memoryId;
|
|
95
|
+
reportEntry.contentPreview = outcome.contentPreview ?? reportEntry.contentPreview;
|
|
96
|
+
|
|
97
|
+
if (outcome.status === 'succeeded') {
|
|
98
|
+
const worklistEntry = worklist.find((e) => e.symbolKey === symbol.symbolKey);
|
|
99
|
+
if (worklistEntry) {
|
|
100
|
+
worklistEntry.status = 'enriched';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const symbols = [...reportBySymbol.values()];
|
|
107
|
+
const symbolsRecovered = symbols.filter((item) => item.finalStatus === 'enriched').length;
|
|
108
|
+
const symbolsStillFailing = symbols.length - symbolsRecovered;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
iterations,
|
|
112
|
+
symbols,
|
|
113
|
+
summary: {
|
|
114
|
+
symbolsRetried: symbols.length,
|
|
115
|
+
symbolsRecovered,
|
|
116
|
+
symbolsStillFailing,
|
|
117
|
+
maxIterationsReached: symbolsStillFailing > 0 && iterations === maxIterations,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: retry-errors
|
|
3
|
-
description: Re-enrich symbols
|
|
3
|
+
description: Re-enrich symbols still in error status through PMC's fallback chain, with a per-symbol report.
|
|
4
4
|
argument-hint: "[--limit N] [--model MODEL] [--concurrency N] [--timeout MS]"
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Bash
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<objective>
|
|
10
|
-
Retry enrichment for
|
|
10
|
+
Retry enrichment for symbols currently in error status, deduped by symbol, while preserving a per-symbol report of previous failures and retry outcomes.
|
|
11
11
|
</objective>
|
|
12
12
|
|
|
13
13
|
<execution>
|
|
14
14
|
Run:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
{{PMC_BIN}} retry-errors . --
|
|
17
|
+
{{PMC_BIN}} retry-errors . --concurrency 1 --timeout 300000
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
- `--limit N` — Only retry the first N error entries (useful for testing)
|
|
22
|
-
- `--model MODEL` — Ollama model to use (default: from config)
|
|
23
|
-
- `--concurrency N` — Parallel slots (recommended: 1 for Ollama)
|
|
24
|
-
- `--timeout MS` — Timeout per symbol in ms (default: 600000 = 10min)
|
|
25
|
-
- `--report PATH` — Custom path for the JSON report
|
|
26
|
-
|
|
27
|
-
The command reads the worklist, finds all entries with `status: "error"`, and re-enriches them using only the local-model (Ollama) provider. A JSON report is saved to `.planning/project-memory-context/enrichment/retry-report.json` with:
|
|
20
|
+
The command reads the worklist, retries each unique `symbolKey` through the configured fallback chain (`local-model -> cloud-api -> agent-subagent`), and stops when all symbols recover or 5 iterations complete. A JSON report is saved to `.planning/project-memory-context/enrichment/retry-report.json` with:
|
|
28
21
|
- Previous error details for each symbol (error type, message, provider)
|
|
29
|
-
- Retry result (succeeded/failed, elapsed time, content preview)
|
|
30
|
-
- Overall summary
|
|
31
|
-
</execution>
|
|
22
|
+
- Retry result per iteration (succeeded/failed, elapsed time, content preview)
|
|
23
|
+
- Overall summary: symbols retried, symbols recovered, symbols still failing, max iterations reached
|
|
24
|
+
</execution>
|