Skip to content
Closed
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
26 changes: 8 additions & 18 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
import * as DesktopObservability from "./DesktopObservability.ts";
import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import { configureShutdownLog, shutdownBreadcrumb, traceTeardown } from "./DesktopShutdownLog.ts";
import { watchShutdown } from "./DesktopShutdownWatchdog.ts";
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
Expand Down Expand Up @@ -321,9 +320,8 @@ const startup = Effect.gen(function* () {
}

yield* appIdentity.configure;
configureShutdownLog(environment.logDir);
yield* traceTeardown("lifecycle listeners", lifecycle.register);
yield* traceTeardown("clerk", clerk.configure);
yield* lifecycle.register;
yield* clerk.configure;

yield* electronApp.whenReady.pipe(
Effect.withSpan("desktop.electron.whenReady"),
Expand All @@ -339,13 +337,10 @@ const startup = Effect.gen(function* () {
yield* appIdentity.configure;
yield* previewPasskeys.configure;
yield* applicationMenu.configure;
yield* traceTeardown("desktop updates", updates.configure);
yield* traceTeardown("remote updates", DesktopRemoteUpdates.listen);
yield* updates.configure;
yield* DesktopRemoteUpdates.listen;
yield* linuxUrlHandler.register;
yield* traceTeardown(
"bootstrap",
bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause))),
);
yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
}).pipe(Effect.withSpan("desktop.startup"));

const scopedProgram = Effect.scoped(
Expand All @@ -363,20 +358,15 @@ const scopedProgram = Effect.scoped(
// cascade, so leaving the WSL instance for its parent scope
// finalizer means it gets hard-killed by the OS instead of
// receiving SIGTERM + grace.
Effect.sync(() => shutdownBreadcrumb("stopping backends")).pipe(
Effect.andThen(stopAllPoolInstances()),
stopAllPoolInstances().pipe(
Effect.ensuring(rendererHistory.shutdown),
Effect.ensuring(
Effect.sync(() => shutdownBreadcrumb("backends stopped; shutdown complete")).pipe(
Effect.andThen(shutdown.markComplete),
),
),
Effect.ensuring(shutdown.markComplete),
),
);

yield* startup;
yield* shutdown.awaitRequest;
shutdownBreadcrumb("shutdown requested; releasing app resources");
// Scope teardown can stall; the watchdog bounds it.
yield* watchShutdown(stopAllPoolInstances).pipe(Effect.forkDetach);
}),
);
Expand Down
72 changes: 0 additions & 72 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import * as DesktopWindow from "../window/DesktopWindow.ts";
function layerElectronApp(
appListeners: Map<string, (...args: readonly unknown[]) => void>,
quit: Effect.Effect<void> = Effect.void,
overrides: Partial<ElectronApp.ElectronApp["Service"]> = {},
) {
const registerListener = (eventName: string, listener: (...args: readonly unknown[]) => void) =>
Effect.acquireRelease(
Expand Down Expand Up @@ -52,7 +51,6 @@ function layerElectronApp(
onBeforeQuitForUpdate: (listener) => registerListener("before-quit-for-update", listener),
on: (eventName, listener) =>
registerListener(eventName, listener as unknown as (...args: readonly unknown[]) => void),
...overrides,
} satisfies ElectronApp.ElectronApp["Service"]);
}

Expand Down Expand Up @@ -104,76 +102,6 @@ function layerDesktopWindow(
}

