Ctrl K
ComunidadGuías…

patches/provider-limit-parent-integration.patch

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
--- 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.";