feat(frontend): make SSE idle timeout configurable via VIBE_TRADING_SSE_TIMEOUT (#157)
CI / test (push) Waiting to run
Wiki / validate (push) Waiting to run
Deploy Wiki / Deploy to Cloudflare Pages (push) Canceled after 0s

The frontend safety timeout was hardcoded at 90 s, causing false
"Execution timed out" toasts for users running slow local models
(e.g. Ollama on limited VRAM or CPU).

- Backend: expose `sse_timeout_seconds` in `LLMSettingsResponse`,
  reading from `VIBE_TRADING_SSE_TIMEOUT` env var (default 90 s)
- Frontend: fetch the value from `/settings/llm` on mount and use it
  instead of the hardcoded 90 000 ms constant
- `.env.example`: document the new variable in the Agent Tuning section

Co-authored-by: Sky <985369746@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-06-01 22:13:15 +08:00
1 parent f9bea768f4
commit a8144e9acf
4 files changed
+15 -2

No files matched your search

+3
View File
@@ -139,3 +139,6 @@ TUSHARE_TOKEN=your-tushare-token
# SUBAGENT_TIMEOUT=300 # SUBAGENT_TIMEOUT=300
# SUBAGENT_MAX_ITER=25 # SUBAGENT_MAX_ITER=25
# TOKEN_THRESHOLD=40000 # TOKEN_THRESHOLD=40000
# Frontend SSE idle timeout in seconds before showing "Execution timed out".
# Increase when using slow local models (e.g. Ollama on CPU/limited VRAM).
# VIBE_TRADING_SSE_TIMEOUT=90
+2
View File
@@ -170,6 +170,7 @@ class LLMSettingsResponse(BaseModel):
timeout_seconds: int timeout_seconds: int
max_retries: int max_retries: int
reasoning_effort: str reasoning_effort: str
sse_timeout_seconds: int
env_path: str env_path: str
providers: List[LLMProviderOption] providers: List[LLMProviderOption]
@@ -926,6 +927,7 @@ def _build_llm_settings_response(values: Optional[Dict[str, str]] = None) -> LLM
timeout_seconds=_coerce_int(env_values.get("TIMEOUT_SECONDS", "120"), 120), timeout_seconds=_coerce_int(env_values.get("TIMEOUT_SECONDS", "120"), 120),
max_retries=_coerce_int(env_values.get("MAX_RETRIES", "2"), 2), max_retries=_coerce_int(env_values.get("MAX_RETRIES", "2"), 2),
reasoning_effort=env_values.get("LANGCHAIN_REASONING_EFFORT", "").strip().lower(), reasoning_effort=env_values.get("LANGCHAIN_REASONING_EFFORT", "").strip().lower(),
sse_timeout_seconds=_coerce_int(env_values.get("VIBE_TRADING_SSE_TIMEOUT", "90"), 90),
env_path=_project_relative_path(ENV_PATH), env_path=_project_relative_path(ENV_PATH),
providers=LLM_PROVIDERS, providers=LLM_PROVIDERS,
) )
+1
View File
@@ -232,6 +232,7 @@ export interface LLMSettings {
timeout_seconds: number; timeout_seconds: number;
max_retries: number; max_retries: number;
reasoning_effort: string; reasoning_effort: string;
sse_timeout_seconds: number;
env_path: string; env_path: string;
providers: LLMProviderOption[]; providers: LLMProviderOption[];
} }
+9 -2
View File
@@ -212,6 +212,7 @@ export function Agent() {
const pendingGoalSessionRef = useRef<string | null>(null); const pendingGoalSessionRef = useRef<string | null>(null);
const [showScrollBtn, setShowScrollBtn] = useState(false); const [showScrollBtn, setShowScrollBtn] = useState(false);
const lastEventRef = useRef(0); const lastEventRef = useRef(0);
const sseTimeoutMsRef = useRef(90_000);
/* tool_progress coalescing — keep latest payload per-tool, flush once per rAF. */ /* tool_progress coalescing — keep latest payload per-tool, flush once per rAF. */
const pendingProgressRef = useRef<Map<string, NonNullable<ToolCallEntry["progress"]>>>(new Map()); const pendingProgressRef = useRef<Map<string, NonNullable<ToolCallEntry["progress"]>>>(new Map());
@@ -709,11 +710,17 @@ export function Agent() {
useEffect(() => () => doDisconnect(), [doDisconnect]); useEffect(() => () => doDisconnect(), [doDisconnect]);
/* Safety timeout: if streaming but no SSE event for 90s, reset to idle */ useEffect(() => {
api.getLLMSettings().then((s) => {
sseTimeoutMsRef.current = s.sse_timeout_seconds * 1000;
}).catch(() => {});
}, []);
/* Safety timeout: if streaming but no SSE event for sseTimeoutMsRef.current ms, reset to idle */
useEffect(() => { useEffect(() => {
if (status !== "streaming") return; if (status !== "streaming") return;
const timer = setInterval(() => { const timer = setInterval(() => {
if (lastEventRef.current && Date.now() - lastEventRef.current > 90_000 && act().status === "streaming") { if (lastEventRef.current && Date.now() - lastEventRef.current > sseTimeoutMsRef.current && act().status === "streaming") {
act().setStatus("idle"); act().setStatus("idle");
toast.warning("Execution timed out, automatically stopped"); toast.warning("Execution timed out, automatically stopped");
} }