Skip to content

feat(api): context-window usage indicator + honest quota-error classification#6215

Merged
houko merged 7 commits into
mainfrom
fix/quota-classify-and-context-usage
Jun 19, 2026
Merged

feat(api): context-window usage indicator + honest quota-error classification#6215
houko merged 7 commits into
mainfrom
fix/quota-classify-and-context-usage

Conversation

@houko

@houko houko commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Why

Two coupled problems behind the "Token quota exceeded. Try /compact or /new to free up space." message users hit even with a large/empty context window.

  1. The WebSocket streaming error classifier (ws.rs: classify_streaming_error) matched a bare "quota" / "Quota" substring before the LLM error classifier ran.
    Any provider 429 / billing error whose body merely contains the word "quota" (e.g. OpenAI "You exceeded your current quota") was relabeled as LibreFang's internal budget message.
    That advice is doubly wrong: a provider rate-limit has nothing to do with context space, and /compact never clears any usage cap.
    This is why setting [budget] to 0 (unlimited) does not make the message go away — the error is not the internal budget at all.

  2. There was no way to see how full the context window actually is, so the usage budget and the context window kept getting conflated.

What

Fix — honest quota-error classification (fix(api))

  • Narrow the ws.rs branch to the exact internal Display prefix of LibreFangError::QuotaExceeded ("Resource quota exceeded:") and give it an honest message: it is a usage/spending cap, /compact will not help, and the remedy is to wait for the window to reset or raise [budget].
  • Every other "quota"-bearing string now falls through to llm_errors::classify_error, so a provider 429 → RateLimit ("Rate limited, wait ~Ns") and 402/billing → Billing, instead of the bogus internal copy.
  • Correct two misleading session_ops.rs comments that claimed /new clears the quota: reset_usage only resets the in-memory per-agent token tracker, not the persisted cost/usage windows.

Feature — context-window usage indicator (feat(api))

  • New authed GET /api/agents/{id}/session/context{used_tokens, max_context_tokens, pct, model, pressure}.
  • The denominator is resolved via Kernel::context_report, which applies the same precedence chain the agent loop uses (manifest override > catalog > persisted session > 8192 fallback), so it is always positive.
  • The endpoint honors an optional ?session_id= exactly like GET /session, with the matching cross-agent ownership guard, so a dashboard tab pinned to a non-active session reports that session.
    context_report is refactored into context_report_for_session(agent_id, Option<SessionId>) with context_report delegating to it.
  • Dashboard renders a small progress-bar indicator in the chat ConnectionBar, wired through api.ts → query hook → hierarchical key factory → i18n (en/zh/uk), polled at 15s, with loading/error states, theme-token colors stepped by pressure, and a11y role="progressbar" attributes.

Review fixes (from the pre-PR adversarial review)

  • Major: the context handler ignored the ?session_id= the dashboard sent and always reported the active session — now resolved via the session-aware path + ownership guard.
  • Minor: dropped the unreachable max <= 0 dead branch in the dashboard component and corrected the max_context_tokens doc comment (it is never 0 — the window always falls back to 8192).

Verification

  • Dashboard: pnpm typecheck, pnpm lint (0 errors), pnpm test --run (812 pass incl. new key-factory anchoring tests), pnpm build — all green locally.
  • Rust: this dev box has no native cargo/rustfmt and a critically low root disk, so the Rust crates were not compiled locally — compile / clippy / rustfmt / openapi-drift are left to CI per the repo's "CI does full validation" model.
    The Rust changes were reviewed for compile-correctness and formatted to rustfmt-stable shapes by hand.
  • New integration tests in crates/librefang-api/tests/agents_routes_integration.rs: context_endpoint_returns_used_and_max_tokens, context_endpoint_unknown_agent_404, context_endpoint_is_authed, context_endpoint_honors_session_id_param, context_endpoint_rejects_cross_agent_session.
  • New ws.rs unit tests assert the internal "Resource quota exceeded:" prefix yields the honest budget message while a provider "...429...quota" string routes to RateLimit and 402 to Billing.

