Make loadConfig async and route through storage layer (S3 or disk)

- loadConfig now uses loadJSON when storage is initialized
- Fix getS3Key to handle config/ and data/ paths (not just data/)
- All loadConfig calls updated to await
- settings.json still bootstraps from disk (needed to know storage type)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 22:47:07 -08:00
parent da95350733
commit ba5cbedcf4
6 changed files with 35 additions and 12 deletions

View File

@@ -45,7 +45,7 @@ async function main() {
const settings = loadConfig(resolve(__dir, 'config/settings.json'));
await initQueue(settings);
const profile = loadConfig(resolve(__dir, 'config/profile.json'));
const profile = await loadConfig(resolve(__dir, 'config/profile.json'));
// Ensure resume is available locally (downloads from S3 if needed)
if (profile.resume_path) {
@@ -53,7 +53,7 @@ async function main() {
}
const answersPath = resolve(__dir, 'config/answers.json');
const answers = existsSync(answersPath) ? loadConfig(answersPath) : [];
const answers = await loadConfig(answersPath).catch(() => []);
const maxApps = settings.max_applications_per_run || Infinity;
const maxRetries = settings.max_retries ?? DEFAULT_MAX_RETRIES;
const enabledTypes = settings.enabled_apply_types || DEFAULT_ENABLED_APPLY_TYPES;
@@ -218,7 +218,7 @@ async function main() {
// Reload answers.json before each job — picks up Telegram replies between jobs
try {
const freshAnswers = existsSync(answersPath) ? loadConfig(answersPath) : [];
const freshAnswers = await loadConfig(answersPath).catch(() => []);
formFiller.answers = freshAnswers;
} catch { /* keep existing answers on read error */ }

View File

@@ -131,7 +131,7 @@ async function collect(state, settings) {
for (const [k, v] of Object.entries(usage)) totalUsage[k] = (totalUsage[k] || 0) + v;
}
const searchConfig = loadConfig(resolve(__dir, 'config/search_config.json'));
const searchConfig = await loadConfig(resolve(__dir, 'config/search_config.json'));
const globalMin = searchConfig.filter_min_score ?? DEFAULT_FILTER_MIN_SCORE;
let passed = 0, filtered = 0, errors = 0;
@@ -306,8 +306,8 @@ async function main() {
const settings = loadConfig(resolve(__dir, 'config/settings.json'));
await initQueue(settings);
const searchConfig = loadConfig(resolve(__dir, 'config/search_config.json'));
const candidateProfile = loadConfig(resolve(__dir, 'config/profile.json'));
const searchConfig = await loadConfig(resolve(__dir, 'config/search_config.json'));
const candidateProfile = await loadConfig(resolve(__dir, 'config/profile.json'));
console.log('🔍 claw-apply: AI Job Filter\n');

View File

@@ -96,10 +96,10 @@ async function main() {
});
// Load config
const searchConfig = loadConfig(resolve(__dir, 'config/search_config.json'));
const searchConfig = await loadConfig(resolve(__dir, 'config/search_config.json'));
// First run detection: if queue is empty, use first_run_days lookback
const profile = loadConfig(resolve(__dir, 'config/profile.json'));
const profile = await loadConfig(resolve(__dir, 'config/profile.json'));
const anthropicKey = process.env.ANTHROPIC_API_KEY || settings.anthropic_api_key;
// Determine lookback: check for an in-progress run first, then fall back to first-run/normal logic

View File

@@ -20,10 +20,23 @@ const LOG_PATH = `${__dir}/../data/applications_log.json`;
const UPDATES_PATH = `${__dir}/../data/queue_updates.jsonl`;
/**
* Load and validate a JSON config file. Throws with a clear message on failure.
* Load and validate a JSON config file.
* Uses the storage layer (S3 or disk) when initialized.
* Falls back to direct disk read for bootstrap (settings.json loaded before initQueue).
*/
export function loadConfig(filePath) {
export async function loadConfig(filePath) {
const resolved = resolve(filePath);
// If storage is initialized, use the storage layer
if (_initialized) {
const data = await loadJSON(resolved, null);
if (data === null) {
throw new Error(`Config file not found: ${resolved}\nCopy the matching .example.json and fill in your values.`);
}
return data;
}
// Bootstrap fallback (settings.json loaded before initQueue)
if (!existsSync(resolved)) {
throw new Error(`Config file not found: ${resolved}\nCopy the matching .example.json and fill in your values.`);
}

View File

@@ -27,7 +27,16 @@ export function storageType() {
}
function getS3Key(filePath) {
return `data/${basename(filePath)}`;
// Extract relative path from project root (e.g. config/foo.json or data/bar.json)
const projectRoot = dirname(dirname(import.meta.url.replace('file://', '')));
const abs = filePath.startsWith('/') ? filePath : join(projectRoot, filePath);
if (abs.startsWith(projectRoot)) {
const rel = abs.slice(projectRoot.length + 1);
return rel;
}
// Fallback: use last two path segments (e.g. data/jobs_queue.json)
const parts = filePath.split('/');
return parts.slice(-2).join('/');
}
async function getS3Client() {
@@ -60,6 +69,7 @@ export async function loadJSON(filePath, defaultValue = []) {
return parsed;
} catch (err) {
if (err.name === 'NoSuchKey') return defaultValue;
if (err.$metadata?.httpStatusCode === 404) return defaultValue;
console.warn(`⚠️ S3 load failed for ${basename(filePath)}: ${err.message}`);
return defaultValue;
}

View File

@@ -63,7 +63,7 @@ export async function processTelegramReplies(settings, answersPath) {
const updates = await getTelegramUpdates(botToken, offset, 1);
if (updates.length === 0) return 0;
// Build lookup: telegram_message_id → job
// Build lookup: telegram_message_id → job (loadQueue returns cached in-memory queue)
const queue = loadQueue();
const jobsByMsgId = new Map();
for (const job of queue) {