Skip to content

Commit 0b95772

Browse files
perf: fix chat timeouts during tool calls, reduce latency and UI jank (#1)
* perf(tools): reduce fetch_url timeout 15s→8s, web_search 10s→6s * perf(storage): stop writing settings.json on every read * perf(backend): parallelize pre-flight ops, cache context providers - Run system prompt build + Brave key fetch in parallel (Promise.all) - Remove duplicate getSettings() call; reuse settings from earlier read - Cache TodoContextProvider and NotesContextProvider instances instead of re-creating on every access via getter * perf(ui): memoize remarkPlugins, debounce scroll-into-view - Move [remarkGfm] array to module-level constant to prevent re-allocation on every render (was defeating React memo) - Debounce scrollIntoView with 80ms timer to prevent DOM thrashing during fast delta streaming (was firing every animation frame) * fix(chat): reset watchdog timer during tool execution The 30s watchdog never reset while tools were executing, causing "Model response timed out" errors during web search flows that take 30-75s across multiple tool rounds. Now resets on every onDelta and onToolStatus event. Increased timeout from 30s to 60s to accommodate multi-round tool loops.
1 parent d0d56ca commit 0b95772

5 files changed

Lines changed: 33 additions & 26 deletions

File tree

src/main/providerService.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,15 @@ export class ProviderService {
147147
checkedAt: number;
148148
}>();
149149

150+
private readonly contextProviders: Array<import("./context/types").ContextProvider>;
151+
150152
constructor(
151153
private readonly storage: AppStorage,
152154
private readonly secureConfig: SecureConfig
153-
) {}
154-
155-
private get contextProviders() {
156-
return [
157-
new TodoContextProvider(this.storage),
158-
new NotesContextProvider(this.storage)
155+
) {
156+
this.contextProviders = [
157+
new TodoContextProvider(storage),
158+
new NotesContextProvider(storage)
159159
];
160160
}
161161

@@ -495,10 +495,11 @@ export class ProviderService {
495495
});
496496

497497
let finalCitations: Citation[] = [];
498-
const systemPrompt = await buildSystemPrompt(this.contextProviders, request.prompt);
499-
const braveApiKey = await this.secureConfig.getToolApiKey("brave");
500-
const toolToggles = (await this.storage.getSettings()).toolToggles;
501-
const toolExecutors = buildToolExecutors(braveApiKey, toolToggles);
498+
const [systemPrompt, braveApiKey] = await Promise.all([
499+
buildSystemPrompt(this.contextProviders, request.prompt),
500+
this.secureConfig.getToolApiKey("brave")
501+
]);
502+
const toolExecutors = buildToolExecutors(braveApiKey, settings.toolToggles);
502503
const toolDefs = getToolDefinitions(toolExecutors);
503504

504505
const onDelta = (delta: string) => {

src/main/storage.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,7 @@ export class AppStorage {
220220

221221
async getSettings(): Promise<SettingsData> {
222222
const raw = await this.readJson<unknown>(this.settingsPath, DEFAULT_SETTINGS);
223-
const normalized = normalizeSettings(raw);
224-
await this.writeJson(this.settingsPath, normalized);
225-
return normalized;
223+
return normalizeSettings(raw);
226224
}
227225

228226
async saveSettings(updater: (current: SettingsData) => SettingsData): Promise<SettingsData> {

src/main/tools/fetchUrl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ToolExecutor } from "./types";
22

33
const MAX_CONTENT_CHARS = 30_000;
4-
const FETCH_TIMEOUT_MS = 15_000;
4+
const FETCH_TIMEOUT_MS = 8_000;
55
const USER_AGENT = "Robin/1.0 (Personal AI Assistant)";
66

77
function stripHtml(html: string): string {

src/main/tools/webSearch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ToolExecutor } from "./types";
22

3-
const SEARCH_TIMEOUT_MS = 10_000;
3+
const SEARCH_TIMEOUT_MS = 6_000;
44

55
interface BraveWebResult {
66
title?: string;

src/renderer/App.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import "@fontsource/gochi-hand";
44
import "@fontsource/dm-sans/500.css";
55
import ReactMarkdown from "react-markdown";
66
import remarkGfm from "remark-gfm";
7+
8+
const REMARK_PLUGINS = [remarkGfm];
79
import {
810
AssistantMode,
911
CLOUD_PROVIDER_IDS,
@@ -738,8 +740,13 @@ export function App() {
738740
});
739741
}
740742

743+
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
741744
useEffect(() => {
742-
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
745+
if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current);
746+
scrollTimerRef.current = setTimeout(() => {
747+
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
748+
}, 80);
749+
return () => { if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current); };
743750
}, [messages.length, messages[messages.length - 1]?.content]);
744751

745752
const localModels = ollamaStatus?.models ?? [];
@@ -1665,15 +1672,14 @@ export function App() {
16651672
setError(null);
16661673
const streamToken = streamSequenceRef.current + 1;
16671674
streamSequenceRef.current = streamToken;
1668-
if (streamWatchdogRef.current) {
1669-
clearTimeout(streamWatchdogRef.current);
1670-
}
1671-
streamWatchdogRef.current = setTimeout(() => {
1672-
if (streamToken !== streamSequenceRef.current) {
1673-
return;
1674-
}
1675-
void stopPendingResponse("Model response timed out. Press Stop and retry.");
1676-
}, 30000);
1675+
const resetWatchdog = () => {
1676+
if (streamWatchdogRef.current) clearTimeout(streamWatchdogRef.current);
1677+
streamWatchdogRef.current = setTimeout(() => {
1678+
if (streamToken !== streamSequenceRef.current) return;
1679+
void stopPendingResponse("Model response timed out. Press Stop and retry.");
1680+
}, 60000);
1681+
};
1682+
resetWatchdog();
16771683
setIsStreaming(true);
16781684
const text = prompt.trim();
16791685
const outgoingAttachments = pendingAttachments;
@@ -1701,6 +1707,7 @@ export function App() {
17011707
if (streamToken !== streamSequenceRef.current) {
17021708
return;
17031709
}
1710+
resetWatchdog();
17041711
queueDelta(messageId, delta);
17051712
},
17061713
onCitations: ({ messageId, citations }) => {
@@ -1720,6 +1727,7 @@ export function App() {
17201727
setTodos(updatedTodos);
17211728
},
17221729
onToolStatus: ({ toolName, status }) => {
1730+
resetWatchdog();
17231731
if (status === "calling") {
17241732
setToolStatus({ toolName, status });
17251733
} else {
@@ -2699,7 +2707,7 @@ export function App() {
26992707
) : null}
27002708
{message.role === "assistant" ? (
27012709
<div className="md-content">
2702-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
2710+
<ReactMarkdown remarkPlugins={REMARK_PLUGINS}>
27032711
{message.content}
27042712
</ReactMarkdown>
27052713
</div>

0 commit comments

Comments
 (0)