feat(KB-157): add soft-pause (enginePaused) alongside hard-stop (globalPause)

- Add enginePaused field to Settings type with soft-pause semantics (drain queue, don't kill agents)
- Gate scheduler, triage, auto-merge, and periodic merge retry on enginePaused
- Add Pause and Stop buttons to dashboard Header replacing single toggle
- Wire enginePaused state through App with optimistic toggle and unpause resume logic
- Add tests for scheduler, triage, dashboard CLI, Header, and App covering pause behavior
This commit is contained in:
Dustin Byrne
2026-03-28 03:50:42 -04:00
parent e1bd4be9be
commit f823161804
14 changed files with 692 additions and 38 deletions

View File

@@ -76,6 +76,7 @@ export class Scheduler {
private scheduling = false;
private wasWorktreeLimited = false;
private wasGlobalPaused = false;
private wasEnginePaused = false;
private pollInterval: ReturnType<typeof setInterval> | null = null;
/** The interval (ms) of the currently active `setInterval` timer. */
private activePollMs: number | null = null;
@@ -98,6 +99,18 @@ export class Scheduler {
this.schedule();
}
});
/**
* Immediate soft-unpause resume: when `enginePaused` transitions from
* `true` to `false`, trigger a scheduling pass right away instead of
* waiting for the next poll interval. Same pattern as the globalPause
* unpause handler above.
*/
this.store.on("settings:updated", ({ settings, previous }) => {
if (previous.enginePaused && !settings.enginePaused && this.running) {
this.schedule();
}
});
}
start(): void {
@@ -197,7 +210,7 @@ export class Scheduler {
// Refresh the poll interval if the persisted setting has changed
this.refreshPollInterval(settings.pollIntervalMs);
// Global pause: halt all scheduling activity
// Global pause (hard stop): halt all scheduling activity
if (settings.globalPause) {
if (!this.wasGlobalPaused) {
schedulerLog.log("Global pause active — scheduling halted");
@@ -207,6 +220,16 @@ export class Scheduler {
}
this.wasGlobalPaused = false;
// Engine paused (soft pause): halt new work dispatch, but let agents finish
if (settings.enginePaused) {
if (!this.wasEnginePaused) {
schedulerLog.log("Engine paused — scheduling halted (in-flight agents continue)");
this.wasEnginePaused = true;
}
return;
}
this.wasEnginePaused = false;
// Count only in-progress tasks toward the worktree limit.
// In-review tasks with worktrees are idle (waiting to merge) and
// should not block new tasks from starting.