fix: unstick the content pipeline, the quota loop and the outcome retries

Three separate things had the pipeline frozen for 27 hours.

browserCrawler leaked page slots. context.newPage() sat outside the try, so a
throw or a hang there took the slot with it, and after maxConcurrentPages of
those every caller parked in acquirePageSlot forever. That is what it looked
like from outside: content workers alive, no logs, no progress, 13 chromium
renderers still up 10 hours after start. newPage is inside the try now, waiting
for a slot times out instead of blocking forever, and page.close() is raced so a
wedged renderer cant strand the slot on the way out either.

graphWorker had no backoff on quota failures. A blown OpenRouter monthly limit
returns an instant 403, so it retried as fast as the network allowed: 2356
failures in 20 minutes, drowning every other line in the log. Quota and auth
errors now pause resolution for 15 minutes and log once per window rather than
once per attempt.

The outcome worker retried unresolvable predictions forever. Yahoo writes class
shares with a dash, so BRK.B 404s every time, and a failed prediction stays open
and comes straight back on the next poll. Dots are translated to dashes, which
matters beyond this one name because the allowlist is full of dotted symbols,
and a prediction that fails five times is marked unresolvable instead of
spinning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
This commit is contained in:
ImBenji
2026-08-31 10:02:04 +01:00
co-authored by Claude Opus 5
parent b8b3987e35
commit 42fb9291b0
4 changed files with 93 additions and 10 deletions
+25 -3
View File
@@ -19,10 +19,18 @@ function httpGet(url) {
});
}
const MAX_OUTCOME_ATTEMPTS = 5;
// Yahoo writes class shares with a dash, BRK.B is BRK-B there. Our allowlist is
// full of dotted symbols, and every one of them 404s forever otherwise.
function yahooSymbol(symbol) {
return String(symbol || '').trim().toUpperCase().replace(/\./g, '-');
}
async function history(symbol) {
// GDELT backfills predate the normal rolling quote window. Use an explicit
// point-in-time range so replay outcomes do not silently become unresolvable.
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?period1=946684800&period2=${Math.floor(Date.now() / 1000)}&interval=1d`;
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(yahooSymbol(symbol))}?period1=946684800&period2=${Math.floor(Date.now() / 1000)}&interval=1d`;
const body = JSON.parse(await httpGet(url));
const result = body?.chart?.result?.[0];
if (!result) return [];
@@ -38,6 +46,7 @@ async function resolveAutonomyOutcomes({ intelligencePath, workerId = `outcome-$
db.pragma('busy_timeout = 5000');
initAutonomySchema(db);
const cache = new Map();
const failures = new Map();
while (true) {
const predictions = db.prepare(`
SELECT p.* FROM autonomy_predictions p
@@ -72,7 +81,20 @@ async function resolveAutonomyOutcomes({ intelligencePath, workerId = `outcome-$
result.excessReturn, result.directionCorrect, result.directionCorrect ? null : 'direction_error');
db.prepare("UPDATE autonomy_predictions SET status = 'resolved' WHERE id = ?").run(prediction.id);
} catch (error) {
console.error(`[autonomy-outcome] ${workerId} prediction ${prediction.id}:`, error.message);
// A prediction that keeps failing stays 'open' and comes straight back on the
// next poll, so a symbol market data will never have just spins forever. Give
// it a few goes for genuinely transient failures, then retire it.
const attempts = (failures.get(prediction.id) || 0) + 1;
failures.set(prediction.id, attempts);
console.error(`[autonomy-outcome] ${workerId} prediction ${prediction.id} (${prediction.instrument})`
+ ` attempt ${attempts}/${MAX_OUTCOME_ATTEMPTS}:`, error.message);
if (attempts >= MAX_OUTCOME_ATTEMPTS) {
db.prepare("UPDATE autonomy_predictions SET status = 'unresolvable' WHERE id = ?").run(prediction.id);
failures.delete(prediction.id);
console.error(`[autonomy-outcome] ${workerId} prediction ${prediction.id} marked unresolvable`
+ ` after ${attempts} failed attempts on ${prediction.instrument}`);
}
cache.delete(prediction.instrument);
}
await sleep(800);
}
@@ -80,4 +102,4 @@ async function resolveAutonomyOutcomes({ intelligencePath, workerId = `outcome-$
}
}
module.exports = { calculateOutcome, resolveAutonomyOutcomes };
module.exports = { calculateOutcome, resolveAutonomyOutcomes, yahooSymbol };