Skip to content

Commit 9ca7c80

Browse files
committed
refactor(workflows): move signal handling logic to dedicated handlers
Consolidate pause/skip signal handling into their respective handler files. Remove duplicate state transition and status update logic from runStepFresh. Add session capture for pause to enable resume functionality.
1 parent d34eb52 commit 9ca7c80

File tree

3 files changed

+55
-18
lines changed

3 files changed

+55
-18
lines changed

src/workflows/signals/handlers/pause.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
* Pause Signal Handler
33
*
44
* Handles workflow:pause process events (user keypress Ctrl+P or 'p').
5-
* Logs, aborts current step, transitions state machine.
5+
* Captures session for resume, updates status, transitions state machine, and aborts.
66
*/
77

88
import { debug } from '../../../shared/logging/logger.js';
9+
import { AgentMonitorService } from '../../../agents/monitoring/index.js';
10+
import { initStepSession } from '../../../shared/workflows/steps.js';
911
import type { SignalContext } from '../manager/types.js';
1012

1113
/**
@@ -20,13 +22,40 @@ export function handlePauseSignal(ctx: SignalContext): void {
2022
return;
2123
}
2224

23-
// Abort step if running
2425
if (ctx.machine.state === 'running') {
26+
// Update UI status
27+
ctx.emitter.updateAgentStatus(stepContext.agentId, 'awaiting');
28+
29+
// Transition state machine
2530
ctx.machine.send({ type: 'PAUSE' });
31+
32+
// Capture session from monitor before aborting so resume can find it
33+
// Agent is registered with base ID (e.g., "bmad-architect" not "bmad-architect-step-2")
34+
const baseAgentId = stepContext.agentId.replace(/-step-\d+$/, '');
35+
const monitor = AgentMonitorService.getInstance();
36+
const agents = monitor.queryAgents({ name: baseAgentId });
37+
38+
if (agents.length > 0) {
39+
const agent = agents.reduce((a, b) => (a.id > b.id ? a : b));
40+
debug('[PauseSignal] Captured agent: id=%d sessionId=%s', agent.id, agent.sessionId);
41+
42+
// Set in machine context so handleWaiting can pass to resume
43+
const machineCtx = ctx.machine.context;
44+
machineCtx.currentMonitoringId = agent.id;
45+
machineCtx.currentOutput = { output: '', monitoringId: agent.id };
46+
47+
// Persist session to step data for resume lookup
48+
if (agent.sessionId) {
49+
initStepSession(ctx.cmRoot, stepContext.stepIndex, agent.sessionId, agent.id)
50+
.catch(err => debug('[PauseSignal] Failed to save session: %s', err.message));
51+
}
52+
}
53+
54+
// Abort the step execution
2655
ctx.getAbortController()?.abort();
2756
}
2857

29-
// Emit mode-change to switch to manual (ignored if already manual)
58+
// Switch to manual mode
3059
(process as NodeJS.EventEmitter).emit('workflow:mode-change', { autonomousMode: false });
3160

3261
debug('[PauseSignal] Pause handled');

src/workflows/signals/handlers/skip.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Skip Signal Handler
33
*
44
* Handles workflow:skip process events (Ctrl+S while agent running).
5-
* Aborts current step execution.
5+
* Updates status, transitions state machine to next step, and aborts.
66
*/
77

88
import { debug } from '../../../shared/logging/logger.js';
@@ -12,6 +12,24 @@ import type { SignalContext } from '../manager/types.js';
1212
* Handle skip signal
1313
*/
1414
export function handleSkipSignal(ctx: SignalContext): void {
15-
debug('[SkipSignal] Skip requested');
16-
ctx.getAbortController()?.abort();
15+
debug('[SkipSignal] workflow:skip received, state=%s', ctx.machine.state);
16+
17+
const stepContext = ctx.getStepContext();
18+
if (!stepContext) {
19+
debug('[SkipSignal] No step context, ignoring skip');
20+
return;
21+
}
22+
23+
if (ctx.machine.state === 'running') {
24+
// Update UI status
25+
ctx.emitter.updateAgentStatus(stepContext.agentId, 'skipped');
26+
27+
// Transition state machine to next step
28+
ctx.machine.send({ type: 'SKIP' });
29+
30+
// Abort the step execution
31+
ctx.getAbortController()?.abort();
32+
33+
debug('[SkipSignal] Skip handled, advancing to next step');
34+
}
1735
}

src/workflows/step/run.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,8 @@ export async function runStepFresh(ctx: RunnerContext): Promise<RunStepResult |
202202
return { output: stepOutput };
203203
} catch (error) {
204204
if (error instanceof Error && error.name === 'AbortError') {
205-
// PAUSE sends PAUSE event (transitions running→awaiting) BEFORE calling abort()
206-
// SKIP only calls abort() without sending any event first
207-
// So if machine is still 'running', this was a skip - send SKIP to advance
208-
// If machine is 'awaiting', this was a pause - preserve that state
209-
if (ctx.machine.state === 'running') {
210-
debug('[step/run] Step aborted via skip, advancing to next step');
211-
ctx.emitter.updateAgentStatus(uniqueAgentId, 'skipped');
212-
ctx.machine.send({ type: 'SKIP' });
213-
} else {
214-
debug('[step/run] Step aborted via pause, entering awaiting state');
215-
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
216-
}
205+
// Signal handlers (pause/skip) handle status updates and state transitions
206+
debug('[step/run] Step aborted');
217207
return null;
218208
}
219209

0 commit comments

Comments
 (0)