describe("DesktopLifecycle", () => {
it.effect.each([false, true])(
"waits for backend shutdown before relaunch (development=%s)",
(isDevelopment) =>
Effect.gen(function* () {
const events: string[] = [];
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
const shutdownRequested = yield* Deferred.make<void>();
const shutdownComplete = yield* Deferred.make<void>();
const exited = yield* Deferred.make<number>();
const layer = DesktopLifecycle.layer.pipe(
Layer.provideMerge(layerElectronTheme),
Layer.provideMerge(
layerElectronApp(appListeners, Effect.void, {
relaunch: () =>
Effect.sync(() => {
events.push("relaunch");
}),
exit: (code) =>
Effect.sync(() => {
events.push(`exit:${code}`);
}).pipe(Effect.andThen(Deferred.succeed(exited, code ?? 0)), Effect.asVoid),
}),
),
Layer.provideMerge(
layerDesktopWindow({
flushMainWindowBounds: Effect.sync(() => {
events.push("flush");
}),
}),
),
Layer.provideMerge(
Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "darwin",
isDevelopment,
} as DesktopEnvironment.DesktopEnvironment["Service"]),
),
Layer.provideMerge(
Layer.succeed(DesktopShutdown.DesktopShutdown, {
request: Effect.sync(() => {
events.push("shutdown");
}).pipe(
Effect.andThen(Deferred.succeed(shutdownRequested, undefined)),
Effect.asVoid,
),
awaitRequest: Deferred.await(shutdownRequested),
awaitComplete: Deferred.await(shutdownComplete),
markComplete: Deferred.succeed(shutdownComplete, undefined).pipe(Effect.asVoid),
isComplete: Deferred.isDone(shutdownComplete),
}),
),
Layer.provideMerge(DesktopState.layer),
);
yield* Effect.gen(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.relaunch("test-only fake relaunch");
yield* Deferred.await(shutdownRequested);
assert.deepEqual(events, ["flush", "shutdown"]);
assert.isFalse(yield* Deferred.isDone(exited));
yield* Deferred.succeed(shutdownComplete, undefined);
assert.equal(yield* Deferred.await(exited), isDevelopment ? 75 : 0);
assert.deepEqual(
events,
isDevelopment
? ["flush", "shutdown", "exit:75"]
: ["flush", "shutdown", "relaunch", "exit:0"],
);
}).pipe(Effect.provide(layer));
}),
);

