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
92 lines
4.7 KiB
JavaScript
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 };
|