Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 22 additions & 54 deletions packages/agent-bundle/src/dev/workbench-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,66 +419,34 @@ export const closeDevServerLifecycle = async ({
producer: 'project',
summary: 'Development workbench shutdown started.',
});
const playgroundResults = playground === undefined ? [] : await Promise.allSettled([playground.close()]);
const appResults = mcpApps === undefined ? [] : await Promise.allSettled([mcpApps.close()]);
const clientSurfaceResults = runtimeResources?.clientSurfaces === undefined
? []
: await Promise.allSettled([runtimeResources.clientSurfaces.close()]);
const runtimeResults = runtimeResources?.runtime === undefined
? []
: await Promise.allSettled([runtimeResources.runtime.close()]);
const sessionResults = await Promise.allSettled([mcpSessions.close()]);
const coordinatorResults = await Promise.allSettled([coordinator.close()]);
// Producers close strictly in this order, each awaited before the next;
// the ordering is load-bearing for the sessions the coordinator drains.
const producers: readonly (readonly [DevServerLifecycleCloseFailure['resource'], Closeable | undefined])[] = [
['playground', playground],
['mcp-apps', mcpApps],
['runtime-client-surfaces', runtimeResources?.clientSurfaces],
['runtime', runtimeResources?.runtime],
['mcp-sessions', mcpSessions],
['coordinator', coordinator],
];
const failures: DevServerLifecycleCloseFailure[] = [];
const closeResource = async (resource: DevServerLifecycleCloseFailure['resource'], closeable: Closeable): Promise<void> => {
const [result] = await Promise.allSettled([closeable.close()]);
if (result?.status === 'rejected') failures.push(Object.freeze({ error: result.reason, resource }));
};
for (const [resource, closeable] of producers) {
if (closeable !== undefined) await closeResource(resource, closeable);
}
try { detachProjectLogs?.(); }
catch { /* The subscription is observability-only and cannot hold shutdown. */ }
const producerFailures = [
...playgroundResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'playground' as const })]
: [],
),
...appResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'mcp-apps' as const })]
: [],
),
...clientSurfaceResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'runtime-client-surfaces' as const })]
: [],
),
...runtimeResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'runtime' as const })]
: [],
),
...sessionResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'mcp-sessions' as const })]
: [],
),
...coordinatorResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'coordinator' as const })]
: [],
),
];
logs?.log({
details: { failures: producerFailures.length },
details: { failures: failures.length },
kind: 'dev.shutdown.completed',
level: producerFailures.length === 0 ? 'info' : 'warning',
level: failures.length === 0 ? 'info' : 'warning',
producer: 'project',
summary: producerFailures.length === 0 ? 'Development workbench shutdown completed.' : 'Development workbench shutdown completed with failures.',
summary: failures.length === 0 ? 'Development workbench shutdown completed.' : 'Development workbench shutdown completed with failures.',
});
const logsResults = logs === undefined ? [] : await Promise.allSettled([logs.close()]);
const failures = [
...producerFailures,
...logsResults.flatMap((result): readonly DevServerLifecycleCloseFailure[] =>
result.status === 'rejected'
? [Object.freeze({ error: result.reason, resource: 'logs' as const })]
: [],
),
];
if (logs !== undefined) await closeResource('logs', logs);
if (failures.length > 0) throw new DevServerLifecycleCloseError(failures);
};

Expand Down
21 changes: 11 additions & 10 deletions packages/workbench/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,8 @@ const Workbench = () => {
const [capabilityRetry, setCapabilityRetry] = useState(0);
const [capabilityState, setCapabilityState] = useState<CapabilityState>({ state: 'empty' });
if (foreground.current === undefined) foreground.current = new ForegroundRouteClient();
if (mcpRoutes.current === undefined) mcpRoutes.current = new WorkbenchMcpRouteClient({ foreground: foreground.current });
const foregroundClient = foreground.current;
if (mcpRoutes.current === undefined) mcpRoutes.current = new WorkbenchMcpRouteClient({ foreground: foregroundClient });

const [mcpController, setMcpController] = useState(() => createMcpController(mcpRoutes.current!));
const [mcpModel, setMcpModel] = useState(() => mcpController.model);
Expand Down Expand Up @@ -909,20 +910,20 @@ const Workbench = () => {
setMcpController(replacement);
setMcpModel(replacement.model);
},
foreground: foreground.current!,
foreground: foregroundClient,
});
}
if (mcpAppClient.current === undefined) {
mcpAppClient.current = new McpAppClient({ foreground: foreground.current!, projectClient: client.current });
mcpAppClient.current = new McpAppClient({ foreground: foregroundClient, projectClient: client.current });
}
if (mcpControllerRef.current !== mcpController) mcpControllerRef.current = mcpController;
if (runtimeClient.current === undefined) runtimeClient.current = new RuntimeClient(foreground.current);
if (artifactClient.current === undefined) artifactClient.current = new ArtifactClient({ foreground: foreground.current! });
if (comparisonClient.current === undefined) comparisonClient.current = new ComparisonClient({ foreground: foreground.current! });
if (evalClient.current === undefined) evalClient.current = new EvalClient({ foreground: foreground.current! });
if (hookClient.current === undefined) hookClient.current = new HookClient({ foreground: foreground.current! });
if (logClient.current === undefined) logClient.current = new LogClient({ foreground: foreground.current! });
if (playgroundClient.current === undefined) playgroundClient.current = new PlaygroundClient({ foreground: foreground.current! });
if (runtimeClient.current === undefined) runtimeClient.current = new RuntimeClient(foregroundClient);
if (artifactClient.current === undefined) artifactClient.current = new ArtifactClient({ foreground: foregroundClient });
if (comparisonClient.current === undefined) comparisonClient.current = new ComparisonClient({ foreground: foregroundClient });
if (evalClient.current === undefined) evalClient.current = new EvalClient({ foreground: foregroundClient });
if (hookClient.current === undefined) hookClient.current = new HookClient({ foreground: foregroundClient });
if (logClient.current === undefined) logClient.current = new LogClient({ foreground: foregroundClient });
if (playgroundClient.current === undefined) playgroundClient.current = new PlaygroundClient({ foreground: foregroundClient });

const runtimeAvailable = runtimeCapability === 'available';
const buildId = status === undefined ? undefined : activeEpochId(status);
Expand Down
Loading