Skip to content

Commit 8ceaca9

Browse files
authored
fix: Fix logout and project switch to properly kill agents, shells, and processes (#832)
1 parent fb3033f commit 8ceaca9

File tree

5 files changed

+48
-4
lines changed

5 files changed

+48
-4
lines changed

apps/twig/src/main/trpc/routers/agent.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ContentBlock } from "@agentclientprotocol/sdk";
22
import { container } from "../../di/container.js";
33
import { MAIN_TOKENS } from "../../di/tokens.js";
4+
import { logger } from "../../lib/logger.js";
45
import {
56
AgentServiceEvent,
67
cancelPermissionInput,
@@ -22,8 +23,13 @@ import {
2223
tokenUpdateInput,
2324
} from "../../services/agent/schemas.js";
2425
import type { AgentService } from "../../services/agent/service.js";
26+
import type { ProcessTrackingService } from "../../services/process-tracking/service.js";
27+
import type { ShellService } from "../../services/shell/service.js";
28+
import type { SleepService } from "../../services/sleep/service.js";
2529
import { publicProcedure, router } from "../trpc.js";
2630

31+
const log = logger.scope("agent-router");
32+
2733
const getService = () => container.get<AgentService>(MAIN_TOKENS.AgentService);
2834

2935
export const agentRouter = router({
@@ -140,6 +146,30 @@ export const agentRouter = router({
140146
getService().markAllSessionsForRecreation(),
141147
),
142148

149+
resetAll: publicProcedure.mutation(async () => {
150+
log.info("Resetting all sessions (logout/project switch)");
151+
152+
// Clean up all agent sessions (flushes logs, stops agents, releases sleep blockers)
153+
const agentService = getService();
154+
await agentService.cleanupAll();
155+
156+
// Destroy all shell PTY sessions
157+
const shellService = container.get<ShellService>(MAIN_TOKENS.ShellService);
158+
shellService.destroyAll();
159+
160+
// Kill any remaining tracked processes (belt and suspenders)
161+
const processTracking = container.get<ProcessTrackingService>(
162+
MAIN_TOKENS.ProcessTrackingService,
163+
);
164+
processTracking.killAll();
165+
166+
// Release any lingering sleep blockers
167+
const sleepService = container.get<SleepService>(MAIN_TOKENS.SleepService);
168+
sleepService.cleanup();
169+
170+
log.info("All sessions reset successfully");
171+
}),
172+
143173
getGatewayModels: publicProcedure
144174
.input(getGatewayModelsInput)
145175
.output(getGatewayModelsOutput)

apps/twig/src/renderer/features/auth/stores/authStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,9 @@ export const useAuthStore = create<AuthState>()(
643643
throw new Error("No access token available");
644644
}
645645

646+
// Clean up all existing sessions before switching projects
647+
resetSessionService();
648+
646649
const apiHost = getCloudUrlFromRegion(cloudRegion);
647650

648651
// Create a new client with the selected project

apps/twig/src/renderer/features/sessions/service/service.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const mockTrpcAgent = vi.hoisted(() => ({
1515
cancelPermission: { mutate: vi.fn() },
1616
onSessionEvent: { subscribe: vi.fn() },
1717
onPermissionRequest: { subscribe: vi.fn() },
18+
resetAll: { mutate: vi.fn().mockResolvedValue(undefined) },
1819
}));
1920

2021
const mockTrpcWorkspace = vi.hoisted(() => ({
@@ -44,6 +45,7 @@ const mockSessionStoreSetters = vi.hoisted(() => ({
4445
setPendingPermissions: vi.fn(),
4546
getSessionByTaskId: vi.fn(),
4647
getSessions: vi.fn(() => ({})),
48+
clearAll: vi.fn(),
4749
}));
4850

4951
vi.mock("@features/sessions/stores/sessionStore", () => ({

apps/twig/src/renderer/features/sessions/service/service.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,17 @@ export function getSessionService(): SessionService {
7373
return serviceInstance;
7474
}
7575

76-
/**
77-
* Reset the session service singleton.
78-
* Call this on logout or when the app needs to fully reset state.
79-
*/
8076
export function resetSessionService(): void {
8177
if (serviceInstance) {
8278
serviceInstance.reset();
8379
serviceInstance = null;
8480
}
81+
82+
sessionStoreSetters.clearAll();
83+
84+
trpcVanilla.agent.resetAll.mutate().catch((err) => {
85+
log.error("Failed to reset all sessions on main process", err);
86+
});
8587
}
8688

8789
export class SessionService {

apps/twig/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,11 @@ export const sessionStoreSetters = {
289289
getSessions: (): Record<string, AgentSession> => {
290290
return useSessionStore.getState().sessions;
291291
},
292+
293+
clearAll: () => {
294+
useSessionStore.setState((state) => {
295+
state.sessions = {};
296+
state.taskIdIndex = {};
297+
});
298+
},
292299
};

0 commit comments

Comments
 (0)