patches/provider-limit-parent-integration.patch
Rama master · réplica de lectura del repositorio original.
--- 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<boolean | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
+ const [notice, setNotice] = useState("");
const [reload, setReload] = useState(0);
+ const saveController = useRef<AbortController | null>(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 <section className="mt-6 space-y-3 border-t border-border pt-5" aria-label="Provider limit recovery">
- <div className="flex items-start justify-between gap-4">
- <div><label htmlFor="provider-limit-resume" className="text-sm font-medium">Resume after provider limits</label>
- <p className="mt-1 text-xs leading-relaxed text-muted-foreground">When a provider reports a usage limit and reset time, keep the run paused and continue automatically after the reset. Enabled by default.</p></div>
- <Switch id="provider-limit-resume" checked={enabled === true} disabled={enabled === null || busy} onCheckedChange={next => void save(next)} />
+ return <section className="space-y-3" aria-busy={busy || (enabled === null && !error)}>
+ <div className="flex items-center justify-between gap-4">
+ <div className="space-y-1">
+ <label htmlFor="provider-limit-resume" className="text-sm font-medium">Wait for provider limits to reset</label>
+ <p className="text-sm text-muted-foreground">Automatically continue the same run when the provider reports its reset time. Enabled by default.</p>
+ </div>
+ <Switch id="provider-limit-resume" checked={enabled === true} disabled={enabled === null || busy} onCheckedChange={value => void save(value)} />
</div>
- <p className="text-xs leading-relaxed text-muted-foreground">Waiting frees the worker slot. Stop still cancels the run. Limits without a reported reset time need your attention.</p>
- {enabled === null && !error ? <p role="status" className="text-xs text-muted-foreground">Loading…</p> : null}
- {error ? <div className="space-y-2"><p role="alert" className="text-xs text-destructive">{error}</p>{enabled === null ? <Button variant="outline" onClick={() => setReload(value => value + 1)}>Retry</Button> : null}</div> : null}
+ {enabled === null && !error && <p role="status" className="text-sm text-muted-foreground">Loading settings…</p>}
+ <p className="text-sm text-muted-foreground">Changes apply to newly reached limits. Use Stop to cancel a run already waiting.</p>
+ {error && <div role="alert" className="space-y-2 text-sm"><p>{error}</p>{enabled === null && <Button variant="outline" onClick={() => setReload(value => value + 1)}>Retry</Button>}</div>}
+ {notice && <p role="status" className="text-sm text-muted-foreground">{notice}</p>}
</section>;
}
--- 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<Record<string, unknown>>;
+ 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<Record<string, unknown>>;
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<string, unknown> = {};
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.";