Skip to content

Commit b139fe0

Browse files
committed
🤖 fix: register background executors for existing workspaces after restart
- Add ensureExecutorRegistered() called from getOrCreateSession() - Add Config.getWorkspaceMetadataSync() for sync metadata lookup - Make registerExecutor() idempotent (no-op if already registered) Without this fix, background execution tools failed for all pre-existing workspaces after app restart with 'No executor registered' error.
1 parent caf0c3e commit b139fe0

File tree

4 files changed

+60
-7
lines changed

4 files changed

+60
-7
lines changed

‎src/node/config.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,33 @@ export class Config {
360360
return workspaceMetadata;
361361
}
362362

363+
/**
364+
* Get workspace metadata by ID synchronously.
365+
* Used for executor registration when creating sessions for existing workspaces.
366+
* Only returns workspaces that have complete metadata in config (not legacy).
367+
*/
368+
getWorkspaceMetadataSync(workspaceId: string): WorkspaceMetadata | null {
369+
const config = this.loadConfigOrDefault();
370+
371+
for (const [projectPath, projectConfig] of config.projects) {
372+
for (const workspace of projectConfig.workspaces) {
373+
// Only check new format workspaces (have id and name in config)
374+
if (workspace.id === workspaceId && workspace.name) {
375+
return {
376+
id: workspace.id,
377+
name: workspace.name,
378+
projectName: this.getProjectName(projectPath),
379+
projectPath,
380+
createdAt: workspace.createdAt,
381+
runtimeConfig: workspace.runtimeConfig ?? DEFAULT_RUNTIME_CONFIG,
382+
};
383+
}
384+
}
385+
}
386+
387+
return null;
388+
}
389+
363390
/**
364391
* Add a workspace to config.json (single source of truth for workspace metadata).
365392
* Creates project entry if it doesn't exist.

‎src/node/services/backgroundProcessManager.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ export class BackgroundProcessManager {
3333

3434
/**
3535
* Register an executor for a workspace.
36-
* Called when workspace is created - local workspaces get LocalBackgroundExecutor,
37-
* SSH workspaces get SSHBackgroundExecutor.
36+
* Called when workspace is created or session is started for existing workspace.
37+
* Local workspaces get LocalBackgroundExecutor, SSH workspaces get SSHBackgroundExecutor.
38+
* Idempotent - no-op if executor already registered.
3839
*/
3940
registerExecutor(workspaceId: string, executor: BackgroundExecutor): void {
41+
if (this.executors.has(workspaceId)) {
42+
return; // Already registered
43+
}
4044
log.debug(`BackgroundProcessManager.registerExecutor(${workspaceId})`);
4145
this.executors.set(workspaceId, executor);
4246
}
@@ -194,7 +198,7 @@ export class BackgroundProcessManager {
194198
proc.exitTime ??= Date.now();
195199
// Ensure handle is cleaned up even on error
196200
if (proc.handle) {
197-
await proc.handle.dispose().catch(() => {});
201+
await proc.handle.dispose();
198202
}
199203
return { success: true };
200204
}

‎src/node/services/ipcMain.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,25 @@ export class IpcMain {
121121
return new LocalBackgroundExecutor(this.bashExecutionService);
122122
}
123123

124+
/**
125+
* Ensure a background executor is registered for a workspace.
126+
* Called when creating sessions for existing workspaces (after app restart).
127+
* No-op if executor already registered or workspace not found (new workspace being created).
128+
*/
129+
private ensureExecutorRegistered(workspaceId: string): void {
130+
// Look up workspace metadata synchronously from config
131+
const metadata = this.config.getWorkspaceMetadataSync(workspaceId);
132+
if (!metadata) {
133+
// Workspace not in config yet - executor will be registered by creation path
134+
return;
135+
}
136+
137+
const runtime = createRuntime(metadata.runtimeConfig);
138+
const executor = this.createBackgroundExecutor(metadata.runtimeConfig, runtime);
139+
// registerExecutor is idempotent - no-op if already registered
140+
this.backgroundProcessManager.registerExecutor(workspaceId, executor);
141+
}
142+
124143
/**
125144
* Initialize the service. Call this after construction.
126145
* This is separate from the constructor to support async initialization.
@@ -420,6 +439,9 @@ export class IpcMain {
420439
return session;
421440
}
422441

442+
// Ensure executor is registered for existing workspaces (handles app restart case)
443+
this.ensureExecutorRegistered(trimmed);
444+
423445
session = new AgentSession({
424446
workspaceId: trimmed,
425447
config: this.config,

‎src/node/services/localBackgroundExecutor.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ class LocalBackgroundHandle implements BackgroundHandle {
8888
}
8989
}
9090

91-
async isRunning(): Promise<boolean> {
92-
return this.disposable.child.exitCode === null;
91+
isRunning(): Promise<boolean> {
92+
return Promise.resolve(this.disposable.child.exitCode === null);
9393
}
9494

9595
async terminate(): Promise<void> {
@@ -126,8 +126,8 @@ class LocalBackgroundHandle implements BackgroundHandle {
126126
this.terminated = true;
127127
}
128128

129-
async dispose(): Promise<void> {
130-
this.disposable[Symbol.dispose]();
129+
dispose(): Promise<void> {
130+
return Promise.resolve(this.disposable[Symbol.dispose]());
131131
}
132132

133133
/** Get the child process (for spawn event waiting) */

0 commit comments

Comments
 (0)