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

@@ -184,6 +184,7 @@ export class TriageProcessor {
private activePollMs: number | null = null;
private processing = new Set<string>();
private wasGlobalPaused = false;
private wasEnginePaused = false;
/** Active agent sessions per task, used to terminate on global pause. */
private activeSessions = new Map<string, { dispose: () => void }>();
/** Tasks that were aborted due to global pause (to avoid reporting as errors). */
@@ -228,6 +229,22 @@ export class TriageProcessor {
this.poll();
}
});
/**
* Immediate soft-unpause resume: when `enginePaused` transitions from
* `true` to `false`, trigger a triage poll right away instead of
* waiting for the next poll interval. Same pattern as the globalPause
* unpause handler above.
*
* Note: the agent-kill listener above only fires on `globalPause`
* transitions (hard stop). `enginePaused` (soft pause) lets in-flight
* agents finish gracefully.
*/
store.on("settings:updated", ({ settings, previous }) => {
if (previous.enginePaused && !settings.enginePaused && this.running) {
this.poll();
}
});
}
start(): void {
@@ -286,7 +303,7 @@ export class TriageProcessor {
const settings = await this.store.getSettings();
this.refreshPollInterval(settings.pollIntervalMs);
// Global pause: halt all triage activity
// Global pause (hard stop): halt all triage activity
if (settings.globalPause) {
if (!this.wasGlobalPaused) {
triageLog.log("Global pause active — triage halted");
@@ -296,6 +313,16 @@ export class TriageProcessor {
}
this.wasGlobalPaused = false;
// Engine paused (soft pause): halt new triage work, but let agents finish
if (settings.enginePaused) {
if (!this.wasEnginePaused) {
triageLog.log("Engine paused — triage halted (in-flight agents continue)");
this.wasEnginePaused = true;
}
return;
}
this.wasEnginePaused = false;
const tasks = await this.store.listTasks();
const triageTasks = tasks.filter(
(t) => t.column === "triage" && !this.processing.has(t.id) && !t.paused,