From fd49334c3369af8805b167ed99a3d958133d92f0 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:30:04 +0530 Subject: [PATCH] move local-model concurrency to a turn-level defer lock Review feedback: drop the model-layer scheduler (per-call-site interactive/classifier/background priorities, local-provider hostname heuristic) in favor of app-level orchestration: - deferBackgroundTasks flag in models.json, surfaced as a settings toggle; auto-enabled once (UI logic) when the user connects Ollama - ChatActivity counter marked by both chat runtimes (sessions layer and legacy AgentRuntime.trigger) - startWhenPossible/runWhenPossible wrappers around the headless agent runner; all background invocations (knowledge pipeline, live notes, background tasks, scheduled + prebuilt agents) go through them and wait for chat-idle when the flag is set createLanguageModel keeps only the Ollama context-window middleware; the LM Studio capability probe now keys off the provider flavor instead of hostname sniffing. Co-Authored-By: Claude Fable 5 --- .../src/components/settings-dialog.tsx | 37 ++- .../core/src/agent-schedule/runner.ts | 4 +- .../packages/core/src/agents/headless-app.ts | 28 ++ apps/x/packages/core/src/agents/runtime.ts | 8 +- .../core/src/application/lib/builtin-tools.ts | 3 +- .../src/application/lib/chat-activity.test.ts | 50 ++++ .../core/src/application/lib/chat-activity.ts | 39 +++ .../src/background-tasks/event-consumer.ts | 2 +- .../core/src/background-tasks/runner.ts | 10 +- .../core/src/knowledge/agent_notes.ts | 4 +- .../core/src/knowledge/build_graph.ts | 6 +- .../core/src/knowledge/classify_thread.ts | 2 +- .../knowledge/email_importance_feedback.ts | 2 +- .../core/src/knowledge/inline_tasks.ts | 8 +- .../core/src/knowledge/label_emails.ts | 4 +- .../src/knowledge/live-note/event-consumer.ts | 2 +- .../core/src/knowledge/live-note/runner.ts | 10 +- .../core/src/knowledge/meeting_prep_brief.ts | 2 +- .../core/src/knowledge/summarize_meeting.ts | 2 +- .../packages/core/src/knowledge/tag_notes.ts | 4 +- apps/x/packages/core/src/models/defaults.ts | 10 + apps/x/packages/core/src/models/local.test.ts | 123 +------- apps/x/packages/core/src/models/local.ts | 278 +++--------------- apps/x/packages/core/src/models/models.ts | 17 +- apps/x/packages/core/src/models/repo.ts | 5 +- apps/x/packages/core/src/pre_built/runner.ts | 4 +- .../security/auto-permission-classifier.ts | 2 +- apps/x/packages/core/src/sessions/sessions.ts | 9 + .../src/turns/bridges/real-model-registry.ts | 224 +++++++------- .../packages/core/src/turns/model-registry.ts | 4 - apps/x/packages/core/src/turns/runtime.ts | 5 - apps/x/packages/shared/src/ipc.ts | 1 + apps/x/packages/shared/src/models.ts | 5 + 33 files changed, 373 insertions(+), 541 deletions(-) create mode 100644 apps/x/packages/core/src/application/lib/chat-activity.test.ts create mode 100644 apps/x/packages/core/src/application/lib/chat-activity.ts diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 657b66b9..248c6cc6 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -364,6 +364,11 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({ status: "idle" }) const [configLoading, setConfigLoading] = useState(true) const [showMoreProviders, setShowMoreProviders] = useState(false) + // "Defer background tasks while a chat is running" — a top-level + // models.json flag. deferExplicit tracks whether the user (or the Ollama + // auto-enable) has ever set it, so we only auto-enable once. + const [deferBackgroundTasks, setDeferBackgroundTasks] = useState(false) + const [deferExplicit, setDeferExplicit] = useState(false) const activeConfig = providerConfigs[provider] const showApiKey = provider === "openai" || provider === "anthropic" || provider === "google" || provider === "openrouter" || provider === "aigateway" || provider === "openai-compatible" @@ -406,6 +411,8 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b path: "config/models.json", }) const parsed = JSON.parse(result.data) + setDeferBackgroundTasks(parsed?.deferBackgroundTasks === true) + setDeferExplicit(typeof parsed?.deferBackgroundTasks === "boolean") if (parsed?.provider?.flavor && parsed?.model) { const flavor = parsed.provider.flavor as LlmProviderFlavor setProvider(flavor) @@ -462,6 +469,17 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b loadCurrentConfig() }, [dialogOpen]) + const handleDeferToggle = useCallback(async (value: boolean) => { + setDeferBackgroundTasks(value) + setDeferExplicit(true) + try { + await window.ipc.invoke("models:updateConfig", { deferBackgroundTasks: value }) + window.dispatchEvent(new Event("models-config-changed")) + } catch { + toast.error("Failed to save setting") + } + }, []) + // Load models catalog useEffect(() => { if (!dialogOpen) return @@ -532,6 +550,12 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b setDefaultProvider(provider) setTestState({ status: "success" }) window.dispatchEvent(new Event('models-config-changed')) + // Local models compete with background agents for the same hardware: + // when the user connects Ollama and has never touched the defer + // flag, enable it for them (they can switch it off below). + if (provider === "ollama" && !deferExplicit && !deferBackgroundTasks) { + void handleDeferToggle(true) + } // Capability probe caveats (local models): saved, but the user should // know when the model can't do tools or has a too-small context. const warnings: string[] = result.warnings ?? [] @@ -551,7 +575,7 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b setTestState({ status: "error", error: "Connection test failed" }) toast.error("Connection test failed") } - }, [canTest, provider, activeConfig, rowboatConnected]) + }, [canTest, provider, activeConfig, rowboatConnected, deferExplicit, deferBackgroundTasks, handleDeferToggle]) const handleSetDefault = useCallback(async (prov: LlmProviderFlavor) => { const config = providerConfigs[prov] @@ -938,6 +962,17 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b )} + {/* Defer background tasks while chatting */} +
+
+
Defer background tasks while chatting
+
+ Background agents (knowledge sync, live notes, tasks) wait until your chat finishes. Recommended for local models. +
+
+ +
+ {/* Test & Save button */}