--- a/components/provider-limit-settings.tsx +++ b/components/provider-limit-settings.tsx @@ -1,39 +1,57 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; + export function ProviderLimitSettings() { const [enabled, setEnabled] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); + const [notice, setNotice] = useState(""); const [reload, setReload] = useState(0); + const saveController = useRef(null); useEffect(() => { const controller = new AbortController(); - void fetch("/api/agent-settings/provider-limit", { cache: "no-store", signal: controller.signal }).then(async response => { + setEnabled(null); + setError(""); + void fetch("/api/agent-settings/provider-limit", { cache: "no-store", signal: controller.signal }) + .then(async response => { + const data = await response.json(); + if (!response.ok || typeof data.enabled !== "boolean") throw new Error(data.error || "Could not load provider limit settings."); + if (!controller.signal.aborted) setEnabled(data.enabled); + }).catch(cause => { + if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : "Could not load provider limit settings."); + }); + return () => { controller.abort(); saveController.current?.abort(); }; + }, [reload]); + async function save(value: boolean) { + if (busy || enabled === null) return; + const controller = new AbortController(); + saveController.current = controller; + setBusy(true); setError(""); setNotice(""); + try { + const response = await fetch("/api/agent-settings/provider-limit", { + method: "PATCH", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: value }), signal: controller.signal, + }); const data = await response.json(); - if (!response.ok) throw new Error(data.error || "Could not load provider limit settings."); - if (!controller.signal.aborted) { setEnabled(data.enabled); setError(""); } - }).catch(cause => { if (!controller.signal.aborted) setError(cause.message); }); - return () => controller.abort(); - }, [reload]); - async function save(next: boolean) { - setBusy(true); setError(""); - try { - const response = await fetch("/api/agent-settings/provider-limit", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: next }) }); - const data = await response.json(); - if (!response.ok) throw new Error(data.error || "Could not save provider limit settings."); - setEnabled(data.enabled); - } catch (cause) { setError(cause instanceof Error ? cause.message : "Could not save settings."); } - finally { setBusy(false); } + if (!response.ok || typeof data.enabled !== "boolean") throw new Error(data.error || "Could not save provider limit settings."); + if (!controller.signal.aborted) { setEnabled(data.enabled); setNotice("Saved."); } + } catch (cause) { + if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : "Could not save provider limit settings."); + } finally { if (!controller.signal.aborted) setBusy(false); } } - return
-
-
-

When a provider reports a usage limit and reset time, keep the run paused and continue automatically after the reset. Enabled by default.

- void save(next)} /> + return
+
+
+ +

Automatically continue the same run when the provider reports its reset time. Enabled by default.

+
+ void save(value)} />
-

Waiting frees the worker slot. Stop still cancels the run. Limits without a reported reset time need your attention.

- {enabled === null && !error ?

Loading…

: null} - {error ?

{error}

{enabled === null ? : null}
: null} + {enabled === null && !error &&

Loading settings…

} +

Changes apply to newly reached limits. Use Stop to cancel a run already waiting.

+ {error &&

{error}

{enabled === null && }
} + {notice &&

{notice}

}
; } --- a/components/app-shell.tsx +++ b/components/app-shell.tsx @@ -3475,7 +3475,7 @@ setRemoteTerminalCwd(nextTabs.find((tab) => tab.id === (nextActiveId || nextTabs[0].id))?.cwd || workspaceDefaultCwd); } - useAccountNotifications(authed, (record, prefs) => { + useAccountNotifications(Boolean(authed), (record, prefs) => { const open = () => { window.focus(); if (record.chatId) void loadChat(record.chatId); @@ -4107,6 +4107,7 @@ snap.runStatus === "running" || snap.runStatus === "waiting_for_user" || snap.runStatus === "waiting_input" || + (snap.runStatus === "paused" && Boolean(snap.queueMessage)) || snap.pendingQuestion, ), ); @@ -4514,6 +4515,7 @@ setBusySynced( next.runStatus === "running" || next.runStatus === "waiting_input" || + (next.runStatus === "paused" && Boolean(next.queueMessage)) || next.runStatus === "waiting_for_user" || Boolean(next.pendingQuestion || next.pendingApproval), ); --- a/lib/jobs.ts +++ b/lib/jobs.ts @@ -16,6 +16,8 @@ | "error"; export type AgentJob = { + /** Durable deferred claim metadata; cleared atomically when the reset is due. */ + providerRateLimit?: import("@/lib/provider-rate-limit").ProviderRateLimit; id: string; chatId: string; userId?: string; --- a/lib/notification-observer.ts +++ b/lib/notification-observer.ts @@ -9,14 +9,24 @@ const { cursor } = db.prepare("SELECT cursor FROM notification_observer WHERE key='runs'").get() as {cursor:number}; const { tail } = db.prepare("SELECT COALESCE(MAX(id),0) AS tail FROM run_events").get() as {tail:number}; const events = db.prepare(`SELECT e.id,e.job_id,e.chat_id,e.user_id,e.event,e.data,e.created_at - FROM run_events e WHERE e.id>? AND e.id<=? AND e.event IN ('done','error','question') ORDER BY e.id LIMIT 500`).all(cursor,tail) as Array>; + FROM run_events e WHERE e.id>? AND e.id<=? AND e.event IN ('done','error','question','status') ORDER BY e.id LIMIT 500`).all(cursor,tail) as Array>; for (const event of events) { const ownerId = String(event.user_id || ""); - if (!ownerId || !["done", "error", "question"].includes(String(event.event))) continue; + if (!ownerId || !["done", "error", "question", "status"].includes(String(event.event))) continue; if (!db.prepare("SELECT 1 FROM chats WHERE id=? AND owner_id=?").get(String(event.chat_id), ownerId)) continue; let data: Record = {}; try { const parsed = JSON.parse(String(event.data)); if (parsed && typeof parsed === "object") data = parsed; } catch {} const kind = String(event.event); + if (kind === "status") { + if (data.status !== "waiting_provider_limit" || typeof data.resetAt !== "string" || !Number.isFinite(Date.parse(data.resetAt))) continue; + const resetAt = new Date(data.resetAt).toISOString(); + createNotification(ownerId, { + title: "Provider-Limit erreicht", + body: `Der Run wartet bis ${resetAt} und wird danach automatisch fortgesetzt.`, + chatId: String(event.chat_id), + }, `run:${ownerId}:provider-limit:${String(event.job_id)}:${resetAt}`, String(event.created_at)); + continue; + } // Never copy run output/tool payloads or error stacks into OS notifications. const title = kind === "done" ? "Run abgeschlossen" : kind === "error" ? "Run fehlgeschlagen" : "Antwort benötigt"; const body = kind === "question" ? "Eine Frage wartet im Chat auf deine Antwort." : kind === "error" ? "Details findest du im Chat." : "Das Ergebnis ist im Chat verfügbar.";