Files
Duriin-API/workers/coordinatorWorker.js
T
ImBenjiandClaude Opus 5 6f1d1eee2d fix: restart the stalled autonomy pipeline and make calibration honest
Archive ingestion had been dead since 2026-08-02 because nothing in the
compose stack actually ran it. Everything downstream starved from there.

- add ingest + enrichment services. server.js only starts the scheduler when
  DURIIN_RUN_SCHEDULER is not "false", and workers/index.js was not running at
  all, so articles never got event_id/content/has_embedding and the coordinator
  had nothing to lease.
- pass an explicit origin from coordinatorWorker. it was never passed, so
  acceptProposal defaulted to 'live' and 464 historical backfill predictions
  were recorded as live. that also meant verifyEvidence got a null cutoff and
  skipped its date check entirely.
- coarsen cohortKey to event families + horizon buckets. 201 free text event
  types produced 221 cohorts averaging 2.76 samples, so the n>=30 gate could
  never be reached and everything abstained for the wrong reason.
- gate on cohort diversity, not just sample count. one ticker was roughly half
  of all resolved outcomes, so a pure count gate was measuring one company.
  unknown diversity abstains rather than passing.
- resolve the admin archive db explicitly and probe it. it relied on a
  Dockerfile symlink, and without it better-sqlite3 quietly creates an empty
  file and serves a phantom archive.
- clamp implausible future publication dates at ingest.
- pin the db backend to sqlite by default. compose hardcoded postgres "true",
  which would have overridden the operator's own .env on the next redeploy and
  pointed everything at a stale snapshot.

scripts/repair-autonomy-labels.js relabels the affected rows. it is dry run by
default and has not been applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
2026-08-29 21:43:24 +01:00

92 lines
4.7 KiB
JavaScript

const os = require('os');
const fs = require('fs');
const path = require('path');
const { openRuntimeDb } = require('../src/db/runtime');
const { initAutonomySchema } = require('../src/autonomy/schema');
const { leaseNextJob, completeJob, failJob } = require('../src/autonomy/jobs');
const { callCoordinator } = require('../src/autonomy/llm');
const { acceptProposal, recordRejectedProposal } = require('../src/autonomy/coordinator');
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
function loadConfig() {
const configPath = path.resolve(process.env.DURIIN_CONFIG || path.join(__dirname, '..', 'config.json'));
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
require('dotenv').config({ path: path.resolve(path.dirname(configPath), '.env') });
raw.openRouter = { ...(raw.openRouter || {}) };
if (process.env.OPEN_ROUTER_API_KEY) raw.openRouter.apiKey = process.env.OPEN_ROUTER_API_KEY;
if (process.env.OPEN_ROUTER_LLM_MODEL) raw.openRouter.llmModel = process.env.OPEN_ROUTER_LLM_MODEL;
return raw;
}
function buildPrompt(event, articles) {
const evidence = articles.map((article, index) =>
`[Evidence ${index + 1}] article_id=${article.id}\nTitle: ${article.title}\n${String(article.content || article.description || '').slice(0, 4000)}`
).join('\n\n---\n\n');
return `Event title: ${event.title}\n\n${evidence}\n\nReturn JSON only in this shape:\n${JSON.stringify({ predictions: [{
instrument: 'NVDA', direction: 'positive|negative', event_type: 'stable_enum',
causal_channel: 'short description', horizon_days: 10,
evidence_article_ids: [123], invalidation_condition: 'condition',
}] }, null, 2)}\n\nUse only instruments and evidence directly supported by the articles. Return an empty predictions array when there is no clear, tradable hypothesis. Never include probabilities, returns, confidence, position sizes, or actions.`;
}
async function runCoordinatorWorker({ archivePath, intelligencePath, workerId = `coordinator-${os.hostname()}-${process.pid}`, pollMs = 1000 } = {}) {
const archiveDb = openRuntimeDb(archivePath, { schema: 'archive', readonly: true });
const intelligenceDb = openRuntimeDb(intelligencePath, { schema: 'intelligence' });
intelligenceDb.pragma('journal_mode = WAL');
intelligenceDb.pragma('busy_timeout = 5000');
initAutonomySchema(intelligenceDb);
const config = loadConfig();
while (true) {
const job = leaseNextJob(intelligenceDb, workerId, 180, ['coordinator_event']);
if (!job) { await sleep(pollMs); continue; }
try {
const event = archiveDb.prepare('SELECT id, title FROM events WHERE id = ?').get(job.entity_id);
if (!event) throw new Error(`event ${job.entity_id} does not exist`);
const articles = archiveDb.prepare(`
SELECT id, title, description, content, pub_date_effective
FROM articles
WHERE event_id = ? AND content IS NOT NULL AND content != '' AND is_index_page = 0
ORDER BY pub_date_effective ASC, id ASC LIMIT 25
`).all(job.entity_id);
const allowlisted = intelligenceDb.prepare(
"SELECT 1 FROM autonomy_instruments WHERE active=1 AND tradable=1 LIMIT 1"
).get();
if (!allowlisted) throw new Error('no tradable instruments are allowlisted');
const historical = job.lane === 'historical';
const informationCutoff = historical
? (articles.map((article) => article.pub_date_effective).filter(Boolean).sort().pop() || new Date().toISOString())
: new Date().toISOString();
const raw = await callCoordinator(config, buildPrompt(event, articles));
try {
acceptProposal(intelligenceDb, archiveDb, raw, {
eventId: event.id,
informationCutoff,
model: config.openRouter.llmModel || 'unknown',
promptVersion: 'coordinator-1',
strategyVersion: 'autonomy-1',
// only a genuine live lane job may ever feed learning
origin: historical ? 'historical' : 'live',
learningEligible: !historical,
});
} catch (validationError) {
console.error(`[${workerId}] proposal rejected for event ${event.id}:`, validationError.message);
recordRejectedProposal(intelligenceDb, raw, {
eventId: event.id,
informationCutoff,
model: config.openRouter.llmModel || 'unknown',
promptVersion: 'coordinator-1',
origin: historical ? 'historical' : 'live',
learningEligible: !historical,
}, validationError.message);
}
completeJob(intelligenceDb, job.id, workerId);
} catch (error) {
failJob(intelligenceDb, job.id, workerId, error);
}
await sleep(pollMs);
}
}
module.exports = { buildPrompt, runCoordinatorWorker };