// Errors & rate limits
function Errors() {
  const shell = useShell();
  const tocItems = [
    { id: 'overview', label: 'Overview' },
    { id: 'status', label: 'Status codes' },
    { id: 'rate-limits', label: 'Rate limits' },
    { id: 'sse-error', label: 'SSE error events' },
    { id: 'retry', label: 'Retries' },
    { id: 'common', label: 'Common error shapes' },
  ];

  return (
    <div className="app">
      <Topbar section="api" theme={shell.theme} setTheme={shell.setTheme}
              onSearch={() => shell.setSearchOpen(true)}
              onMenuToggle={() => shell.setMobileMenuOpen(true)} />
      <div className="main">
        <Sidebar activeId="errors"
                 mobileOpen={shell.mobileMenuOpen}
                 onMobileClose={() => shell.setMobileMenuOpen(false)} />
        <article className="content">
          <div className="crumbs">
            <a href="index.html">Docs</a>
            <span className="sep">/</span>
            <a href="api-reference.html">API</a>
            <span className="sep">/</span>
            <span>Errors & rate limits</span>
          </div>

          <div className="eyebrow">API reference · errors</div>
          <h1 className="h1">Errors <em>& rate limits.</em></h1>
          <p className="lede">
            Every ClawAgen response is JSON (or a Server-Sent Events stream of JSON events). Errors
            surface as standard HTTP status codes for non-streaming responses and as{' '}
            <code>event: error</code> frames within a stream. No silent fallbacks.
          </p>

          <h2 id="overview" className="h2">Overview</h2>
          <p>
            Errors follow one of two shapes. Pre-stream errors (auth, rate limit, invalid body)
            return a normal HTTP response with a JSON body. Mid-stream errors (LLM provider down,
            tool throws) surface as an SSE <code>error</code> event; the stream then closes
            without a <code>done</code> frame.
          </p>

          <h2 id="status" className="h2">Status codes</h2>
          <div style={{overflow:'auto', margin:'12px 0 28px'}}>
          <table className="doc-table">
            <thead><tr><th>Status</th><th>Meaning</th><th>Body</th></tr></thead>
            <tbody>
              <tr><td><strong>200 OK</strong></td><td>Request accepted; body is the SSE stream (chat) or JSON (list endpoints).</td><td><em>varies</em></td></tr>
              <tr><td><strong>400 Bad Request</strong></td><td>Missing or malformed body field. Body is parseable but invalid.</td><td><code>&#123;"error":"message required"&#125;</code></td></tr>
              <tr><td><strong>401 Unauthorized</strong></td><td>Missing, invalid, or revoked <code>X-API-Key</code>.</td><td><code>&#123;"error":"missing X-API-Key"&#125;</code></td></tr>
              <tr><td><strong>404 Not Found</strong></td><td>Path doesn't exist, or a resource (session, skill) is unknown.</td><td><code>&#123;"error":"session not found"&#125;</code></td></tr>
              <tr><td><strong>429 Too Many Requests</strong></td><td>Per-minute or per-hour rate limit hit. Includes <code>Retry-After</code> header.</td><td>See <a className="inline" href="#rate-limits">Rate limits</a></td></tr>
              <tr><td><strong>500 Internal Server Error</strong></td><td>Unexpected failure in the runtime. Logs capture the stack; response is generic.</td><td><code>&#123;"error":"internal error"&#125;</code></td></tr>
              <tr><td><strong>502 Bad Gateway</strong></td><td>Upstream LLM provider returned a non-2xx or is unreachable. No fallback.</td><td><code>&#123;"error":"provider unavailable"&#125;</code></td></tr>
            </tbody>
          </table>
          </div>

          <h2 id="rate-limits" className="h2">Rate limits</h2>
          <p>
            Two rolling windows, applied per-tenant-key:
          </p>
          <ul>
            <li><strong>Per-minute cap</strong> — default 30 chat requests / minute. Configurable via <code>RATE_LIMIT_PER_MIN</code>.</li>
            <li><strong>Per-hour cap</strong> — default 300 chat requests / hour. Configurable via <code>RATE_LIMIT_PER_HOUR</code>. Catches slow-drip abuse that would slip under the minute cap.</li>
          </ul>
          <p>When either cap is hit, the response is <code>429 Too Many Requests</code>:</p>
          <CodeBlock tabs={[{ label: '429 response', raw: `HTTP/1.1 429 Too Many Requests\nRetry-After: 42\nContent-Type: application/json\n\n{\n  "error": "Too many requests in the last minute (limit 30). Wait 42s.",\n  "limit": 30,\n  "window": "minute",\n  "retry_after_sec": 42\n}`,
            code: `HTTP/1.1 <span class="tok-num">429</span> Too Many Requests
<span class="tok-var">Retry-After</span>: <span class="tok-num">42</span>
<span class="tok-var">Content-Type</span>: application/json

{
  <span class="tok-str">"error"</span>: <span class="tok-str">"Too many requests in the last minute (limit 30). Wait 42s."</span>,
  <span class="tok-str">"limit"</span>: <span class="tok-num">30</span>,
  <span class="tok-str">"window"</span>: <span class="tok-str">"minute"</span>,
  <span class="tok-str">"retry_after_sec"</span>: <span class="tok-num">42</span>
}` }]} />
          <Callout type="note" title="Rate-limit state survives restarts">
            Counters are stored in Redis so a deploy or container restart doesn't reset them.
            On self-hosted single-node deployments you can drop Redis and fall back to in-memory
            counters; that's a config toggle.
          </Callout>

          <h2 id="sse-error" className="h2">SSE error events</h2>
          <p>
            Mid-stream errors emit one <code>event: error</code> frame and close the stream
            without a <code>done</code> event. Clients should treat missing <code>done</code> as a
            partial failure.
          </p>
          <CodeBlock tabs={[{ label: 'SSE stream', raw: `event: text_delta\ndata: {"delta":"Let me check the "}\n\nevent: tool_call_start\ndata: {"name":"bash","input":{"command":"tunder memory search ..."}}\n\nevent: error\ndata: {"error":"sandbox timeout after 30000ms","tool":"bash"}\n\n# stream closes; no 'done' frame`,
            code: `event: <span class="tok-kw">text_delta</span>
data: {<span class="tok-str">"delta"</span>:<span class="tok-str">"Let me check the "</span>}

event: <span class="tok-kw">tool_call_start</span>
data: {<span class="tok-str">"name"</span>:<span class="tok-str">"bash"</span>,<span class="tok-str">"input"</span>:{<span class="tok-str">"command"</span>:<span class="tok-str">"tunder memory search ..."</span>}}

event: <span class="tok-kw">error</span>
data: {<span class="tok-str">"error"</span>:<span class="tok-str">"sandbox timeout after 30000ms"</span>,<span class="tok-str">"tool"</span>:<span class="tok-str">"bash"</span>}

<span class="tok-com"># stream closes; no 'done' frame</span>` }]} />

          <h2 id="retry" className="h2">Retries</h2>
          <div style={{overflow:'auto', margin:'12px 0 28px'}}>
          <table className="doc-table">
            <thead><tr><th>Error</th><th>Safe to retry?</th><th>Strategy</th></tr></thead>
            <tbody>
              <tr><td><strong>429</strong></td><td>After <code>Retry-After</code></td><td>Honor the header; exponential backoff beyond that isn't needed.</td></tr>
              <tr><td><strong>502</strong></td><td>Yes, with backoff</td><td>Upstream provider hiccup. Exponential backoff, max 3 attempts.</td></tr>
              <tr><td><strong>500</strong></td><td>Idempotent reads only</td><td>Chat POSTs may have partial effects (session saved, turn counted). Check the session before retrying.</td></tr>
              <tr><td><strong>401 / 404</strong></td><td>No</td><td>Key or resource issue. Retrying won't help.</td></tr>
              <tr><td><strong>400</strong></td><td>No</td><td>Fix the request body.</td></tr>
              <tr><td><strong>SSE error mid-stream</strong></td><td>Usually yes</td><td>The partial assistant message is saved. Retrying sends a new turn.</td></tr>
            </tbody>
          </table>
          </div>

          <h2 id="common" className="h2">Common error shapes</h2>
          <CodeBlock tabs={[
            { label: 'missing key', raw: `{"error":"missing X-API-Key"}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"missing X-API-Key"</span>}` },
            { label: 'invalid key', raw: `{"error":"invalid key"}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"invalid key"</span>}` },
            { label: 'revoked key', raw: `{"error":"key revoked"}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"key revoked"</span>}` },
            { label: 'empty message', raw: `{"error":"message required"}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"message required"</span>}` },
            { label: 'rate limit', raw: `{"error":"Hourly request limit reached (limit 300). Wait ~18 min.","retry_after_sec":1080}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"Hourly request limit reached (limit 300). Wait ~18 min."</span>,<span class="tok-str">"retry_after_sec"</span>:<span class="tok-num">1080</span>}` },
            { label: 'provider down', raw: `{"error":"provider unavailable: azure-openai returned 503"}`,
              code: `{<span class="tok-str">"error"</span>:<span class="tok-str">"provider unavailable: azure-openai returned 503"</span>}` },
          ]} />

          <Feedback />
          <PageFoot
            prev={{ label: 'API overview', href: 'api-reference.html' }}
            next={{ label: 'Changelog', href: 'changelog.html' }}
          />
        </article>
        <TOC items={tocItems} />
      </div>
      <SearchOverlay open={shell.searchOpen} onClose={() => shell.setSearchOpen(false)} />
      <TweaksPanel visible={shell.tweaksVisible} theme={shell.theme} setTheme={shell.setTheme} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<Errors />);
