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

@@ -61,12 +61,22 @@ export interface SchedulerOptions {
* It respects:
* - Dependency ordering (tasks depending on others wait)
* - Concurrency limits (max N tasks in-progress at once)
*
* **Dynamic settings reload:** On every `schedule()` call the scheduler
* reads `maxConcurrent`, `maxWorktrees`, and `pollIntervalMs` from the
* persisted store settings (`store.getSettings()`). This means changes
* made via the dashboard Settings modal (`PUT /settings`) take effect on
* the very next poll cycle without an engine restart. The poll interval
* itself is also refreshed: if `pollIntervalMs` differs from the active
* timer, the `setInterval` is transparently restarted.
*/
export class Scheduler {
private running = false;
private scheduling = false;
private wasWorktreeLimited = false;
private pollInterval: ReturnType<typeof setInterval> | null = null;
/** The interval (ms) of the currently active `setInterval` timer. */
private activePollMs: number | null = null;
constructor(
private store: TaskStore,
@@ -78,10 +88,11 @@ export class Scheduler {
this.running = true;
const interval = this.options.pollIntervalMs ?? 15_000;
this.activePollMs = interval;
this.pollInterval = setInterval(() => this.schedule(), interval);
this.schedule();
console.log(
`[scheduler] Started (max concurrent: ${this.options.maxConcurrent ?? 2}, max worktrees: ${this.options.maxWorktrees ?? 4})`,
`[scheduler] Started (poll interval: ${interval}ms)`,
);
}
@@ -90,10 +101,27 @@ export class Scheduler {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
this.activePollMs = null;
}
console.log("[scheduler] Stopped");
}
/**
* If `newIntervalMs` differs from the currently active timer, restart
* the `setInterval` so the new cadence takes effect immediately.
*/
private refreshPollInterval(newIntervalMs?: number): void {
if (!this.running || !newIntervalMs) return;
if (newIntervalMs === this.activePollMs) return;
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
this.activePollMs = newIntervalMs;
this.pollInterval = setInterval(() => this.schedule(), newIntervalMs);
console.log(`[scheduler] Poll interval updated to ${newIntervalMs}ms`);
}
/**
* Delegates to the module-level {@link pathsOverlap} for testability.
*/
@@ -119,8 +147,11 @@ export class Scheduler {
try {
const tasks = await this.store.listTasks();
const settings = await this.store.getSettings();
const maxConcurrent = this.options.maxConcurrent ?? 2;
const maxWorktrees = this.options.maxWorktrees ?? 4;
const maxConcurrent = settings.maxConcurrent ?? this.options.maxConcurrent ?? 2;
const maxWorktrees = settings.maxWorktrees ?? this.options.maxWorktrees ?? 4;
// Refresh the poll interval if the persisted setting has changed
this.refreshPollInterval(settings.pollIntervalMs);
// Count all tasks with active worktrees (in-progress or in-review with worktree set)
const activeWorktrees = tasks.filter(