@aabadin/project-memory-context 0.2.0 → 0.2.1
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
CHANGED
|
@@ -29,6 +29,8 @@ let _symbolIndex = {};
|
|
|
29
29
|
let _worklistFile = '';
|
|
30
30
|
let _symbolIndexFile = '';
|
|
31
31
|
let _enrichmentDir = '';
|
|
32
|
+
let _queueStateFile = '';
|
|
33
|
+
let _startedAt = '';
|
|
32
34
|
|
|
33
35
|
async function loadJson(path) {
|
|
34
36
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -320,6 +322,30 @@ export async function runQueueSymbolEnrichment({
|
|
|
320
322
|
throw createQueueEnrichmentError(failure);
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
export function buildQueueState({ status, pid, startedAt, heartbeatAt, finishedAt = null, lastError = null, summary }) {
|
|
326
|
+
return {
|
|
327
|
+
status,
|
|
328
|
+
pid,
|
|
329
|
+
startedAt,
|
|
330
|
+
heartbeatAt,
|
|
331
|
+
finishedAt,
|
|
332
|
+
lastError,
|
|
333
|
+
summary: {
|
|
334
|
+
pending: summary?.pending ?? 0,
|
|
335
|
+
enriched: summary?.enriched ?? 0,
|
|
336
|
+
errors: summary?.errors ?? 0,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function writeQueueState(input) {
|
|
342
|
+
await saveJson(input.queueStateFile, buildQueueState(input));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function finalizeQueueState(input) {
|
|
346
|
+
await writeQueueState(input);
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
async function checkpointSave() {
|
|
324
350
|
if (!_worklistFile) return;
|
|
325
351
|
console.error('\n[checkpoint] Saving progress...');
|
|
@@ -371,6 +397,9 @@ async function main() {
|
|
|
371
397
|
_worklist = worklist;
|
|
372
398
|
_symbolIndex = symbolIndex;
|
|
373
399
|
|
|
400
|
+
const queueStateFile = resolve(enrichmentDir, 'queue-state.json');
|
|
401
|
+
_queueStateFile = queueStateFile;
|
|
402
|
+
|
|
374
403
|
for (const entry of worklist) {
|
|
375
404
|
if (entry.status === 'enriched' && entry.memoryId) {
|
|
376
405
|
entry.status = 'already_enriched';
|
|
@@ -385,10 +414,29 @@ async function main() {
|
|
|
385
414
|
|
|
386
415
|
if (pending.length === 0) {
|
|
387
416
|
const summary = buildQueueSummary(worklist);
|
|
417
|
+
await finalizeQueueState({
|
|
418
|
+
queueStateFile,
|
|
419
|
+
status: 'finished',
|
|
420
|
+
pid: process.pid,
|
|
421
|
+
startedAt: new Date().toISOString(),
|
|
422
|
+
heartbeatAt: new Date().toISOString(),
|
|
423
|
+
finishedAt: new Date().toISOString(),
|
|
424
|
+
summary,
|
|
425
|
+
});
|
|
388
426
|
console.log(JSON.stringify({ complete: true, total, enriched: summary.enriched, errors: summary.errors }));
|
|
389
427
|
return;
|
|
390
428
|
}
|
|
391
429
|
|
|
430
|
+
_startedAt = new Date().toISOString();
|
|
431
|
+
await writeQueueState({
|
|
432
|
+
queueStateFile,
|
|
433
|
+
status: 'running',
|
|
434
|
+
pid: process.pid,
|
|
435
|
+
startedAt: _startedAt,
|
|
436
|
+
heartbeatAt: _startedAt,
|
|
437
|
+
summary: buildQueueSummary(worklist),
|
|
438
|
+
});
|
|
439
|
+
|
|
392
440
|
console.error(`[queue] Starting continuous enrichment: ${pending.length} pending (${staleCount} stale), ${alreadyEnriched} already enriched, ${PMC_CONCURRENCY} parallel slots`);
|
|
393
441
|
console.error(`[queue] Modes: ${enrichmentConfig.preferredModes.join(', ')} | Local: ${enrichmentConfig.localModel.baseUrl} | Model: ${enrichmentConfig.localModel.model} | Timeout: ${TIMEOUT_MS}ms per symbol\n`);
|
|
394
442
|
|
|
@@ -458,7 +506,7 @@ async function main() {
|
|
|
458
506
|
await Promise.all(initPromises);
|
|
459
507
|
|
|
460
508
|
await new Promise((resolve) => {
|
|
461
|
-
const checkInterval = setInterval(() => {
|
|
509
|
+
const checkInterval = setInterval(async () => {
|
|
462
510
|
if (!tracker.hasActiveSlots() && queue.length === 0) {
|
|
463
511
|
clearInterval(checkInterval);
|
|
464
512
|
resolve();
|
|
@@ -468,6 +516,14 @@ async function main() {
|
|
|
468
516
|
const active = tracker.activeCount();
|
|
469
517
|
const done = results.length + errors.length;
|
|
470
518
|
console.error(`[queue status] done=${done} active=${active} queued=${remaining} elapsed=${Math.round(elapsedTotal / 1000)}s`);
|
|
519
|
+
await writeQueueState({
|
|
520
|
+
queueStateFile,
|
|
521
|
+
status: 'running',
|
|
522
|
+
pid: process.pid,
|
|
523
|
+
startedAt: _startedAt,
|
|
524
|
+
heartbeatAt: new Date().toISOString(),
|
|
525
|
+
summary: buildQueueSummary(worklist),
|
|
526
|
+
}).catch(() => {});
|
|
471
527
|
}
|
|
472
528
|
}, REPORT_INTERVAL_MS);
|
|
473
529
|
});
|
|
@@ -504,6 +560,17 @@ async function main() {
|
|
|
504
560
|
const summary = buildQueueSummary(worklist);
|
|
505
561
|
const avgTime = results.length > 0 ? results.reduce((a, r) => a + r.elapsed, 0) / results.length : 0;
|
|
506
562
|
|
|
563
|
+
const finishedAt = new Date().toISOString();
|
|
564
|
+
await finalizeQueueState({
|
|
565
|
+
queueStateFile,
|
|
566
|
+
status: 'finished',
|
|
567
|
+
pid: process.pid,
|
|
568
|
+
startedAt: _startedAt,
|
|
569
|
+
heartbeatAt: finishedAt,
|
|
570
|
+
finishedAt,
|
|
571
|
+
summary,
|
|
572
|
+
});
|
|
573
|
+
|
|
507
574
|
console.log(JSON.stringify({
|
|
508
575
|
complete: summary.pending === 0,
|
|
509
576
|
total: worklist.length,
|
|
@@ -518,7 +585,21 @@ async function main() {
|
|
|
518
585
|
}
|
|
519
586
|
|
|
520
587
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
521
|
-
main().catch(err => {
|
|
588
|
+
main().catch(async (err) => {
|
|
589
|
+
try {
|
|
590
|
+
const summary = Array.isArray(_worklist) ? buildQueueSummary(_worklist) : { pending: 0, enriched: 0, errors: 0 };
|
|
591
|
+
const enrichmentDir = _enrichmentDir || resolve(PROJECT_ROOT, '.planning/project-memory-context/enrichment');
|
|
592
|
+
await finalizeQueueState({
|
|
593
|
+
queueStateFile: resolve(enrichmentDir, 'queue-state.json'),
|
|
594
|
+
status: 'failed',
|
|
595
|
+
pid: process.pid,
|
|
596
|
+
startedAt: _startedAt || new Date().toISOString(),
|
|
597
|
+
heartbeatAt: new Date().toISOString(),
|
|
598
|
+
finishedAt: new Date().toISOString(),
|
|
599
|
+
lastError: err.message,
|
|
600
|
+
summary,
|
|
601
|
+
});
|
|
602
|
+
} catch {}
|
|
522
603
|
console.error('[fatal]', err.message);
|
|
523
604
|
process.exit(1);
|
|
524
605
|
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { appendProviderEvent, withRecordedAttempt } from '../src/enrichment-attempts.mjs';
|
|
8
|
+
import { resolveEnrichmentConfig, PMC_ENRICHMENT_CONFIG_FILE } from '../src/enrichment-config.mjs';
|
|
9
|
+
import { runEnrichmentWithFallback } from '../src/enrichment-driver.mjs';
|
|
10
|
+
import { createLocalModelProvider } from '../src/providers/local-model-provider.mjs';
|
|
11
|
+
import { appendSyncEntry, createSyncEntry } from '../src/sync-manifest.mjs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
async function loadJson(path) {
|
|
17
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function loadOptionalJson(path, fallback = null) {
|
|
21
|
+
try {
|
|
22
|
+
return await loadJson(path);
|
|
23
|
+
} catch {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function saveJson(path, data) {
|
|
29
|
+
await mkdir(dirname(path), { recursive: true });
|
|
30
|
+
await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeKey(key) {
|
|
34
|
+
return key.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function buildSymbolPrompt(symbol, projectRoot, maxCodeLines = 80) {
|
|
38
|
+
const absoluteFile = resolve(projectRoot, symbol.filePath);
|
|
39
|
+
const content = await readFile(absoluteFile, 'utf8');
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
const totalSymbolLines = symbol.range.endLine - symbol.range.startLine + 1;
|
|
42
|
+
let codeSection = lines.slice(symbol.range.startLine - 1, symbol.range.endLine).join('\n');
|
|
43
|
+
let truncated = false;
|
|
44
|
+
if (totalSymbolLines > maxCodeLines) {
|
|
45
|
+
const header = lines.slice(symbol.range.startLine - 1, symbol.range.startLine - 1 + Math.floor(maxCodeLines * 0.6)).join('\n');
|
|
46
|
+
const footer = lines.slice(symbol.range.endLine - Math.floor(maxCodeLines * 0.4), symbol.range.endLine).join('\n');
|
|
47
|
+
codeSection = header + '\n // ... truncated ...\n' + footer;
|
|
48
|
+
truncated = true;
|
|
49
|
+
}
|
|
50
|
+
const importsSection = lines.slice(0, symbol.range.startLine - 1).filter(l => /^\s*import\b/.test(l) || /^\s*using\b/.test(l)).join('\n');
|
|
51
|
+
return `Symbol: ${symbol.name}\nKind: ${symbol.kind}\nLanguage: ${symbol.language}\nLocation: ${symbol.filePath}:${symbol.range.startLine}-${symbol.range.endLine}${truncated ? ` (truncated from ${totalSymbolLines} lines)` : ''}\n\nContext (imports):\n${importsSection || '(none)'}\n\nCode:\n${codeSection}\n\nReturn a compact structured explanation (max 150 words) with:\n- responsibility\n- primary inputs\n- output\n- immediate dependencies\n- role in module`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const args = { concurrency: 4, timeoutMs: 600000, model: null, baseUrl: null, reportFile: null, limit: null };
|
|
56
|
+
for (let i = 2; i < argv.length; i++) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
if (arg === '--concurrency' && argv[i + 1]) { args.concurrency = parseInt(argv[++i], 10); }
|
|
59
|
+
else if (arg === '--timeout' && argv[i + 1]) { args.timeoutMs = parseInt(argv[++i], 10); }
|
|
60
|
+
else if (arg === '--model' && argv[i + 1]) { args.model = argv[++i]; }
|
|
61
|
+
else if (arg === '--base-url' && argv[i + 1]) { args.baseUrl = argv[++i]; }
|
|
62
|
+
else if (arg === '--report' && argv[i + 1]) { args.reportFile = argv[++i]; }
|
|
63
|
+
else if (arg === '--limit' && argv[i + 1]) { args.limit = parseInt(argv[++i], 10); }
|
|
64
|
+
else if (arg === '--help' || arg === '-h') { args.help = true; }
|
|
65
|
+
}
|
|
66
|
+
return args;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printHelp() {
|
|
70
|
+
console.log(`
|
|
71
|
+
PMC Retry Errors — Re-enriches symbols that failed using Ollama directly
|
|
72
|
+
|
|
73
|
+
Usage: node tools/project-memory-context/cli/retry-errors.mjs [options]
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--concurrency N Parallel slots (default: 4)
|
|
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
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const rawArgs = process.argv.slice(2);
|
|
88
|
+
const projectRoot = rawArgs.find(a => !a.startsWith('-')) || process.cwd();
|
|
89
|
+
const args = parseArgs(process.argv);
|
|
90
|
+
if (args.help) { printHelp(); return 0; }
|
|
91
|
+
|
|
92
|
+
const enrichmentDir = resolve(projectRoot, '.planning/project-memory-context/enrichment');
|
|
93
|
+
await mkdir(enrichmentDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
const worklistFile = resolve(enrichmentDir, 'worklist.json');
|
|
96
|
+
const symbolIndexFile = resolve(enrichmentDir, 'symbol-index.json');
|
|
97
|
+
|
|
98
|
+
let worklist;
|
|
99
|
+
try {
|
|
100
|
+
worklist = await loadJson(worklistFile);
|
|
101
|
+
} catch {
|
|
102
|
+
console.error('[retry] No worklist found.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const errorEntries = worklist.filter(e => e.status === 'error');
|
|
107
|
+
if (errorEntries.length === 0) {
|
|
108
|
+
console.log('[retry] No error entries found in worklist. Nothing to retry.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (args.limit && args.limit < errorEntries.length) {
|
|
113
|
+
console.error(`[retry] Limiting to first ${args.limit} of ${errorEntries.length} error entries`);
|
|
114
|
+
errorEntries.splice(args.limit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let symbolIndex = {};
|
|
118
|
+
try { symbolIndex = await loadJson(symbolIndexFile); } catch {}
|
|
119
|
+
|
|
120
|
+
const projectConfig = await loadOptionalJson(resolve(projectRoot, '.opencode', PMC_ENRICHMENT_CONFIG_FILE));
|
|
121
|
+
const globalConfig = await loadOptionalJson(resolve(homedir(), '.config', 'opencode', PMC_ENRICHMENT_CONFIG_FILE));
|
|
122
|
+
let config = resolveEnrichmentConfig({ projectConfig, globalConfig, env: process.env });
|
|
123
|
+
|
|
124
|
+
config.preferredModes = ['local-model'];
|
|
125
|
+
|
|
126
|
+
if (args.model) {
|
|
127
|
+
config.localModel.model = args.model;
|
|
128
|
+
}
|
|
129
|
+
if (args.baseUrl) {
|
|
130
|
+
config.localModel.baseUrl = args.baseUrl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const projectSlug = process.env.PMC_PROJECT_SLUG || basename(projectRoot);
|
|
134
|
+
|
|
135
|
+
const providers = [createLocalModelProvider()];
|
|
136
|
+
|
|
137
|
+
console.error(`\n[retry] ═════════════════════════════════════════════════════`);
|
|
138
|
+
console.error(`[retry] Retrying ${errorEntries.length} error symbols`);
|
|
139
|
+
console.error(`[retry] Provider: ${config.localModel.provider} | Model: ${config.localModel.model}`);
|
|
140
|
+
console.error(`[retry] Base URL: ${config.localModel.baseUrl}`);
|
|
141
|
+
console.error(`[retry] Concurrency: ${args.concurrency} | Timeout: ${args.timeoutMs / 1000}s`);
|
|
142
|
+
console.error(`[retry] ═════════════════════════════════════════════════════\n`);
|
|
143
|
+
|
|
144
|
+
const report = {
|
|
145
|
+
startedAt: new Date().toISOString(),
|
|
146
|
+
config: { model: config.localModel.model, baseUrl: config.localModel.baseUrl, concurrency: args.concurrency, timeoutMs: args.timeoutMs },
|
|
147
|
+
previousErrors: {},
|
|
148
|
+
results: [],
|
|
149
|
+
summary: { succeeded: 0, failed: 0, skipped: 0 },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const entry of errorEntries) {
|
|
153
|
+
const lastLocalAttempt = (entry.attempts ?? []).find(a => a.mode === 'local-model');
|
|
154
|
+
report.previousErrors[entry.symbolKey] = {
|
|
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
|
+
}
|
|
164
|
+
|
|
165
|
+
const queue = [...errorEntries];
|
|
166
|
+
const activeSlots = new Map();
|
|
167
|
+
const results = [];
|
|
168
|
+
let slotIdCounter = 0;
|
|
169
|
+
|
|
170
|
+
async function dispatchSlot() {
|
|
171
|
+
while (queue.length > 0) {
|
|
172
|
+
const entry = queue.shift();
|
|
173
|
+
|
|
174
|
+
entry.status = 'pending';
|
|
175
|
+
delete entry.error;
|
|
176
|
+
delete entry.failedAt;
|
|
177
|
+
|
|
178
|
+
const slotId = slotIdCounter++;
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
|
|
181
|
+
activeSlots.set(slotId, { entry, startTime });
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const prompt = await buildSymbolPrompt(entry, projectRoot);
|
|
185
|
+
const result = await runEnrichmentWithFallback({
|
|
186
|
+
request: { prompt, timeoutMs: args.timeoutMs },
|
|
187
|
+
config,
|
|
188
|
+
providers,
|
|
189
|
+
env: process.env,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const elapsed = Date.now() - startTime;
|
|
193
|
+
activeSlots.delete(slotId);
|
|
194
|
+
|
|
195
|
+
for (const attempt of result.attempts ?? []) {
|
|
196
|
+
await appendProviderEvent(enrichmentDir, { symbolKey: entry.symbolKey, name: entry.name, ...attempt });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const reportEntry = {
|
|
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
|
+
};
|
|
208
|
+
|
|
209
|
+
if (result.status === 'succeeded') {
|
|
210
|
+
const memoryId = `queue-${safeKey(entry.symbolKey)}`;
|
|
211
|
+
const enrichedAt = new Date().toISOString();
|
|
212
|
+
const memoryFile = resolve(enrichmentDir, `${safeKey(entry.symbolKey)}.memory.json`);
|
|
213
|
+
|
|
214
|
+
await saveJson(memoryFile, {
|
|
215
|
+
content: result.content,
|
|
216
|
+
category: 'architecture',
|
|
217
|
+
tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
|
|
221
|
+
Object.assign(entry, {
|
|
222
|
+
status: 'enriched',
|
|
223
|
+
memoryId,
|
|
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
|
+
}
|
|
286
|
+
|
|
287
|
+
results.push(reportEntry);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const elapsed = Date.now() - startTime;
|
|
290
|
+
activeSlots.delete(slotId);
|
|
291
|
+
|
|
292
|
+
entry.status = 'error';
|
|
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: [],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
results.push(reportEntry);
|
|
309
|
+
report.summary.failed++;
|
|
310
|
+
|
|
311
|
+
const done = report.summary.succeeded + report.summary.failed;
|
|
312
|
+
console.error(` [ERR] ${entry.name} — ${err.message} — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await saveJson(worklistFile, worklist);
|
|
316
|
+
await saveJson(symbolIndexFile, symbolIndex);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const slots = [];
|
|
321
|
+
for (let i = 0; i < args.concurrency; i++) {
|
|
322
|
+
slots.push(dispatchSlot());
|
|
323
|
+
}
|
|
324
|
+
await Promise.all(slots);
|
|
325
|
+
|
|
326
|
+
report.results = results;
|
|
327
|
+
report.finishedAt = new Date().toISOString();
|
|
328
|
+
report.totalElapsedMs = results.reduce((a, r) => a + (r.elapsedMs ?? 0), 0);
|
|
329
|
+
|
|
330
|
+
const reportPath = args.reportFile || resolve(enrichmentDir, 'retry-report.json');
|
|
331
|
+
await saveJson(reportPath, report);
|
|
332
|
+
|
|
333
|
+
console.error(`\n[retry] ═════════════════════════════════════════════════════`);
|
|
334
|
+
console.error(`[retry] DONE — ${report.summary.succeeded} succeeded, ${report.summary.failed} failed, ${report.summary.skipped} skipped`);
|
|
335
|
+
console.error(`[retry] Report: ${reportPath}`);
|
|
336
|
+
console.error(`[retry] ═════════════════════════════════════════════════════\n`);
|
|
337
|
+
|
|
338
|
+
console.log(JSON.stringify({
|
|
339
|
+
ok: true,
|
|
340
|
+
command: 'retry-errors',
|
|
341
|
+
total: errorEntries.length,
|
|
342
|
+
succeeded: report.summary.succeeded,
|
|
343
|
+
failed: report.summary.failed,
|
|
344
|
+
reportPath,
|
|
345
|
+
}, null, 2));
|
|
346
|
+
|
|
347
|
+
return report.summary.failed > 0 ? 1 : 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
351
|
+
const exitCode = await main().catch(err => {
|
|
352
|
+
console.error('[retry] FATAL:', err.message);
|
|
353
|
+
return 1;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (exitCode !== 0) {
|
|
357
|
+
process.exit(exitCode);
|
|
358
|
+
}
|
|
359
|
+
}
|
package/package.json
CHANGED
package/src/command-dispatch.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const COMMANDS = new Map([
|
|
|
15
15
|
['new-project', 'cli/new-project.mjs'],
|
|
16
16
|
['project-context', 'cli/project-context.mjs'],
|
|
17
17
|
['query', 'cli/query.mjs'],
|
|
18
|
+
['retry-errors', 'cli/retry-errors.mjs'],
|
|
18
19
|
['sanitize', 'cli/sanitize.mjs'],
|
|
19
20
|
['setup', 'cli/setup.mjs'],
|
|
20
21
|
['status', 'cli/status.mjs'],
|
|
@@ -53,7 +53,14 @@ When the user types `/sanitize`, run:
|
|
|
53
53
|
When starting a session in a project with `.planning/project-memory-context/`:
|
|
54
54
|
|
|
55
55
|
1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
|
|
56
|
-
2. If pending/stale entries exist, launch background enrichment:
|
|
56
|
+
2. If pending/stale entries exist, launch background enrichment:
|
|
57
|
+
```powershell
|
|
58
|
+
if (Test-Path "tools/project-memory-context/cli/enrich-queue.mjs") {
|
|
59
|
+
Start-Process -FilePath "node" -ArgumentList "tools/project-memory-context/cli/enrich-queue.mjs" -WindowStyle Hidden
|
|
60
|
+
} else {
|
|
61
|
+
Start-Process -FilePath "npx" -ArgumentList "--yes","--package","@aabadin/project-memory-context","pmc-enrich","." -WindowStyle Hidden
|
|
62
|
+
}
|
|
63
|
+
```
|
|
57
64
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
58
65
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run `/sync-context` to apply them."
|
|
59
66
|
5. Search agent-memory with `query: "project context overview"` and `tags: ["project-context"]` to fetch base context.
|
|
@@ -53,7 +53,14 @@ When the user types "/sanitize", run:
|
|
|
53
53
|
When starting a session in a project with `.planning/project-memory-context/`:
|
|
54
54
|
|
|
55
55
|
1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
|
|
56
|
-
2. If pending/stale entries exist, launch background enrichment:
|
|
56
|
+
2. If pending/stale entries exist, launch background enrichment:
|
|
57
|
+
```powershell
|
|
58
|
+
if (Test-Path "tools/project-memory-context/cli/enrich-queue.mjs") {
|
|
59
|
+
Start-Process -FilePath "node" -ArgumentList "tools/project-memory-context/cli/enrich-queue.mjs" -WindowStyle Hidden
|
|
60
|
+
} else {
|
|
61
|
+
Start-Process -FilePath "npx" -ArgumentList "--yes","--package","@aabadin/project-memory-context","pmc-enrich","." -WindowStyle Hidden
|
|
62
|
+
}
|
|
63
|
+
```
|
|
57
64
|
3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
|
|
58
65
|
4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run /sync-context to apply them."
|
|
59
66
|
5. Search agent-memory with query "project context overview" and tags ["project-context"] to fetch base context.
|