Skip to content

Commit 4eab46d

Browse files
authored
Opt Pass (#256)
1 parent f551b09 commit 4eab46d

13 files changed

Lines changed: 270 additions & 41 deletions

File tree

apps/ade-cli/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ ade ios-sim devices --text
7575
ade --socket ios-sim apps --text
7676
ade --socket ios-sim launch --target target-id --text
7777
ade --socket ios-sim preview-render --source apps/ios/ADE/Views/Home.swift --index 0 --text
78+
ade --socket app-control launch --command "npm run dev" --text
79+
ade --socket browser open http://localhost:5173 --new-tab --text
7880
ade --socket update status --text
7981
ade --socket update check --text
8082
ade --socket update install --text

apps/ade-cli/src/cli.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,44 @@ describe("ADE CLI", () => {
18291829
});
18301830
});
18311831

1832+
it("update commands map to auto-update actions", () => {
1833+
const status = buildCliPlan(["update", "status"]);
1834+
expect(status.kind).toBe("execute");
1835+
if (status.kind !== "execute") return;
1836+
expect(status.steps[0]?.params).toMatchObject({
1837+
arguments: { domain: "update", action: "getSnapshot", args: {} },
1838+
});
1839+
1840+
const check = buildCliPlan(["auto-update", "check"]);
1841+
expect(check.kind).toBe("execute");
1842+
if (check.kind !== "execute") return;
1843+
expect(check.steps[0]?.params).toMatchObject({
1844+
arguments: { domain: "update", action: "checkForUpdates", args: {} },
1845+
});
1846+
1847+
const install = buildCliPlan(["updates", "install"]);
1848+
expect(install.kind).toBe("execute");
1849+
if (install.kind !== "execute") return;
1850+
expect(install.steps[0]?.params).toMatchObject({
1851+
arguments: { domain: "update", action: "quitAndInstall", args: {} },
1852+
});
1853+
1854+
const dismiss = buildCliPlan(["update", "dismiss"]);
1855+
expect(dismiss.kind).toBe("execute");
1856+
if (dismiss.kind !== "execute") return;
1857+
expect(dismiss.steps[0]?.params).toMatchObject({
1858+
arguments: { domain: "update", action: "dismissInstalledNotice", args: {} },
1859+
});
1860+
1861+
const actions = buildCliPlan(["update", "actions"]);
1862+
expect(actions.kind).toBe("execute");
1863+
if (actions.kind !== "execute") return;
1864+
expect(actions.steps[0]?.params).toMatchObject({
1865+
name: "list_ade_actions",
1866+
arguments: { domain: "update" },
1867+
});
1868+
});
1869+
18321870
it("attaches a rendered lane graph when the plan has the lanes visualizer", () => {
18331871
const connection = {
18341872
mode: "headless" as const,

apps/desktop/resources/ade-cli-help.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ _ ____ _____
3838
$ ade browser open | tabs | screenshot Use ADE's built-in browser pane
3939
$ ade memory add | search | pin Use ADE memory
4040
$ ade settings action <method> Call project config actions
41+
$ ade update status | check | install | dismiss Read auto-update state and drive install
4142
$ ade actions list | run | status Escape hatch for every ADE service action
4243
$ ade cursor cloud agents | runs | artifacts | repos | models | me
4344
Drive Cursor Cloud agents via @cursor/sdk
@@ -121,6 +122,7 @@ _ ____ _____
121122
$ ade browser open | tabs | screenshot Use ADE's built-in browser pane
122123
$ ade memory add | search | pin Use ADE memory
123124
$ ade settings action <method> Call project config actions
125+
$ ade update status | check | install | dismiss Read auto-update state and drive install
124126
$ ade actions list | run | status Escape hatch for every ADE service action
125127
$ ade cursor cloud agents | runs | artifacts | repos | models | me
126128
Drive Cursor Cloud agents via @cursor/sdk
@@ -204,6 +206,7 @@ _ ____ _____
204206
$ ade browser open | tabs | screenshot Use ADE's built-in browser pane
205207
$ ade memory add | search | pin Use ADE memory
206208
$ ade settings action <method> Call project config actions
209+
$ ade update status | check | install | dismiss Read auto-update state and drive install
207210
$ ade actions list | run | status Escape hatch for every ADE service action
208211
$ ade cursor cloud agents | runs | artifacts | repos | models | me
209212
Drive Cursor Cloud agents via @cursor/sdk
@@ -614,6 +617,10 @@ _ ____ _____
614617
$ ade --socket ios-sim shutdown --force Force-release a session owned by another chat
615618
$ ade ios-sim actions --text List every callable ios_simulator action
616619

620+
Project discovery scans root-level .xcodeproj bundles and apps/*/*.xcodeproj
621+
projects; do not create symlink shims or fake schemes before checking
622+
"ios-sim apps --text" and the build output.
623+
617624
Capture and inspection:
618625
$ ade ios-sim screenshot --text One-shot PNG via simctl (screenshot)
619626
$ ade ios-sim snapshot --text Screenshot + selectable elements (getScreenSnapshot)
@@ -791,6 +798,7 @@ _ ____ _____
791798
$ ade browser open | tabs | screenshot Use ADE's built-in browser pane
792799
$ ade memory add | search | pin Use ADE memory
793800
$ ade settings action <method> Call project config actions
801+
$ ade update status | check | install | dismiss Read auto-update state and drive install
794802
$ ade actions list | run | status Escape hatch for every ADE service action
795803
$ ade cursor cloud agents | runs | artifacts | repos | models | me
796804
Drive Cursor Cloud agents via @cursor/sdk
@@ -837,6 +845,32 @@ _ ____ _____
837845

838846
Start with: ade doctor --text
839847

848+
## ade update --help
849+
_ ____ _____
850+
/ \ | _ \| ____|
851+
/ _ \ | | | | _|
852+
/ ___ \| |_| | |___
853+
/_/ \_\____/|_____|
854+
855+
Auto-update
856+
857+
Auto-update commands query and drive ADE desktop's auto-updater. The desktop
858+
app owns the updater process; CLI parity exists so agents can read state and,
859+
when explicitly requested by the user, trigger a check, install, or dismiss
860+
the post-install notice. quitAndInstall relaunches the desktop app and only
861+
succeeds when status is "ready".
862+
863+
$ ade --socket update status --text Read AutoUpdateSnapshot (status, version, progress)
864+
$ ade --socket update check --text Trigger a background update check
865+
$ ade --socket update install --text Refresh latest, then quit and install when ready
866+
$ ade --socket update dismiss --text Clear the recently-installed banner
867+
$ ade --socket update actions --text List callable update actions
868+
869+
Snapshot status values: idle, checking, downloading, ready, installing, error.
870+
"installing" appears between quitAndInstall and the desktop relaunch; if the
871+
install fails, status falls back to error and the pending-install record is
872+
cleared automatically.
873+
840874
## ade actions --help
841875
_ ____ _____
842876
/ \ | _ \| ____|

apps/desktop/scripts/regen-ade-cli-help.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const SUBCOMMANDS = [
3030
"browser",
3131
"memory",
3232
"settings",
33+
"update",
3334
"actions",
3435
"cursor",
3536
];

apps/desktop/src/main/services/memory/embeddingWorkerService.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createMemoryService } from "./memoryService";
88
import {
99
createEmbeddingWorkerService,
1010
DEFAULT_ACTIVE_BATCH_SIZE,
11+
DEFAULT_ACTIVE_SESSION_COLD_START_DEFER_MS,
1112
DEFAULT_IDLE_BATCH_SIZE,
1213
} from "./embeddingWorkerService";
1314
import { DEFAULT_EMBEDDING_MODEL_ID, EXPECTED_EMBEDDING_DIMENSIONS } from "./embeddingService";
@@ -35,9 +36,11 @@ function buildVector(seed: string): Float32Array {
3536
async function createFixture(opts: {
3637
withActiveSession?: boolean;
3738
embedImpl?: (text: string) => Promise<Float32Array>;
39+
isAvailable?: () => boolean;
3840
sleep?: (ms: number) => Promise<void>;
3941
idleBatchSize?: number;
4042
activeBatchSize?: number;
43+
activeSessionColdStartDeferMs?: number;
4144
attachQueueHook?: boolean;
4245
} = {}) {
4346
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-embedding-worker-"));
@@ -76,7 +79,7 @@ async function createFixture(opts: {
7679
const sessionService = createSessionService({ db });
7780
const embeddingService = {
7881
embed: vi.fn(opts.embedImpl ?? (async (text: string) => buildVector(text))),
79-
isAvailable: vi.fn(() => true),
82+
isAvailable: vi.fn(opts.isAvailable ?? (() => true)),
8083
getModelId: vi.fn(() => DEFAULT_EMBEDDING_MODEL_ID),
8184
};
8285

@@ -98,6 +101,7 @@ async function createFixture(opts: {
98101
sessionService,
99102
idleBatchSize: opts.idleBatchSize ?? DEFAULT_IDLE_BATCH_SIZE,
100103
activeBatchSize: opts.activeBatchSize ?? DEFAULT_ACTIVE_BATCH_SIZE,
104+
activeSessionColdStartDeferMs: opts.activeSessionColdStartDeferMs ?? DEFAULT_ACTIVE_SESSION_COLD_START_DEFER_MS,
101105
sleep: opts.sleep,
102106
});
103107

@@ -268,6 +272,51 @@ describe("embeddingWorkerService", () => {
268272
expect(sleep).toHaveBeenCalledWith(100);
269273
});
270274

275+
it("defers a cold embedding model load while sessions are active", async () => {
276+
vi.useFakeTimers();
277+
try {
278+
let modelReady = false;
279+
const { db, logger, worker, memoryService, embeddingService } = await createFixture({
280+
withActiveSession: true,
281+
activeSessionColdStartDeferMs: 25,
282+
isAvailable: () => modelReady,
283+
});
284+
285+
memoryService.addMemory({
286+
projectId: "project-1",
287+
scope: "project",
288+
category: "fact",
289+
content: "Defer this until the active session is not competing with a cold model load.",
290+
importance: "medium",
291+
});
292+
293+
await vi.advanceTimersByTimeAsync(0);
294+
295+
expect(embeddingService.embed).not.toHaveBeenCalled();
296+
expect(worker.getStatus()).toEqual(expect.objectContaining({
297+
batchesProcessed: 0,
298+
queueDepth: 1,
299+
}));
300+
expect(logger.info).toHaveBeenCalledWith(
301+
"memory.embedding_worker.cold_start_deferred",
302+
expect.objectContaining({
303+
projectId: "project-1",
304+
queueDepth: 1,
305+
retryInMs: 25,
306+
}),
307+
);
308+
309+
modelReady = true;
310+
await vi.advanceTimersByTimeAsync(25);
311+
await worker.waitForIdle();
312+
313+
expect(embeddingService.embed).toHaveBeenCalledTimes(1);
314+
expect(countEmbeddings(db)).toBe(1);
315+
} finally {
316+
vi.useRealTimers();
317+
}
318+
});
319+
271320
it("logs and skips a failed entry without blocking the rest of the batch", async () => {
272321
const { db, logger, worker, memoryService } = await createFixture({
273322
idleBatchSize: 10,

apps/desktop/src/main/services/memory/embeddingWorkerService.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { createEmbeddingService } from "./embeddingService";
77

88
export const DEFAULT_IDLE_BATCH_SIZE = 50;
99
export const DEFAULT_ACTIVE_BATCH_SIZE = 10;
10+
export const DEFAULT_ACTIVE_SESSION_COLD_START_DEFER_MS = 60_000;
1011
const MIN_BATCH_SIZE = 10;
1112
const MAX_BATCH_SIZE = 50;
1213
const DEFAULT_YIELD_MS = 100;
@@ -26,6 +27,7 @@ type CreateEmbeddingWorkerServiceOpts = {
2627
sessionService?: Pick<ReturnType<typeof createSessionService>, "list">;
2728
idleBatchSize?: number;
2829
activeBatchSize?: number;
30+
activeSessionColdStartDeferMs?: number;
2931
yieldMs?: number;
3032
now?: () => Date;
3133
sleep?: (ms: number) => Promise<void>;
@@ -75,6 +77,10 @@ export function createEmbeddingWorkerService(opts: CreateEmbeddingWorkerServiceO
7577

7678
const idleBatchSize = normalizeBatchSize(opts.idleBatchSize, DEFAULT_IDLE_BATCH_SIZE);
7779
const activeBatchSize = normalizeBatchSize(opts.activeBatchSize, DEFAULT_ACTIVE_BATCH_SIZE);
80+
const activeSessionColdStartDeferMs = Math.max(
81+
1,
82+
Math.floor(opts.activeSessionColdStartDeferMs ?? DEFAULT_ACTIVE_SESSION_COLD_START_DEFER_MS),
83+
);
7884
const yieldMs = Math.max(0, Math.floor(opts.yieldMs ?? DEFAULT_YIELD_MS));
7985

8086
const queue: string[] = [];
@@ -129,9 +135,15 @@ export function createEmbeddingWorkerService(opts: CreateEmbeddingWorkerServiceO
129135
return isSessionActive() ? activeBatchSize : idleBatchSize;
130136
}
131137

132-
function scheduleProcessing() {
138+
function shouldDeferColdStartForActiveSession(): boolean {
139+
if (embeddingService.isAvailable()) return false;
140+
return isSessionActive();
141+
}
142+
143+
function scheduleProcessing(delayMs = 0) {
133144
if (scheduled || processingPromise || queue.length === 0) return;
134145
scheduled = true;
146+
const normalizedDelayMs = Math.max(0, Math.floor(delayMs));
135147
setTimeout(() => {
136148
scheduled = false;
137149
void processQueue().catch((error) => {
@@ -140,7 +152,7 @@ export function createEmbeddingWorkerService(opts: CreateEmbeddingWorkerServiceO
140152
error: getErrorMessage(error),
141153
});
142154
});
143-
}, 0);
155+
}, normalizedDelayMs);
144156
}
145157

146158
function queueMemory(memoryId: string) {
@@ -227,8 +239,19 @@ export function createEmbeddingWorkerService(opts: CreateEmbeddingWorkerServiceO
227239
async function processQueue() {
228240
if (processingPromise) return processingPromise;
229241

242+
let rescheduleDelayMs = 0;
230243
processingPromise = (async () => {
231244
while (queue.length > 0) {
245+
if (shouldDeferColdStartForActiveSession()) {
246+
rescheduleDelayMs = activeSessionColdStartDeferMs;
247+
logger.info("memory.embedding_worker.cold_start_deferred", {
248+
projectId,
249+
queueDepth: queue.length,
250+
retryInMs: activeSessionColdStartDeferMs,
251+
});
252+
return;
253+
}
254+
232255
const batchSize = Math.min(nextBatchSize(), queue.length);
233256
const batchIds = queue.splice(0, batchSize);
234257
for (const id of batchIds) {
@@ -272,7 +295,7 @@ export function createEmbeddingWorkerService(opts: CreateEmbeddingWorkerServiceO
272295
processingPromise = null;
273296
resolveIdleIfNeeded();
274297
if (queue.length > 0) {
275-
scheduleProcessing();
298+
scheduleProcessing(rescheduleDelayMs);
276299
}
277300
});
278301

apps/desktop/src/renderer/components/app/AppShell.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,27 @@ export function AppShell({ children }: { children: React.ReactNode }) {
429429
}
430430
};
431431

432-
const disposeProjectChanged = window.ade.app.onProjectChanged(() => {
433-
void initializeProjectState();
432+
const disposeProjectChanged = window.ade.app.onProjectChanged((nextProject) => {
433+
const state = useAppStore.getState();
434+
const nextRoot = nextProject?.rootPath ?? null;
435+
const currentRoot = state.project?.rootPath ?? null;
436+
const expectedShowWelcome = !nextProject;
437+
const alreadyApplied =
438+
state.projectHydrated &&
439+
currentRoot === nextRoot &&
440+
state.showWelcome === expectedShowWelcome;
441+
442+
if (state.projectTransition) return;
443+
444+
if (alreadyApplied) {
445+
setProject(nextProject);
446+
setShowWelcome(expectedShowWelcome);
447+
return;
448+
}
449+
450+
setProjectHydrated(false);
451+
applyProjectState(nextProject);
452+
setProjectHydrated(true);
434453
});
435454

436455
void initializeProjectState();

0 commit comments

Comments
 (0)