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
217 changes: 217 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow, nativeImage, protocol, shell } from "electron";
import path from "node:path";
type NodePtyType = typeof import("node-pty");

Check warning on line 3 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
import { registerIpc } from "./services/ipc/registerIpc";
import { createFileLogger } from "./services/logging/logger";
import { openKvDb } from "./services/state/kvDb";
Expand Down Expand Up @@ -703,8 +703,11 @@
const projectInitPromises = new Map<string, Promise<AppContext>>();
const closeContextPromises = new Map<string, Promise<void>>();
const mcpSocketCleanupByRoot = new Map<string, () => void>();
const projectLastActivatedAt = new Map<string, number>();
const MAX_WARM_IDLE_PROJECT_CONTEXTS = 1;
let activeProjectRoot: string | null = null;
let dormantContext!: AppContext;
let projectContextRebalancePromise: Promise<void> = Promise.resolve();

const emitProjectChanged = (project: ProjectInfo | null): void => {
broadcast(IPC.appProjectChanged, project);
Expand All @@ -713,6 +716,7 @@
const setActiveProject = (projectRoot: string | null): void => {
activeProjectRoot = projectRoot ? normalizeProjectRoot(projectRoot) : null;
if (activeProjectRoot) {
projectLastActivatedAt.set(activeProjectRoot, Date.now());
try {
adeArtifactAllowedDir =
resolveAdeLayout(activeProjectRoot).artifactsDir;
Expand Down Expand Up @@ -743,6 +747,194 @@
broadcast(channel, payload);
};

const hasActiveProjectWorkloads = async (
projectRoot: string,
ctx: AppContext,
): Promise<boolean> => {
const keepAliveOnProbeFailure = (
probe: string,
error: unknown,
): boolean => {
ctx.logger.warn("project.context_workload_probe_failed", {
projectRoot,
probe,
error: error instanceof Error ? error.message : String(error),
});
return true;
};

try {
if (ctx.sessionService.list({ status: "running", limit: 1 }).length > 0) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("sessions", error);
}

try {
if (ctx.missionService.list({ status: "active", limit: 1 }).length > 0) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("missions", error);
}

try {
if (ctx.testService.hasActiveRuns()) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("tests", error);
}

try {
const lanes = await ctx.laneService.list({
includeArchived: false,
includeStatus: false,
});
for (const lane of lanes) {
if (
ctx.processService.listRuntime(lane.id).some((runtime) =>
runtime.status === "starting"
|| runtime.status === "running"
|| runtime.status === "degraded"
|| runtime.status === "stopping"
)
) {
return true;
}
}
} catch (error) {
return keepAliveOnProbeFailure("processes", error);
}

try {
if ((ctx.laneProxyService?.getStatus().routes.length ?? 0) > 0) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("proxy_routes", error);
}

try {
if (
ctx.oauthRedirectService?.listSessions().some((session) =>
session.status === "pending" || session.status === "active"
) ?? false
) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("oauth_sessions", error);
}

try {
if ((ctx.getActiveMcpConnectionCount?.() ?? 0) > 0) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("mcp_connections", error);
}

try {
if ((ctx.syncHostService?.getPeerStates().length ?? 0) > 0) {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("sync_peers", error);
}

try {
const syncStatus = await ctx.syncService?.getStatus?.();
if (syncStatus?.client.state === "connected") {
return true;
}
} catch (error) {
return keepAliveOnProbeFailure("sync_client", error);
}

return false;
};

const rebalanceProjectContexts = async (): Promise<void> => {
const currentActiveRoot = activeProjectRoot;
if (!currentActiveRoot) return;

const idleRoots: string[] = [];
for (const [projectRoot, ctx] of projectContexts.entries()) {
if (projectRoot === currentActiveRoot) continue;
if (await hasActiveProjectWorkloads(projectRoot, ctx)) {
ctx.logger.info("project.context_retained", {
projectRoot,
policy: "active_workload",
});
continue;
}
idleRoots.push(projectRoot);
}

idleRoots.sort(
(left, right) =>
(projectLastActivatedAt.get(right) ?? 0)
- (projectLastActivatedAt.get(left) ?? 0),
);
const warmRoots = new Set(
idleRoots.slice(0, MAX_WARM_IDLE_PROJECT_CONTEXTS),
);

for (const projectRoot of idleRoots) {
if (activeProjectRoot !== currentActiveRoot) {
return;
}
const ctx = projectContexts.get(projectRoot);
if (!ctx) continue;
if (projectRoot === activeProjectRoot) continue;
if (warmRoots.has(projectRoot)) {
ctx.logger.info("project.context_retained", {
projectRoot,
policy: "warm_idle",
activeProjectRoot: currentActiveRoot,
});
continue;
}
// Re-check workloads immediately before eviction to avoid TOCTOU races
if (await hasActiveProjectWorkloads(projectRoot, ctx)) {
ctx.logger.info("project.context_retained", {
projectRoot,
policy: "became_active_during_rebalance",
activeProjectRoot: currentActiveRoot,
});
continue;
}
ctx.logger.info("project.context_evicted", {
projectRoot,
policy: "idle_after_switch",
activeProjectRoot: currentActiveRoot,
});
await closeProjectContext(projectRoot);
}
};

const scheduleProjectContextRebalance = (): void => {
projectContextRebalancePromise = projectContextRebalancePromise
.catch(() => {
// Swallow previous rebalance failures so future rebalances still run.
})
.then(async () => {
try {
await rebalanceProjectContexts();
} catch (error) {
const logger = activeProjectRoot
? projectContexts.get(activeProjectRoot)?.logger ?? dormantContext.logger
: dormantContext.logger;
logger.warn("project.context_rebalance_failed", {
activeProjectRoot,
error: error instanceof Error ? error.message : String(error),
});
}
});
};

const initContextForProjectRoot = async ({
projectRoot,
baseRef,
Expand Down Expand Up @@ -2697,6 +2889,7 @@
projectId,
adeDir: adePaths.adeDir,
hasUserSelectedProject: userSelectedProject,
getActiveMcpConnectionCount: () => activeMcpConnections.size,
disposeHeadWatcher,
keybindingsService,
agentToolsService,
Expand Down Expand Up @@ -2797,6 +2990,7 @@
hasUserSelectedProject: false,
projectId: "",
adeDir: "",
getActiveMcpConnectionCount: () => 0,
disposeHeadWatcher: () => {},
keybindingsService: null,
agentToolsService: null,
Expand Down Expand Up @@ -2951,6 +3145,26 @@
} catch {
// ignore
}
try {
ctx.embeddingService?.stopHealthCheck?.();
} catch {
// ignore
}
try {
await ctx.embeddingService?.dispose?.();
} catch {
// ignore
}
Comment on lines +3148 to +3157
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

worker_file="$(fd 'embeddingWorkerService\.ts$' apps/desktop/src/main/services/memory | head -n1)"
main_file="apps/desktop/src/main/main.ts"

printf '== embedding worker API ==\n'
sed -n '1,260p' "$worker_file" | rg -n -C2 'createEmbeddingWorkerService|return \{|start\(|stop\(|dispose\('

printf '\n== current teardown sites in main.ts ==\n'
rg -n -C3 'embeddingWorkerService|embeddingService\?\.dispose|stopHealthCheck' "$main_file"

Repository: arul28/ADE

Length of output: 3471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and read the complete embeddingWorkerService.ts file
worker_file="$(fd 'embeddingWorkerService\.ts$' apps/desktop/src/main/services/memory | head -n1)"

echo "=== Complete embeddingWorkerService.ts ==="
wc -l "$worker_file"
cat -n "$worker_file"

Repository: arul28/ADE

Length of output: 11665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the complete teardown section around lines 3139-3148 in main.ts
echo "=== Teardown section in main.ts (lines 3100-3200) ==="
sed -n '3100,3200p' apps/desktop/src/main/main.ts | cat -n

Repository: arul28/ADE

Length of output: 2654


Wait for embedding worker to complete processing before disposing embeddingService.

The worker queue is in-memory and can have pending embedding work. Currently, embeddingService is disposed without ensuring the worker has finished processing. If the worker calls embeddingService.embed() on queued items after the service is disposed, it will fail.

The embeddingWorkerService does not expose a stop() or dispose() method. Instead, call await embeddingWorkerService.waitForIdle() before disposing embeddingService to ensure all queued work completes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/main.ts` around lines 3139 - 3148, Before disposing
embeddingService, ensure the embeddingWorkerService finishes queued work by
awaiting its idle state: call await embeddingWorkerService.waitForIdle() (or
equivalent) after stopping health checks and before calling
ctx.embeddingService.dispose(), so any pending embeddingService.embed() calls
complete; keep the existing try/catch around stopHealthCheck and dispose but
insert the await embeddingWorkerService.waitForIdle() between them, referencing
ctx.embeddingService and embeddingWorkerService.waitForIdle to locate where to
change.

try {
await ctx.laneProxyService?.dispose?.();
} catch {
// ignore
}
try {
ctx.oauthRedirectService?.dispose?.();
} catch {
// ignore
}
try {
await ctx.externalMcpService?.dispose?.();
} catch {
Expand Down Expand Up @@ -3037,6 +3251,7 @@
const closePromise = (async () => {
await disposeContextResources(ctx);
projectContexts.delete(normalizedRoot);
projectLastActivatedAt.delete(normalizedRoot);
if (activeProjectRoot === normalizedRoot) {
activeProjectRoot = null;
}
Expand Down Expand Up @@ -3080,6 +3295,7 @@
recordRecent: false,
});
emitProjectChanged(existing.project);
scheduleProjectContextRebalance();
return existing.project;
}

Expand Down Expand Up @@ -3111,6 +3327,7 @@
recordRecent: false,
});
emitProjectChanged(ctx.project);
scheduleProjectContextRebalance();
return ctx.project;
};

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/services/ai/providerTaskRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ async function runCommand(args: {
let settled = false;
const timeoutMs = Math.max(1_000, args.timeoutMs ?? 120_000);
const timeoutHandle = setTimeout(() => {
if (settled) return;
settled = true;
child.kill("SIGTERM");
reject(new Error(`Provider task timed out after ${timeoutMs}ms.`));
}, timeoutMs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ export function createFeedbackReporterService({
jsonSchema: FEEDBACK_ISSUE_JSON_SCHEMA,
permissionMode: "read-only",
oneShot: true,
timeoutMs: 300_000,
...(submission.reasoningEffort ? { reasoningEffort: submission.reasoningEffort } : {}),
});

const structuredCandidate = result.structuredOutput ?? parseStructuredOutput(result.text);
Expand All @@ -298,7 +300,19 @@ export function createFeedbackReporterService({
submission.generatedBody = normalized.draft.body;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Generation failed: ${message}`);
logger.warn("feedback.generation_failed_using_fallback", {
id: submission.id,
category: submission.category,
modelId: submission.modelId,
error: message,
});
normalizedDraft = {
title: fallbackTitle(submission),
body: fallbackBody(submission),
labels: defaultLabelsForCategory(submission.category),
};
submission.generatedTitle = normalizedDraft.title;
submission.generatedBody = normalizedDraft.body;
}

// -- Post to GitHub --
Expand Down Expand Up @@ -357,6 +371,7 @@ export function createFeedbackReporterService({
category: args.category,
userDescription: args.userDescription,
modelId: args.modelId,
reasoningEffort: args.reasoningEffort ?? null,
status: "pending",
generatedTitle: null,
generatedBody: null,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ export type AppContext = {
hasUserSelectedProject: boolean;
projectId: string;
adeDir: string;
getActiveMcpConnectionCount?: (() => number) | null;
disposeHeadWatcher: () => void;
keybindingsService: ReturnType<typeof createKeybindingsService>;
agentToolsService: ReturnType<typeof createAgentToolsService>;
Expand Down
Loading
Loading