feat(HAI-061): make scheduler and triage processor read settings dynamically

- Update Scheduler to read settings dynamically from the store instead of using static config
- Make TriageProcessor poll interval dynamic so it responds to settings changes
- Update dashboard.ts wiring to pass dynamic settings through to engine components
- Add scheduler and triage processor tests for dynamic settings behavior
- Remove unused InlineCreateCard and stale test files
This commit is contained in:
Dustin Byrne
2026-03-26 00:04:50 -04:00
parent c2166723ee
commit afcd074caa
5 changed files with 275 additions and 21 deletions

View File

@@ -162,8 +162,6 @@ export async function runDashboard(port: number, opts: { engine?: boolean; open?
const scheduler = new Scheduler(store, {
semaphore,
maxConcurrent: settings.maxConcurrent,
maxWorktrees: settings.maxWorktrees,
onSchedule: (t) => console.log(`[engine] Scheduled ${t.id}`),
onBlocked: (t, deps) => console.log(`[engine] ${t.id} blocked by ${deps.join(", ")}`),
});
@@ -191,27 +189,36 @@ export async function runDashboard(port: number, opts: { engine?: boolean; open?
}
// ── Periodic retry: catch failed merges on each poll cycle ────────
// Uses the same interval as the scheduler so failed merges are
// retried at a predictable cadence without adding extra timers.
const mergeRetryInterval = setInterval(async () => {
try {
const currentSettings = await store.getSettings();
// Refresh the cached limit so the semaphore picks up live changes
cachedMaxConcurrent = currentSettings.maxConcurrent;
if (!currentSettings.autoMerge) return;
const tasks = await store.listTasks();
for (const t of tasks) {
if (t.column === "in-review") {
enqueueMerge(t.id);
// Uses a setTimeout chain so the interval dynamically follows
// settings.pollIntervalMs without requiring an engine restart.
let mergeRetryTimer: ReturnType<typeof setTimeout> | null = null;
async function scheduleMergeRetry(): Promise<void> {
const currentSettings = await store.getSettings().catch(() => settings);
const interval = currentSettings.pollIntervalMs ?? 15_000;
mergeRetryTimer = setTimeout(async () => {
try {
const s = await store.getSettings();
// Refresh the cached limit so the semaphore picks up live changes
cachedMaxConcurrent = s.maxConcurrent;
if (s.autoMerge) {
const tasks = await store.listTasks();
for (const t of tasks) {
if (t.column === "in-review") {
enqueueMerge(t.id);
}
}
}
}
} catch { /* ignore errors in periodic sweep */ }
}, settings.pollIntervalMs ?? 15_000);
} catch { /* ignore errors in periodic sweep */ }
scheduleMergeRetry();
}, interval);
}
// Kick off the first retry after the current poll interval
scheduleMergeRetry();
process.on("SIGINT", () => {
triage.stop();
scheduler.stop();
clearInterval(mergeRetryInterval);
if (mergeRetryTimer) clearTimeout(mergeRetryTimer);
store.stopWatching();
process.exit(0);
});