Files
claw-apply/lib/search_progress.mjs
Matthew Jackson c8357bb358 Per-track lookback and per-track profiles
Searcher: each track independently tracks when it was last searched
via data/track_history.json. New tracks get full lookback (90d),
existing tracks look back since their last completion. Keyword-level
crash resume preserved.

Profiles: search tracks can specify profile_overrides (inline) or
profile_path (external file) to customize resume, cover letter, and
experience per track. Filter and applier both use the track-specific
profile. Base profile.json provides shared info (name, contact, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:20:07 -08:00

100 lines
3.2 KiB
JavaScript

/**
* search_progress.mjs — Track which searches have completed
* Enables resume on restart — skips completed tracks and completed keywords within a track
*/
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
let progressPath = null;
let progress = null;
export function initProgress(dataDir, maxLookbackDays) {
progressPath = `${dataDir}/search_progress.json`;
if (existsSync(progressPath)) {
const saved = JSON.parse(readFileSync(progressPath, 'utf8'));
// Resume if an in-progress run exists (has uncompleted tracks)
if (saved.started_at && !saved.finished) {
progress = saved;
const done = progress.completed?.length ?? 0;
if (done > 0) {
console.log(`🔁 Resuming — completed tracks: ${progress.completed.join(', ')}\n`);
}
return progress;
}
}
progress = {
lookback_days: maxLookbackDays,
track_lookback: {}, // per-track lookback days, saved on init for crash resume
started_at: Date.now(),
completed: [],
keyword_progress: {},
};
save();
return progress;
}
/** Save per-track lookback so crash resume uses the right value */
export function saveTrackLookback(trackName, days) {
if (!progress) return;
if (!progress.track_lookback) progress.track_lookback = {};
progress.track_lookback[trackName] = days;
save();
}
/** Save generated keywords for a track — reused on resume, never regenerated mid-run */
export function saveKeywords(platform, track, keywords) {
if (!progress) return;
if (!progress.keywords) progress.keywords = {};
progress.keywords[`${platform}:${track}`] = keywords;
save();
}
/** Get saved keywords for a track, or null if not yet generated */
export function getSavedKeywords(platform, track) {
return progress?.keywords?.[`${platform}:${track}`] ?? null;
}
export function isCompleted(platform, track) {
if (!progress) return false;
return progress.completed.includes(`${platform}:${track}`);
}
/** Returns the index of the first keyword to run (skips already-completed ones) */
export function getKeywordStart(platform, track) {
if (!progress) return 0;
const key = `${platform}:${track}`;
const last = progress.keyword_progress?.[key] ?? -1;
return last + 1; // resume from next keyword after last completed
}
/** Call after each keyword completes */
export function markKeywordComplete(platform, track, keywordIndex) {
if (!progress) return;
const key = `${platform}:${track}`;
if (!progress.keyword_progress) progress.keyword_progress = {};
progress.keyword_progress[key] = keywordIndex;
save();
}
export function markComplete(platform, track, stats) {
if (!progress) return;
const key = `${platform}:${track}`;
if (!progress.completed.includes(key)) progress.completed.push(key);
// Clean up per-keyword progress for completed track
if (progress.keyword_progress) delete progress.keyword_progress[key];
progress[`stats:${key}`] = { ...stats, completed_at: Date.now() };
save();
}
export function clearProgress() {
try { if (progressPath) unlinkSync(progressPath); } catch {}
progress = null;
}
function save() {
if (progressPath && progress) {
writeFileSync(progressPath, JSON.stringify(progress, null, 2));
}
}