diff --git a/apps/desktop/electron-builder.ts b/apps/desktop/electron-builder.ts index a050b62aeec..77cbb168300 100644 --- a/apps/desktop/electron-builder.ts +++ b/apps/desktop/electron-builder.ts @@ -101,6 +101,10 @@ const config: Configuration = { "!**/.DS_Store", "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**", "!**/node_modules/@openai/codex*/**", + "!**/*.map", + "!**/*.d.ts", + "!**/*.d.mts", + "!**/*.d.cts", ], // Rebuild native modules for Electron's Node.js version diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index c895085c1b1..3cb32085af4 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -9,6 +9,7 @@ import { autoUpdater, type UpdateCheckResult } from "electron-updater"; import { env } from "main/env.main"; import { setSkipQuitConfirmation } from "main/index"; import { appState } from "main/lib/app-state"; +import { createDownloadProgressMilestones } from "main/lib/download-progress-milestones"; import { isEnvironmentUpdateError, isUpstreamServerError, @@ -508,7 +509,10 @@ export function setupAutoUpdater(): void { emitStatus(AUTO_UPDATE_STATUS.CHECKING); }); + const progressLogMilestones = createDownloadProgressMilestones(); + autoUpdater.on("update-available", (info) => { + progressLogMilestones.reset(); log.info( `[auto-updater] Update available: ${app.getVersion()} → ${info.version} (files: ${info.files?.map((f: { url: string }) => f.url).join(", ")})`, ); @@ -526,9 +530,11 @@ export function setupAutoUpdater(): void { const PROGRESS_EMIT_INTERVAL_MS = 500; let lastProgressEmitAt = 0; autoUpdater.on("download-progress", (progress) => { - log.info( - `[auto-updater] Download progress: ${progress.percent.toFixed(1)}% (${(progress.transferred / 1024 / 1024).toFixed(1)}MB / ${(progress.total / 1024 / 1024).toFixed(1)}MB)`, - ); + if (progressLogMilestones.shouldLog(progress.percent)) { + log.info( + `[auto-updater] Download progress: ${progress.percent.toFixed(1)}% (${(progress.transferred / 1024 / 1024).toFixed(1)}MB / ${(progress.total / 1024 / 1024).toFixed(1)}MB)`, + ); + } const now = Date.now(); if (now - lastProgressEmitAt < PROGRESS_EMIT_INTERVAL_MS) return; lastProgressEmitAt = now; diff --git a/apps/desktop/src/main/lib/download-progress-milestones.test.ts b/apps/desktop/src/main/lib/download-progress-milestones.test.ts new file mode 100644 index 00000000000..7ea908b226c --- /dev/null +++ b/apps/desktop/src/main/lib/download-progress-milestones.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test"; +import { createDownloadProgressMilestones } from "./download-progress-milestones"; + +describe("createDownloadProgressMilestones", () => { + test("logs once per crossed milestone", () => { + const milestones = createDownloadProgressMilestones(); + const logged = [1, 24.9, 25, 30, 49, 80, 99, 100, 100].filter((percent) => + milestones.shouldLog(percent), + ); + expect(logged).toEqual([25, 80, 100]); + }); + + test("starts again after reset", () => { + const milestones = createDownloadProgressMilestones(); + expect(milestones.shouldLog(100)).toBe(true); + expect(milestones.shouldLog(100)).toBe(false); + milestones.reset(); + expect(milestones.shouldLog(26)).toBe(true); + }); +}); diff --git a/apps/desktop/src/main/lib/download-progress-milestones.ts b/apps/desktop/src/main/lib/download-progress-milestones.ts new file mode 100644 index 00000000000..f37a6b668ae --- /dev/null +++ b/apps/desktop/src/main/lib/download-progress-milestones.ts @@ -0,0 +1,22 @@ +const MILESTONES = [25, 50, 75, 100]; + +export function createDownloadProgressMilestones() { + let nextIndex = 0; + + return { + shouldLog(percent: number): boolean { + let crossed = false; + while ( + nextIndex < MILESTONES.length && + percent >= MILESTONES[nextIndex] + ) { + nextIndex++; + crossed = true; + } + return crossed; + }, + reset(): void { + nextIndex = 0; + }, + }; +} diff --git a/apps/desktop/src/main/lib/local-db/index.ts b/apps/desktop/src/main/lib/local-db/index.ts index 296c0daa938..6a9041b7422 100644 --- a/apps/desktop/src/main/lib/local-db/index.ts +++ b/apps/desktop/src/main/lib/local-db/index.ts @@ -82,6 +82,7 @@ try { // Best-effort; directory permissions should still protect the DB. } sqlite.pragma("journal_mode = WAL"); +sqlite.pragma("synchronous = NORMAL"); sqlite.pragma("foreign_keys = OFF"); sqlite.function("uuid_v4", () => randomUUID()); sqlite.function("uuid_is_valid_v4", (value: unknown) => { diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-addons.ts b/apps/desktop/src/renderer/lib/terminal/terminal-addons.ts index 0d33f8ad28f..6b1eed7f851 100644 --- a/apps/desktop/src/renderer/lib/terminal/terminal-addons.ts +++ b/apps/desktop/src/renderer/lib/terminal/terminal-addons.ts @@ -42,7 +42,9 @@ export function loadAddons( terminal.loadAddon(unicode11); terminal.unicode.activeVersion = "11"; - terminal.loadAddon(new ImageAddon()); + terminal.loadAddon( + new ImageAddon({ storageLimit: 16, pixelLimit: 2048 * 2048 }), + ); const searchAddon = new SearchAddon(); terminal.loadAddon(searchAddon); diff --git a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx index 4dad050a783..86790dd6ade 100644 --- a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx +++ b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx @@ -85,7 +85,7 @@ const persister = createAsyncStoragePersister({ key: "superset-rq-cache", // Every cache event re-serializes the whole persisted set on the main // thread, including events from queries that are never persisted. - throttleTime: 10_000, + throttleTime: 20_000, // Query data carries Dates (tRPC's superjson transformer); plain JSON would // restore them as strings. serialize: superjson.stringify, diff --git a/packages/chat-runtime/src/db/createChatDb/createChatDb.ts b/packages/chat-runtime/src/db/createChatDb/createChatDb.ts index 6f621bf6e08..d5edbbc93b4 100644 --- a/packages/chat-runtime/src/db/createChatDb/createChatDb.ts +++ b/packages/chat-runtime/src/db/createChatDb/createChatDb.ts @@ -26,6 +26,7 @@ export function createChatDb(options: ChatDbOptions) { const sqlite = new Database(join(options.dataDir, CHAT_DB_FILENAME)); sqlite.pragma("journal_mode = WAL"); + sqlite.pragma("synchronous = NORMAL"); sqlite.pragma("busy_timeout = 5000"); sqlite.pragma("foreign_keys = ON"); diff --git a/packages/host-service/src/db/db.ts b/packages/host-service/src/db/db.ts index 6fa21976f73..5378f479532 100644 --- a/packages/host-service/src/db/db.ts +++ b/packages/host-service/src/db/db.ts @@ -34,6 +34,7 @@ export function createDb(dbPath: string, migrationsFolder: string) { const sqlite = new Database(dbPath); sqlite.pragma("journal_mode = WAL"); + sqlite.pragma("synchronous = NORMAL"); sqlite.pragma("busy_timeout = 5000"); const db = drizzle(sqlite, { schema }); diff --git a/packages/host-service/src/terminal/clean-shell-env.test.ts b/packages/host-service/src/terminal/clean-shell-env.test.ts index d47a617d71a..09158117122 100644 --- a/packages/host-service/src/terminal/clean-shell-env.test.ts +++ b/packages/host-service/src/terminal/clean-shell-env.test.ts @@ -13,6 +13,7 @@ import { augmentPathForMacOS, buildMinimalEnv, clearStrictShellEnvCache, + createShellEnvCache, getStrictShellEnvironment, parseEnvOutput, } from "./clean-shell-env.ts"; @@ -92,6 +93,52 @@ describe("augmentPathForMacOS", () => { }); }); +describe("createShellEnvCache", () => { + test("concurrent callers share one spawn and each get a copy", async () => { + let spawns = 0; + const cache = createShellEnvCache(async () => { + spawns++; + return { HOME: "/x" }; + }); + const [a, b] = await Promise.all([cache.get(), cache.get()]); + expect(spawns).toBe(1); + expect(a).toEqual({ HOME: "/x" }); + expect(a).not.toBe(b); + }); + + test("does not cache a failed spawn", async () => { + let spawns = 0; + const cache = createShellEnvCache(async () => { + spawns++; + if (spawns === 1) throw new Error("boom"); + return { HOME: "/x" }; + }); + await expect(cache.get()).rejects.toThrow("boom"); + expect(await cache.get()).toEqual({ HOME: "/x" }); + expect(spawns).toBe(2); + }); + + test("spawns again after the TTL", async () => { + let spawns = 0; + let time = 0; + const cache = createShellEnvCache( + async () => { + spawns++; + return { HOME: "/x" }; + }, + () => time, + 1_000, + ); + await cache.get(); + time = 999; + await cache.get(); + expect(spawns).toBe(1); + time = 1_000; + await cache.get(); + expect(spawns).toBe(2); + }); +}); + const DELIMITER = "__SUPERSET_SHELL_ENV__"; function withDelimiters(body: string): string { diff --git a/packages/host-service/src/terminal/clean-shell-env.ts b/packages/host-service/src/terminal/clean-shell-env.ts index a677707e357..0c1b702a52e 100644 --- a/packages/host-service/src/terminal/clean-shell-env.ts +++ b/packages/host-service/src/terminal/clean-shell-env.ts @@ -5,7 +5,7 @@ import { getManagedEnv } from "../runtime/sandbox-managed-env/sandbox-managed-en import { resolveConfiguredShell } from "./user-shell.ts"; const SHELL_ENV_TIMEOUT_MS = 8_000; -const CACHE_TTL_MS = 60_000; +const CACHE_TTL_MS = 15 * 60_000; const DELIMITER = "__SUPERSET_SHELL_ENV__"; const DIAGNOSTIC_OUTPUT_LIMIT = 200; @@ -130,6 +130,8 @@ function truncateForDiagnostics(value: string): string { return `${trimmed.slice(0, DIAGNOSTIC_OUTPUT_LIMIT)}…`; } +let loggedRcStderr = false; + function spawnCleanShellEnv(): Promise> { return new Promise((resolve, reject) => { const shell = resolveShellForEnv(); @@ -243,7 +245,8 @@ function spawnCleanShellEnv(): Promise> { settle(() => { const stdout = Buffer.concat(stdoutBuffers).toString("utf8"); const stderr = Buffer.concat(stderrBuffers).toString("utf8").trim(); - if (stderr) { + if (stderr && !loggedRcStderr) { + loggedRcStderr = true; console.debug("[terminal-clean-shell-env] stderr:", stderr); } @@ -277,20 +280,52 @@ function spawnCleanShellEnv(): Promise> { }); } -let cache: Record | null = null; -let cacheTime = 0; +export function createShellEnvCache( + resolveEnv: () => Promise>, + now: () => number = Date.now, + ttlMs = CACHE_TTL_MS, +) { + let cache: Record | null = null; + let cacheTime = 0; + let inFlight: Promise> | null = null; + + async function get(): Promise> { + if (cache && now() - cacheTime < ttlMs) { + return { ...cache }; + } + if (!inFlight) { + const promise = resolveEnv().then( + (env) => { + if (inFlight === promise) { + cache = env; + cacheTime = now(); + inFlight = null; + } + return env; + }, + (error) => { + if (inFlight === promise) inFlight = null; + throw error; + }, + ); + inFlight = promise; + } + return { ...(await inFlight) }; + } -export async function getStrictShellEnvironment(): Promise< - Record -> { - if (cache && Date.now() - cacheTime < CACHE_TTL_MS) { - return { ...cache }; + function clear(): void { + cache = null; + cacheTime = 0; + inFlight = null; } - const env = await spawnCleanShellEnv(); - cache = env; - cacheTime = Date.now(); - return { ...cache }; + return { get, clear }; +} + +const shellEnvCache = createShellEnvCache(spawnCleanShellEnv); + +export function getStrictShellEnvironment(): Promise> { + return shellEnvCache.get(); } /** @@ -311,6 +346,5 @@ export async function getToolEnvironment(): Promise> { } export function clearStrictShellEnvCache(): void { - cache = null; - cacheTime = 0; + shellEnvCache.clear(); }