- Introduced `ContextUsageAnchor` struct to track context input tokens and message count for conversations.
- Updated token estimation functions to utilize the context usage anchor, enhancing accuracy in estimating tokens for projected messages.
- Refactored compaction logic to incorporate context usage anchor, allowing for more efficient management of token budgets during model runs.
- Added tests to validate the behavior of the context usage anchor across different scenarios, including model switching and message additions.
- Updated the `merge_usage` function to include `total_tokens` in the usage merging process.
- Implemented logic to calculate `total_tokens` based on the sum of `context_input_tokens` and `output_tokens`.
- Added a new test to verify that streamed usage correctly includes cached input in the total token count.
- Removed the `retry_count` field from `ProviderConfig` as it is no longer needed.
- Introduced `argument_error` field in `ToolCall` to capture errors related to tool arguments.
- Updated various components to handle argument errors more gracefully, including in the `ToolDispatcher` and `ConversationOutput`.
- Enhanced tests to validate the new error handling and ensure proper functionality of tool calls.
- Added `estimate_context_tokens` function to calculate provider-visible context size based on prompt specifications and projected messages.
- Updated `CheckpointBuilder` to record estimated context tokens during message processing.
- Refactored compaction logic to utilize the new token estimation, ensuring proper context management during model runs.
- Introduced tests to validate context estimation and compaction behavior under various scenarios.
ReadLints accepts an array of paths, but codec::request encodes only
paths[0] into the DiagnosticsArgs exec, and the exec protocol cannot
carry more than one path. Nothing downstream mentions the drop: for
{"paths": ["a.ts", "b.ts", "c.ts"]} the model is handed
"No diagnostics found in a.ts" with is_error false, so it concludes
b.ts and c.ts are clean when neither was ever opened. The existing
truncation notice cannot catch this, since it compares total_diagnostics
against a per-file diagnostics count.
Name the unread paths in the model-facing result, using the bracketed
notice convention already in this file and the ToolCall that output()
is already given (as task() and render::read() already use it).
Single-path calls are unchanged.
This does not close the capability gap. A real fix fans out one
DiagnosticsArgs exec per path and aggregates the results into the
repeated FileDiagnostics that ReadLintsToolSuccess already defines,
which needs multi-exec reservation and a completion barrier because
take_exec drops the pending entry on the first result. That is a
separate change; this one only stops the silent misinformation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`make check` currently fails on main before any change is made: one
`cargo fmt --all -- --check` diff and four `cargo clippy --workspace
--all-targets -- -D warnings` errors. All five are pre-existing and
none of them change behaviour.
- `server/tests/knowledge_rules.rs:125` — rustfmt wants the long
`assert!` split across lines. Applied `cargo fmt --all` verbatim.
- `server/src/plugin/data.rs:206,215` — `path` is only read under
`#[cfg(unix)]`, so every other target sees an unused binding. Added
a `#[cfg(not(unix))] { let _ = path; }` arm, matching the
`let _ = error;` idiom already used at line 189 of the same file.
Windows behaviour is unchanged: these helpers stay no-ops there.
- `server/src/provider/openai_responses.rs:158` — `collapsible_match`.
Applied clippy's own suggestion (move `thinking_open` into a match
guard). The match ends in `_ => {}`, so a failed guard falls through
to a no-op exactly as the inner `if` did.
- `server/src/store/models.rs:327` — `items_after_test_module`. Moved
`optional_u64` and `to_i64` above `mod tests`; the bodies are
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Usage::add_assign` folds every field through
fn sum(left: Option<u64>, right: Option<u64>) -> Option<u64> {
left?.checked_add(right?)
}
so `sum(Some(900), None)` is `None`, not `Some(900)`. Adding a record that does
not report a field therefore does not leave the running total alone - it wipes
it.
Both accumulators are per-turn and run over every provider call in the turn:
`run/engine.rs::accumulate_usage` (the compaction call plus each model cycle)
and `cursor/conversation/output.rs::turn_usage`, which is what `turn_ended`
reports to Cursor.
Providers report these fields inconsistently *across calls of one turn*, which
is all it takes. `openai_usage` reads `cache_read_tokens` from
`prompt_tokens_details`, an object most OpenAI-compatible gateways omit until
the prompt cache warms: call 1 (cold) yields `None`, call 2 yields
`Some(1024)`, and the turn reports `None`. The same happens to
`reasoning_tokens` when only some cycles reason, and to `total_tokens` on
gateways that omit it from the streaming usage chunk. Any tool-using turn makes
more than one call, so this is the normal case rather than an edge case.
Treat an unreported count as zero and keep the field unknown only when neither
side reported it. `checked_add` also becomes `saturating_add`: with the new
rule, silently turning an overflow into `None` would be the same erasure by
another route, and token counts never approach `u64::MAX` anyway.
`context_input_tokens` is untouched.
Before (with `left?.checked_add(right?)` restored):
cargo test -p cursor-server --lib model::observability
-> 2 failed: assertion `left == right` failed: left: None, right: Some(900)
After:
cargo test -p cursor-server --lib model::observability -> 3 passed
`truncate_text` looks for a fixed point where the kept prefix length equals the
byte count printed in its own truncation notice. Two things go wrong when the
limit is small, and both are reachable because two call sites pass a *remaining*
budget rather than a constant:
* `gate_grep_content` -> `truncate_text("Grep", .., budget.content_bytes)`
* `gate_mcp` -> `truncate_text("MCP text", .., remaining_text)`
1. The loop can spin forever. `notice.len()` grows with the decimal digit count
of `shown`, so `kept.len()` alternates between two values across a power-of-
ten boundary and never equals `shown`. With `tool_name = "Grep"` this happens
at `limit` 78 and 170; with `"MCP text"` at 82 and 174. The conversation task
then spins at 100% CPU and the turn never completes. `truncate_middle` in
`model/tool_result_replay.rs` already guards against exactly this.
2. When the notice itself does not fit, `available` saturates to 0 and the
function returns the ~68 byte notice alone, i.e. *more* than `limit`.
`gate_grep_content` then evaluates `budget.content_bytes -= <68 bytes>` on a
budget of at most 68, which panics with "attempt to subtract with overflow"
in debug/test builds and wraps in release, silently disabling the 32 KiB
content cap for the rest of the result.
Both are ordinary Grep results away: 16 matches of ~2 KiB leave a two-digit
remainder of the 32 KiB budget, and the next match then hits the small-limit
path.
Fix: return a plain prefix when the notice cannot fit, and stop as soon as the
kept length repeats a previous value. The reported byte count is then off by one
at most in that rare oscillating case, and the result is guaranteed to be at
most `limit` bytes. Every constant-limit call site is unaffected: their notices
always fit and their limits do not oscillate. `budget.content_bytes` now uses
`saturating_sub`, matching every other subtraction in this file.
Verified against the unfixed function:
cargo test -p cursor-server --lib gate::tests::truncate_text_never_exceeds_its_limit
-> FAILED: limit 1 produced 67 bytes
cargo test -p cursor-server --lib gate::tests::grep_content_gate_survives
-> FAILED: panicked at gate.rs:261: attempt to subtract with overflow
cargo test -p cursor-server --lib gate::tests::truncate_text_terminates
-> never returns (killed after 45s)
cargo test -p cursor-server --lib gate::tests::grep_content_gate_terminates
-> never returns (killed after 60s)
After: cargo test -p cursor-server --lib tools::tool_call_result::gate -> 4 passed
`map_finish` matched `"stop" | "content_filter"` before the `has_tools`
fallback, so the fallback only ever applied to finish reasons the adapter did
not recognise. When an OpenAI-compatible server streams tool calls and then
reports `finish_reason: "stop"` the adapter yielded `Done(Stop)` even though
`ToolCallStart` / `ToolCallEnd` had already been emitted.
`run/model_cycle.rs` rejects that combination:
let has_tool_calls = !calls.is_empty();
if matches!(finish_reason, FinishReason::ToolUse) != has_tool_calls {
return Err(failure(RunFailure::Protocol(
"finish reason and tool calls disagree".into()), ...));
}
so the whole turn fails with a protocol error and the tool never runs. Servers
that report `"stop"` alongside `tool_calls` are common in BYOK setups
(llama.cpp, Ollama's OpenAI shim, several proxies), which makes those models
unusable for anything agentic.
Move the `has_tools` arm ahead of `"stop" | "content_filter"` so an observed
tool call outranks the label the provider attached to the stop. This matches
the two sibling adapters (`anthropic.rs` checks `_ if saw_tool` before every
reason except `Length`, `openai_responses.rs` derives the reason from
`saw_tool` alone) and this adapter's own `[DONE]` fallback, which already
infers `ToolUse` from `!tools.is_empty()`. `"length"` still wins so a
truncated response is still reported as truncated, and behaviour with no tool
calls is byte-for-byte unchanged.
Before (with the arm restored):
cargo test -p cursor-server --lib provider::openai_chat
-> observed_tool_calls_outrank_a_stop_finish_reason FAILED
assertion `left == right` failed: left: Stop, right: ToolUse
After:
cargo test -p cursor-server --lib provider::openai_chat -> 3 passed
`local_markdown_rules_land_in_the_request_context_message` never finishes:
`cargo test --workspace` fails on `main` with
panicked at server\tests\local_rules_context.rs:64:14:
run finishes within timeout: Elapsed(())
The Run publishes the conversation checkpoint by asking the client to write
Blobs, and it does not continue until every `KvServerMessage` is answered
with a `SetBlobResult`. The test drained the output stream without replying,
so the Run stalled after the first frame, the provider was never invoked, and
none of the assertions the test exists for were ever reached.
Answer the Blob writes the way every other transport test already does
(`error_lifecycle.rs`, `conversation_delivery.rs`, `interrupt.rs`). With the
acknowledgement in place the Run reaches `EndStream` in ~0.3s and the original
assertions run and pass, so `merge_local_rules` is now genuinely covered:
exactly one `request-context:` message is projected and it carries
`<user_rule>Always answer in haiku.</user_rule>`.
No production code changes.
Before: `cargo test -p cursor-server --test local_rules_context`
-> FAILED (0 passed; 1 failed) after a 5s timeout
After: `cargo test -p cursor-server --test local_rules_context`
-> ok (1 passed; 0 failed) in 0.28s
- Introduced a new `Task` presentation type in the `ToolCallStream` to handle task-related projections.
- Implemented the `TaskProjection` struct with fields for description, prompt, subagent type, model, resume, and environment.
- Added a `project` method to `TaskProjection` to process task-related events and generate interaction updates.
- Created a `task_partial` function to format task updates for the agent server message.
- Included unit tests to verify the correct behavior of task description projections.
- Introduced a new `group_name` field in the model configuration to allow for custom provider-group display names.
- Updated the `CursorModelCards`, `CursorModelEditor`, and `CursorSettingsPage` components to support group settings.
- Enhanced the UI to include group settings options, allowing users to modify group names and associated configurations.
- Added localization strings for new group settings features in both English and Chinese.
- Implemented a database migration to add the `group_name` column to the model configurations.
- Introduced a new Grok authentication plugin, including essential files such as main.ts, provider.ts, and resources.ts.
- Implemented OAuth2 device authorization flow in oauth.ts, allowing users to sign in with xAI.
- Added model discovery and quota management functionalities in models.ts and resources.ts.
- Created a JSON configuration file (plugin.json) for plugin metadata and permissions.
- Included SVG asset for the Grok icon.
- Developed comprehensive tests in grok_test.ts to ensure functionality and reliability of the plugin.
- Replaced asynchronous file operations with a synchronous approach to ensure file handles are properly closed before replacement.
- Introduced a new `write_once` function for atomic file writing, including directory creation, temporary file writing, and target file replacement.
- Enhanced error handling to retry on transient errors specific to Windows, improving robustness during file operations.
- Updated the `clear` method in `PluginRegistry` to ensure OAuth sessions are only removed after successful persistence of resources.
- Implemented a new button to copy the user code in the OAuthMethodCard, enhancing user experience.
- Added styles for the copy button to match the UI design.
- Updated localization files to include new strings for the copy action in both English and Chinese.
- Added detailed error messages for plugin data read/write failures, including file paths for better debugging.
- Updated logging levels for upstream request rejections in account services to debug for less critical issues.
- Enhanced error handling in the plugin worker to provide clearer context when starting the plugin worker fails.
- Introduced new functions for merging extra parameters and applying body allowlists in provider services, improving request validation.
- Updated the UI components in CursorModelCards and PluginManagementPage to use TruncatedButton for better text handling and display.
- Adjusted styles in CursorSettings and PluginManagementPage to ensure proper button layout and responsiveness.
- Added new ActionMenu component for handling additional actions in PluginManagementPage.
- Enhanced localization files to include new strings for the ActionMenu and TruncatedButton components.
- Introduced a new `provider_stream_idle_timeout` configuration to manage idle timeouts for provider streams.
- Enhanced error handling in the `ProviderRouter` to include specific timeout errors for both request and stream idle scenarios.
- Updated the `AnthropicProvider` and `OpenAiChatProvider` to utilize the new error handling functions for improved SSE error reporting.
- Added tests to verify the correct behavior of timeout handling and error extraction from provider events.
A tool call that carries no arguments streams no argument text, so
`arguments_text` is empty and `from_str("")` fails with `EOF while parsing
a value`, aborting the whole run. The model cycle already guards this, but
two other consumers did not:
- `ConversationOutput` re-parses the streamed text on `ToolCallEnd`; and
- `create_tool_round` stored the empty text verbatim in the
`arguments_json` column, so re-loading the round (`commit_tool_result`
and the round loader) then failed on `from_str("")`.
Treat empty argument text as an empty object in the output projection, and
persist `{}` for it so the `arguments_json` column always holds valid JSON.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime user messages and context injections both queue into
`pending_injections`, but with different keys: injections use the raw
injection id (committed under `inject-context:{id}`) while user messages
use the full `user-message:{id}` event id. The commit-correlation handler
only stripped the `inject-context:` prefix, so a user message's entry was
never removed.
Consequences:
- the client never received `ContextInjectionDelivered` /
`UserMessageAppended` for the message; and
- `pending_injections` stayed non-empty, so every later `ExecuteToolRound`
was detached without dispatching its tools and `tool_round::execute`
blocked forever -- a hung turn whenever the model made a tool call after
the interruption.
Derive the lookup key by stripping the injection prefix when present and
otherwise using the event id verbatim, so both kinds are cleared and their
delivered/appended events fire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tool dispatcher already treats `bash`/`Bash` as an alias of `Shell`
(routing, `is_shell_tool`, and `block_until_ms` normalization), but the
codec only matched `shell`:
- `tool_placeholder` returned `unsupported tool: bash`, which aborts the
turn while streaming the tool call, before it ever runs;
- `request` returned `tool bash is not executed through ExecServerMessage`
(after already reserving an exec slot); and
- `stream_closed` built its shell-specific error result only for `Shell`.
Anthropic models frequently emit `Bash` even when the tool is advertised
as `Shell`, so the alias must hold across the codec. Match `bash` wherever
the codec special-cases `shell`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StrReplace rejects an empty `old_string`, but the EditNotebook cell-edit
path did not. Because `str::match_indices("")` matches at every byte
boundary, editing a non-empty cell with an empty `old_string` failed with
a misleading "old_string is not unique in the notebook cell; found N
occurrences" error, and editing an empty cell silently prepended
`new_string`.
Add the same guard StrReplace already uses so both edit tools reject an
empty `old_string` consistently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Introduced a new `WebCache` module to manage web content caching.
- Added functionality to store fetched content and serve it via a dedicated route.
- Integrated web cache into the search module for improved content retrieval.
- Implemented database migration management with detailed diagnostics for better error handling during startup.
- Updated the SQLite store to utilize the new migration system for enhanced database management.
- Introduced a new `startup` module to capture and manage startup diagnostics.
- Implemented logging functionality with daily rotation and error reporting for application startup failures.
- Updated the main application run function to return an exit code based on startup success or failure.
- Added new dependencies for logging and diagnostics in `Cargo.toml`.
- Increased request timeout in the control service for improved stability.
- Expanded maximum search results in the federation module for better search capabilities.
- Added initial server setup with Cargo.toml defining dependencies and project structure.
- Created build.rs for generating protobuf bindings and validating wire contracts.
- Established database schema with initial migration files for conversations, messages, and runs.
- Introduced tools and prompts for Cursor functionality, enhancing user interaction capabilities.
- Introduced `first_valid_response_ms` and `ttfr_ms` to track the timing of the first valid response in LLM calls.
- Updated relevant interfaces and components to display and utilize the new metrics, including CallDetails, CallTable, and LatencyChart.
- Enhanced the database schema to accommodate the new timing fields.
- Implemented logic in the service layer to record the first valid response during model interactions.
- Replaced direct API call handling with a retry mechanism using `send_with_retry`.
- Improved error handling and response management for better reliability in network requests.
- Added a refreshCalls function to appStore to fetch updated call data.
- Integrated useEffect in CallsPage to periodically refresh calls every 2 seconds and on visibility change.