Research: add configurable run timeout

Surfaces the research_run_timeout_seconds setting (added in #783) in
Settings → Research as a "Max Time" field, and lets 0 disable the
wall-clock cap entirely for long deep-research runs.

- settings.py: document that 0 disables the cap; default stays 1800s.
- research_handler.py: resolve 0 (or negative) to no timeout
  (asyncio.wait_for timeout=None); other values stay bounded to
  [60, 86400] as before.
- index.html / settings.js: "Max Time" input bound to
  research_run_timeout_seconds, validated to {0} ∪ [60, 86400], with
  copy making explicit that 0 = no limit (unbounded model/API cost).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nikita Rozanov
2026-06-02 13:57:57 +02:00
committed by GitHub
parent c3228f8b59
commit 119075f368
4 changed files with 43 additions and 8 deletions
+4
View File
@@ -1463,6 +1463,10 @@
<label class="settings-label">Extract Parallel</label>
<input id="set-researchExtractConcurrency" type="text" inputmode="numeric" placeholder="3" class="settings-select" style="width:120px;">
</div>
<div class="settings-row">
<label class="settings-label">Max Time</label>
<input id="set-researchRunTimeout" type="text" inputmode="numeric" placeholder="1800 sec (0 = no limit)" class="settings-select" style="width:120px;">
</div>
<div id="set-researchMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
</div>
</div>
+18
View File
@@ -1402,6 +1402,7 @@ async function initResearchSettings() {
var tokensInput = el('set-researchMaxTokens');
var extractTimeoutInput = el('set-researchExtractTimeout');
var extractConcurrencyInput = el('set-researchExtractConcurrency');
var runTimeoutInput = el('set-researchRunTimeout');
var msg = el('set-researchMsg');
var endpoints = [];
@@ -1424,6 +1425,9 @@ async function initResearchSettings() {
if (settings.research_max_tokens) tokensInput.value = settings.research_max_tokens;
if (settings.research_extraction_timeout_seconds) extractTimeoutInput.value = settings.research_extraction_timeout_seconds;
if (settings.research_extraction_concurrency) extractConcurrencyInput.value = settings.research_extraction_concurrency;
if (settings.research_run_timeout_seconds !== undefined && settings.research_run_timeout_seconds !== null) {
runTimeoutInput.value = settings.research_run_timeout_seconds;
}
} catch (e) { console.warn('Failed to load research settings', e); }
function showStatus() {
@@ -1442,6 +1446,12 @@ async function initResearchSettings() {
if (extractConcurrencyInput.value) {
parts.push('Parallel: ' + extractConcurrencyInput.value);
}
if (runTimeoutInput.value !== '') {
var rtv = parseInt(runTimeoutInput.value, 10);
if (!isNaN(rtv)) {
parts.push(rtv === 0 ? 'Max time: no limit' : 'Max time: ' + rtv + 's');
}
}
if (parts.length) {
msg.textContent = parts.join(' · ');
msg.style.color = 'var(--fg)';
@@ -1463,6 +1473,13 @@ async function initResearchSettings() {
if (et && et >= 15 && et <= 3600) payload.research_extraction_timeout_seconds = et;
var ec = parseInt(extractConcurrencyInput.value, 10);
if (ec && ec >= 1 && ec <= 12) payload.research_extraction_concurrency = ec;
if (runTimeoutInput.value !== '') {
var rt = parseInt(runTimeoutInput.value, 10);
// 0 = no limit (disables the hard timeout); otherwise 60s..86400s (24h)
if (!isNaN(rt) && (rt === 0 || (rt >= 60 && rt <= 86400))) {
payload.research_run_timeout_seconds = rt;
}
}
try {
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
@@ -1481,6 +1498,7 @@ async function initResearchSettings() {
tokensInput.addEventListener('change', saveResearch);
extractTimeoutInput.addEventListener('change', saveResearch);
extractConcurrencyInput.addEventListener('change', saveResearch);
runTimeoutInput.addEventListener('change', saveResearch);
_registerAiEndpointRefresh(function(nextEndpoints) {
endpoints = nextEndpoints;