it.effect.each(["darwin", "win32", "linux"] satisfies ReadonlyArray<NodeJS.Platform>)(
"lets the updater's quit event proceed on %s",
(platform) => {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type * as Electron from "electron";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import { makeComponentLogger } from "./DesktopObservability.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import { shutdownBreadcrumb } from "./DesktopShutdownLog.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
Expand Down Expand Up @@ -103,7 +102,6 @@ function handleBeforeQuit(
allowQuit: () => boolean,
markQuitAllowed: () => void,
): void {
shutdownBreadcrumb(`before-quit (${allowQuit() ? "quit allowed" : "shutting down first"})`);
if (allowQuit()) {
void runEffect(
Effect.gen(function* () {
Expand Down Expand Up @@ -153,7 +151,6 @@ function quitFromSignal(
const electronApp = yield* ElectronApp.ElectronApp;
const state = yield* DesktopState.DesktopState;
const wasQuitting = yield* Ref.getAndSet(state.quitting, true);
shutdownBreadcrumb(`${signal} received${wasQuitting ? " while already quitting" : ""}`);
if (wasQuitting) return;
yield* logLifecycleInfo("process signal received", { signal });
yield* requestDesktopShutdownAndWait();
Expand Down
42 changes: 0 additions & 42 deletions apps/desktop/src/app/DesktopShutdownLog.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/desktop/src/app/DesktopShutdownWatchdog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ it.effect("stalled resource cleanup stops backends and allows the quit to finish
yield* Deferred.await(stopped);
yield* shutdown.awaitComplete;
yield* TestClock.adjust("10 seconds");
assert.strictEqual(yield* Deferred.await(exited), 0);
assert.strictEqual(yield* Deferred.await(exited), 1);
yield* Fiber.join(fiber);
}).pipe(Effect.provide(DesktopShutdown.layer)),
);
Expand All @@ -45,7 +45,7 @@ it.effect("a stalled emergency backend stop cannot stall the independent exit de
Effect.forkChild,
);
yield* TestClock.adjust("30 seconds");
assert.strictEqual(yield* Deferred.await(exited), 0);
assert.strictEqual(yield* Deferred.await(exited), 1);
yield* Fiber.join(fiber);
}).pipe(Effect.provide(DesktopShutdown.layer)),
);
Expand Down
17 changes: 10 additions & 7 deletions apps/desktop/src/app/DesktopShutdownWatchdog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";

import * as DesktopShutdown from "./DesktopShutdown.ts";
import { shutdownBreadcrumb } from "./DesktopShutdownLog.ts";
import { makeComponentLogger } from "./DesktopObservability.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";

const { logWarning } = makeComponentLogger("desktop-shutdown-watchdog");

/**
* Teardown can stall before its finalizer stops the backends. The independent
* exit deadline must also survive a stalled emergency backend stop.
* Bounds a requested shutdown. Teardown can stall before its finalizer stops
* the backends, so after 20s the watchdog stops them itself and completes the
* shutdown; an independent 30s deadline exits even if that stop stalls too.
* The callback belongs to the app, which owns the complete backend pool.
*/
export const watchShutdown = Effect.fn("desktop.app.shutdownWatchdog")(function* <R>(
Expand All @@ -18,15 +21,15 @@ export const watchShutdown = Effect.fn("desktop.app.shutdownWatchdog")(function*
const recover = Effect.gen(function* () {
yield* Effect.sleep(Duration.seconds(20));
if (yield* shutdown.isComplete) return;
shutdownBreadcrumb("shutdown stalled; stopping backends directly");
yield* logWarning("shutdown stalled; stopping backends directly");
yield* stopBackends();
shutdownBreadcrumb("backends stopped by the watchdog");
yield* shutdown.markComplete;
});
const exit = Effect.gen(function* () {
yield* Effect.sleep(Duration.seconds(30));
shutdownBreadcrumb("app did not exit after shutdown; exiting");
yield* electronApp.exit(0);
yield* logWarning("app did not exit after shutdown; exiting");
// Non-zero, so a supervisor sees that the quit had to be forced.
yield* electronApp.exit(1);
});
// In the real app exit terminates the process. In tests it completes the
// deadline and interrupts any still-stalled recovery fiber.
Expand Down
98 changes: 0 additions & 98 deletions apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,104 +99,6 @@ it.effect("attributes a launched thread's first message to the calling thread",
}),
);

it.effect(
"an auto caller launches with inherited workspace and sender, without escalating modes",
() =>
Effect.gen(function* () {
const sourceThreadId = ThreadId.make("source-thread");
const projectId = ProjectId.make("project");
const providerInstanceId = ProviderInstanceId.make("codex");
const modelSelection = { instanceId: providerInstanceId, model: "gpt-5" };
let caller = {
id: sourceThreadId,
projectId,
providerInstanceId,
modelSelection,
runtimeMode: "auto",
interactionMode: "default",
activeRunId: "active-run",
archivedAt: null,
deletedAt: null,
} as OrchestrationV2ThreadShell;
const launched: Array<ThreadLaunch.ThreadLaunchInput> = [];
const layerDependencies = Layer.mergeAll(
NodeCrypto.layer,
Layer.succeed(McpInvocationContext.McpInvocationContext, {
environmentId: EnvironmentId.make("environment"),
requestNamespace: "session",
thread: {
threadId: sourceThreadId,
providerSessionId: "session",
providerInstanceId,
},
client: undefined,
issuedAt: 0,
capabilities: new Set(["orchestration" as const]),
}),
Layer.mock(ThreadManagement.ThreadManagementService)({
getThreadShell: () => Effect.succeed(caller),
}),
Layer.mock(ThreadLaunch.ThreadLaunchService)({
launch: (input) => {
launched.push(input);
return Effect.succeed({
threadId: input.threadId,
projection: {
thread: { id: input.threadId, projectId, modelSelection },
runs: [],
},
resumed: false,
} as unknown as ThreadLaunch.ThreadLaunchResult);
},
}),
Layer.mock(Project.ProjectService)({}),
Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/projects" }),
Layer.mock(GitVcsDriver.GitVcsDriver)({}),
NodeServices.layer,
ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-link-" }).pipe(
Layer.provide(NodeServices.layer),
),
);
const toolkit = yield* ProjectToolkit.pipe(
Effect.provide(
McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(
Layer.provide(layerDependencies),
),
),
);
const handle = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>
toolkit
.handle("t3_thread_launch", params)
.pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
expect((yield* handle({ title: "Inherited", message: "Go" })).at(-1)?.result).toMatchObject({
projectId,
modelSelection,
});
yield* handle({ title: "Narrower", message: "Go", runtimeMode: "approval-required" });
expect(launched.map((input) => input.runtimeMode)).toEqual(["auto", "approval-required"]);
expect(launched.map((input) => input.workspaceStrategy)).toEqual([
{ type: "root" },
{ type: "root" },
]);
expect(
launched.every((input) => input.initialMessage?.senderThreadId === sourceThreadId),
).toBe(true);
expect(
(yield* handle({ title: "Broader", runtimeMode: "full-access" })).at(-1)?.result,
).toMatchObject({ code: "runtime_mode_escalation_denied" });
caller = { ...caller, interactionMode: "plan" };
expect(
(yield* handle({ title: "Broader interaction", interactionMode: "default" })).at(-1)
?.result,
).toMatchObject({ code: "interaction_mode_escalation_denied" });
caller = { ...caller, activeRunId: null };
expect((yield* handle({ title: "Inactive" })).at(-1)?.result).toMatchObject({
code: "parent_not_active",
});
expect(launched).toHaveLength(2);
}),
);

it.effect("launches a scratch thread into the Scratch project", () =>
Effect.gen(function* () {
const sourceThreadId = ThreadId.make("source-thread");
Expand Down
Loading
Loading