Deferred

  • Structured error-category plumbing (replace the ws.rs string matching with a typed LlmErrorCategory carried on LibreFangError::LlmDriver, KernelHandle trait uses Result<_, String> for ~40 methods — destroys structured errors #3541-aligned).
    Assessed as feasible but spanning 3 crates plus the kernel-config golden-fixture regen, and not a correctness prerequisite — the substring narrowing here already fully resolves the reported bug.
    Left for its own PR to keep this diff focused.

Evan added 2 commits June 18, 2026 15:56
…udget cap

The WebSocket streaming error classifier matched a bare "quota"/"Quota" substring before the LLM error classifier ran.
Any provider 429/billing error whose body contains the word "quota" (e.g. OpenAI "You exceeded your current quota") was relabeled as LibreFang's internal "Token quota exceeded. Try /compact or /new to free up space."
That advice is doubly wrong: a provider rate-limit has nothing to do with context space, and /compact never clears any usage cap.

Narrow the branch to the exact internal Display prefix of LibreFangError::QuotaExceeded ("Resource quota exceeded:") and give it an honest message — it is a usage/spending cap, /compact will not help, and the remedy is to wait for the window to reset or raise [budget].
Every other "quota"-bearing string now falls through to the LLM classifier and gets the correct RateLimit / Billing message.

Also correct two misleading session_ops comments that claimed /new clears the quota: reset_usage only resets the in-memory per-agent token tracker, not the persisted cost/usage windows.
Add GET /api/agents/{id}/session/context returning {used_tokens, max_context_tokens, pct, model, pressure} for an agent's session, so users can see how full the context window is and stop confusing it with the usage budget.
The window denominator is resolved via Kernel::context_report, which applies the same precedence chain the agent loop uses (manifest override > catalog > persisted session > 8192 fallback).

The endpoint honors an optional ?session_id= the same way GET /session does, with the matching cross-agent ownership guard, so a dashboard tab pinned to a non-active session reports that session.
context_report is refactored into context_report_for_session(agent_id, Option<SessionId>), with context_report delegating to it.

The dashboard renders a small progress-bar indicator in the chat ConnectionBar wired through api.ts to a query hook, a hierarchical key factory, and i18n in en/zh/uk, polled at 15s, with loading/error states and a11y attributes.

Integration tests cover the response shape, unknown-agent 404, auth enforcement, the pinned-session path, and the cross-agent rejection.
@github-actions github-actions Bot added size/L 250-999 lines changed area/kernel Core kernel (scheduling, RBAC, workflows) labels Jun 18, 2026
@github-actions github-actions Bot added the area/sdk JavaScript and Python SDKs label Jun 18, 2026
Evan added 3 commits June 18, 2026 16:42
The budget message intentionally mentions /compact to say it will NOT help; the test asserted !msg.contains("/compact"), contradicting its own intent (don't *recommend* /compact) and failing the Test/Unit lane. Assert the message steers away from /compact instead. Also apply the two rustfmt diffs (sessions.rs match scrutinee, ws.rs single-line test call) that failed the Quality job.
The context-window integration test asserted body["model"] == "test-model" (the global config default_model), but spawn_named builds agents from AgentManifest::default(), whose model is "default". The endpoint correctly returns the agent's own manifest model, so the test expectation was wrong — assert "default".
…t skeleton, drop dead key

The new get_agent_session_context handler returned three hardcoded
English error strings; route them through ErrorTranslator (reusing
api-error-session-invalid-id / api-error-session-not-found and adding
api-error-context-report-failed across all 7 locales). Translate the
context-report message before drop(t) since ErrorTranslator is !Send.

ContextUsageIndicator pinned a permanent skeleton whenever the query was
disabled (no sessionId): a disabled TanStack query reports
isLoading=false/data=undefined, so guard on isFetching and render
nothing when idle. Drop the unused context_usage_unknown i18n key.
@github-actions github-actions Bot added the has-conflicts PR has merge conflicts that need resolution label Jun 18, 2026
@houko
houko enabled auto-merge (squash) June 18, 2026 15:38
# Conflicts:
#	.secrets.baseline
@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 19, 2026
@houko
houko merged commit 2f7658c into main Jun 19, 2026
34 checks passed
@houko
houko deleted the fix/quota-classify-and-context-usage branch June 19, 2026 06:02
houko pushed a commit that referenced this pull request Jun 19, 2026
…classifier

The #6211 token/context-limit banner keys off the chat surface error string,
which on current main is the output of the kernel's `classify_streaming_error`
(#6215 merged).
Two cases were wrong against that output:

- False negative: the canonical context-overflow message is collapsed to
  "Context is full. Try /compact or /new.", which matched none of the phrase
  list, so the banner missed the exact case it exists for.
  Add "context is full".
- False positive with harmful advice: an internal usage/spending-budget cap is
  reported as "Usage budget reached ... not a full context window — /compact
  will NOT help ...".
  That string contains "context window" (inside a negation) and so fired the
  banner, telling the user to start a new session — which does nothing for a
  persisted budget window.
  Suppress the banner for usage-budget wording.

Add regression cases pinning both behaviours (they fail without the fix) and
refresh the CHANGELOG entry to reflect that #6215 has merged and only adds a
usage indicator, not a per-turn error classification.
houko added a commit that referenced this pull request Jun 19, 2026
…mit (#6211) (#6231)

* feat(dashboard/chat): guide user to a new session on token/context limit

When the latest agent-chat turn fails with a token / context-window or
length / quota limit, the chat view now renders an inline guidance banner
with a one-click "Start a new session" action that reuses the existing
useCreateAgentSession mutation, instead of leaving only a raw error bubble.

Detection is a frontend heuristic over the daemon / provider error string
(isContextLimitError), because main carries no structured context-exhaustion
signal on the chat surface.
A structured signal can replace it later (related: in-flight #6215).

Scoped to the agent session chat view — channels are config-only surfaces in
the dashboard with no conversation UI.

Adds en/zh/uk locale strings and ChatPage.limit.test.ts.

Closes #6211

* refactor(dashboard/chat): trim multi-line comment blocks and remove issue refs from source

* fix(dashboard/chat): align context-limit heuristic with kernel error classifier

The #6211 token/context-limit banner keys off the chat surface error string,
which on current main is the output of the kernel's `classify_streaming_error`
(#6215 merged).
Two cases were wrong against that output:

- False negative: the canonical context-overflow message is collapsed to
  "Context is full. Try /compact or /new.", which matched none of the phrase
  list, so the banner missed the exact case it exists for.
  Add "context is full".
- False positive with harmful advice: an internal usage/spending-budget cap is
  reported as "Usage budget reached ... not a full context window — /compact
  will NOT help ...".
  That string contains "context window" (inside a negation) and so fired the
  banner, telling the user to start a new session — which does nothing for a
  persisted budget window.
  Suppress the banner for usage-budget wording.

Add regression cases pinning both behaviours (they fail without the fix) and
refresh the CHANGELOG entry to reflect that #6215 has merged and only adds a
usage indicator, not a per-turn error classification.

---------

Co-authored-by: Evan <[email protected]>
Co-authored-by: Claude <[email protected]>
GQAdonis pushed a commit to GQAdonis/librefang that referenced this pull request Jun 19, 2026
Merge upstream/main into the BossFang fork; origin/main was 15 commits behind.

Notable upstream changes:
- librefang#6225 scope the compaction-summary banner to the compacted session (adds canonical_sessions.compacted_summary_session_id).
- librefang#6196 deny WASM fs_write to the audit anchor via a capability deny-list.
- librefang#6226 / librefang#6227 add agent label and exit-reason metrics to the agent loop.
- librefang#6215 context-window usage indicator + honest quota-error classification.
- librefang#6217 per-instance sidecar secrets so each agent owns its own handle.
- librefang#6194 global Auto-Dream on/off switch on the Memory tab.
- librefang#6211 / librefang#6214 / librefang#6212 token/context cap fixes; librefang#6208 refuse to delete the active prompt version.
- librefang#6218 browser_tools.rs ToolError migration; librefang#6203 launchctl let-binding build fix; librefang#6224 docs dep bumps; librefang#6193 drop five orphaned email deps.

Conflict resolution:
- deny.toml: keep our RUSTSEC-2025-0141 (bincode 2.0.x) ignore entry (take ours).
- deny.toml: ignore three pre-existing unmaintained advisories newly tripped by the RustSec DB (all transitive via the UAR provider chain, none introduced by this merge): RUSTSEC-2024-0384 (instant), RUSTSEC-2024-0436 (paste), RUSTSEC-2025-0119 (number_prefix).

BossFang preservation (SurrealDB schema parity for upstream SQLite v46 / librefang#6225):
- Add crates/librefang-storage/src/migrations/sql/032_canonical_sessions_compacted_summary_session_id.surql declaring the new field on the SCHEMAFULL canonical_sessions table (SCHEMAFULL silently drops undefined fields on write), registered as version 32 in migrations/mod.rs.
- migrate/sqlite_to_surreal.rs: copy the new column so a SQLite to SurrealDB migration preserves the owning-session pointer rather than dropping it.
- backends/surreal_session.rs: preserve compacted_summary_session_id across canonical appends, since the upsert replaces the whole record.
- Cargo.lock: pick up the workspace version bump (beta.19 to beta.20) the merge introduced.

Verification:
- cargo check --workspace --lib — clean.
- cargo check -p librefang-storage -p librefang-memory -p librefang-uar-spec — clean.
- cargo test -p librefang-storage migration — ok (migration ordering / SurrealDB-3 flexible-syntax invariants).
- cargo test -p librefang-memory --lib session — 50 passed (incl. upstream's store_llm_summary round-trip test).
- cargo clippy -p librefang-storage -p librefang-memory — clean.
- python3 scripts/enforce-branding.py --check — clean; Tauri desktop audit and URL-drift scan both clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kernel Core kernel (scheduling, RBAC, workflows) area/sdk JavaScript and Python SDKs ready-for-review PR is ready for maintainer review size/L 250-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant