Skip to content

Commit 6329419

Browse files
committed
feat(controller): enhance recovery flow with conversation loop
Implement conversation loop for existing controller sessions to handle both user input and continue signals. The loop processes input prompts, executes agent turns, and properly cleans up event listeners. This provides a more robust recovery mechanism compared to the previous simple continue-wait approach.
1 parent dc2c89b commit 6329419

1 file changed

Lines changed: 79 additions & 4 deletions

File tree

src/workflows/controller/view.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export async function runControllerView(
105105
debug('[ControllerView] Recovery: originalControllerView=%s', originalControllerView);
106106

107107
if (originalControllerView) {
108-
// Resume to controller view - wait for user to trigger workflow:controller-continue
108+
// Resume to controller view - set up conversation loop for existing session
109109
await setAutonomousMode(cmRoot, 'never');
110110
emitter.setWorkflowView('controller');
111111
emitter.updateControllerStatus('awaiting');
@@ -114,15 +114,90 @@ export async function runControllerView(
114114
monitoringId: existingConfig.controllerConfig.monitoringId,
115115
});
116116

117-
debug('[ControllerView] Recovery: waiting for workflow:controller-continue');
118-
await new Promise<void>((resolve) => {
119-
const onContinue = () => {
117+
debug('[ControllerView] Recovery: starting conversation loop for existing session');
118+
119+
// Conversation loop for recovery - handles both workflow:input and workflow:controller-continue
120+
await new Promise<void>((resolve, reject) => {
121+
const controllerConfig = {
122+
agentId: existingConfig.controllerConfig!.agentId,
123+
sessionId: existingConfig.controllerConfig!.sessionId,
124+
monitoringId: existingConfig.controllerConfig!.monitoringId,
125+
engine: controllerAgent?.engine,
126+
model: controllerAgent?.modelName,
127+
};
128+
129+
const cleanup = () => {
130+
;(process as NodeJS.EventEmitter).off('workflow:input', onInput);
120131
;(process as NodeJS.EventEmitter).off('workflow:controller-continue', onContinue);
132+
};
133+
134+
const onInput = async (data: { prompt?: string; skip?: boolean }) => {
135+
debug('[ControllerView] Recovery: received input: prompt="%s" skip=%s',
136+
data.prompt?.slice(0, 50) ?? '(empty)', data.skip ?? false);
137+
138+
// Skip signal means user wants to end controller view
139+
if (data.skip) {
140+
debug('[ControllerView] Recovery: skip signal - ending conversation');
141+
cleanup();
142+
resolve();
143+
return;
144+
}
145+
146+
// Empty prompt means user is done
147+
if (!data.prompt || data.prompt.trim() === '') {
148+
debug('[ControllerView] Recovery: empty prompt - ending conversation');
149+
cleanup();
150+
resolve();
151+
return;
152+
}
153+
154+
// Run conversation turn
155+
emitter.updateControllerStatus('running');
156+
emitter.setInputState(null);
157+
158+
try {
159+
// Log input and execute agent
160+
const formatted = formatUserInput(data.prompt);
161+
AgentLoggerService.getInstance().write(controllerConfig.monitoringId, `\n${formatted}\n`);
162+
163+
// Add USER prefix with system username so controller knows input source
164+
const prefixedPrompt = controllerPrefixUser(os.userInfo().username, data.prompt);
165+
166+
await executeAgent(controllerConfig.agentId, prefixedPrompt, {
167+
workingDir: cwd,
168+
resumeSessionId: controllerConfig.sessionId,
169+
resumePrompt: prefixedPrompt,
170+
resumeMonitoringId: controllerConfig.monitoringId,
171+
engine: controllerConfig.engine,
172+
model: controllerConfig.model,
173+
});
174+
175+
// After response, go back to awaiting input
176+
emitter.updateControllerStatus('awaiting');
177+
emitter.setInputState({
178+
active: true,
179+
monitoringId: controllerConfig.monitoringId,
180+
});
181+
debug('[ControllerView] Recovery: turn complete, waiting for next input');
182+
} catch (error) {
183+
debug('[ControllerView] Recovery: error during conversation: %s', (error as Error).message);
184+
cleanup();
185+
reject(error);
186+
}
187+
};
188+
189+
const onContinue = () => {
190+
debug('[ControllerView] Recovery: received controller-continue signal');
191+
cleanup();
121192
resolve();
122193
};
194+
195+
;(process as NodeJS.EventEmitter).on('workflow:input', onInput);
123196
;(process as NodeJS.EventEmitter).on('workflow:controller-continue', onContinue);
124197
});
125198

199+
debug('[ControllerView] Recovery: conversation loop ended, transitioning to executing view');
200+
126201
// User triggered continue - transition to executing
127202
emitter.setInputState(null);
128203
emitter.updateControllerStatus('completed');

0 commit comments

Comments
 (0)